[2026-09-23] eagle: family/how-to/truenas-access.md personal/tech/truenas-nfs4-acl-and-arrmultiuser.md
This commit is contained in:
1 parent
564e052294
commit
a3f1cb3ab8
2 files changed
+161
-1
No files matched your search
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: TrueNAS NFS4 ACL repair + cross-container media PUID
|
||||
created: 2026-09-02
|
||||
updated: 2026-09-22
|
||||
updated: 2026-09-23
|
||||
type: tech
|
||||
namespace: personal
|
||||
tags: [truenas, nfs4, acl, midclt, arr, radarr, sonarr, jellyfin, transmission, docker, git]
|
||||
@@ -222,6 +222,78 @@ Working fix = **full dacl replacement** (not per-ACE edit) with **inheritance fl
|
||||
|
||||
**Vault topology (for future sessions):** one bare repo on TrueNAS is the single hub. Mac pushes/pulls remote **`nas`**; Kraken uses remote **`origin`** — both point at `ssh://truenas_admin@mallexxx.duckdns.org/mnt/RED_2TB/storage/git/obsidian-vault.git`. Mac and Kraken never talk directly. Host `truenas` does **not** resolve as an SSH alias from the Mac — use `truenas_admin@mallexxx.duckdns.org`.
|
||||
|
||||
## Sharing a whole tree by GROUP (`nas_users`) — the simplest fix (2026-09-23)
|
||||
|
||||
**Trigger:** a directory tree inside a dataset is unreachable — e.g. `/mnt/RED_2TB/Edu` and
|
||||
`/mnt/RED_2TB/Edu/Маскелиаде` gave "no access" both via WebDAV and via SSH as `truenas_admin`,
|
||||
although `getacl` showed a plain ALLOW.
|
||||
|
||||
**When to prefer this over per-file/uid-950 recipes above:** the requirement is "make the whole
|
||||
tree accessible to a set of humans/services", not "make one container read another's files".
|
||||
One `dacl` write covers everything — no per-uid chown of thousands of files.
|
||||
|
||||
🔴 **THE key insight — `group@` in NFSv4 is the file's POSIX gid, not a standalone ACE.**
|
||||
Setting a recursive `setacl` with `group@ ALLOW + INHERIT` **does not grant anything by itself**:
|
||||
`group@` resolves to each object's own `gid`. If the gid is still `0` (root) or `921`, the ACE
|
||||
grants the *wrong* group. **The `gid` must be changed too.**
|
||||
|
||||
Second trap: **`chgrp -R` as `truenas_admin` FAILS** with `Operation not permitted` on every file
|
||||
(uid 950 is neither the owner nor in gid 0 — same limitation as the 2026-09-21 repo fix). So the
|
||||
gid change must go through midclt (`filesystem.setacl` carries `uid`/`gid` and runs the recursive
|
||||
job as root). Plan for **two passes**: pass 1 = ACL with INHERIT, pass 2 = `setacl` with the new
|
||||
`gid` (and `uid` passed through unchanged).
|
||||
|
||||
**Choose the group by membership, not by guess:**
|
||||
```bash
|
||||
id truenas_admin
|
||||
# uid=950(truenas_admin) gid=950(truenas_admin)
|
||||
# groups=950(truenas_admin),544(builtin_administrators),921(transmission),
|
||||
# 999(docker),1000(nas),3000(nas_users)
|
||||
```
|
||||
`nas_users` (gid **3000**) contains `root`, `truenas_admin`, `mallexxx`, `nas`, `transmission` —
|
||||
one ACE reaches all of them. Backup-target trees on this NAS already use `nas`/`nas_users`.
|
||||
|
||||
**Working recipe (VM-verified 2026-09-23 on `/mnt/RED_2TB/Edu`):**
|
||||
|
||||
```python
|
||||
# pass 1 — ACL: set INHERIT on ALL ACEs of the tree root (read-modify-write, keep id)
|
||||
cur = getacl(PATH) # id is already populated — never build dacl from scratch
|
||||
for ace in cur["acl"]:
|
||||
ace["type"] = "ALLOW" # group@ must be ALLOW
|
||||
if ace["tag"] == "group@":
|
||||
ace["perms"] = { …, "READ_DATA":True,"WRITE_DATA":True,"EXECUTE":True,
|
||||
"APPEND_DATA":True,"DELETE_CHILD":True,"DELETE":True,
|
||||
"WRITE_ACL":True,"WRITE_OWNER":True } # full grant
|
||||
ace["flags"] = {"BASIC": "INHERIT"} # STRING, not a boolean dict
|
||||
setacl({"path":PATH,"dacl":cur["acl"],
|
||||
"options":{"recursive":True,"traverse":True,"stripacl":False}}) # poll job → SUCCESS
|
||||
|
||||
# pass 2 — gid: without this the group@ ACE matches the OLD group and grants nothing
|
||||
setacl({"path":PATH,"uid":cur["uid"],"gid":3000,"acltype":"NFS4","dacl":cur["acl"],
|
||||
"options":{"recursive":True,"traverse":True,"stripacl":False}})
|
||||
```
|
||||
|
||||
**Verification (all three):**
|
||||
```bash
|
||||
ls -ld /mnt/RED_2TB/Edu # drwxrwxr-x root nas_users ← gid showed up
|
||||
ls /mnt/RED_2TB/Edu/ # contents now list as truenas_admin
|
||||
touch /mnt/RED_2TB/Edu/.wrtest && echo WRITE_OK && rm -f /mnt/RED_2TB/Edu/.wrtest
|
||||
midclt call filesystem.getacl "<child dir>" # every ACE must show INHERITED: True + gid 3000
|
||||
```
|
||||
Observed: `Edu` → `drwxrwxr-x root:nas_users`, `gid=3000`; children `Маскелиаде`,
|
||||
`Книги, учебники` → `gid=3000` with `INHERITED: True` on all three ACEs; `WRITE_OK`.
|
||||
Pre-change ACL backed up to `/mnt/RED_2TB/storage/acl-edu-before-<ts>.json`.
|
||||
|
||||
> 📌 **Cyrillic directory names inside shell commands.** `.../Edu/Маскелиаде` tripped the security
|
||||
> scanner as `confusable_text` (HIGH) twice, each time blocking the command. Wrap such paths in
|
||||
> double quotes and never let a Cyrillic word touch an ASCII word; where possible use `getacl` with
|
||||
> the path as a bare string or `find -iname` patterns instead of typing the name by hand.
|
||||
|
||||
**Mac-side corollary — WebDAV was never the blocker.** The `webdav` container runs **as root**
|
||||
(`docker top webdav` → `root … webdav --config /config.yml`), and `owner@` was ALLOW, so the ACL
|
||||
never denied it. Container `ls`/`id` are unavailable (`hacdias/webdav` is a distroless image —
|
||||
`exec: "ls": executable file not found`), so use `docker top` to learn its uid.
|
||||
|
||||
## Docker network persistency (per-service compose)
|
||||
- `docker network connect <net> <container>` is LOST on recreate → declare `<net>` in the service's compose `networks:`.
|
||||
- Caddy must co-own a network with its reverse-proxy target. transmission Web UI works because caddy is ALSO in `transmission_default`; radarr↔transmission works because both in `media_net`.
|
||||
|
||||
Reference in new issue
Block a user