AD-Integrated Containerized Samba File Server
This repository provides a production-oriented Samba file server container that joins an existing Active Directory domain, exposes static and dynamic SMB shares, and persists share identity by AD objectGUID.
Architecture
- Samba runs in ADS mode with
winbindidentity mapping. - Static shares:
\\server\Private->/data/private\\server\Shared->/data/public
- Dynamic shares are generated from AD groups matching
FileShare_*orFS_*and written to/etc/samba/generated/shares.conf. - Dynamic share records are persisted in SQLite at
/state/shares.db. - Backing storage is GUID-based and stable across group rename:
/data/groups/<objectGUID>
- Samba machine trust/key material is persisted in
/var/lib/sambato survive container recreation. - Container hostname is fixed (
SAMBA_HOSTNAME) to keep AD computer identity stable. - NetBIOS name defaults to
ADSAMBAFSRVand is clamped to 15 characters (NETBIOS_NAMEoverride 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 usersrules. - 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:
- once on startup
- every 5 minutes via cron
Dynamic Share Lifecycle (objectGUID-first)
The reconciliation script (/app/reconcile_shares.py) enforces these rules:
- New matching group -> insert DB row, create
/data/groups/<objectGUID>, expose share. - Group renamed but still
FileShare_-> updatesamAccountNameandshareName, keep same path. - Group removed or no longer matches prefix -> set
isActive=0, remove Samba exposure, keep data. - Previously inactive group returns -> set
isActive=1, reuse existing path and data.
SQLite State Database
Database path: /state/shares.db
Table schema:
CREATE TABLE shares (
objectGUID TEXT PRIMARY KEY,
samAccountName TEXT NOT NULL,
shareName TEXT NOT NULL,
path TEXT NOT NULL,
createdAt TIMESTAMP NOT NULL,
lastSeenAt TIMESTAMP NOT NULL,
isActive INTEGER NOT NULL
);
AD Requirements
- Existing AD DS domain reachable from the Docker host.
- Initial admin credentials with rights to create/reset
FileShare_ServiceAccountduring./setup. FileShare_ServiceAccountmust 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_<ShareName>orFS_<ShareName>
DNS Requirements
- Container must resolve AD DNS records (especially SRV records for domain controllers).
DOMAINshould resolve from inside the container.- Preferred setup: Docker host uses AD-integrated DNS or forwards to AD DNS.
Time Sync Requirements
Kerberos requires close time alignment.
- Docker host clock must be synchronized (NTP/chrony/systemd-timesyncd).
- AD domain controllers must also be time-synchronized.
- If join/authentication fails unexpectedly, check time skew first.
Repository Layout
Dockerfiledocker-compose.ymlsetup.env.exampleREADME.mdapp/init.shapp/reconcile_shares.pyetc/samba/smb.confetc/samba/generated/
Setup
-
Run interactive setup:
./setup -
If
.envis missing, you will be prompted for:REALMWORKGROUPDOMAIN- initial admin credentials (used once for provisioning)
DOMAIN_USERS_SIDDOMAIN_ADMINS_SID- optional
PUBLIC_GROUP_SID(defaults toDOMAIN_USERS_SID)
Optional:
SAMBA_HOSTNAME(defaults toadsambafsrv)NETBIOS_NAME(defaults toADSAMBAFSRV, max 15 chars)
-
Setup behavior:
- creates or updates AD service account from desired name
FileShare_ServiceAccount - uses a valid AD
sAMAccountName(max 20 chars); default effective value isFileShare_ServiceAcc - always sets a long random password
- writes only service-account credentials to
.env(initial admin credentials are not stored)
- creates or updates AD service account from desired name
-
The setup script then starts the service with:
docker compose up -d -
After startup:
- container joins AD (idempotent)
- startup reconciliation runs
- cron runs reconciliation every 5 minutes
SMB Shares
Private
- Share:
\\server\Private - Root path:
/data/private - Per-user path:
/data/private/<samAccountName> - Script ensures user directories exist and assigns ownership through winbind identity resolution.
- Root
/data/privateis enforced read/execute-only (0555) to prevent folder creation directly under\\server\Private. - SMB-side ACL changes on
\\server\Privateare blocked (nt acl support = no). - Auto-creation skips well-known/service/non-login accounts (disabled, locked, or expired).
- Each private user tree is reconciled recursively to homogeneous permissions (dirs
0700, files0600, user/admin ACLs). - Permissions:
- owner user: full control
- Domain Admins: ACL full control
- mode:
700
hide unreadable = yes+ ACLs enforce that users only see their own folder.
Shared
- Share:
\\server\Shared - Path:
/data/public - Read/write for authenticated users in configurable
PUBLIC_GROUP_SID(default:DOMAIN_USERS_SID, resolved through winbind). - No guest access.
- Permissions are reconciled recursively so all descendants remain homogeneous (dirs
2770, files0660, shared group/admin ACLs).
Dynamic Group Shares
- AD groups:
FileShare_*andFS_* - Share name: group title from AD (
displayName->name/cnfallback). Prefix stripping fromsAMAccountNameis only a fallback when no title exists. - Backing path:
/data/groups/<objectGUID> - Share exposure generated in
/etc/samba/generated/shares.conf - Dynamic share names are validated for SMB compatibility and deduplicated case-insensitively.
- Group membership changes are refreshed continuously via winbind cache updates (
winbind cache time = 60) and Samba config reload during reconciliation. - Dynamic share trees are reconciled recursively so all descendants keep homogeneous permissions.
Useful Commands
docker compose logs -f samba
docker compose exec samba python3 /app/reconcile_shares.py
docker compose exec samba sqlite3 /state/shares.db 'SELECT * FROM shares;'
docker compose exec samba testparm -s
docker compose exec samba sh -lc 'tail -n 200 /var/log/samba/log.*'
Troubleshooting
Domain join fails
-
Verify service account credentials in
.env. -
Verify DNS resolution from container:
docker compose exec samba getent hosts "$DOMAIN" -
Verify time sync on host and AD DCs.
-
Verify NetBIOS name length is <= 15:
docker compose exec samba testparm -s | grep -i 'netbios name'
Winbind user/group resolution fails
-
Check trust:
docker compose exec samba wbinfo -t -
List users/groups:
docker compose exec samba wbinfo -u docker compose exec samba wbinfo -g
Dynamic shares not appearing
-
Confirm AD groups match
FileShare_*orFS_*. -
Run manual reconciliation and inspect logs:
docker compose exec samba python3 /app/reconcile_shares.py docker compose exec samba tail -n 100 /var/log/reconcile.log -
Validate generated config:
docker compose exec samba cat /etc/samba/generated/shares.conf
acl_xattr.so or full_audit.so module load error
-
If logs show
Error loading module .../vfs/acl_xattr.so(orfull_audit.so), your running image is missing Samba VFS modules. -
Rebuild and restart with the updated image:
docker compose down docker compose up -d --build -
Verify modules exist:
docker compose exec samba sh -lc 'mods="$(smbd -b | sed -n "s/^ *MODULESDIR: //p" | head -n1)/vfs"; ls -1 "$mods"/acl_xattr.so "$mods"/full_audit.so'
Permissions in Private share are incorrect
-
Re-run reconciliation to rebuild private directories and ACLs:
docker compose exec samba python3 /app/reconcile_shares.py -
Check identity resolution for a user:
docker compose exec samba id 'EXAMPLE\\alice'
Notes
- User data is never automatically deleted.
- Inactive/deleted groups only stop being exposed as shares.
- Data and state survive container restarts via named Docker volumes (
/data/*,/state,/var/lib/samba).