From f23aa64155722115b91ea8c3566053651acc9fcc Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Wed, 24 Jun 2026 10:33:52 +0000 Subject: [PATCH] fix: publish host IP in AD DNS --- .env.example | 3 ++ README.md | 17 +++++++ app/init.sh | 53 ++++++++++++++++++++- redeploy | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup | 70 ++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 2d9dbe6..537fb80 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ JOIN_PASSWORD=ReplaceWithLongRandomPassword DOMAIN_USERS_SID=S-1-5-21-1111111111-2222222222-3333333333-513 DOMAIN_ADMINS_SID=S-1-5-21-1111111111-2222222222-3333333333-512 FSLOGIX_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513 +# AD_DNS_IP=192.0.2.10 +# AD_DNS_NAME=adsambafsrv.example.com +# AD_DNS_IP_AUTO=1 # SAMBA_HOSTNAME=adsambafsrv # NETBIOS_NAME=ADSAMBAFSRV # LDAP_URI=ldaps://example.com diff --git a/README.md b/README.md index 35a71c7..7274a70 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This repository provides a production-oriented Samba file server container that - inactive/deleted groups: `/data/groups/archive/` - Samba machine trust/key material is persisted in `/var/lib/samba` to survive container recreation. - Container hostname is fixed (`SAMBA_HOSTNAME`) to keep AD computer identity stable. +- In bridge-mode Docker networking, startup can publish the host LAN IP in AD DNS with `AD_DNS_IP`/`AD_DNS_NAME` instead of the container bridge IP. - NetBIOS name defaults to `ADSAMBAFSRV` and is clamped to 15 characters (`NETBIOS_NAME` override supported). - Setup prompts for well-known authorization groups by SID (`DOMAIN_USERS_SID`, `DOMAIN_ADMINS_SID`) to avoid localized group names. - `FSLOGIX_GROUP_SID` controls who can access the default FSLogix share (defaults to `DOMAIN_USERS_SID`). @@ -71,6 +72,9 @@ CREATE TABLE shares ( - Container must resolve AD DNS records (especially SRV records for domain controllers). - `DOMAIN` should resolve from inside the container. - Preferred setup: Docker host uses AD-integrated DNS or forwards to AD DNS. +- If Docker bridge networking is used, set `AD_DNS_IP` to the Docker host LAN IP that clients should use for SMB, not the container `172.x` address. +- When `AD_DNS_IP` is set, domain join uses `--no-dns-updates`; each container startup unregisters/re-registers `AD_DNS_NAME -> AD_DNS_IP` in AD DNS using the machine account. +- `./setup` and `./redeploy` auto-refresh `AD_DNS_IP` from the host route when `AD_DNS_IP_AUTO=1`. ## Time Sync Requirements @@ -104,6 +108,8 @@ Kerberos requires close time alignment. - `REALM` - `WORKGROUP` - `DOMAIN` + - `AD_DNS_IP` (host LAN IP to publish in AD DNS) + - optional `AD_DNS_NAME` (defaults to `SAMBA_HOSTNAME.DOMAIN`) - initial admin credentials (used once for provisioning) - `DOMAIN_USERS_SID` - `DOMAIN_ADMINS_SID` @@ -124,6 +130,7 @@ Kerberos requires close time alignment. - uses a valid AD `sAMAccountName` (max 20 chars); default effective value is `FileShare_ServiceAcc` - always sets a long random password - writes only service-account credentials to `.env` (initial admin credentials are not stored) + - writes `AD_DNS_IP` so container restarts keep AD DNS pointed at the host LAN IP 4. The setup script then starts the service with: @@ -239,6 +246,16 @@ docker compose exec samba sh -lc 'tail -n 200 /var/log/backup.log' docker compose exec samba testparm -s | grep -i 'netbios name' ``` +### AD DNS points at 172.x + +- This means Samba registered the container bridge IP. +- Ensure `.env` contains `AD_DNS_IP=` and `AD_DNS_NAME=`. +- Recreate/restart the container so startup re-registers AD DNS: + + ```bash + docker compose up -d --force-recreate samba + ``` + ### Winbind user/group resolution fails - Check trust: diff --git a/app/init.sh b/app/init.sh index c005bfe..643bae0 100755 --- a/app/init.sh +++ b/app/init.sh @@ -73,6 +73,30 @@ derive_backup_start_hour() { printf '2\n' } +derive_ad_dns_name() { + local raw_name="${AD_DNS_NAME:-}" + local host_name="${SAMBA_HOSTNAME:-}" + + if [[ -z "$host_name" ]]; then + host_name="$(hostname -s 2>/dev/null || true)" + fi + if [[ -z "$host_name" ]]; then + host_name="${NETBIOS_NAME,,}" + fi + + if [[ -n "$raw_name" ]]; then + if [[ "$raw_name" == *.* ]]; then + AD_DNS_NAME="$raw_name" + else + AD_DNS_NAME="${raw_name}.${DOMAIN}" + fi + else + AD_DNS_NAME="${host_name}.${DOMAIN}" + fi + + export AD_DNS_NAME="${AD_DNS_NAME,,}" +} + resolve_sid_to_group() { local sid="$1" local resolved_name="" @@ -164,6 +188,10 @@ write_runtime_env_file() { printf 'export WORKGROUP=%q\n' "$WORKGROUP" printf 'export DOMAIN=%q\n' "$DOMAIN" printf 'export NETBIOS_NAME=%q\n' "$NETBIOS_NAME" + printf 'export AD_DNS_NAME=%q\n' "$AD_DNS_NAME" + if [[ -n "${AD_DNS_IP:-}" ]]; then + printf 'export AD_DNS_IP=%q\n' "$AD_DNS_IP" + fi printf 'export DOMAIN_USERS_SID=%q\n' "$DOMAIN_USERS_SID" printf 'export DOMAIN_ADMINS_SID=%q\n' "$DOMAIN_ADMINS_SID" printf 'export FSLOGIX_GROUP_SID=%q\n' "$FSLOGIX_GROUP_SID" @@ -211,10 +239,29 @@ join_domain_if_needed() { require_env JOIN_USER require_env JOIN_PASSWORD + local join_options=() + if [[ -n "${AD_DNS_IP:-}" ]]; then + join_options=(--no-dns-updates "dnshostname=${AD_DNS_NAME}") + fi + log "Joining AD domain ${REALM}" - if ! printf '%s\n' "$JOIN_PASSWORD" | net ads join -U "$JOIN_USER" -S "$DOMAIN"; then + if ! printf '%s\n' "$JOIN_PASSWORD" | net ads join "${join_options[@]}" -U "$JOIN_USER" -S "$DOMAIN"; then log 'Join using explicit server failed, retrying automatic DC discovery.' - printf '%s\n' "$JOIN_PASSWORD" | net ads join -U "$JOIN_USER" + printf '%s\n' "$JOIN_PASSWORD" | net ads join "${join_options[@]}" -U "$JOIN_USER" + fi +} + +register_ad_dns_record() { + if [[ -z "${AD_DNS_IP:-}" ]]; then + log 'AD_DNS_IP is unset; skipping explicit AD DNS registration.' + return + fi + + log "Registering AD DNS ${AD_DNS_NAME} -> ${AD_DNS_IP}" + net ads dns unregister -P "$AD_DNS_NAME" >/dev/null 2>&1 || true + if ! net ads dns register -P "$AD_DNS_NAME" "$AD_DNS_IP" >/dev/null; then + printf '[init] ERROR: failed to register AD DNS %s -> %s\n' "$AD_DNS_NAME" "$AD_DNS_IP" >&2 + return 1 fi } @@ -273,6 +320,7 @@ touch /var/log/reconcile.log /var/log/backup.log append_winbind_to_nss require_vfs_modules derive_netbios_name +derive_ad_dns_name render_krb5_conf render_smb_conf join_domain_if_needed @@ -283,6 +331,7 @@ winbindd -F --no-process-group & wait_for_winbind +register_ad_dns_record resolve_share_groups_from_sids render_smb_conf write_runtime_env_file diff --git a/redeploy b/redeploy index c22ded8..375a0b3 100755 --- a/redeploy +++ b/redeploy @@ -5,11 +5,140 @@ log() { printf '[redeploy] %s\n' "$*" } +read_env_value() { + local key="$1" + local line="" + + if [[ ! -f .env ]]; then + return 1 + fi + + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == "${key}="* ]]; then + printf '%s\n' "${line#*=}" + return 0 + fi + done < .env + + return 1 +} + +upsert_env_value() { + local key="$1" + local value="$2" + local temp_file="" + local line="" + local found=0 + + temp_file="$(mktemp)" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == "${key}="* ]]; then + printf '%s=%s\n' "$key" "$value" >> "$temp_file" + found=1 + else + printf '%s\n' "$line" >> "$temp_file" + fi + done < .env + + if [[ "$found" -eq 0 ]]; then + printf '%s=%s\n' "$key" "$value" >> "$temp_file" + fi + + chmod --reference=.env "$temp_file" 2>/dev/null || chmod 600 "$temp_file" + mv "$temp_file" .env +} + +parse_route_source_ip() { + local route="$1" + local previous="" + local token + + for token in $route; do + if [[ "$previous" == "src" ]]; then + printf '%s\n' "$token" + return 0 + fi + previous="$token" + done + + return 1 +} + +detect_ad_dns_ip() { + local lookup_name="$1" + local target_ip="" + local route="" + + if ! command -v ip >/dev/null 2>&1; then + return 1 + fi + + if [[ -n "$lookup_name" ]] && command -v getent >/dev/null 2>&1; then + while read -r target_ip _; do + if [[ ! "$target_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + continue + fi + route="$(ip -o -4 route get "$target_ip" 2>/dev/null || true)" + parse_route_source_ip "$route" && return 0 + done < <(getent ahostsv4 "$lookup_name" 2>/dev/null) + fi + + route="$(ip -o -4 route get 1.1.1.1 2>/dev/null || true)" + parse_route_source_ip "$route" +} + +refresh_ad_dns_env() { + local auto="" + local domain="" + local detected_ip="" + local samba_hostname="" + local netbios_name="" + local host_part="" + + if [[ ! -f .env ]]; then + log '.env missing; skipping AD DNS IP refresh' + return + fi + + auto="$(read_env_value AD_DNS_IP_AUTO || true)" + case "${auto,,}" in + 0|false|no) + log 'AD_DNS_IP_AUTO disabled; keeping configured AD_DNS_IP' + return + ;; + esac + + domain="$(read_env_value DOMAIN || true)" + if [[ -z "$domain" ]]; then + log 'DOMAIN missing in .env; skipping AD DNS IP refresh' + return + fi + + detected_ip="$(detect_ad_dns_ip "$domain" || true)" + if [[ -z "$detected_ip" ]]; then + log 'Unable to detect host LAN IP; keeping configured AD_DNS_IP' + return + fi + + upsert_env_value AD_DNS_IP "$detected_ip" + if ! read_env_value AD_DNS_NAME >/dev/null; then + samba_hostname="$(read_env_value SAMBA_HOSTNAME || true)" + netbios_name="$(read_env_value NETBIOS_NAME || true)" + host_part="${samba_hostname:-${netbios_name,,}}" + host_part="${host_part:-adsambafsrv}" + upsert_env_value AD_DNS_NAME "${host_part}.${domain}" + fi + upsert_env_value AD_DNS_IP_AUTO "1" + log "Updated AD_DNS_IP=${detected_ip}" +} + if [[ ! -d .git ]]; then printf '[redeploy] ERROR: run this script from the repository root.\n' >&2 exit 1 fi +refresh_ad_dns_env + OLD_COMPOSE_FILE="$(mktemp)" cleanup() { diff --git a/setup b/setup index b7ef42a..1976393 100755 --- a/setup +++ b/setup @@ -57,6 +57,45 @@ sanitize_sam_account_name() { printf '%s' "${cleaned_name:0:20}" } +parse_route_source_ip() { + local route="$1" + local previous="" + local token + + for token in $route; do + if [[ "$previous" == "src" ]]; then + printf '%s\n' "$token" + return 0 + fi + previous="$token" + done + + return 1 +} + +detect_ad_dns_ip() { + local lookup_name="$1" + local target_ip="" + local route="" + + if ! command -v ip >/dev/null 2>&1; then + return 1 + fi + + if [[ -n "$lookup_name" ]] && command -v getent >/dev/null 2>&1; then + while read -r target_ip _; do + if [[ ! "$target_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + continue + fi + route="$(ip -o -4 route get "$target_ip" 2>/dev/null || true)" + parse_route_source_ip "$route" && return 0 + done < <(getent ahostsv4 "$lookup_name" 2>/dev/null) + fi + + route="$(ip -o -4 route get 1.1.1.1 2>/dev/null || true)" + parse_route_source_ip "$route" +} + prompt_value() { local var_name="$1" local prompt_text="$2" @@ -92,11 +131,17 @@ write_env_file() { local backup_retention_yearly="1" local samba_hostname="adsambafsrv" local netbios_name="ADSAMBAFSRV" + local ad_dns_ip="" + local ad_dns_name="" + local ad_dns_ip_auto="0" local service_password="" local service_account_sam="" local fslogix_group_prompt="" local samba_hostname_input="" local netbios_name_input="" + local ad_dns_ip_input="" + local ad_dns_name_input="" + local detected_ad_dns_ip="" local sanitized_netbios_name="" prompt_value realm "REALM (e.g. EXAMPLE.COM)" @@ -128,6 +173,25 @@ write_env_file() { fi netbios_name="$sanitized_netbios_name" + detected_ad_dns_ip="$(detect_ad_dns_ip "$domain" || true)" + if [[ -n "$detected_ad_dns_ip" ]]; then + read -r -p "AD_DNS_IP (host LAN IP to publish in AD DNS) [${detected_ad_dns_ip}]: " ad_dns_ip_input + if [[ -n "$ad_dns_ip_input" ]]; then + ad_dns_ip="$ad_dns_ip_input" + else + ad_dns_ip="$detected_ad_dns_ip" + ad_dns_ip_auto="1" + fi + else + prompt_value ad_dns_ip "AD_DNS_IP (host LAN IP to publish in AD DNS)" + fi + + ad_dns_name="${samba_hostname}.${domain}" + read -r -p "AD_DNS_NAME [${ad_dns_name}]: " ad_dns_name_input + if [[ -n "$ad_dns_name_input" ]]; then + ad_dns_name="$ad_dns_name_input" + fi + read -r -p "BACKUP_DESTINATION (optional URL, press Enter to disable): " backup_destination read -r -p "BACKUP_START_HOUR [2]: " backup_start_hour backup_start_hour="${backup_start_hour:-2}" @@ -176,6 +240,9 @@ SERVICE_ACCOUNT_PASSWORD=${service_password} DOMAIN_USERS_SID=${domain_users_sid} DOMAIN_ADMINS_SID=${domain_admins_sid} FSLOGIX_GROUP_SID=${fslogix_group_sid} +AD_DNS_IP=${ad_dns_ip} +AD_DNS_NAME=${ad_dns_name} +AD_DNS_IP_AUTO=${ad_dns_ip_auto} BACKUP_DESTINATION=${backup_destination} BACKUP_START_HOUR=${backup_start_hour} BACKUP_RETENTION_DAILY=${backup_retention_daily} @@ -233,6 +300,9 @@ JOIN_PASSWORD=${service_password} DOMAIN_USERS_SID=${domain_users_sid} DOMAIN_ADMINS_SID=${domain_admins_sid} FSLOGIX_GROUP_SID=${fslogix_group_sid} +AD_DNS_IP=${ad_dns_ip} +AD_DNS_NAME=${ad_dns_name} +AD_DNS_IP_AUTO=${ad_dns_ip_auto} BACKUP_DESTINATION=${backup_destination} BACKUP_START_HOUR=${backup_start_hour} BACKUP_RETENTION_DAILY=${backup_retention_daily}