2026 Guide: Samba + Native ZFS Windows ACLs in an Unprivileged Proxmox LXC

MrPete

Well-Known Member
Aug 6, 2021
131
68
48
68
Part 1 - too long to fit in one post.

A Definitive Guide: High-Performance Samba + Native ZFS Windows ACLs in an Unprivileged Proxmox LXC

This blueprint configures:
  • a standalone Samba file server running
  • inside an unprivileged Proxmox LXC container while
  • storing Windows NT ACLs directly inside high-performance OpenZFS inodes (xattr=sa).
By bridging the Linux User Namespace, Samba’s user-space metadata mechanisms, and OpenZFS VFS behaviors, this setup avoids kernel-level privilege errors (EPERM) and connection timeouts without sacrificing the container security boundary. I have had NO issues using the Security->Advanced tab in Windows to set up various user permissions.

Variable Placeholders Used in This Guide (with examples from my setup)

  • {smbpool}: The name of your ZFS storage pool on the host system, including the top dataset. (e.g. bfpool/smb)
  • {CTID}: The numeric ID of your Proxmox LXC container (e.g., 101).
  • {slxc}: The hostname of your Linux container. (eg "fern")
  • {smbmnt}: The base mount directory path inside the LXC container (e.g., /bfp).
  • {share1}, {share2}: names of individual ZFS datasets / SMB network shares. (see sample below)
  • {mywkgrp}: Your Windows network Workgroup name. (default is WORKGROUP)
  • {szuser} / {szgrp}: The storage user/group identity that owns the files. (you can have more than one ;) )
  • {szuid} / {szgid}: The explicit host-side integer numeric UID / GID assigned to the storage user/group.

0. Crucial Pre-Setup Architectural Decisions

Choose UID/GID's to own your datasets​

In an unprivileged Proxmox LXC container, UID 0 (root) inside the container is shifted by default to UID 100000 on the host system.
Samba relies on a privilege escalation tool (become_root) to atomically modify file metadata. If you attempt a naive bind-mount deployment, the host kernel intercepts these root calls as an unprivileged user (100000), blocking modifications to the file metadata and throwing an Operation not permitted (EPERM) error.
To bypass this safely, we use a segmented User Namespace layout:
  • System service and other uid's: All UID's/GID's from 0 (root) to the end -- other than those that will own your datasets, remain secure by being mapped to the host's unprivileged subordinate block (100000+). This maintains container isolation. (Note: mapping root 1:1 to the host causes this to become a privileged container. Avoiding that is a key attribute of this Guide!
  • Data Storage Space: Your user(s) that owns the dataset(s) ({szuid}) maps 1:1 directly to the host system. (You can have more than one of these.)

ZFS System Attribute & Authentication Reality

The ZFS Bypass: Because OpenZFS maps extended attributes using System Attributes (xattr=sa), metadata updates write straight into internal dataset inodes. This entirely bypasses the standard Linux VFS directory engine, allowing an unprivileged container to process user.NTACL streams without requiring zfs xattr capabilities. (ntfsv4 zfs support is coming, but not available yet other than on freebsd etc.)
Standalone Server Authentication: If your network users mirror your local Unix accounts 1:1, Samba skips global identity allocation databases (idmap config). You can safely omit global ID mapping ranges if access is restricted to this dedicated storage user.
Permissions Context (chmod 775): Because we explicitly sever the POSIX permission sync with acl_xattr:ignore system acls = yes, Windows handles inheritance entirely in user space [3]. Reverting to standard Unix group inheritance configurations like chmod 2770 is redundant and can misdirect Windows metadata sync workers. A baseline of 775 provides the ideal uninhibited path for user-space metadata.

ZFS DataSet Simplified Architecture

A variety of settings apply to all, and/or various groups, of desired SMB shares.
Managing all of that can become painful when one has a number of shares.
One of the nicest ways to manage this is to configure the ZFS datasets in a nested folder tree. This guide assumes you’ll follow the same pattern.
Here’s a representation of my own set:
Pool: mypool​
Top level dataset: smb (not a share!)
Intermediate datasets: back, misc (not shares)
Leaf datasets:
/mypool/smb/back/bk1​
/mypool/smb/back/bk2​
/mypool/smb/misc/grpfiles​
/mypool/smb/misc/project1​
/mypool/smb/misc/project2​
/mypool/smb/misc/temp​
All of the above are recursively bound to the LXC using a single config line:

lxc.mount.entry: /mypool/smb {smbmnt} none rbind,create=dir,rw 0 0

With this setup, it’s also easy to add more shares at any time without rebooting the LXC.
The traditional method requires host set up for every dataset/share:
mp0: /{smbpool}, mp={smbmnt}
With this architecture, the placeholder host dataset is {smbpool} == mypool/smb

Assumptions before what follows​

This guide assumes you have already set up the following:
  • Created a ZFS pool, and subsidiary datasets to match your desired SMB shares. you may have pre-populated it with files.
  • The whole tree is chown to {szuser}:{szgrp} with Uid:Gid {szuid}:{szgid}b) (or some small known set of UID/GID.)
  • You have created a simple Proxmox unprivileged LXC, running debian 12 (for now), with backports available, with the ZFS pool rbind mapped. to properly configure the needed ZFS datasets, LXC *.conf .
  • LXC id is {CTID}, mount point is {smbmnt}. LXC name is {slxc}
  • You have installed but not fully configured SAMBA, latest backport version. Standalone server. Workgroup {mywkgrp}. Desired shares {share1} {share2}
  • The LXC is stopped at the outset (we have some host work to do!)
  • You have acl and xattr commands available (getfacl, getfattr, etc)

1. Pre-Flight Host Audit & Dataset Prep (Host CLI)

Before assigning storage pools or booting your container environment, you must audit the ZFS dataset tree to ensure no corrupt, malformed, or legacy permissions exist. Run these recursive diagnostic and cleaning commands on your Proxmox Host Terminal:

# 1. Audit: Scan the entire tree for extended attributes (these settings search for any/all such attributes)
getfattr -R -d -m '-' /{smbpool}
# 2. Audit: Check for existing POSIX ACL enforcement flags in the disk tree, beyond standard rwx permissions:
getfacl -cstR /{smbpool}
# 3. Clean xattr: If anything showed up in #1, recursively strip all user.NTACL and legacy blocks from the entire dataset
find /{smbpool} -type f -exec setfattr -x user.NTACL {} + 2>/dev/null
find /{smbpool} -type d -exec setfattr -x user.NTACL {} + 2>/dev/null
# 4. Clean ACLs: Strip legacy POSIX ACL structures down to Unix mode bits
setfacl -R -b /{smbpool}
# 5. Optimization: Force OpenZFS to record extended attributes directly inside file system inodes
zfs set xattr=sa {smbpool}
# 6. Optimization: Disable file access time writes to eliminate high-volume metadata amplification stalls
zfs set atime=off {smbpool}
# 7. Optimization: Expose internal snapshot trees so the shadow_copy2 module can locate timelines
zfs set snapdir=visible {smbpool}


2. Proxmox Host Mapping (/etc/pve/lxc/{CTID}.conf)

Open the container configuration file on the host system and map your explicit user and group numerical space using non-overlapping mathematical zones:

# Recursively bind-mount the parent ZFS dataset to the top level LXC folder
lxc.mount.entry: /mypool/smb {smbmnt} none rbind,create=dir,rw 0 0
# UID SEGMENTED MAPPING BOUNDS
lxc.idmap: u 0 100000 {szuid}
lxc.idmap: u {szuid} {szuid} 1
lxc.idmap: u {szuid+1} {100001+szuid} {65535-szuid}
# GID SEGMENTED MAPPING BOUNDS
lxc.idmap: g 0 100000 {szgid}
lxc.idmap: g {szgid} {szgid} 1
lxc.idmap: g {szgid+1} {100001+szgid} {65535-szgid}

3. Authorize Host Namespace Execution

The host system must explicitly trust the container to execute mappings inside these boundaries. Open /etc/subuid and /etc/subgid on the host terminal and append your exact identities and calculated remaining offsets:

# Append to /etc/subuid
root:{szuid}:1
root:{szuid+1}:{65535-szuid}
# Append to /etc/subgid
root:{szgid}:1
root:{szgid+1}:{65535-szgid}

4. LXC Environment Pre-Configuration (LXC CLI)

Start your container {slxc}, login, and execute the system account and authentication database initialization steps:

# 1. Create the matching system group using your exact host GID
groupadd -g {szgid} {szgrp}
# 2. Create the system user with shell execution disabled for network-only storage safety
useradd -u {szuid} -g {szgid} -s /usr/sbin/nologin -M {szuser}
# 3. Synchronize permissions on the root mount point container path
chown -R {szuser}:{szgrp} {smbmnt}
chmod 775 {smbmnt}
# 4. Initialize and register the account into the internal Samba SAM authentication database
smbpasswd -a {szuser}

(continued...)
 
  • Like
Reactions: UdoB
Part 2:

5. Streamlined smb.conf Architecture

Open /etc/samba/smb.conf inside the container. By exploiting Samba's internal macro overrides (where ignore system acls = yes automatically presets store dos attributes = yes, sets all map options to no, and clears creation masks to allow pure Windows inheritance) and moving share-invariant logic to the global tier, the configuration collapses to this clean layout [3]:

[global]
# Standard Server Identification
workgroup = {mywkgrp}
server string = Samba LXC Enterprise Storage
security = user
map to guest = Bad User
log file = /var/log/samba/log.%m
max log size = 1000

# =====================================================================
# High-Performance Metadata Core Stack
# =====================================================================
vfs objects = shadow_copy2 acl_xattr streams_xattr

# Redirect NT ACL storage away from kernel-blocked security.* to ZFS inodes
acl_xattr:security_acl_name = user.NTACL

# SEVER POSIX SYNC LOOP
# Implicitly forces: store dos attributes=yes, all map options=no,
# and opens create/directory masks completely to allow pure Windows inheritance.
# also, default to standard Windows default ACLs (owner and SYSTEM w/ Full Control)
acl_xattr:ignore system acls = yes
acl_xattr:default_acl_style = windows

# =====================================================================
# Transport Tuning (on our LAN, even 256KB/sec is 10% above this!)
# =====================================================================
socket options = TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=131072 SO_SNDBUF=131072

# =====================================================================
# Global OpenZFS Shadow Copy Parameters. This will work until year 2100.
# NOTE: for recursively bound (nested) zfs datasets, shadow:mountpoint is required for each share (below)
# =====================================================================
shadow:snapdir = .zfs/snapshot
shadow:basedir = /
shadow:crossmountpoints = no
shadow:format = -%Y-%m-%d-%H%M
shadow:delimiter = -20
shadow:sort = desc
shadow:localtime = no

# =====================================================================
# OPTIONAL Standalone Server UID Fallback (if no linux user is created)
# Not needed if all users are correctly mapped (smbpasswd) and/or you use AD/DC/etc
# =====================================================================
idmap config * : backend = tdb
idmap config * : range = 3000-7999

# =====================================================================
# Individual Share Definitions
# =====================================================================

[{share1}]
path = {smbmnt}/{share1}
# Path-specific anchor mapping (necessary for nested dataset architecture)
shadow:mountpoint = {smbmnt}/{share1}
writeable = yes
browseable = yes
guest ok = no

[{share2}]
path = {smbmnt}/{share2}
# Path-specific anchor mapping (necessary for nested dataset architecture)
shadow:mountpoint = {smbmnt}/{share2}

writeable = yes
browseable = yes
guest ok = no

6. Client Windows Registry Tuning (OPTIONAL)

⚠️ CRITICAL WARNING FOR MULTI-USER SHARE CONTEXTS: The 600-second (10-minute) caching registry entries below are optimized for single-user, dedicated high-volume nodes, such as for backup automation. If applied to an active multi-user file share, workstations will fail to see associated modifications made by other concurrent users for up to 10 minutes. For multi-user clusters, drop the values to 2 seconds or leave them at Windows defaults[4].

When Windows Explorer or automated backup pipelines process massive, deeply-nested directory trees over user-space Samba allocations (user.NTACL), the Windows SMB client driver (rdbss.sys) aggressively queries metadata hooks.[4]
To prevent network transport overhead from triggering client-side caching delays (and associated occasional errors due to timeout), run these commands in Windows PowerShell (Admin) on your backup master node to optimize metadata performance:

# 1. Extend the local SMB directory metadata cache lifetime (Default is 10s)
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\LanmanWorkstation\Parameters" -Name "DirectoryCacheLifetime" -Value 600 -Type DWord
# 2. Extend the file informational attribute cache window
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\LanmanWorkstation\Parameters" -Name "FileInfoCacheLifetime" -Value 600 -Type DWord
# 3. Extend the negative (Not Found) metadata item lookup cache window
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\LanmanWorkstation\Parameters" -Name "FileNotFoundCacheLifetime" -Value 600 -Type DWord
# 4. Disable bandwidth throttling for high-throughput local network storage streams
Set-ItemProperty -Path "HKLM:\System\CurrentControlSet\Services\LanmanWorkstation\Parameters" -Name "DisableBandwidthThrottling" -Value 1 -Type DWord

7. Verification Cycle

  • Run systemctl restart smbd inside the container.
  • Connect your Windows Backup software or Explorer client to the network path.
  • Apply a recursive permissions modification down the tree. (Useful example: modify owner permission to be Full Control, applied to This folder, subfolders and files; and checking Replace all child object permission... with inheritable…)
  • Verify the raw results in the LXC by executing the following command from the LXC Terminal:
getfattr -d -m ".*NTACL" {smbmnt}/{share1}/somefile.txt

Output will cleanly read: user.NTACL=0sBAA... proving that the structural permissions are safely embedded directly inside your high-performance ZFS dataset.

REFERENCES​

[^1] ZFS ARC Metadata Cache Optimization
Content: OpenZFS stores native metadata and system attribute blocks (xattr=sa) directly in the Adaptive Replacement Cache (ARC). This completely removes the random read penalty associated with traditional directory lookup streams.
Reference: OpenZFS Module Parameters - zfs_arc_meta_balance

[^2] Proxmox / LXC User Namespace Math Mapping
Content: Proxmox LXC containers leverage Linux kernel user namespaces to isolate execution spaces. Subordinate ID mappings isolate boundaries by offsetting container boundaries to host slots starting at 100000. The link also explains the math used in UID / GID mapping.
Reference: Proxmox Wiki - Unprivileged Containers

[^3] Samba vfs_acl_xattr Behavioral Defaults
Content: Setting acl_xattr:ignore system acls = yes explicitly disconnects the Windows client from touching the underlying Linux POSIX permission flags (chmod/setfacl), dropping execution fallback errors. My assumption in this guide is a desire for maximum Windows compatibility.
Reference: Samba.org vfs_acl_xattr man page

[^4] Microsoft SMB Client Metadata Caching Windows
Content: Microsoft's LanmanWorkstation redirector governs client-side metadata caching behaviors (DirectoryCacheLifetime, FileInfoCacheLifetime). Increasing these intervals beyond 2 seconds directly drops multi-workstation concurrency views.
Reference: Microsoft Learn - SMB Client Caching Settings

[^5] GitHub-Flavored Markdown Alerts & Callouts
Content: The syntax > [!WARNING] is the standardized notation for building alert callout banners across GitHub and modern Markdown parsers.
Reference: GitHub Docs - Basic Writing and Formatting Syntax (Alerts)