#!/usr/bin/env bash # ===================================================================================== # Samba AD DC container entrypoint. # # Idempotent. Safe to restart. Behaviour is driven entirely by environment variables # (see .env.example / README.md). Decision tree: # # /var/lib/samba/private/sam.ldb exists ? # yes -> start the existing domain (primary or replica, we don't care) # no -> AD_JOIN_SERVER set ? # yes -> join the existing forest as an additional DC # no -> provision a brand new forest # # Then: apply hardening params -> validate smb.conf with testparm (fail fast) -> # self-referencing resolv.conf -> keep DNS records fresh -> exec samba in foreground. # ===================================================================================== set -Eeuo pipefail ts() { date -u +%H:%M:%S; } log() { printf '\033[1;36m[ad-dc %s]\033[0m %s\n' "$(ts)" "$*"; } warn() { printf '\033[1;33m[ad-dc %s] WARN: %s\033[0m\n' "$(ts)" "$*" >&2; } die() { printf '\033[1;31m[ad-dc %s] FATAL: %s\033[0m\n' "$(ts)" "$*" >&2; exit 1; } on_err() { printf '\033[1;31m[ad-dc %s] FATAL: entrypoint failed at line %s (rc=%s)\033[0m\n' "$(ts)" "$2" "$1" >&2; exit "$1"; } trap 'on_err "$?" "$LINENO"' ERR SAMDB=/var/lib/samba/private/sam.ldb SMBCONF=/etc/samba/smb.conf SECRETS_DIR="${AD_SECRETS_DIR:-/run/ad-secrets}" # ------------------------------------------------------------------------------------- # 1. Read + normalise configuration # ------------------------------------------------------------------------------------- : "${AD_REALM:?AD_REALM is required, e.g. AD.EXAMPLE.COM (uppercase)}" AD_REALM="${AD_REALM^^}" AD_REALM="${AD_REALM//[[:space:]]/}" AD_DNS_DOMAIN="${AD_DNS_DOMAIN:-${AD_REALM,,}}" AD_DNS_DOMAIN="${AD_DNS_DOMAIN,,}" AD_HOSTNAME="${AD_HOSTNAME:-dc1}" AD_HOSTNAME="${AD_HOSTNAME,,}" FQDN="${AD_HOSTNAME}.${AD_DNS_DOMAIN}" NETBIOS_DOMAIN="${AD_NETBIOS_DOMAIN:-${AD_REALM%%.*}}" NETBIOS_DOMAIN="${NETBIOS_DOMAIN^^}" NETBIOS_HOST="${AD_HOSTNAME^^}" NETBIOS_HOST="${NETBIOS_HOST:0:15}" NETBIOS_DOMAIN="${NETBIOS_DOMAIN:0:15}" AD_DNS_BACKEND="${AD_DNS_BACKEND:-SAMBA_INTERNAL}" AD_SITE="${AD_SITE:-Default-First-Site-Name}" AD_FUNCTION_LEVEL="${AD_FUNCTION_LEVEL:-2008_R2}" AD_BASE_SCHEMA="${AD_BASE_SCHEMA:-2019}" AD_USE_RFC2307="${AD_USE_RFC2307:-yes}" AD_DNS_FORWARDER="${AD_DNS_FORWARDER:-1.1.1.1 8.8.8.8}" AD_ADMIN_USER="${AD_ADMIN_USER:-administrator}" AD_MANAGE_RESOLV="${AD_MANAGE_RESOLV:-yes}" AD_APPLY_HARDENING="${AD_APPLY_HARDENING:-yes}" AD_LOG_LEVEL="${AD_LOG_LEVEL:-1}" AD_DNS_REFRESH_SECONDS="${AD_DNS_REFRESH_SECONDS:-300}" # Hardening knobs -> smb.conf [global] AD_SMB_SIGNING="${AD_SMB_SIGNING:-required}" # required | yes | no AD_SMB_ENCRYPT="${AD_SMB_ENCRYPT:-desired}" # required | desired | off | on AD_NTLM_AUTH="${AD_NTLM_AUTH:-ntlmv2-only}" # disabled | ntlmv2-only | ms-capable AD_RESTRICT_ANONYMOUS="${AD_RESTRICT_ANONYMOUS:-2}" # 2 = reject anonymous (no guest fallback) AD_SMB_MIN_PROTOCOL="${AD_SMB_MIN_PROTOCOL:-SMB2_02}" # SMB1 stays off, Win11 removed it anyway # ------------------------------------------------------------------------------------- # 2. Validation # ------------------------------------------------------------------------------------- [[ "$AD_REALM" =~ ^[A-Z0-9][A-Z0-9.-]*[A-Z0-9]\.[A-Z][A-Z]+$ ]] \ || die "AD_REALM must be an uppercase DNS realm without underscores (got '$AD_REALM')" [[ "$AD_DNS_DOMAIN" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] \ || die "AD_DNS_DOMAIN invalid (got '$AD_DNS_DOMAIN')" [[ "$AD_HOSTNAME" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] \ || die "AD_HOSTNAME must be a short hostname, a-z0-9- (got '$AD_HOSTNAME')" [ "${#NETBIOS_DOMAIN}" -ge 1 ] || die "NETBIOS_DOMAIN derived to empty string" case "$AD_DNS_BACKEND" in SAMBA_INTERNAL|BIND9_DLZ|NONE) ;; *) die "AD_DNS_BACKEND must be SAMBA_INTERNAL | BIND9_DLZ | NONE";; esac # ------------------------------------------------------------------------------------- # 3. Credentials: env var, or a 0600 file under the secrets mount. # ------------------------------------------------------------------------------------- read_secret() { # $1=env value $2=file path if [ -n "${1:-}" ]; then printf '%s' "$1"; return 0; fi if [ -r "$2" ]; then tr -d '\r\n' < "$2"; return 0; fi return 1 } pw_quality() { # $1 = password on stdin (kept out of argv) printf '%s' "$1" | python3 -c 'import samba,sys; sys.exit(0 if samba.check_password_quality(sys.stdin.read()) else 1)' 2>/dev/null } # Resolved lazily: a restore run must NOT need the admin password, so only the code paths # that actually create/join a directory call require_admin_pw. ADMIN_PW="$(read_secret "${AD_ADMIN_PASSWORD:-}" "$SECRETS_DIR/ad_admin.password" || true)" require_admin_pw() { [ -n "${ADMIN_PW:-}" ] || die "no administrator password. Set AD_ADMIN_PASSWORD or mount a file at ${SECRETS_DIR}/ad_admin.password (chmod 600)" pw_quality "$ADMIN_PW" || die "administrator password fails AD quality check (need >=12 chars and 3 of 4 classes: upper/lower/digit/symbol)" } # ------------------------------------------------------------------------------------- # 4. Own IP address (this is what lands in the DNS A/AAAA records -> must be reachable) # ------------------------------------------------------------------------------------- detect_ip() { ip -4 route get 1.1.1.1 2>/dev/null | sed -n 's/.* src \([^ ]*\).*/\1/p' | head -1 } if [ -z "${AD_IP:-}" ]; then AD_IP="$(detect_ip || true)" [ -n "$AD_IP" ] || die "cannot determine my IPv4 address - set AD_IP explicitly" n4=$(ip -4 -o addr show scope global 2>/dev/null | grep -c . || true) [ "${n4:-1}" -gt 1 ] && warn "multiple global IPv4 addresses found; auto-picked $AD_IP. Set AD_IP to be deterministic." fi if [ "${AD_IP6:-}" = "auto" ]; then AD_IP6="$(ip -6 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | grep -v '^[fd]' | head -1 || true)" fi log "identity: hostname=$AD_HOSTNAME fqdn=$FQDN realm=$AD_REALM netbios=$NETBIOS_DOMAIN ip=${AD_IP}${AD_IP6:+/${AD_IP6}}" # AD_IP is what we publish in the A record and what every dynamic update is pinned to, so it # must actually be bound here. When it isn't, the DC cannot reach its own advertised address: # updates fail with "not reachable by the transport" and the DC comes up healthy while serving # NO SRV records - every client then fails to find a domain controller. Observed in a DR # restore where the container landed on .2 while AD_IP said .30. Fail loudly instead. addr_present() { ip -o addr show 2>/dev/null | grep -qF " $1/"; } # matches 'inet /' if ! addr_present "$AD_IP"; then have="$(ip -4 -o addr show scope global 2>/dev/null | awk '{printf "%s ", $4}')" if [ "${AD_IP_STRICT:-yes}" = "yes" ]; then die "AD_IP=${AD_IP} is not assigned to any interface here (global addresses: ${have:-none}). Clients follow AD_IP, so it has to be this machine's address. Correct AD_IP in .env, or set AD_IP_STRICT=no if something else assigns it later." else warn "AD_IP=${AD_IP} is not bound here (have: ${have:-none}); continuing because AD_IP_STRICT=no" fi fi if [ -n "${AD_IP6:-}" ] && ! addr_present "$AD_IP6"; then [ "${AD_IP_STRICT:-yes}" = "yes" ] \ && die "AD_IP6=${AD_IP6} is not assigned to any interface here (AD_IP_STRICT=no to allow)" \ || warn "AD_IP6=${AD_IP6} is not bound here; continuing because AD_IP_STRICT=no" fi # /etc/hosts (append in place: the file may be a docker bind-mount, never unlink it) if ! grep -q "[[:space:]]${FQDN}\([[:space:]]\|$\)" /etc/hosts 2>/dev/null; then printf '%s\t%s %s\n' "$AD_IP" "$FQDN" "$AD_HOSTNAME" >> /etc/hosts fi if [ -n "${AD_EXTRA_HOSTS:-}" ]; then # e.g. AD_EXTRA_HOSTS="192.0.2.5 dc1.ad.example.com dc1" printf '%s\n' "${AD_EXTRA_HOSTS//;//}" >> /etc/hosts fi # ------------------------------------------------------------------------------------- # 5. /etc/krb5.conf (explicit KDCs; we do not trust DNS ordering during boot) # ------------------------------------------------------------------------------------- KDC_HOSTS="$FQDN" if [ -n "${AD_JOIN_SERVER:-}" ]; then KDC_HOSTS="$AD_JOIN_SERVER $FQDN" fi { echo "# rendered by ad-entrypoint at $(date -u '+%FT%TZ') - do not edit inside the container" echo "[libdefaults]" echo " default_realm = ${AD_REALM}" echo " dns_lookup_realm = false" echo " dns_lookup_kdc = false" echo " rdns = false" echo " udp_preference_limit = 1465" # Win11 + many group SIDs => PAC > 1465 bytes echo " default_ccache_name = FILE:/tmp/krb5cc_%{uid}" echo " ticket_lifetime = 10h" echo " renew_lifetime = 7d" echo " forwardable = true" echo echo "[realms]" echo " ${AD_REALM} = {" for h in $KDC_HOSTS; do echo " kdc = ${h}" echo " admin_server = ${h}" echo " kpasswd_server = ${h}" done echo " default_domain = ${AD_DNS_DOMAIN}" echo " }" echo echo "[domain_realm]" echo " .${AD_DNS_DOMAIN} = ${AD_REALM}" echo " ${AD_DNS_DOMAIN} = ${AD_REALM}" } > /etc/krb5.conf # ------------------------------------------------------------------------------------- # 6. smb.conf helpers # ------------------------------------------------------------------------------------- # Upsert "key = value" inside [global] (replaces in place, otherwise inserts). smb_set() { # $1=key $2=value local key="$1" val="$2" tmp tmp="$(mktemp)" awk -v k="$key" -v v="$val" ' BEGIN { insec = 0; done = 0 } /^[[:space:]]*#/ { print; next } /^\[/ { if (insec && !done) { print " " k " = " v; done = 1 } insec = ($0 ~ /^\[global\]/) print; next } { if (insec && !done && $0 ~ "^[[:space:]]*" k "[[:space:]]*=") { print " " k " = " v; done = 1; next } print } END { if (insec && !done) print " " k " = " v } ' "$SMBCONF" > "$tmp" cat "$tmp" > "$SMBCONF"; rm -f "$tmp" } smb_get() { # $1=key -> value or empty awk -v k="$1" ' /^[[:space:]]*#/ { next } /^\[/ { insec = ($0 ~ /^\[global\]/); next } insec && $0 ~ "^[[:space:]]*" k "[[:space:]]*=" { sub(/^[[:space:]]*[^=]*=[[:space:]]*/, ""); print; exit }' "$SMBCONF" } smb_unset() { # $1=key (regex-escaped-ish) local key="$1" tmp tmp="$(mktemp)" awk -v k="$key" ' !($0 ~ "^[[:space:]]*" k "[[:space:]]*=") { print }' "$SMBCONF" > "$tmp" cat "$tmp" > "$SMBCONF"; rm -f "$tmp" } # ------------------------------------------------------------------------------------- # 7. Optional: restore an online backup into a FRESH data volume, then stop. # # IMPORTANT semantics of `samba-tool domain backup restore`: it does a *join*, i.e. it # restores the database and then ADDS this host as a new domain controller. Restoring under # the same server name that the backup was taken from fails with "Entry CN=,OU=Domain # Controllers... already exists" - so this must run with a NEW AD_HOSTNAME (and a free IP). # The archive carries its own smb.conf at ./etc/smb.conf; we promote it to /etc/samba. # ------------------------------------------------------------------------------------- if [ -n "${AD_RESTORE_FROM:-}" ]; then [ "${AD_RESTORE_CONFIRM:-no}" = "yes" ] || die "AD_RESTORE_FROM set but AD_RESTORE_CONFIRM!=yes (this writes directory data)" [ -f "$AD_RESTORE_FROM" ] || die "backup file not found: $AD_RESTORE_FROM" [ ! -f "$SAMDB" ] || die "refusing to restore over an existing database at $SAMDB - use a fresh data volume" # Read the archive manifest once, parse with bash regex - no `head` in a pipeline, which # under `set -o pipefail` turns a normal SIGPIPE into a fatal error. backup_txt="$(tar -xOf "$AD_RESTORE_FROM" ./backup.txt 2>/dev/null || true)" src_dc=""; src_netbios="" [[ "$backup_txt" =~ using\ DC\ \'([^\']+)\' ]] && src_dc="${BASH_REMATCH[1]}" [[ "$backup_txt" =~ Backup\ for\ domain\ ([A-Z0-9-]+)\ \(NetBIOS\) ]] && src_netbios="${BASH_REMATCH[1]}" log "archive origin: dc=${src_dc:-unknown} netbios=${src_netbios:-unknown}" if [ -n "$src_dc" ] && [ "${src_dc%%.*}" = "$AD_HOSTNAME" ]; then die "restore under the same hostname as the backup source (${src_dc}) is not supported by Samba's restore tool. Set AD_HOSTNAME to a NEW name (and a free AD_IP) and retry." fi log "restoring ${AD_RESTORE_FROM} as NEW domain controller ${FQDN} (${AD_IP})" samba-tool domain backup restore \ --backup-file="$AD_RESTORE_FROM" \ --targetdir=/var/lib/samba \ --newservername="$NETBIOS_HOST" \ --host-ip="$AD_IP" \ ${AD_IP6:+--host-ip6="$AD_IP6"} \ --site="$AD_SITE" if [ -f /var/lib/samba/etc/smb.conf ]; then mkdir -p /etc/samba cp /var/lib/samba/etc/smb.conf /etc/samba/smb.conf rm -rf /var/lib/samba/etc log "promoted the archive's smb.conf to /etc/samba/smb.conf" fi printf 'realm=%s\nfqdn=%s\nsource=%s\nrestored_from_dc=%s\nts=%s\n' \ "$AD_REALM" "$FQDN" "$AD_RESTORE_FROM" "${src_dc:-unknown}" "$(date -u '+%FT%TZ')" > /var/lib/samba/.ad-provisioned log "restore complete. Unset AD_RESTORE_FROM and 'docker compose up -d' to serve it." exit 0 fi # A completed provision/join leaves a marker. A provision that dies half way (e.g. SYSVOL # NT-ACL write refused by the underlying filesystem/capability set) otherwise looks like a # healthy database to the next start, which then serves a broken domain. Fail loudly here. MARKER=/var/lib/samba/.ad-provisioned if [ -f "$SAMDB" ] && [ -f "$MARKER" ]; then log "existing AD database found -> starting domain '${AD_REALM}'" elif [ -f "$SAMDB" ]; then if [ "${AD_FORCE_START:-no}" = "yes" ]; then warn "sam.ldb present but $(basename "$MARKER") missing; AD_FORCE_START=yes -> continuing" else die "sam.ldb exists but $(basename "$MARKER") is missing: a previous provision/join was interrupted. Inspect with 'samba-tool dbcheck', or remove $SAMDB and restart, or set AD_FORCE_START=yes" fi else if [ -n "${AD_JOIN_SERVER:-}" ]; then log "joining existing domain ${AD_DNS_DOMAIN} via ${AD_JOIN_SERVER} as ${AD_HOSTNAME}" JOIN_PW="$(read_secret "${AD_JOIN_PASSWORD:-}" "$SECRETS_DIR/join.password" || true)" [ -n "$JOIN_PW" ] || die "AD_JOIN_PASSWORD (or ${SECRETS_DIR}/join.password) is required to join" samba-tool domain join "${AD_DNS_DOMAIN}" DC \ --realm="$AD_REALM" \ --server-role=dc \ --host-name="$NETBIOS_HOST" \ --host-ip="$AD_IP" \ ${AD_IP6:+--host-ip6="$AD_IP6"} \ --target-server="${AD_JOIN_SERVER}" \ -U "${AD_REALM}\\${AD_JOIN_USER:-${AD_ADMIN_USER}}%${JOIN_PW}" \ ${AD_USE_RFC2307:+--use-rfc2307} else # IMPORTANT: never pre-create smb.conf before provisioning. `samba-tool domain # provision` refuses to configure a DC over a config that has no [netlogon] # share, so the realm/forwarder are passed via --option and Samba generates the # authoritative config (incl. the NETLOGON/SYSVOL shares) itself. if [ -f "$SMBCONF" ]; then backup="${SMBCONF}.discarded-$(date -u +%Y%m%dT%H%M%SZ)" warn "moving pre-existing smb.conf out of the way -> $backup" mv "$SMBCONF" "$backup" fi require_admin_pw log "provisioning NEW forest ${AD_REALM} (this takes 30-90s, heavy CPU/IO)" samba-tool domain provision \ --option="realm=${AD_REALM}" \ --option="dns forwarder = ${AD_DNS_FORWARDER}" \ --server-role=dc \ --domain="$NETBIOS_DOMAIN" \ --host-name="$NETBIOS_HOST" \ --host-ip="$AD_IP" \ ${AD_IP6:+--host-ip6="$AD_IP6"} \ --dns-backend="$AD_DNS_BACKEND" \ --function-level="$AD_FUNCTION_LEVEL" \ --base-schema="$AD_BASE_SCHEMA" \ --site="$AD_SITE" \ ${AD_USE_RFC2307:+--use-rfc2307} \ --adminpass="$ADMIN_PW" log "provisioning complete" fi [ -f "$SAMDB" ] || die "directory database still missing after provision/join: $SAMDB" printf 'realm=%s\nfqdn=%s\njoined_via=%s\nts=%s\n' \ "$AD_REALM" "$FQDN" "${AD_JOIN_SERVER:-none}" "$(date -u '+%FT%TZ')" > "$MARKER" fi # ------------------------------------------------------------------------------------- # 8. Hardening (applied on every start, so .env changes take effect on restart) # ------------------------------------------------------------------------------------- if [ "$AD_APPLY_HARDENING" = "yes" ]; then smb_set "dns forwarder" "$AD_DNS_FORWARDER" smb_set "server signing" "$AD_SMB_SIGNING" smb_set "smb encrypt" "$AD_SMB_ENCRYPT" smb_set "ntlm auth" "$AD_NTLM_AUTH" smb_set "restrict anonymous" "$AD_RESTRICT_ANONYMOUS" smb_set "map to guest" "never" smb_set "server min protocol" "$AD_SMB_MIN_PROTOCOL" smb_set "log level" "$AD_LOG_LEVEL" smb_set "max log size" "10000" log "hardening: signing=$AD_SMB_SIGNING encrypt=$AD_SMB_ENCRYPT ntlm=$AD_NTLM_AUTH anon=$AD_RESTRICT_ANONYMOUS min=$AD_SMB_MIN_PROTOCOL" else warn "AD_APPLY_HARDENING=no -> smb.conf is used exactly as you wrote it" fi # LDAPS: only if certs are present. Samba's own auto-generated cert is untrusted by # Windows, so bring your own CA (scripts/tls.sh) for anything beyond smoke tests. # # Samba refuses to load a private key that is not root-owned / not group-other readable # (CVE-2013-4476). Bind-mounted host certs are owned by the host uid, so we copy them into # our own private dir as root and set the mode explicitly. The secrets mount stays read-only. if [ -n "${AD_TLS_CERT:-}" ] && [ -r "${AD_TLS_CERT:-/nonexistent}" ]; then [ -r "${AD_TLS_KEY:-/nonexistent}" ] || die "AD_TLS_CERT set but AD_TLS_KEY is unreadable" staged="/var/lib/samba/private/tls" mkdir -p "$staged" cp -f "$AD_TLS_CERT" "$staged/ad_server.pem" cp -f "$AD_TLS_KEY" "$staged/ad_server.key" chmod 0644 "$staged/ad_server.pem" chmod 0600 "$staged/ad_server.key" chown root:root "$staged/ad_server.pem" "$staged/ad_server.key" 2>/dev/null || true conf_cert="$staged/ad_server.pem" conf_key="$staged/ad_server.key" if [ -n "${AD_TLS_CA:-}" ] && [ -r "${AD_TLS_CA:-/nonexistent}" ]; then cp -f "$AD_TLS_CA" "$staged/ad_ca.pem" chmod 0644 "$staged/ad_ca.pem" smb_set "tls cafile" "$staged/ad_ca.pem" fi # sanity: key must match cert, otherwise samba fails late and cryptically c_mod="$(openssl x509 -noout -modulus -in "$conf_cert" 2>/dev/null | openssl md5 2>/dev/null)" k_mod="$(openssl rsa -noout -modulus -in "$conf_key" 2>/dev/null | openssl md5 2>/dev/null)" [ -n "$c_mod" ] && [ "$c_mod" = "$k_mod" ] || die "AD_TLS_CERT and AD_TLS_KEY do not match (modulus mismatch)" san="$(openssl x509 -noout -ext subjectAltName -in "$conf_cert" 2>/dev/null | tr -d ' \n')" printf '%s' "$san" | grep -qi "$FQDN" || warn "leaf cert SAN does not include $FQDN - Windows will reject LDAPS: ${san:-none}" smb_set "tls enabled" "yes" smb_set "tls certfile" "$conf_cert" smb_set "tls keyfile" "$conf_key" log "LDAPS enabled (staged $conf_cert / $conf_key, mode 0600, SAN: ${san:-none})" else log "LDAPS: no AD_TLS_CERT mounted (Samba will self-sign an untrusted cert on 636)" fi # ------------------------------------------------------------------------------------- # 9. Validate the rendered config. Fail loudly instead of half-working. # ------------------------------------------------------------------------------------- tp_out="$(testparm -s "$SMBCONF" 2>&1 || true)" if printf '%s' "$tp_out" | grep -qiE 'unknown parameter encountered|cannot parse|invalid parameter'; then printf '%s\n' "$tp_out" | grep -iE 'unknown parameter encountered|cannot parse|invalid parameter' | sort -u >&2 die "smb.conf contains rejected parameters (see above) - refusing to start" fi log "smb.conf validated by testparm" # ------------------------------------------------------------------------------------- # 10. Self-referencing resolver (classic AD DC requirement). # In host network mode this file lives in the container only; the host is untouched. # ------------------------------------------------------------------------------------- if [ "$AD_MANAGE_RESOLV" = "yes" ]; then printf '# rendered by ad-entrypoint: the DC resolves its own zone from itself\nsearch %s\nnameserver 127.0.0.1\noptions timeout:2 attempts:2\n' \ "$AD_DNS_DOMAIN" > /etc/resolv.conf log "/etc/resolv.conf -> nameserver 127.0.0.1 search ${AD_DNS_DOMAIN} (upstream via 'dns forwarder = ${AD_DNS_FORWARDER}')" fi # Pin the daemon's OWN updater too (not just our loop above): every `dns update command` # invocation from the samba daemon goes through the wrapper, which reads the advertised # addresses from /run/ad-dc-addresses. printf '%s\n%s\n' "$AD_IP" "${AD_IP6:-}" | sed '/^$/d' > /run/ad-dc-addresses smb_set "dns update command" "/usr/local/bin/ad-dnsupdate" log "DNS registration pinned to: $(tr '\n' ' ' < /run/ad-dc-addresses)" # ------------------------------------------------------------------------------------- # 11. Keep DNS records aligned with the current IP (laptop/DHCP or after failover) # # We pin registration to AD_IP on purpose. A Docker host is multi-homed (docker0 and every # user-defined bridge, plus stale DHCP addresses), and Samba happily registers ALL of them # into the DC's A record - clients then sometimes connect to 172.17.0.1 and fail in ways # that look like Kerberos bugs. --current-ip keeps exactly one address published. # ------------------------------------------------------------------------------------- ( trap 'exit 0' TERM INT sleep 25 while true; do if samba_dnsupdate --use-samba-tool \ --current-ip="$AD_IP" \ ${AD_IP6:+--current-ip="$AD_IP6"} \ --verbose >/tmp/dnsupdate.out 2>&1; then : else warn "samba_dnsupdate failed (see /tmp/dnsupdate.out)" fi # Self-heal: stale A records left behind by an earlier (unpinned) updater are not # removed by dnsupdate, because they are no longer in its update cache. Delete any # address on our own FQDN that we are not advertising. Uses the DC's own machine # account (-P) so no admin password needs to exist inside the container. escaped="$(printf '%s' "$AD_IP" | sed 's/\./\\./g')" extra="$(dig +short @127.0.0.1 "$FQDN" A 2>/dev/null | grep -E '^[0-9.]+$' | grep -vx "$escaped" || true)" for bad in $extra; do log "removing stale A record ${FQDN} -> ${bad}" samba-tool dns delete 127.0.0.1 "$AD_DNS_DOMAIN" "$FQDN" A "$bad" -P \ >/dev/null 2>&1 \ || warn "could not delete stale A record ${FQDN} -> ${bad}; run: samba-tool dns delete 127.0.0.1 ${AD_DNS_DOMAIN} ${FQDN} A ${bad} -P" done sleep "$AD_DNS_REFRESH_SECONDS" done ) & DNS_UPDATER_PID=$! trap 'kill "$DNS_UPDATER_PID" 2>/dev/null || true' EXIT # ------------------------------------------------------------------------------------- # 12. Foreground, exactly like the Debian systemd unit, so SIGTERM shuts down cleanly. # ------------------------------------------------------------------------------------- log "starting ${FQDN} for realm ${AD_REALM}" exec samba --foreground --no-process-group --option="log level = ${AD_LOG_LEVEL}" -s "$SMBCONF"