Create zmb-ad_tls_manager

This commit is contained in:
Thorsten Spille
2026-08-01 09:09:05 +02:00
committed by GitHub
parent 0021a175ea
commit fae29e928d
+709
View File
@@ -0,0 +1,709 @@
#!/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: "<REALM> 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 <guid>._msdcs.<realm> 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 <<EOF
Realm .......... $REALM
NetBIOS ........ ${NETBIOS:-?}
FQDN ........... $FQDN
private dir .... $PRIVATE_DIR
tls enabled .... ${TLS_ENABLED:-?}
tls certfile ... $TLS_CERT
tls keyfile .... $TLS_KEY
tls cafile ..... $TLS_CA
CA-Verzeichnis . $CA_DIR
EOF
[[ -n "${DSA_GUID:-}" ]] && printf ' DSA-GUID ....... %s\n' "$DSA_GUID"
if [[ "${TLS_ENABLED,,}" == "no" ]]; then
warn "'tls enabled = no' - Samba nutzt die Zertifikate dann nicht."
warn "In der [global]-Sektion setzen: tls enabled = yes"
fi
}
# ---------------------------------------------------------------------------
# OpenSSL-Konfiguration erzeugen
# ---------------------------------------------------------------------------
# $1 = CN/FQDN, $2 = Zielpfad der Config, $3 = optionale DSA-GUID
write_cert_conf() {
local cn="$1" out="$2" guid="${3:-}"
local -a dns=() ips=()
local n
dns+=("$cn")
[[ "$cn" == *.* ]] && dns+=("${cn%%.*}")
dns+=("$REALM_LC")
[[ -n "$guid" ]] && dns+=("${guid}._msdcs.${REALM_LC}")
for n in ${EXTRA_DNS//,/ }; do [[ -n "$n" ]] && dns+=("${n,,}"); done
for n in ${EXTRA_IP//,/ }; do [[ -n "$n" ]] && ips+=("$n"); done
# Duplikate entfernen, Reihenfolge erhalten
local -a udns=(); local -A seen=()
for n in "${dns[@]}"; do [[ -n "${seen[$n]:-}" ]] || { udns+=("$n"); seen[$n]=1; }; done
{
cat <<EOF
[req]
default_md = sha256
prompt = no
distinguished_name = dn
req_extensions = v3_req
[dn]
O = $ORG
CN = $cn
[v3_req]
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectAltName = @alt
[v3_sign]
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth,clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
subjectAltName = @alt
[alt]
EOF
local i=1
for n in "${udns[@]}"; do printf 'DNS.%d = %s\n' "$i" "$n"; ((i++)); done
i=1
for n in "${ips[@]}"; do printf 'IP.%d = %s\n' "$i" "$n"; ((i++)); done
} >"$out"
log "SANs: ${udns[*]} ${ips[*]:-}"
}
write_ca_conf() {
cat >"$1" <<EOF
[req]
default_md = sha256
prompt = no
distinguished_name = dn
x509_extensions = v3_ca
[dn]
O = $ORG
CN = $CA_CN
[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
EOF
}
# ---------------------------------------------------------------------------
# CA
# ---------------------------------------------------------------------------
cmd_init_ca() {
need_root
if [[ -f "$CA_KEY" && -f "$CA_CERT" ]] && (( ! FORCE )); then
log "CA existiert bereits: $CA_CERT"
openssl x509 -in "$CA_CERT" -noout -subject -enddate | sed 's/^/ /'
return 0
fi
(( FORCE )) && [[ -f "$CA_CERT" ]] && warn "--force: bestehende CA wird ersetzt!"
log "Erzeuge CA in $CA_DIR"
run install -d -m 0700 -o root -g root "$CA_DIR"
(( DRY_RUN )) && return 0
local conf; conf="$(mktemp)"; write_ca_conf "$conf"
openssl req -x509 -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \
-days "$CA_DAYS" -keyout "$CA_KEY.new" -out "$CA_CERT.new" -config "$conf"
rm -f "$conf"
mv "$CA_KEY.new" "$CA_KEY"; chmod 0600 "$CA_KEY"
mv "$CA_CERT.new" "$CA_CERT"; chmod 0644 "$CA_CERT"
openssl rand -hex 16 >"$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 >/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<del; i++)); do rm -rf "${old[$i]}"; done
fi
printf '%s' "$bdir"
}
install_bundle() {
local src="$1" # Verzeichnis mit cert.pem, key.pem, ca.pem
check_pair "$src/cert.pem" "$src/key.pem" "$src/ca.pem" "$FQDN"
log "Aktuelle Zertifikate:"
if [[ -f "$TLS_CERT" ]]; then
openssl x509 -in "$TLS_CERT" -noout -subject -enddate 2>/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 <fqdn.des.dc> [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 <<EOF
Uebertragen und einspielen:
scp $tgz root@$host:/root/
ssh root@$host '$PROG install /root/$(basename "$tgz")'
EOF
}
cmd_request() {
need_root; detect_dsa_guid
local out="${OUT_DIR:-/root/samba-tls-request}"
install -d -m 0700 "$out"
local conf="$out/openssl.cnf"
write_cert_conf "$FQDN" "$conf" "${DSA_GUID:-}"
(( DRY_RUN )) && return 0
openssl req -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \
-keyout "$out/key.pem" -out "$out/csr.pem" -config "$conf"
chmod 0600 "$out/key.pem"
ok "CSR: $out/csr.pem (Key bleibt hier: $out/key.pem)"
cat <<EOF
Auf dem CA-DC signieren lassen:
scp $out/csr.pem root@<ca-dc>:/tmp/
ssh root@<ca-dc> '$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 <<EOF
Verteilung an Clients:
Windows : per GPO nach 'Vertrauenswuerdige Stammzertifizierungsstellen'
Debian : cp $out /usr/local/share/ca-certificates/ && update-ca-certificates
RHEL : cp $out /etc/pki/ca-trust/source/anchors/ && update-ca-trust
Nicht vergessen: sssd/realmd, Keycloak, Nextcloud, Zammad, Backup-Tools,
Drucker, NAS - alles was per LDAPS/StartTLS an die DCs geht.
EOF
}
usage() {
cat <<EOF
$PROG $VERSION - CA und TLS-Zertifikate fuer Samba AD DCs neu bauen
Verwendung: $PROG [Optionen] <Kommando> [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 <fqdn> [dsa-guid] Bundle fuer einen anderen DC erzeugen (auf dem CA-DC)
install <bundle.tar.gz|dir> Bundle auf diesem DC installieren
request Key + CSR lokal erzeugen (CA-Key bleibt auf DC1)
sign <csr> [cn] CSR auf dem CA-DC signieren
install-files <cert> <key> <ca>
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