Files
aad-local-file-share/README.md
Ludwig Lehnert 70fe6076a4 initial commit
2026-02-03 16:39:37 +01:00

167 lines
5.8 KiB
Markdown

# 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\private` is a single share that maps to `/data/private/%U` with per-user content.
- `\\server\<shareName>` are separate shares pointing to `/data/shares/<shareName>`.
- **No per-folder ACLs:** Samba config uses `force user = filesvc` and `force group = filesvc` for shared areas. `nt acl support = no` and related toggles prevent ACL edits from clients.
- **Dynamic share reload:** Web UI regenerates `/etc/samba/shares.generated.conf` atomically and Samba reloads via `smbcontrol all reload-config` on change.
- **Backups:** Scheduled ISO backups with a tar-based payload (`data.tar.zst`) + SQLite `.backup` file + `manifest.json` containing 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` (inside `webui` container, persisted via volume)
## Quick start
1. **Clone repo and create env file**
```bash
cp .env.example .env
```
2. **Generate self-signed TLS certs for Traefik**
```bash
./scripts/generate-self-signed.sh ./traefik/certs files.example.com
```
3. **Create host directories**
```bash
sudo mkdir -p /srv/files/data /srv/files/remote-backup
```
4. **Start the stack**
```bash
docker compose up -d
```
5. **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 enable `smb encrypt = desired` or `required` based on performance and compliance needs.
- **No ACL editing**: `nt acl support = no`, `dos filemode = no`, and `unix extensions = no` reduce client-side permission manipulation.
## Share behavior
- **Private share:** `\\server\private` maps to `/data/private/%U` and creates the user directory on first connect. Directory is `0700` and 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 via `write 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:
```bash
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/:id` manage membership
- `/admin` logs and statistics
### APIs
- `POST /api/shares` create share
- `GET /api/shares` list shares visible to current user
- `GET /api/shares/:id` share detail
- `POST /api/shares/:id/members` add/remove users or local groups
- `DELETE /api/shares/:id` disable share
Local groups are managed in the UI on each share page and can be assigned roles per share.
### Create share flow
1. Validate share name (strict regex, length limits, reserved names).
2. Insert into SQLite with state `creating`.
3. Create `/data/shares/<shareName>`.
4. `chown root:filesvc` and `chmod 2770`.
5. Regenerate `/samba-generated/shares.generated.conf` atomically.
6. Mark share `ready`.
## Backup job
Backups run on the `BACKUP_CRON` schedule and produce:
- `data.tar.zst` (tar archive of `/data` with xattrs/acl metadata)
- `app.db` (SQLite `.backup`)
- `manifest.json` (timestamp, sizes, sha256)
- `backup-<timestamp>.iso` containing 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
1. Copy the compressed ISO back from `/remote`.
2. Decompress it.
3. Extract the ISO contents.
4. Decompress `data.tar.zst` and restore `/data` on the host.
5. Restore `app.db` to the web UI volume.
6. 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/private`
- `smb://server/shareName`
### Linux
```bash
smbclient -U user@domain //server/private
smbclient -U user@domain //server/shareName
```
## Files of interest
- `docker-compose.yml`
- `samba/smb.conf.template`
- `webui/src/index.js`
- `backupd/backup.sh`