#!/usr/bin/env bash # # samba-dc-tls.sh - CA und TLS-Zertifikate fuer Samba AD DCs generisch neu bauen # # Auslegung: mehrere DCs teilen sich EINE CA. Die CA liegt auf DC1, die # weiteren DCs bekommen ein Bundle (cert/key/ca) oder lassen einen CSR # signieren. Alle Aenderungen werden gesichert; schlaegt der Health-Check # nach dem Restart fehl, wird automatisch zurueckgerollt. # # Getestet mit Samba 4.17+ / OpenSSL 1.1.1+ / 3.x auf Debian und RHEL. # # Lizenz: MIT # set -euo pipefail umask 077 VERSION="1.1" PROG="$(basename "$0")" # --------------------------------------------------------------------------- # Defaults - per Umgebungsvariable oder Option ueberschreibbar # --------------------------------------------------------------------------- CA_DIR="${CA_DIR:-/var/lib/samba/private/tls-ca}" CA_DAYS="${CA_DAYS:-3650}" # CA-Laufzeit (10 Jahre) CERT_DAYS="${CERT_DAYS:-825}" # DC-Zertifikat; 825 = Apple/Chrome-Limit KEY_BITS="${KEY_BITS:-4096}" # RSA; breiteste Client-Kompatibilitaet CA_CN="${CA_CN:-}" # Default: " Samba AD CA" ORG="${ORG:-}" # Default: Realm EXTRA_DNS="${EXTRA_DNS:-}" # zusaetzliche DNS-SANs, komma-/leerzeichensepariert EXTRA_IP="${EXTRA_IP:-}" # zusaetzliche IP-SANs BACKUP_KEEP="${BACKUP_KEEP:-10}" # wie viele Backup-Generationen behalten DO_RESTART=1 DO_ROLLBACK=1 DRY_RUN=0 FORCE=0 ASSUME_YES=0 # --------------------------------------------------------------------------- # Ausgabe-Helfer # --------------------------------------------------------------------------- if [[ -t 1 ]]; then C_R=$'\e[31m'; C_G=$'\e[32m'; C_Y=$'\e[33m'; C_B=$'\e[34m'; C_0=$'\e[0m' else C_R=""; C_G=""; C_Y=""; C_B=""; C_0="" fi log() { printf '%s[*]%s %s\n' "$C_B" "$C_0" "$*"; } ok() { printf '%s[+]%s %s\n' "$C_G" "$C_0" "$*"; } warn() { printf '%s[!]%s %s\n' "$C_Y" "$C_0" "$*" >&2; } die() { printf '%s[x]%s %s\n' "$C_R" "$C_0" "$*" >&2; exit 1; } run() { if (( DRY_RUN )); then printf ' would run: %s\n' "$*"; else "$@"; fi; } confirm() { (( ASSUME_YES )) && return 0 local a; read -r -p " $1 [j/N] " a || true [[ "$a" =~ ^([jJ]|[yY])$ ]] } need_root() { [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausfuehren."; } need_bin() { local m=() for b in "$@"; do command -v "$b" >/dev/null 2>&1 || m+=("$b"); done ((${#m[@]})) && die "Fehlende Programme: ${m[*]}" return 0 } # --------------------------------------------------------------------------- # Samba-Umgebung einlesen # --------------------------------------------------------------------------- tp() { testparm -s --parameter-name="$1" 2>/dev/null | tail -n1 | tr -d '\r'; } abspath_in_private() { local p="$1" [[ "$p" == /* ]] && { printf '%s' "$p"; return; } printf '%s/%s' "$PRIVATE_DIR" "$p" } detect_env() { need_bin testparm openssl SERVER_ROLE="$(tp 'server role')" [[ "$SERVER_ROLE" == *"domain controller"* ]] || \ warn "server role = '${SERVER_ROLE:-?}' - das sieht nicht nach einem AD DC aus." REALM="$(tp realm)" [[ -n "$REALM" ]] || die "Kein 'realm' in der smb.conf gefunden." REALM_LC="${REALM,,}" NETBIOS="$(tp 'netbios name')" PRIVATE_DIR="$(tp 'private dir')" PRIVATE_DIR="${PRIVATE_DIR:-/var/lib/samba/private}" TLS_ENABLED="$(tp 'tls enabled')" TLS_CERT="$(abspath_in_private "$(tp 'tls certfile')")" TLS_KEY="$(abspath_in_private "$(tp 'tls keyfile')")" TLS_CA="$(abspath_in_private "$(tp 'tls cafile')")" TLS_DIR="$(dirname "$TLS_CERT")" # FQDN ermitteln - hostname -f kann bei kaputtem DNS luegen local short; short="$(hostname -s 2>/dev/null || true)" FQDN="$(hostname -f 2>/dev/null || true)" if [[ -z "$FQDN" || "$FQDN" != *.* || "${FQDN,,}" != *"$REALM_LC" ]]; then FQDN="${short,,}.${REALM_LC}" fi FQDN="${FQDN,,}" SHORTNAME="${short,,}" ORG="${ORG:-$REALM}" CA_CN="${CA_CN:-$REALM Samba AD CA}" CA_KEY="$CA_DIR/ca-key.pem" CA_CERT="$CA_DIR/ca-cert.pem" CA_SRL="$CA_DIR/ca.srl" STATE_FILE="$TLS_DIR/.samba-dc-tls.state" } # NTDS-DSA-GUID fuer den ._msdcs. SAN detect_dsa_guid() { local samdb="$PRIVATE_DIR/sam.ldb" dsdn guid DSA_GUID="" command -v ldbsearch >/dev/null 2>&1 || return 0 [[ -r "$samdb" ]] || return 0 dsdn="$(ldbsearch -H "$samdb" -s base -b '' dsServiceName 2>/dev/null \ | sed -n 's/^dsServiceName: //p' | head -n1)" || true [[ -n "$dsdn" ]] || return 0 guid="$(ldbsearch -H "$samdb" -s base -b "$dsdn" objectGUID 2>/dev/null \ | sed -n 's/^objectGUID: //p' | head -n1)" || true [[ "$guid" =~ ^[0-9a-fA-F-]{36}$ ]] && DSA_GUID="${guid,,}" return 0 } print_env() { cat <"$out" log "SANs: ${udns[*]} ${ips[*]:-}" } write_ca_conf() { cat >"$1" <"$CA_SRL" ok "CA erstellt (gueltig $CA_DAYS Tage)." openssl x509 -in "$CA_CERT" -noout -subject -enddate | sed 's/^/ /' } require_ca() { [[ -f "$CA_KEY" && -f "$CA_CERT" ]] || \ die "Keine CA unter $CA_DIR. Erst '$PROG init-ca' ausfuehren (auf dem CA-DC)." } # --------------------------------------------------------------------------- # Zertifikat erzeugen / signieren # --------------------------------------------------------------------------- # $1 = FQDN, $2 = Ausgabeverzeichnis, $3 = optionale DSA-GUID make_cert() { local cn="$1" dir="$2" guid="${3:-}" local conf="$dir/openssl.cnf" install -d -m 0700 "$dir" write_cert_conf "$cn" "$conf" "$guid" openssl req -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \ -keyout "$dir/key.pem" -out "$dir/csr.pem" -config "$conf" chmod 0600 "$dir/key.pem" [[ -s "$CA_SRL" ]] || openssl rand -hex 16 >"$CA_SRL" openssl x509 -req -in "$dir/csr.pem" -sha256 -days "$CERT_DAYS" \ -CA "$CA_CERT" -CAkey "$CA_KEY" -CAserial "$CA_SRL" \ -extfile "$conf" -extensions v3_sign -out "$dir/cert.pem" 2>/dev/null chmod 0644 "$dir/cert.pem" cp -f "$CA_CERT" "$dir/ca.pem"; chmod 0644 "$dir/ca.pem" rm -f "$dir/csr.pem" } # --------------------------------------------------------------------------- # Pruefungen # --------------------------------------------------------------------------- check_pair() { local cert="$1" key="$2" ca="$3" fqdn="${4:-}" local a b a="$(openssl x509 -in "$cert" -noout -pubkey | openssl pkey -pubin -outform DER 2>/dev/null | openssl dgst -sha256 -r | cut -d' ' -f1)" b="$(openssl pkey -in "$key" -pubout -outform DER 2>/dev/null | openssl dgst -sha256 -r | cut -d' ' -f1)" [[ -n "$a" && "$a" == "$b" ]] || die "Key und Zertifikat passen nicht zusammen: $cert / $key" openssl verify -CAfile "$ca" "$cert" >/dev/null 2>&1 \ || die "Zertifikat wird von der CA nicht verifiziert: $cert" openssl x509 -in "$cert" -noout -ext extendedKeyUsage 2>/dev/null | grep -q 'TLS Web Server Authentication' \ || die "Zertifikat hat kein serverAuth-EKU." if [[ -n "$fqdn" ]]; then openssl x509 -in "$cert" -noout -ext subjectAltName 2>/dev/null | grep -qi "DNS:$fqdn" \ || die "SAN enthaelt den FQDN '$fqdn' nicht." fi ok "Zertifikat, Key und CA sind konsistent." } health_check() { local host="${1:-$FQDN}" rc=0 log "Health-Check gegen ldaps://$host:636" if ! timeout 15 openssl s_client -connect "127.0.0.1:636" -servername "$host" \ -CAfile "$TLS_CA" -verify_return_error /dev/null 2>&1; then warn "LDAPS-Handshake auf 127.0.0.1:636 fehlgeschlagen." rc=1 else ok "LDAPS-Handshake ok (Kette gegen eigene CA verifiziert)." fi if command -v ldbsearch >/dev/null 2>&1; then if ldbsearch -H "ldaps://$host" -k yes -s base -b '' defaultNamingContext >/dev/null 2>&1 \ || ldbsearch -H "ldap://$host" -k yes -s base -b '' defaultNamingContext >/dev/null 2>&1; then ok "LDAP-Abfrage gegen $host beantwortet." else warn "LDAP-Abfrage gegen $host fehlgeschlagen (evtl. kein Kerberos-Ticket - nicht zwingend ein Fehler)." fi fi if command -v samba-tool >/dev/null 2>&1; then if samba-tool drs showrepl >/tmp/.drs.$$ 2>&1; then if grep -qiE 'consecutive failure|WERR_|failed' /tmp/.drs.$$; then warn "drs showrepl meldet Fehler:"; sed 's/^/ /' /tmp/.drs.$$ | head -n 30 rc=1 else ok "Replikation (drs showrepl) ohne Fehler." fi else warn "samba-tool drs showrepl fehlgeschlagen."; rc=1 fi rm -f /tmp/.drs.$$ fi return $rc } # --------------------------------------------------------------------------- # Dienststeuerung # --------------------------------------------------------------------------- detect_unit() { local u for u in samba-ad-dc samba sernet-samba-ad; do if systemctl cat "$u.service" >/dev/null 2>&1; then if systemctl is-enabled "$u.service" >/dev/null 2>&1 || systemctl is-active "$u.service" >/dev/null 2>&1; then printf '%s' "$u.service"; return 0 fi fi done return 1 } restart_samba() { local unit if ! unit="$(detect_unit)"; then warn "Keine aktive Samba-AD-Unit gefunden - bitte manuell neu starten." return 0 fi log "Starte $unit neu ..." run systemctl restart "$unit" (( DRY_RUN )) && return 0 sleep 4 systemctl is-active --quiet "$unit" || die "$unit ist nach dem Restart nicht aktiv!" ok "$unit laeuft." } # --------------------------------------------------------------------------- # Installation mit Backup / Rollback # --------------------------------------------------------------------------- backup_current() { local ts bdir ts="$(date +%Y%m%d-%H%M%S)" bdir="$TLS_DIR/backup-$ts" install -d -m 0700 "$bdir" local f for f in "$TLS_CERT" "$TLS_KEY" "$TLS_CA"; do [[ -f "$f" ]] && cp -a "$f" "$bdir/" done printf '%s\n' "$bdir" >"$STATE_FILE"; chmod 0600 "$STATE_FILE" ok "Backup: $bdir" # alte Generationen aufraeumen local -a old mapfile -t old < <(ls -1d "$TLS_DIR"/backup-* 2>/dev/null | sort) if (( ${#old[@]} > BACKUP_KEEP )); then local del=$(( ${#old[@]} - BACKUP_KEEP )) i for ((i=0; i/dev/null | sed 's/^/ alt: /' || true else printf ' alt: (keine vorhanden)\n' fi openssl x509 -in "$src/cert.pem" -noout -subject -enddate | sed 's/^/ neu: /' if ! confirm "Zertifikate installieren und Samba neu starten?"; then die "Abgebrochen." fi local bdir; bdir="$(backup_current)" install -d -m 0700 -o root -g root "$TLS_DIR" install -m 0600 -o root -g root "$src/key.pem" "$TLS_KEY.new" install -m 0644 -o root -g root "$src/cert.pem" "$TLS_CERT.new" install -m 0644 -o root -g root "$src/ca.pem" "$TLS_CA.new" mv -f "$TLS_KEY.new" "$TLS_KEY" mv -f "$TLS_CERT.new" "$TLS_CERT" mv -f "$TLS_CA.new" "$TLS_CA" command -v restorecon >/dev/null 2>&1 && restorecon -R "$TLS_DIR" >/dev/null 2>&1 || true ok "Dateien installiert." restart_samba if ! health_check "$FQDN"; then if (( DO_ROLLBACK )); then warn "Health-Check fehlgeschlagen - rolle zurueck auf $bdir" restore_backup "$bdir" restart_samba health_check "$FQDN" || warn "Auch nach Rollback nicht sauber - bitte manuell pruefen!" die "Rollback durchgefuehrt. Zertifikate wurden NICHT geaendert." fi warn "Health-Check fehlgeschlagen (Rollback deaktiviert)." return 1 fi ok "Fertig. Neues Zertifikat ist aktiv." } restore_backup() { local bdir="$1" [[ -d "$bdir" ]] || die "Backup-Verzeichnis nicht gefunden: $bdir" [[ -f "$bdir/$(basename "$TLS_KEY")" ]] && install -m 0600 "$bdir/$(basename "$TLS_KEY")" "$TLS_KEY" [[ -f "$bdir/$(basename "$TLS_CERT")" ]] && install -m 0644 "$bdir/$(basename "$TLS_CERT")" "$TLS_CERT" [[ -f "$bdir/$(basename "$TLS_CA")" ]] && install -m 0644 "$bdir/$(basename "$TLS_CA")" "$TLS_CA" ok "Backup zurueckgespielt: $bdir" } # --------------------------------------------------------------------------- # Kommandos # --------------------------------------------------------------------------- cmd_renew() { need_root; require_ca; detect_dsa_guid local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN log "Erzeuge Zertifikat fuer $FQDN" (( DRY_RUN )) && { log "(dry-run) kein Schluesselmaterial erzeugt"; return 0; } make_cert "$FQDN" "$tmp" "${DSA_GUID:-}" install_bundle "$tmp" } cmd_issue_for() { need_root; require_ca local host="${1,,}"; shift || true local guid="${1:-}" [[ -n "$host" ]] || die "Verwendung: $PROG issue-for [dsa-guid]" [[ "$host" == *.* ]] || die "Bitte den vollstaendigen FQDN angeben." local out="${OUT_DIR:-/root/samba-tls-$host}" log "Erzeuge Bundle fuer $host in $out" (( DRY_RUN )) && return 0 rm -rf "$out" make_cert "$host" "$out" "$guid" check_pair "$out/cert.pem" "$out/key.pem" "$out/ca.pem" "$host" local tgz="/root/samba-tls-$host.tar.gz" tar -czf "$tgz" -C "$out" cert.pem key.pem ca.pem chmod 0600 "$tgz" ok "Bundle: $tgz" cat <:/tmp/ ssh root@ '$PROG sign /tmp/csr.pem $FQDN' Danach cert.pem + ca.pem zurueckkopieren und: $PROG install-files $out/cert.pem $out/key.pem $out/ca.pem EOF } cmd_sign() { need_root; require_ca local csr="$1" cn="${2:-}" [[ -f "$csr" ]] || die "CSR nicht gefunden: $csr" [[ -n "$cn" ]] || cn="$(openssl req -in "$csr" -noout -subject | sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')" [[ -n "$cn" ]] || die "CN konnte nicht ermittelt werden - bitte als 2. Argument angeben." local out="${OUT_DIR:-/root/samba-tls-signed-$cn}" install -d -m 0700 "$out" local conf="$out/openssl.cnf" write_cert_conf "${cn,,}" "$conf" "${DSA_GUID:-}" (( DRY_RUN )) && return 0 [[ -s "$CA_SRL" ]] || openssl rand -hex 16 >"$CA_SRL" openssl x509 -req -in "$csr" -sha256 -days "$CERT_DAYS" \ -CA "$CA_CERT" -CAkey "$CA_KEY" -CAserial "$CA_SRL" \ -extfile "$conf" -extensions v3_sign -out "$out/cert.pem" 2>/dev/null cp -f "$CA_CERT" "$out/ca.pem" chmod 0644 "$out/cert.pem" "$out/ca.pem" ok "Signiert: $out/cert.pem (+ ca.pem)" } cmd_install() { need_root local arg="$1" local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN if [[ -d "$arg" ]]; then cp -f "$arg/cert.pem" "$arg/key.pem" "$arg/ca.pem" "$tmp/" elif [[ -f "$arg" ]]; then tar -xzf "$arg" -C "$tmp" else die "Bundle nicht gefunden: $arg" fi chmod 0600 "$tmp/key.pem" install_bundle "$tmp" } cmd_install_files() { need_root local c="$1" k="$2" a="$3" local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN install -m 0644 "$c" "$tmp/cert.pem" install -m 0600 "$k" "$tmp/key.pem" install -m 0644 "$a" "$tmp/ca.pem" install_bundle "$tmp" } cmd_status() { print_env echo local f for f in "$CA_CERT:CA" "$TLS_CERT:DC-Zertifikat"; do local p="${f%%:*}" n="${f##*:}" if [[ -f "$p" ]]; then printf ' %s (%s)\n' "$n" "$p" openssl x509 -in "$p" -noout -subject -issuer -startdate -enddate 2>/dev/null | sed 's/^/ /' openssl x509 -in "$p" -noout -ext subjectAltName 2>/dev/null | sed -n '2p' | sed 's/^ */ SAN: /' if ! openssl x509 -in "$p" -noout -checkend $((30*86400)) >/dev/null 2>&1; then warn " laeuft in weniger als 30 Tagen ab!" fi echo else warn "$n fehlt: $p" fi done [[ -f "$STATE_FILE" ]] && printf ' letztes Backup: %s\n' "$(cat "$STATE_FILE")" return 0 } cmd_verify() { need_root [[ -f "$TLS_CERT" && -f "$TLS_KEY" && -f "$TLS_CA" ]] || die "Zertifikatsdateien unvollstaendig." check_pair "$TLS_CERT" "$TLS_KEY" "$TLS_CA" "$FQDN" health_check "$FQDN" } cmd_rollback() { need_root local bdir="${1:-}" if [[ -z "$bdir" ]]; then [[ -f "$STATE_FILE" ]] || die "Kein Backup-Zeiger gefunden. Pfad bitte angeben." bdir="$(cat "$STATE_FILE")" fi confirm "Auf $bdir zurueckrollen?" || die "Abgebrochen." restore_backup "$bdir" restart_samba health_check "$FQDN" || warn "Health-Check nach Rollback nicht sauber." } cmd_export_ca() { require_ca local out="${1:-/root/$(printf '%s' "$REALM_LC" | tr '.' '-')-ca.crt}" cp -f "$CA_CERT" "$out"; chmod 0644 "$out" ok "CA-Zertifikat exportiert: $out" cat < [Argumente] Kommandos: status Umgebung und aktuelle Zertifikate anzeigen init-ca Neue CA anlegen (einmalig, auf DC1) renew Zertifikat fuer DIESEN DC neu bauen + installieren issue-for [dsa-guid] Bundle fuer einen anderen DC erzeugen (auf dem CA-DC) install Bundle auf diesem DC installieren request Key + CSR lokal erzeugen (CA-Key bleibt auf DC1) sign [cn] CSR auf dem CA-DC signieren install-files verify Konsistenz- und Health-Check rollback [backup-dir] Letzten Stand zurueckspielen export-ca [datei] CA-Zertifikat zur Client-Verteilung exportieren Optionen: --ca-dir DIR CA-Verzeichnis (Default: $CA_DIR) --days N Laufzeit DC-Zertifikat (Default: $CERT_DAYS) --ca-days N Laufzeit CA (Default: $CA_DAYS) --bits N RSA-Schluessellaenge (Default: $KEY_BITS) --dns a,b,c zusaetzliche DNS-SANs (z.B. Alias-Namen, ldap.firma.de) --ip 10.0.0.1,... zusaetzliche IP-SANs --out DIR Ausgabeverzeichnis fuer issue-for/request/sign --no-restart Samba nicht neu starten --no-rollback kein automatischer Rollback bei fehlgeschlagenem Check --yes keine Rueckfragen --force bestehende CA ueberschreiben --dry-run nur anzeigen, nichts aendern -h, --help diese Hilfe Ablauf bei zwei DCs (empfohlen): DC1: $PROG init-ca $PROG renew $PROG issue-for dc2.${REALM_LC:-example.local} scp /root/samba-tls-dc2.${REALM_LC:-example.local}.tar.gz root@dc2:/root/ DC2: $PROG install /root/samba-tls-dc2.${REALM_LC:-example.local}.tar.gz DC1: $PROG export-ca # CA an alle Clients verteilen Immer nur EINEN DC nach dem anderen anfassen und dazwischen 'samba-tool drs showrepl' auf beiden Seiten pruefen. EOF } # --------------------------------------------------------------------------- # Argumente # --------------------------------------------------------------------------- OUT_DIR="${OUT_DIR:-}" ARGS=() while (( $# )); do case "$1" in --ca-dir) CA_DIR="$2"; shift 2 ;; --days) CERT_DAYS="$2"; shift 2 ;; --ca-days) CA_DAYS="$2"; shift 2 ;; --bits) KEY_BITS="$2"; shift 2 ;; --dns) EXTRA_DNS="$2"; shift 2 ;; --ip) EXTRA_IP="$2"; shift 2 ;; --out) OUT_DIR="$2"; shift 2 ;; --ca-cn) CA_CN="$2"; shift 2 ;; --org) ORG="$2"; shift 2 ;; --no-restart) DO_RESTART=0; shift ;; --no-rollback) DO_ROLLBACK=0; shift ;; --yes|-y) ASSUME_YES=1; shift ;; --force) FORCE=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; --) shift; ARGS+=("$@"); break ;; -*) die "Unbekannte Option: $1" ;; *) ARGS+=("$1"); shift ;; esac done if (( ! DO_RESTART )); then restart_samba() { warn "Restart uebersprungen (--no-restart) - das Zertifikat wird erst nach einem Neustart aktiv." } fi CMD="${ARGS[0]:-status}" detect_env case "$CMD" in status) cmd_status ;; init-ca) cmd_init_ca ;; renew) cmd_init_ca; cmd_renew ;; issue-for) cmd_issue_for "${ARGS[1]:-}" "${ARGS[2]:-}" ;; install) cmd_install "${ARGS[1]:?Bundle angeben}" ;; install-files) cmd_install_files "${ARGS[1]:?cert}" "${ARGS[2]:?key}" "${ARGS[3]:?ca}" ;; request) cmd_request ;; sign) cmd_sign "${ARGS[1]:?CSR angeben}" "${ARGS[2]:-}" ;; verify) cmd_verify ;; rollback) cmd_rollback "${ARGS[1]:-}" ;; export-ca) cmd_export_ca "${ARGS[1]:-}" ;; help) usage ;; *) usage; die "Unbekanntes Kommando: $CMD" ;; esac