From 29392fd4caf598ba1375683daa703d47c1b347ad Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Wed, 18 Feb 2026 12:09:38 +0100 Subject: [PATCH] [POSTFIX] first progress --- .env.example | 11 ++-- README.md | 39 +++++++++---- app/init.sh | 70 ++++++++++++++++++++++- app/reconcile_shares.py | 6 +- docker-compose.yml | 4 +- etc/samba/smb.conf | 15 ++--- setup | 119 +++++++++++++++++++++++++++++++++++++--- 7 files changed, 229 insertions(+), 35 deletions(-) diff --git a/.env.example b/.env.example index fd68dd4..4085db6 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,12 @@ REALM=EXAMPLE.COM WORKGROUP=EXAMPLE DOMAIN=example.com -JOIN_USER=administrator -JOIN_PASSWORD=ChangeMe -PUBLIC_GROUP=Domain Users -# SAMBA_HOSTNAME=ad-samba-file-server +JOIN_USER=FileShare_ServiceAccount +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 +PUBLIC_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513 +# SAMBA_HOSTNAME=adsambafsrv +# NETBIOS_NAME=ADSAMBAFSRV # LDAP_URI=ldaps://example.com # LDAP_BASE_DN=DC=example,DC=com diff --git a/README.md b/README.md index ec40059..18cc0e0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ This repository provides a production-oriented Samba file server container that - `/data/groups/` - 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. +- 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. - Reconciliation is executed: - once on startup - every 5 minutes via cron @@ -48,7 +50,8 @@ CREATE TABLE shares ( ## AD Requirements - Existing AD DS domain reachable from the Docker host. -- A service account with rights to join computers to the domain (`net ads join`). +- Initial admin credentials with rights to create/reset `FileShare_ServiceAccount` during `./setup`. +- `FileShare_ServiceAccount` must be allowed to join computers to the domain (`net ads join`) in your AD policy. - Dynamic group discovery primarily uses machine-account LDAP (`net ads search -P`); join credentials are only used as a fallback LDAP bind path. - Group naming convention for dynamic shares: - `FileShare_` @@ -91,19 +94,30 @@ Kerberos requires close time alignment. - `REALM` - `WORKGROUP` - `DOMAIN` - - `JOIN_USER` - - `JOIN_PASSWORD` + - initial admin credentials (used once for provisioning) + - `DOMAIN_USERS_SID` + - `DOMAIN_ADMINS_SID` + - optional `PUBLIC_GROUP_SID` (defaults to `DOMAIN_USERS_SID`) -3. The setup script writes `.env` and starts the service with: + Optional: + - `SAMBA_HOSTNAME` (defaults to `adsambafsrv`) + - `NETBIOS_NAME` (defaults to `ADSAMBAFSRV`, max 15 chars) + +3. Setup behavior: + - creates or updates AD account `FileShare_ServiceAccount` + - always sets a long random password + - writes only service-account credentials to `.env` (initial admin credentials are not stored) + +4. The setup script then starts the service with: ```bash docker compose up -d ``` -4. After startup: - - container joins AD (idempotent) - - startup reconciliation runs - - cron runs reconciliation every 5 minutes +5. After startup: + - container joins AD (idempotent) + - startup reconciliation runs + - cron runs reconciliation every 5 minutes ## SMB Shares @@ -123,7 +137,7 @@ Kerberos requires close time alignment. - Share: `\\server\Public` - Path: `/data/public` -- Read/write for authenticated users in configurable `PUBLIC_GROUP` (default `Domain Users`). +- Read/write for authenticated users in configurable `PUBLIC_GROUP_SID` (default: `DOMAIN_USERS_SID`, resolved through winbind). - No guest access. ### Dynamic Group Shares @@ -147,7 +161,7 @@ docker compose exec samba testparm -s ### Domain join fails -- Verify credentials in `.env`. +- Verify service account credentials in `.env`. - Verify DNS resolution from container: ```bash @@ -155,6 +169,11 @@ docker compose exec samba testparm -s ``` - Verify time sync on host and AD DCs. +- Verify NetBIOS name length is <= 15: + + ```bash + docker compose exec samba testparm -s | grep -i 'netbios name' + ``` ### Winbind user/group resolution fails diff --git a/app/init.sh b/app/init.sh index cfd4dc2..3b28db6 100755 --- a/app/init.sh +++ b/app/init.sh @@ -18,6 +18,55 @@ append_winbind_to_nss() { sed -ri '/^group:/ { /winbind/! s/$/ winbind/ }' /etc/nsswitch.conf } +derive_netbios_name() { + local raw_name="${NETBIOS_NAME:-ADSAMBAFSRV}" + local upper_name="${raw_name^^}" + local cleaned_name + cleaned_name="$(printf '%s' "$upper_name" | tr -cd 'A-Z0-9')" + + if [[ -z "$cleaned_name" ]]; then + cleaned_name="SAMBAFS" + fi + + if [[ ${#cleaned_name} -gt 15 ]]; then + log "NETBIOS_NAME derived from '${raw_name}' exceeds 15 chars, truncating." + fi + + export NETBIOS_NAME="${cleaned_name:0:15}" +} + +resolve_sid_to_group() { + local sid="$1" + local group_name="" + local sid_output="" + + if sid_output="$(wbinfo --sid-to-fullname "$sid" 2>/dev/null)"; then + group_name="${sid_output%%$'\t'*}" + fi + + if [[ -z "$group_name" ]] && sid_output="$(wbinfo -s "$sid" 2>/dev/null)"; then + group_name="$(printf '%s' "$sid_output" | sed -E 's/[[:space:]]+[0-9]+$//')" + fi + + if [[ -z "$group_name" ]]; then + printf '[init] ERROR: unable to resolve SID %s via winbind\n' "$sid" >&2 + return 1 + fi + + printf '%s\n' "$group_name" +} + +resolve_share_groups_from_sids() { + export DOMAIN_USERS_GROUP + DOMAIN_USERS_GROUP="$(resolve_sid_to_group "$DOMAIN_USERS_SID")" + + export DOMAIN_ADMINS_GROUP + DOMAIN_ADMINS_GROUP="$(resolve_sid_to_group "$DOMAIN_ADMINS_SID")" + + export PUBLIC_GROUP + PUBLIC_GROUP="$(resolve_sid_to_group "$PUBLIC_GROUP_SID")" +} + render_krb5_conf() { cat > /etc/krb5.conf < None: "create mask = 0660", "directory mask = 2770", "inherit permissions = yes", - "access based share enumeration = yes", + "access based share enum = yes", "", ] ) @@ -433,7 +433,9 @@ def list_domain_users() -> List[str]: def sync_public_directory() -> None: workgroup = os.environ["WORKGROUP"] public_group = os.getenv("PUBLIC_GROUP", "Domain Users") - qualified_group = f"{workgroup}\\{public_group}" + qualified_group = ( + public_group if "\\" in public_group else f"{workgroup}\\{public_group}" + ) os.makedirs(PUBLIC_ROOT, exist_ok=True) gid = resolve_group_gid(qualified_group) diff --git a/docker-compose.yml b/docker-compose.yml index b99375c..a55621d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,10 +3,12 @@ services: build: context: . container_name: ad-samba-file-server - hostname: ${SAMBA_HOSTNAME:-ad-samba-file-server} + hostname: ${SAMBA_HOSTNAME:-adsambafsrv} restart: unless-stopped env_file: - .env + environment: + NETBIOS_NAME: ${NETBIOS_NAME:-ADSAMBAFSRV} ports: - "445:445" - "139:139" diff --git a/etc/samba/smb.conf b/etc/samba/smb.conf index d7b4683..51befe3 100644 --- a/etc/samba/smb.conf +++ b/etc/samba/smb.conf @@ -3,6 +3,7 @@ kerberos method = secrets and keytab realm = ${REALM} workgroup = ${WORKGROUP} + netbios name = ${NETBIOS_NAME} idmap config * : backend = tdb idmap config * : range = 3000-7999 @@ -21,7 +22,7 @@ server min protocol = SMB2 client min protocol = SMB2 - access based share enumeration = yes + access based share enum = yes dedicated keytab file = /var/lib/samba/private/krb5.keytab kerberos encryption types = all @@ -41,10 +42,10 @@ read only = no browseable = yes guest ok = no - valid users = @"${WORKGROUP}\\Domain Users" - admin users = @"${WORKGROUP}\\Domain Admins" + valid users = @"${DOMAIN_USERS_GROUP}" + admin users = @"${DOMAIN_ADMINS_GROUP}" hide unreadable = yes - access based share enumeration = yes + access based share enum = yes ea support = yes [Public] @@ -52,9 +53,9 @@ read only = no browseable = yes guest ok = no - valid users = @"${WORKGROUP}\\${PUBLIC_GROUP}" - force group = "${WORKGROUP}\\${PUBLIC_GROUP}" + valid users = @"${PUBLIC_GROUP}" + force group = "${PUBLIC_GROUP}" create mask = 0660 directory mask = 2770 inherit permissions = yes - access based share enumeration = yes + access based share enum = yes diff --git a/setup b/setup index ec51b03..8b9e4cc 100755 --- a/setup +++ b/setup @@ -2,6 +2,26 @@ set -euo pipefail ENV_FILE=".env" +SERVICE_ACCOUNT_NAME="FileShare_ServiceAccount" + +BOOTSTRAP_ENV_FILE="" +cleanup() { + if [[ -n "$BOOTSTRAP_ENV_FILE" && -f "$BOOTSTRAP_ENV_FILE" ]]; then + rm -f "$BOOTSTRAP_ENV_FILE" + fi +} +trap cleanup EXIT + +sanitize_netbios_name() { + local raw_name="$1" + local upper_name="${raw_name^^}" + local cleaned_name + cleaned_name="$(printf '%s' "$upper_name" | tr -cd 'A-Z0-9')" + if [[ -z "$cleaned_name" ]]; then + cleaned_name="ADSAMBAFSRV" + fi + printf '%s' "${cleaned_name:0:15}" +} prompt_value() { local var_name="$1" @@ -25,23 +45,106 @@ write_env_file() { local realm="" local workgroup="" local domain="" - local join_user="" - local join_password="" + local admin_user="" + local admin_password="" + local domain_users_sid="" + local domain_admins_sid="" + local public_group_sid="" + local samba_hostname="adsambafsrv" + local netbios_name="ADSAMBAFSRV" + local service_password="" + local public_group_prompt="" + local samba_hostname_input="" + local netbios_name_input="" + local sanitized_netbios_name="" prompt_value realm "REALM (e.g. EXAMPLE.COM)" prompt_value workgroup "WORKGROUP (NetBIOS, e.g. EXAMPLE)" prompt_value domain "DOMAIN (AD DNS name or reachable DC FQDN)" - prompt_value join_user "JOIN_USER (AD account with join rights)" - prompt_value join_password "JOIN_PASSWORD" true + prompt_value admin_user "Initial admin user (for provisioning service account)" + prompt_value admin_password "Initial admin password" true + prompt_value domain_users_sid "DOMAIN_USERS_SID (e.g. ...-513)" + prompt_value domain_admins_sid "DOMAIN_ADMINS_SID (e.g. ...-512)" + + public_group_prompt="PUBLIC_GROUP_SID (press Enter to reuse DOMAIN_USERS_SID)" + read -r -p "${public_group_prompt}: " public_group_sid + if [[ -z "$public_group_sid" ]]; then + public_group_sid="$domain_users_sid" + fi + + read -r -p "SAMBA_HOSTNAME [adsambafsrv]: " samba_hostname_input + if [[ -n "${samba_hostname_input:-}" ]]; then + samba_hostname="$samba_hostname_input" + fi + + read -r -p "NETBIOS_NAME [ADSAMBAFSRV]: " netbios_name_input + if [[ -n "${netbios_name_input:-}" ]]; then + netbios_name="$netbios_name_input" + fi + sanitized_netbios_name="$(sanitize_netbios_name "$netbios_name")" + if [[ "$sanitized_netbios_name" != "$netbios_name" ]]; then + printf "Using sanitized NETBIOS_NAME: %s\n" "$sanitized_netbios_name" + fi + netbios_name="$sanitized_netbios_name" + + service_password="$(tr -dc 'A-Za-z0-9@#%+=:_-' "$BOOTSTRAP_ENV_FILE" < /tmp/bootstrap-smb.conf < "$ENV_FILE" <