added FSLogix share

This commit is contained in:
Ludwig Lehnert
2026-03-16 18:50:29 +01:00
parent 3381aadc30
commit 9bf0694bf2
8 changed files with 100 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ JOIN_PASSWORD=ReplaceWithLongRandomPassword
DOMAIN_USERS_SID=S-1-5-21-1111111111-2222222222-3333333333-513 DOMAIN_USERS_SID=S-1-5-21-1111111111-2222222222-3333333333-513
DOMAIN_ADMINS_SID=S-1-5-21-1111111111-2222222222-3333333333-512 DOMAIN_ADMINS_SID=S-1-5-21-1111111111-2222222222-3333333333-512
PUBLIC_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513 PUBLIC_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513
FSLOGIX_GROUP_SID=S-1-5-21-1111111111-2222222222-3333333333-513
# SAMBA_HOSTNAME=adsambafsrv # SAMBA_HOSTNAME=adsambafsrv
# NETBIOS_NAME=ADSAMBAFSRV # NETBIOS_NAME=ADSAMBAFSRV
# LDAP_URI=ldaps://example.com # LDAP_URI=ldaps://example.com

View File

@@ -19,7 +19,7 @@ RUN apt-get update \
winbind \ winbind \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app /data/private /data/public /data/groups /state /etc/samba/generated RUN mkdir -p /app /data/private /data/public /data/fslogix /data/groups /state /etc/samba/generated
COPY app/reconcile_shares.py /app/reconcile_shares.py COPY app/reconcile_shares.py /app/reconcile_shares.py
COPY app/init.sh /app/init.sh COPY app/init.sh /app/init.sh

View File

@@ -8,6 +8,7 @@ This repository provides a production-oriented Samba file server container that
- Static shares: - Static shares:
- `\\server\Privat` -> `/data/private` - `\\server\Privat` -> `/data/private`
- `\\server\Geteilt` -> `/data/public` - `\\server\Geteilt` -> `/data/public`
- `\\server\FSLogix` -> `/data/fslogix`
- Dynamic shares are generated from AD groups matching `FileShare_*` or `FS_*` and written to `/etc/samba/generated/shares.conf`. - Dynamic shares are generated from AD groups matching `FileShare_*` or `FS_*` and written to `/etc/samba/generated/shares.conf`.
- Dynamic share records are persisted in SQLite at `/state/shares.db`. - Dynamic share records are persisted in SQLite at `/state/shares.db`.
- Backing storage is GUID-based and stable across group rename: - Backing storage is GUID-based and stable across group rename:
@@ -16,6 +17,7 @@ This repository provides a production-oriented Samba file server container that
- Container hostname is fixed (`SAMBA_HOSTNAME`) to keep AD computer identity stable. - 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). - 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. - Setup prompts for well-known authorization groups by SID (`DOMAIN_USERS_SID`, `DOMAIN_ADMINS_SID`) to avoid localized group names.
- `FSLOGIX_GROUP_SID` controls who can access the default FSLogix share (defaults to `DOMAIN_USERS_SID`).
- 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`). - Private home creation skips well-known/service accounts by default (including `krbtgt`, `msol_*`, `FileShare_ServiceAcc`).
@@ -101,6 +103,7 @@ Kerberos requires close time alignment.
- `DOMAIN_USERS_SID` - `DOMAIN_USERS_SID`
- `DOMAIN_ADMINS_SID` - `DOMAIN_ADMINS_SID`
- optional `PUBLIC_GROUP_SID` (defaults to `DOMAIN_USERS_SID`) - optional `PUBLIC_GROUP_SID` (defaults to `DOMAIN_USERS_SID`)
- optional `FSLOGIX_GROUP_SID` (defaults to `DOMAIN_USERS_SID`)
Optional: Optional:
- `SAMBA_HOSTNAME` (defaults to `adsambafsrv`) - `SAMBA_HOSTNAME` (defaults to `adsambafsrv`)
@@ -149,6 +152,14 @@ Kerberos requires close time alignment.
- No guest access. - No guest access.
- Permissions are reconciled recursively so all descendants remain homogeneous (dirs `2770`, files `0660`, shared group/admin ACLs). - Permissions are reconciled recursively so all descendants remain homogeneous (dirs `2770`, files `0660`, shared group/admin ACLs).
### FSLogix
- Share: `\\server\FSLogix`
- Path: `/data/fslogix`
- Access for authenticated users in configurable `FSLOGIX_GROUP_SID` (default: `DOMAIN_USERS_SID`, resolved through winbind).
- Semantics intentionally differ from `Geteilt`: only the share root is reconciled (`03770` + ACL defaults), while user-created profile container folders/files are not recursively normalized.
- Samba masks are profile-container oriented (`create mask = 0600`, `directory mask = 0700`) so profile payload stays user-private by default.
### Dynamic Group Shares ### Dynamic Group Shares
- AD groups: `FileShare_*` and `FS_*` - AD groups: `FileShare_*` and `FS_*`

View File

@@ -115,9 +115,13 @@ resolve_share_groups_from_sids() {
export PUBLIC_GROUP export PUBLIC_GROUP
PUBLIC_GROUP="$(resolve_sid_to_group "$PUBLIC_GROUP_SID")" PUBLIC_GROUP="$(resolve_sid_to_group "$PUBLIC_GROUP_SID")"
export FSLOGIX_GROUP
FSLOGIX_GROUP="$(resolve_sid_to_group "$FSLOGIX_GROUP_SID")"
log "Resolved DOMAIN_USERS_SID to '${DOMAIN_USERS_GROUP}'" log "Resolved DOMAIN_USERS_SID to '${DOMAIN_USERS_GROUP}'"
log "Resolved DOMAIN_ADMINS_SID to '${DOMAIN_ADMINS_GROUP}'" log "Resolved DOMAIN_ADMINS_SID to '${DOMAIN_ADMINS_GROUP}'"
log "Resolved PUBLIC_GROUP_SID to '${PUBLIC_GROUP}'" log "Resolved PUBLIC_GROUP_SID to '${PUBLIC_GROUP}'"
log "Resolved FSLOGIX_GROUP_SID to '${FSLOGIX_GROUP}'"
} }
render_krb5_conf() { render_krb5_conf() {
@@ -156,9 +160,11 @@ write_runtime_env_file() {
printf 'export DOMAIN_USERS_SID=%q\n' "$DOMAIN_USERS_SID" printf 'export DOMAIN_USERS_SID=%q\n' "$DOMAIN_USERS_SID"
printf 'export DOMAIN_ADMINS_SID=%q\n' "$DOMAIN_ADMINS_SID" printf 'export DOMAIN_ADMINS_SID=%q\n' "$DOMAIN_ADMINS_SID"
printf 'export PUBLIC_GROUP_SID=%q\n' "$PUBLIC_GROUP_SID" printf 'export PUBLIC_GROUP_SID=%q\n' "$PUBLIC_GROUP_SID"
printf 'export FSLOGIX_GROUP_SID=%q\n' "$FSLOGIX_GROUP_SID"
printf 'export DOMAIN_USERS_GROUP=%q\n' "$DOMAIN_USERS_GROUP" printf 'export DOMAIN_USERS_GROUP=%q\n' "$DOMAIN_USERS_GROUP"
printf 'export DOMAIN_ADMINS_GROUP=%q\n' "$DOMAIN_ADMINS_GROUP" printf 'export DOMAIN_ADMINS_GROUP=%q\n' "$DOMAIN_ADMINS_GROUP"
printf 'export PUBLIC_GROUP=%q\n' "$PUBLIC_GROUP" printf 'export PUBLIC_GROUP=%q\n' "$PUBLIC_GROUP"
printf 'export FSLOGIX_GROUP=%q\n' "$FSLOGIX_GROUP"
if [[ -n "${JOIN_USER:-}" ]]; then if [[ -n "${JOIN_USER:-}" ]]; then
printf 'export JOIN_USER=%q\n' "$JOIN_USER" printf 'export JOIN_USER=%q\n' "$JOIN_USER"
fi fi
@@ -222,9 +228,11 @@ require_env DOMAIN_ADMINS_SID
export REALM WORKGROUP DOMAIN export REALM WORKGROUP DOMAIN
export PUBLIC_GROUP_SID="${PUBLIC_GROUP_SID:-${DOMAIN_USERS_SID}}" export PUBLIC_GROUP_SID="${PUBLIC_GROUP_SID:-${DOMAIN_USERS_SID}}"
export FSLOGIX_GROUP_SID="${FSLOGIX_GROUP_SID:-${DOMAIN_USERS_SID}}"
export DOMAIN_USERS_GROUP="${DOMAIN_USERS_SID}" export DOMAIN_USERS_GROUP="${DOMAIN_USERS_SID}"
export DOMAIN_ADMINS_GROUP="${DOMAIN_ADMINS_SID}" export DOMAIN_ADMINS_GROUP="${DOMAIN_ADMINS_SID}"
export PUBLIC_GROUP="${PUBLIC_GROUP_SID}" export PUBLIC_GROUP="${PUBLIC_GROUP_SID}"
export FSLOGIX_GROUP="${FSLOGIX_GROUP_SID}"
if [[ -n "${JOIN_USER:-}" ]]; then if [[ -n "${JOIN_USER:-}" ]]; then
export JOIN_USER export JOIN_USER
fi fi
@@ -232,7 +240,7 @@ if [[ -n "${JOIN_PASSWORD:-}" ]]; then
export JOIN_PASSWORD export JOIN_PASSWORD
fi fi
mkdir -p /data/private /data/public /data/groups /state /etc/samba/generated /var/log/samba mkdir -p /data/private /data/public /data/fslogix /data/groups /state /etc/samba/generated /var/log/samba
touch /etc/samba/generated/shares.conf /var/log/reconcile.log touch /etc/samba/generated/shares.conf /var/log/reconcile.log
append_winbind_to_nss append_winbind_to_nss

View File

@@ -20,6 +20,7 @@ LOCK_PATH = "/state/reconcile.lock"
GROUP_ROOT = "/data/groups" GROUP_ROOT = "/data/groups"
PRIVATE_ROOT = "/data/private" PRIVATE_ROOT = "/data/private"
PUBLIC_ROOT = "/data/public" PUBLIC_ROOT = "/data/public"
FSLOGIX_ROOT = "/data/fslogix"
GENERATED_CONF = "/etc/samba/generated/shares.conf" GENERATED_CONF = "/etc/samba/generated/shares.conf"
LDAP_FILTER = ( LDAP_FILTER = (
@@ -715,6 +716,51 @@ def sync_public_directory() -> None:
log(f"Unable to resolve GID for {group_display}; public ACLs unchanged") log(f"Unable to resolve GID for {group_display}; public ACLs unchanged")
def sync_fslogix_directory() -> None:
workgroup = os.environ["WORKGROUP"]
fslogix_group = os.getenv("FSLOGIX_GROUP", "")
fslogix_group_sid = os.getenv("FSLOGIX_GROUP_SID", "")
qualified_group = fslogix_group
os.makedirs(FSLOGIX_ROOT, exist_ok=True)
gid = None
if qualified_group:
gid = resolve_group_gid_flexible(workgroup, qualified_group)
if gid is None and fslogix_group_sid:
gid = resolve_gid_from_sid(fslogix_group_sid)
if gid is None:
group_display = qualified_group or fslogix_group_sid or "<unset>"
log(f"Unable to resolve GID for {group_display}; fslogix ACLs unchanged")
return
admin_group = os.getenv("DOMAIN_ADMINS_GROUP", "")
admin_gid = None
if admin_group:
admin_gid = resolve_group_gid_flexible(workgroup, admin_group)
if admin_gid is None:
admin_gid = resolve_gid_from_sid(os.getenv("DOMAIN_ADMINS_SID", ""))
os.chown(FSLOGIX_ROOT, 0, gid)
os.chmod(FSLOGIX_ROOT, 0o3770)
run_command(["setfacl", "-b", FSLOGIX_ROOT], check=False)
acl_entries = [f"g:{gid}:rwx", f"d:g:{gid}:rwx"]
if admin_gid is not None and admin_gid != gid:
acl_entries.append(f"g:{admin_gid}:rwx")
acl_entries.append(f"d:g:{admin_gid}:rwx")
result = run_command(
["setfacl", "-m", ",".join(acl_entries), FSLOGIX_ROOT], check=False
)
if result.returncode != 0:
log(
"setfacl failed for fslogix root: "
f"{result.stderr.strip() or result.stdout.strip()}"
)
def sync_private_directories() -> None: def sync_private_directories() -> None:
workgroup = os.environ["WORKGROUP"] workgroup = os.environ["WORKGROUP"]
admin_group = os.getenv("DOMAIN_ADMINS_GROUP", "") admin_group = os.getenv("DOMAIN_ADMINS_GROUP", "")
@@ -800,6 +846,7 @@ def with_lock() -> bool:
conn.close() conn.close()
sync_public_directory() sync_public_directory()
sync_fslogix_directory()
sync_private_directories() sync_private_directories()
reload_samba() reload_samba()
log("Reconciliation completed") log("Reconciliation completed")

View File

@@ -15,6 +15,7 @@ services:
volumes: volumes:
- private_data:/data/private - private_data:/data/private
- public_data:/data/public - public_data:/data/public
- fslogix_data:/data/fslogix
- group_data:/data/groups - group_data:/data/groups
- state_data:/state - state_data:/state
- samba_lib_data:/var/lib/samba - samba_lib_data:/var/lib/samba
@@ -28,6 +29,7 @@ services:
volumes: volumes:
private_data: private_data:
public_data: public_data:
fslogix_data:
group_data: group_data:
state_data: state_data:
samba_lib_data: samba_lib_data:

View File

@@ -71,3 +71,22 @@
directory mask = 2770 directory mask = 2770
inherit permissions = yes inherit permissions = yes
access based share enum = yes access based share enum = yes
[FSLogix]
path = /data/fslogix
read only = no
browseable = yes
guest ok = no
vfs objects = acl_xattr full_audit
full_audit:prefix = %T|%u|%I|%m|%S
full_audit:success = all
full_audit:failure = all
full_audit:syslog = false
valid users = @"${FSLOGIX_GROUP}"
force group = "${FSLOGIX_GROUP}"
create mask = 0600
directory mask = 0700
force create mode = 0600
force directory mode = 0700
hide unreadable = yes
access based share enum = yes

10
setup
View File

@@ -84,11 +84,13 @@ write_env_file() {
local domain_users_sid="" local domain_users_sid=""
local domain_admins_sid="" local domain_admins_sid=""
local public_group_sid="" local public_group_sid=""
local fslogix_group_sid=""
local samba_hostname="adsambafsrv" local samba_hostname="adsambafsrv"
local netbios_name="ADSAMBAFSRV" local netbios_name="ADSAMBAFSRV"
local service_password="" local service_password=""
local service_account_sam="" local service_account_sam=""
local public_group_prompt="" local public_group_prompt=""
local fslogix_group_prompt=""
local samba_hostname_input="" local samba_hostname_input=""
local netbios_name_input="" local netbios_name_input=""
local sanitized_netbios_name="" local sanitized_netbios_name=""
@@ -107,6 +109,12 @@ write_env_file() {
public_group_sid="$domain_users_sid" public_group_sid="$domain_users_sid"
fi fi
fslogix_group_prompt="FSLOGIX_GROUP_SID (press Enter to reuse DOMAIN_USERS_SID)"
read -r -p "${fslogix_group_prompt}: " fslogix_group_sid
if [[ -z "$fslogix_group_sid" ]]; then
fslogix_group_sid="$domain_users_sid"
fi
read -r -p "SAMBA_HOSTNAME [adsambafsrv]: " samba_hostname_input read -r -p "SAMBA_HOSTNAME [adsambafsrv]: " samba_hostname_input
if [[ -n "${samba_hostname_input:-}" ]]; then if [[ -n "${samba_hostname_input:-}" ]]; then
samba_hostname="$samba_hostname_input" samba_hostname="$samba_hostname_input"
@@ -158,6 +166,7 @@ SERVICE_ACCOUNT_PASSWORD=${service_password}
DOMAIN_USERS_SID=${domain_users_sid} DOMAIN_USERS_SID=${domain_users_sid}
DOMAIN_ADMINS_SID=${domain_admins_sid} DOMAIN_ADMINS_SID=${domain_admins_sid}
PUBLIC_GROUP_SID=${public_group_sid} PUBLIC_GROUP_SID=${public_group_sid}
FSLOGIX_GROUP_SID=${fslogix_group_sid}
SAMBA_HOSTNAME=${samba_hostname} SAMBA_HOSTNAME=${samba_hostname}
NETBIOS_NAME=${netbios_name} NETBIOS_NAME=${netbios_name}
EOF EOF
@@ -209,6 +218,7 @@ JOIN_PASSWORD=${service_password}
DOMAIN_USERS_SID=${domain_users_sid} DOMAIN_USERS_SID=${domain_users_sid}
DOMAIN_ADMINS_SID=${domain_admins_sid} DOMAIN_ADMINS_SID=${domain_admins_sid}
PUBLIC_GROUP_SID=${public_group_sid} PUBLIC_GROUP_SID=${public_group_sid}
FSLOGIX_GROUP_SID=${fslogix_group_sid}
SAMBA_HOSTNAME=${samba_hostname} SAMBA_HOSTNAME=${samba_hostname}
NETBIOS_NAME=${netbios_name} NETBIOS_NAME=${netbios_name}
# Optional overrides: # Optional overrides: