From 231fb8da8fae2745fceb5bc68e886092c57471ce Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Wed, 18 Feb 2026 17:59:02 +0100 Subject: [PATCH] attempted fix on authentication failures --- README.md | 1 + app/init.sh | 30 ++++++++++++++++++--- app/reconcile_shares.py | 59 ++++++++++++++++++++++++++++++----------- etc/samba/smb.conf | 6 ++--- 4 files changed, 73 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7084716..c4d5557 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This repository provides a production-oriented Samba file server container that - 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. +- Startup resolves those SIDs to NSS group names via winbind, then uses those resolved groups in Samba `valid users` rules. - Reconciliation is executed: - once on startup - every 5 minutes via cron diff --git a/app/init.sh b/app/init.sh index 3b28db6..d9cdfbe 100755 --- a/app/init.sh +++ b/app/init.sh @@ -37,22 +37,40 @@ derive_netbios_name() { resolve_sid_to_group() { local sid="$1" + local resolved_name="" local group_name="" + local short_name="" local sid_output="" if sid_output="$(wbinfo --sid-to-fullname "$sid" 2>/dev/null)"; then - group_name="${sid_output%%$'\t'*}" + resolved_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]+$//')" + if [[ -z "$resolved_name" ]] && sid_output="$(wbinfo -s "$sid" 2>/dev/null)"; then + resolved_name="$(printf '%s' "$sid_output" | sed -E 's/[[:space:]]+[0-9]+$//')" fi - if [[ -z "$group_name" ]]; then + if [[ -z "$resolved_name" ]]; then printf '[init] ERROR: unable to resolve SID %s via winbind\n' "$sid" >&2 return 1 fi + group_name="$resolved_name" + if getent group "$group_name" >/dev/null 2>&1; then + printf '%s\n' "$group_name" + return 0 + fi + + short_name="$group_name" + if [[ "$short_name" == *\\* ]]; then + short_name="${short_name#*\\}" + fi + if [[ -n "$short_name" ]] && getent group "$short_name" >/dev/null 2>&1; then + printf '%s\n' "$short_name" + return 0 + fi + + log "SID ${sid} resolved to '${resolved_name}', but NSS group lookup failed; using raw name." printf '%s\n' "$group_name" } @@ -65,6 +83,10 @@ resolve_share_groups_from_sids() { export PUBLIC_GROUP PUBLIC_GROUP="$(resolve_sid_to_group "$PUBLIC_GROUP_SID")" + + log "Resolved DOMAIN_USERS_SID to '${DOMAIN_USERS_GROUP}'" + log "Resolved DOMAIN_ADMINS_SID to '${DOMAIN_ADMINS_GROUP}'" + log "Resolved PUBLIC_GROUP_SID to '${PUBLIC_GROUP}'" } render_krb5_conf() { diff --git a/app/reconcile_shares.py b/app/reconcile_shares.py index 97fd048..608f860 100755 --- a/app/reconcile_shares.py +++ b/app/reconcile_shares.py @@ -275,10 +275,7 @@ def reconcile_db(conn: sqlite3.Connection, ad_groups: List[Dict[str, str]]) -> N def qualify_group(group_name: str) -> str: - workgroup = os.getenv("WORKGROUP", "").strip() - if workgroup: - return f'@"{workgroup}\\{group_name}"' - return f"@{group_name}" + return f'+"{group_name}"' def is_valid_share_name(share_name: str) -> bool: @@ -372,6 +369,38 @@ def resolve_group_gid(qualified_group: str) -> Optional[int]: return None +def resolve_user_uid_flexible(workgroup: str, username: str) -> Optional[int]: + candidates: List[str] = [] + if "\\" in username: + candidates.append(username) + candidates.append(username.split("\\", 1)[1]) + else: + candidates.append(f"{workgroup}\\{username}") + candidates.append(username) + + for candidate in candidates: + uid = resolve_user_uid(candidate) + if uid is not None: + return uid + return None + + +def resolve_group_gid_flexible(workgroup: str, group_name: str) -> Optional[int]: + candidates: List[str] = [] + if "\\" in group_name: + candidates.append(group_name) + candidates.append(group_name.split("\\", 1)[1]) + else: + candidates.append(f"{workgroup}\\{group_name}") + candidates.append(group_name) + + for candidate in candidates: + gid = resolve_group_gid(candidate) + if gid is not None: + return gid + return None + + def set_acl(path: str, user_uid: int, admin_gid: Optional[int]) -> None: run_command(["setfacl", "-b", path], check=False) acl_entries = [f"u:{user_uid}:rwx", f"d:u:{user_uid}:rwx"] @@ -433,12 +462,10 @@ 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 = ( - public_group if "\\" in public_group else f"{workgroup}\\{public_group}" - ) + qualified_group = public_group os.makedirs(PUBLIC_ROOT, exist_ok=True) - gid = resolve_group_gid(qualified_group) + gid = resolve_group_gid_flexible(workgroup, qualified_group) if gid is not None: os.chown(PUBLIC_ROOT, 0, gid) @@ -452,18 +479,17 @@ def sync_public_directory() -> None: def sync_private_directories() -> None: workgroup = os.environ["WORKGROUP"] - admin_group = f"{workgroup}\\Domain Admins" - admin_gid = resolve_group_gid(admin_group) + admin_group = os.getenv("DOMAIN_ADMINS_GROUP", "Domain Admins") + admin_gid = resolve_group_gid_flexible(workgroup, admin_group) os.makedirs(PRIVATE_ROOT, exist_ok=True) os.chmod(PRIVATE_ROOT, 0o755) users = list_domain_users() for username in users: - qualified_user = f"{workgroup}\\{username}" - uid = resolve_user_uid(qualified_user) + uid = resolve_user_uid_flexible(workgroup, username) if uid is None: - log(f"Unable to resolve UID for {qualified_user}, skipping private folder") + log(f"Unable to resolve UID for {username}, skipping private folder") continue user_path = os.path.join(PRIVATE_ROOT, username) @@ -475,7 +501,8 @@ def sync_private_directories() -> None: def sync_dynamic_directory_permissions(conn: sqlite3.Connection) -> None: workgroup = os.environ["WORKGROUP"] - admin_gid = resolve_group_gid(f"{workgroup}\\Domain Admins") + admin_group = os.getenv("DOMAIN_ADMINS_GROUP", "Domain Admins") + admin_gid = resolve_group_gid_flexible(workgroup, admin_group) rows = conn.execute( "SELECT samAccountName, path FROM shares WHERE isActive = 1" @@ -486,9 +513,9 @@ def sync_dynamic_directory_permissions(conn: sqlite3.Connection) -> None: os.makedirs(path, exist_ok=True) os.chmod(path, 0o2770) - gid = resolve_group_gid(f"{workgroup}\\{sam}") + gid = resolve_group_gid_flexible(workgroup, sam) if gid is None: - log(f"Unable to resolve GID for {workgroup}\\{sam}; leaving existing ACLs") + log(f"Unable to resolve GID for {sam}; leaving existing ACLs") continue os.chown(path, 0, gid) diff --git a/etc/samba/smb.conf b/etc/samba/smb.conf index 51befe3..f1e4f94 100644 --- a/etc/samba/smb.conf +++ b/etc/samba/smb.conf @@ -42,8 +42,8 @@ read only = no browseable = yes guest ok = no - valid users = @"${DOMAIN_USERS_GROUP}" - admin users = @"${DOMAIN_ADMINS_GROUP}" + valid users = +"${DOMAIN_USERS_GROUP}" + admin users = +"${DOMAIN_ADMINS_GROUP}" hide unreadable = yes access based share enum = yes ea support = yes @@ -53,7 +53,7 @@ read only = no browseable = yes guest ok = no - valid users = @"${PUBLIC_GROUP}" + valid users = +"${PUBLIC_GROUP}" force group = "${PUBLIC_GROUP}" create mask = 0660 directory mask = 2770