fix: publish host IP in AD DNS

This commit is contained in:
Ludwig Lehnert
2026-06-24 10:33:52 +00:00
parent 61b698890e
commit f23aa64155
5 changed files with 270 additions and 2 deletions

View File

@@ -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

View File

@@ -16,6 +16,7 @@ This repository provides a production-oriented Samba file server container that
- inactive/deleted groups: `/data/groups/archive/<groupName>`
- 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=<host LAN IP>` and `AD_DNS_NAME=<server FQDN>`.
- 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:

View File

@@ -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

129
redeploy
View File

@@ -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() {

70
setup
View File

@@ -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}