| 1 | # Storage ACL cutover |
| 2 | |
| 3 | Zenith mounts `storage1/clover` and `storage1/media` with `nfs4acl`. Their NFSv4 ACLs are nontrivial: Clover grants `group:apps` and `user:1000`; Media grants `user:clo` and TrueNAS built-in groups. Media has 2,431 directories and 28,729 files. Of those, 543 directories and 12,885 files are `root:root` with mode `770`, so UID 3000 relies on the NFSv4 ACL to reach them. |
| 4 | |
| 5 | [Stock OpenZFS on Linux does not enforce NFSv4 ACLs](https://openzfs.github.io/openzfs-docs/Basic%20Concepts/Datasets/ACLs.html). The NixOS VM accepted `acltype=nfsv4` on a disposable dataset but rejected `setfacl` and denied UID 3000 access to a root-owned `770` directory. Setting `acltype=posixacl` and granting UID 3000 access made the same check pass. A property value of `nfsv4` alone is therefore insufficient proof of access on NixOS. |
| 6 | |
| 7 | `tools/studio.py pool` rejects a mounted Clover or Media ZFS dataset with `acltype=nfsv4` before provisioning service volumes. It also rejects NFSv4 ACLs on the production or staging dataset parent, including values inherited from the pool; a disposable pool proved all three cases and accepted POSIX ACLs. Promotion runs the Clover/Media and production preflight before switching `/opt/studio/current`; a disposable NFSv4 dataset caused promotion to fail while its release link and history stayed unchanged. |
| 8 | |
| 9 | The media rename and mountpoint change in [media-cutover.md](media-cutover.md) must be paired with a permission migration. Rehearse it on copy-on-write clones of both datasets, leaving the originals and their snapshots intact. A shared numeric group `3000` with group read/write and setgid directories matches the VM's working Clover, Samba, and Copyparty permissions; a clone test must also verify media writers and readers before choosing that policy for the real tree. Changing `acltype` does not translate existing NFSv4 entries into POSIX ACLs, and [TrueNAS warns against recursive ACL changes without a snapshot](https://www.truenas.com/docs/scale/datasets/permissions/permissions/). |
| 10 | |
| 11 | A disposable POSIX-ACL ZFS fixture started with root-owned `0770` directories and a `0660` file that UID 3000 could not read. Granting named group 3000 access and a default ACL on directories, without changing owners, let UID 3000 and Jellyfin UID 3106 read the file. qBittorrent UID 3114 created a file that Copyparty UID 3116 could append to and UID 3000 could edit. The new file kept group 0 from its setgid parent, but inherited the named group 3000 ACL; the fixture was destroyed. This proves local service access on representative modes; SMB has a separate limitation below. |
| 12 | |
| 13 | A macOS SMB mount reached the VM over Tailscale and wrote a file as `3000:3000` with mode `0644`, despite Samba's `force create mode = 0660`; Copyparty's group identity could not edit it. A temporary Samba configuration with `acl_xattr` and `acl_xattr:ignore system acls = yes` yielded `0666`. Adding a parent default POSIX ACL (`user::rwx,group::rwx,other::---`) yielded files at `0660`, but Mac-created directories at `2777`. Adding `inherit permissions = yes` yielded directories at `2770` and files at `0660`; Copyparty's UID 3116/GID 3000 then appended to a Mac-created file and created a sibling in its directory. `inherit permissions` and the default ACL without `acl_xattr` instead yielded `0755` directories and `0644` files. The tested Samba settings are now in `service/samba/service.pkl`, retaining the image's `catia fruit streams_xattr` Mac VFS modules. A disposable write through the staged Samba share again produced a `0660` file and `2770` directory, both writable by UID 3116/GID 3000; the fixture was removed. VM promotion `b1a2cdde0f6f12a4` passed all 19 HTTP routes and kept four PostgreSQL dumps in its pre-switch backup. The real dataset cutover still needs inherited POSIX ACLs applied through writable directories and verified on clones. [Samba documents the `acl_xattr` behavior](https://www.samba.org/samba/docs/current/man-html/vfs_acl_xattr.8.html). |
| 14 | |
| 15 | `acl_xattr:ignore system acls = yes` ignores named POSIX ACL grants for SMB permission checks. A staged Samba version without `acl_xattr` let a Linux SMB client write root-owned files through group ACLs, but a macOS mount then created `0644` files and `0755` directories that Copyparty could not edit. That version was rolled back. With the current Samba settings, a Mac mount created `0660` files and `2770` directories in a group-owned `3000` fixture, and Copyparty could append. The current configuration therefore needs group ownership as well as ACLs on a production clone rehearsal; the local UID test alone does not establish Finder access. |
| 16 | |
| 17 | A later disposable Samba test changed only `acl_xattr:ignore system acls` to `no`. In a root-owned, group-0 directory with a named/default POSIX ACL for group 3000, a macOS SMB mount created a file and directory, overwrote an existing root-owned file, and Copyparty UID 3116/GID 3000 appended to the new file. This worked both with and without `inherit permissions`. The new file and directory were `3000:3000` mode `0770`; the existing file retained root ownership. This offers a way to avoid changing ownership across the existing tree, but the executable bits on new regular files need review. The disposable dataset and container were removed, and the managed Samba job was restored without changing its configuration. |
| 18 | |
| 19 | An isolated Samba container on port 1445 was reachable from macOS through an SSH tunnel using `mount_smbfs //clo@127.0.0.1:1445/clover`; the managed share stayed online. In that fixture, disabling DOS archive/hidden/system mode mapping and then tightening `create mask` to `0660`, `directory mask` to `0770`, and `acl map full control` to `no` still produced `0770` Finder files and directories. Copyparty could append. The execute bits therefore appear to come from the ACL mapping in this combination, not the tested mode masks. The isolated container, dataset, and tunnel were removed. |
| 20 | |
| 21 | The same isolated setup without `acl_xattr` and with `inherit permissions = no` let Finder overwrite an existing root-owned file through its named ACL. New Finder files were `0644` and directories `0755` despite Samba's forced `0660`/`2770` modes, and the new file's inherited group ACL was masked to read-only; Copyparty could not append. That configuration cannot satisfy shared editing without a further permission mechanism. The fixture was removed. |
| 22 | |
| 23 | With Samba's default oplocks, a Mac SMB mount returned stale bytes and NULs immediately after Copyparty appended to a mounted file; remounting showed the correct server data. A disposable Samba container with `oplocks = no` returned the correct bytes immediately in the same test. The managed Samba job was restored afterward. Disabling oplocks may reduce SMB client caching performance and has not been promoted. |
| 24 | |
| 25 | Zenith's `storage1` pool root is unencrypted; `storage1/apps`, `storage1/clover`, and `storage1/media` are separate AES-256-GCM encryption roots with `keyformat=hex` and `keylocation=prompt`. Creating `storage1/prod` beneath the pool root without encryption would silently expose new app data. Snow Globe now requires an encrypted `prod` dataset mounted at `/srv/prod` whenever Clover or Media is encrypted. A disposable encrypted Clover dataset failed this preflight against the VM's unencrypted demo `prod`. A fresh disposable pool with separate encrypted Clover and `prod` roots passed: the `shale` service dataset inherited `prod` as its encryption root. The current VM predates the mounted-parent change and still has `studio-demo/prod` with `mountpoint=none`; its service children are mounted individually. |
| 26 | |
| 27 | The real host must load the selected keys before mounting datasets and starting Nomad; [loading a key does not itself mount the dataset](https://openzfs.github.io/openzfs-docs/man/v2.2/8/zfs-load-key.8.html). Keep keys out of the repository. A separate VM test confirmed that renaming one independent encrypted root beneath another preserves its independent encryption root. |
| 28 | |
| 29 | Copyparty's previous `df: 16` reserve rejected every upload on the 8 GB VM pool. Its prepared config now reserves one quarter of the dataset capacity, capped at the original 16 GB. The staged and production VM instances accepted a file and directory upload; the resulting `0664` file and setgid `2775` directory were writable by Clover's separate UID/GID 3000. Test files were removed, the Source of Truth preview was restarted, and the Copyparty preview was stopped to return the VM's reserved memory. Promotion `ec5322fadd8ae7a8` passed all 19 HTTP routes with 22 ZFS snapshots and four PostgreSQL dumps in its backup. |
| 30 | |
| 31 | Snow Globe's `zfs clone` of a disposable encrypted Media source stayed AES-256-GCM encrypted under an unencrypted staging parent and shared the Media encryption root; writes to the clone left the source unchanged. [OpenZFS specifies that clones always share their origin's encryption key](https://openzfs.github.io/openzfs-docs/man/master/7/zfsprops.7.html). Fresh stages have no encrypted origin to inherit, so `tools/studio.py stage` now requires an encrypted `pool/staging` mounted at `/srv/staging` when `pool/prod` is encrypted. A disposable pool proved missing and unencrypted staging parents are rejected, while an encrypted parent passes. The VM's existing unencrypted pool still stages normally. |
| 32 | |
| 33 | `storage1/apps` is one encrypted dataset with no child datasets: 47.8 GiB live and 65.3 GiB held by 207 snapshots at inspection. It also contains stacks excluded from Snow Globe. Renaming it to `storage1/prod` would preserve that history but leave the service folders as ordinary directories; moving selected data into per-service child datasets would still be necessary. Creating a separate encrypted `prod` root keeps the old tree and its snapshots available during migration. |