AAD Local File Share (Samba + Web UI)
Dockerized SMB/CIFS file sharing system for a single on-prem Linux host. Authentication is via on-prem AD DS (Samba security = ADS with winbind). Authorization and share management live exclusively in local SQLite, driven by the web UI.
Architecture highlights
- Authentication: Samba joins AD as a member server (
security = ADS). SMB1 is disabled; SMB2+ only. NTLMv2 is allowed for non-domain-joined devices. - Authorization: SQLite is the source of truth (no AD groups). Share membership is expanded to per-user lists in a generated Samba config file.
- Share model:
\\server\privateis a single share that maps to/data/private/%Uwith per-user content.\\server\<shareName>are separate shares pointing to/data/shares/<shareName>.
- No per-folder ACLs: Samba config uses
force user = filesvcandforce group = filesvcfor shared areas.nt acl support = noand related toggles prevent ACL edits from clients. - Dynamic share reload: Web UI regenerates
/etc/samba/shares.generated.confatomically and Samba reloads viasmbcontrol all reload-configon change. - Backups: Scheduled ISO backups with a tar-based payload (
data.tar.zst) + SQLite.backupfile +manifest.jsoncontaining hashes/sizes.
Directory layout
Host directories:
/srv/files/data->/data/data/private/<username>/data/shares/<shareName>
/srv/files/remote-backup->/remote(bind-mounted remote share)
SQLite location:
/var/lib/webui/app.db(insidewebuicontainer, persisted via volume)
Quick start
-
Clone repo and create env file
cp .env.example .env -
Generate self-signed TLS certs for Traefik
./scripts/generate-self-signed.sh ./traefik/certs files.example.com -
Create host directories
sudo mkdir -p /srv/files/data /srv/files/remote-backup -
Start the stack
docker compose up -d -
Open the Web UI
https://files.example.com/
Samba security notes
- SMB1 is disabled (
server min protocol = SMB2). - NTLMv2 is allowed for non-domain-joined clients (
ntlm auth = ntlmv2-only).- Recommended hardening: prefer Kerberos and domain-joined devices, set
server signing = mandatory, and enablesmb encrypt = desiredorrequiredbased on performance and compliance needs.
- Recommended hardening: prefer Kerberos and domain-joined devices, set
- No ACL editing:
nt acl support = no,dos filemode = no, andunix extensions = noreduce client-side permission manipulation.
Share behavior
- Private share:
\\server\privatemaps to/data/private/%Uand creates the user directory on first connect. Directory is0700and owned by the user (mapped via winbind). - Shared shares: Each share is a distinct Samba stanza in
/etc/samba/shares.generated.conf. Users are allowed via per-user lists from SQLite:valid users = <owner+rw+ro users>write list = <owner+rw users>read only = yes(writes allowed viawrite list)
Important: Shared area permissions are enforced by Samba config (lists), not filesystem ACLs. All files are owned by filesvc inside the Samba container due to force user/group.
The filesvc UID/GID is 10050 by default in both webui and samba containers so ownership stays consistent on the host bind mount.
Web UI
The Web UI authenticates via Entra ID OIDC (authorization code flow) and stores sessions in an HTTP-only cookie. Users are authorized if their UPN suffix matches ALLOWED_UPN_SUFFIX.
Admin page
/admin shows access logs and aggregated statistics with Chart.js (daily activity and action breakdown). Access is restricted to the admin user via a bcrypt hash in .env.
To generate a bcrypt hash for the admin UPN:
node -e "const bcrypt=require('bcryptjs'); const upn=process.argv[1]; console.log(bcrypt.hashSync(upn, 10));" "admin@example.com"
Set the output in ADMIN_UPN_BCRYPT.
Pages
/list shares, create share/shares/:idmanage membership/adminlogs and statistics
APIs
POST /api/sharescreate shareGET /api/shareslist shares visible to current userGET /api/shares/:idshare detailPOST /api/shares/:id/membersadd/remove users or local groupsDELETE /api/shares/:iddisable share
Local groups are managed in the UI on each share page and can be assigned roles per share.
Create share flow
- Validate share name (strict regex, length limits, reserved names).
- Insert into SQLite with state
creating. - Create
/data/shares/<shareName>. chown root:filesvcandchmod 2770.- Regenerate
/samba-generated/shares.generated.confatomically. - Mark share
ready.
Backup job
Backups run on the BACKUP_CRON schedule and produce:
data.tar.zst(tar archive of/datawith xattrs/acl metadata)app.db(SQLite.backup)manifest.json(timestamp, sizes, sha256)backup-<timestamp>.isocontaining the above, then compressed to.iso.zst(or.iso.gz)
Note: The ISO file itself does not preserve POSIX ACLs; the tar file inside does.
Restore steps
- Copy the compressed ISO back from
/remote. - Decompress it.
- Extract the ISO contents.
- Decompress
data.tar.zstand restore/dataon the host. - Restore
app.dbto the web UI volume. - Redeploy compose. Re-join AD if the Samba secrets volume is lost.
Connecting clients
Windows
\\server\private\\server\shareName
Use DOMAIN\user or user@domain when prompted. NTLMv2 is supported for non-domain-joined devices.
macOS
- Finder -> Go -> Connect to Server
smb://server/privatesmb://server/shareName
Linux
smbclient -U user@domain //server/private
smbclient -U user@domain //server/shareName
Files of interest
docker-compose.ymlsamba/smb.conf.templatewebui/src/index.jsbackupd/backup.sh