better Private share folder handling

This commit is contained in:
Ludwig Lehnert
2026-02-18 19:04:33 +01:00
parent 621bbf3d9c
commit 6d9476c578
5 changed files with 55 additions and 2 deletions

View File

@@ -10,3 +10,5 @@ PUBLIC_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513
# NETBIOS_NAME=ADSAMBAFSRV # NETBIOS_NAME=ADSAMBAFSRV
# LDAP_URI=ldaps://example.com # LDAP_URI=ldaps://example.com
# LDAP_BASE_DN=DC=example,DC=com # LDAP_BASE_DN=DC=example,DC=com
# PRIVATE_SKIP_USERS=svc_backup,svc_sql
# PRIVATE_SKIP_PREFIXES=svc_,sql_

View File

@@ -18,6 +18,7 @@ This repository provides a production-oriented Samba file server container that
- Setup prompts for well-known authorization groups by SID (`DOMAIN_USERS_SID`, `DOMAIN_ADMINS_SID`) to avoid localized group names. - 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. - Startup resolves those SIDs to NSS group names via winbind, then uses those resolved groups in Samba `valid users` rules.
- Share operations are audited with Samba `full_audit` (connect, list, read, write, create, delete, rename) and written to Samba log files. - Share operations are audited with Samba `full_audit` (connect, list, read, write, create, delete, rename) and written to Samba log files.
- Private home creation skips well-known/service accounts by default (including `krbtgt`, `msol_*`, `FileShare_ServiceAcc`).
- Reconciliation is executed: - Reconciliation is executed:
- once on startup - once on startup
- every 5 minutes via cron - every 5 minutes via cron
@@ -130,6 +131,8 @@ Kerberos requires close time alignment.
- Root path: `/data/private` - Root path: `/data/private`
- Per-user path: `/data/private/<samAccountName>` - Per-user path: `/data/private/<samAccountName>`
- Script ensures user directories exist and assigns ownership through winbind identity resolution. - Script ensures user directories exist and assigns ownership through winbind identity resolution.
- Root `/data/private` is enforced read/execute-only (`0555`) to prevent folder creation directly under `\\server\Private`.
- SMB-side permission changes on `\\server\Private` are blocked (`nt acl support = no` and security masks set to `0000`).
- Permissions: - Permissions:
- owner user: full control - owner user: full control
- Domain Admins: ACL full control - Domain Admins: ACL full control

View File

@@ -28,6 +28,16 @@ GROUP_PREFIX = "FileShare_"
REQUIRED_ENV = ["REALM", "WORKGROUP", "DOMAIN"] REQUIRED_ENV = ["REALM", "WORKGROUP", "DOMAIN"]
ATTR_RE = re.compile(r"^([^:]+)(::?):\s*(.*)$") ATTR_RE = re.compile(r"^([^:]+)(::?):\s*(.*)$")
SHARE_NAME_INVALID_RE = re.compile(r"[\\/:*?\"<>|;\[\],+=]") SHARE_NAME_INVALID_RE = re.compile(r"[\\/:*?\"<>|;\[\],+=]")
PRIVATE_SKIP_EXACT = {
"krbtgt",
"administrator",
"guest",
"defaultaccount",
"wdagutilityaccount",
"fileshare_serviceacc",
"fileshare_serviceaccount",
}
PRIVATE_SKIP_PREFIXES = ("msol_", "fileshare_service", "aad_")
def now_utc() -> str: def now_utc() -> str:
@@ -460,10 +470,40 @@ def list_domain_users() -> List[str]:
candidate = candidate.split("\\", 1)[1] candidate = candidate.split("\\", 1)[1]
if not candidate or candidate.endswith("$"): if not candidate or candidate.endswith("$"):
continue continue
if should_skip_private_user(candidate):
continue
users.append(candidate) users.append(candidate)
return sorted(set(users)) return sorted(set(users))
def should_skip_private_user(username: str) -> bool:
normalized = username.strip().lower()
if not normalized:
return True
if normalized in PRIVATE_SKIP_EXACT:
return True
if any(normalized.startswith(prefix) for prefix in PRIVATE_SKIP_PREFIXES):
return True
extra_skip_users = {
value.strip().lower()
for value in os.getenv("PRIVATE_SKIP_USERS", "").split(",")
if value.strip()
}
if normalized in extra_skip_users:
return True
extra_skip_prefixes = [
value.strip().lower()
for value in os.getenv("PRIVATE_SKIP_PREFIXES", "").split(",")
if value.strip()
]
if any(normalized.startswith(prefix) for prefix in extra_skip_prefixes):
return True
return False
def sync_public_directory() -> None: def sync_public_directory() -> None:
workgroup = os.environ["WORKGROUP"] workgroup = os.environ["WORKGROUP"]
public_group = os.getenv("PUBLIC_GROUP", "Domain Users") public_group = os.getenv("PUBLIC_GROUP", "Domain Users")
@@ -488,7 +528,9 @@ def sync_private_directories() -> None:
admin_gid = resolve_group_gid_flexible(workgroup, admin_group) admin_gid = resolve_group_gid_flexible(workgroup, admin_group)
os.makedirs(PRIVATE_ROOT, exist_ok=True) os.makedirs(PRIVATE_ROOT, exist_ok=True)
os.chmod(PRIVATE_ROOT, 0o755) os.chown(PRIVATE_ROOT, 0, 0)
run_command(["setfacl", "-b", PRIVATE_ROOT], check=False)
os.chmod(PRIVATE_ROOT, 0o555)
users = list_domain_users() users = list_domain_users()
for username in users: for username in users:

View File

@@ -49,10 +49,14 @@
full_audit:failure = all full_audit:failure = all
full_audit:syslog = false full_audit:syslog = false
valid users = +"${DOMAIN_USERS_GROUP}" valid users = +"${DOMAIN_USERS_GROUP}"
admin users = +"${DOMAIN_ADMINS_GROUP}"
hide unreadable = yes hide unreadable = yes
access based share enum = yes access based share enum = yes
ea support = yes ea support = yes
nt acl support = no
security mask = 0000
force security mode = 0000
directory security mask = 0000
force directory security mode = 0000
[Public] [Public]
path = /data/public path = /data/public

2
setup
View File

@@ -214,6 +214,8 @@ NETBIOS_NAME=${netbios_name}
# Optional overrides: # Optional overrides:
# LDAP_URI=ldaps://${domain} # LDAP_URI=ldaps://${domain}
# LDAP_BASE_DN=DC=example,DC=com # LDAP_BASE_DN=DC=example,DC=com
# PRIVATE_SKIP_USERS=svc_backup,svc_sql
# PRIVATE_SKIP_PREFIXES=svc_,sql_
EOF EOF
chmod 600 "$ENV_FILE" chmod 600 "$ENV_FILE"