archive-kanban: RET-11 Nachtrag - chattr +i EPERM ist Infrastrukturgrenze, kein Code-Bug

Reale Reproduktion auf Host 204 (chattr, roher FS_IOC_SETFLAGS-Ioctl,
Gegenprobe auf tmpfs im selben Container) zeigt: Ursache ist der
unprivilegierte LXC-Container (capable(CAP_LINUX_IMMUTABLE) prueft
immer gegen den Host-Namespace, nicht gegen den Container), nicht ZFS
wie bisher in RET-11-PRUEFPROTOKOLL.md vermutet - betrifft jedes
Dateisystem im Container. Acht geprueften Alternativen (ZFS readonly/
hold, ACL/chmod, Proxmox-Features, Capabilities, verschachtelte
User-Namespaces, FUSE, externes Object-Lock-Backend) sind im
Container-Scope entweder nicht verfuegbar oder strukturell keine
vollwertige Entsprechung. Keine Aenderung an wormstore.go, da der
bestehende Fail-Closed-Mechanismus (ImmutableLockSupported) bereits
korrekt reagiert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
patrickandClaude Sonnet 5 authored and root committed 2026-10-16 18:58:54 +00:00
1 parent dac7fff504
commit 07a4b904c4
1 file changed
+180
@@ -0,0 +1,180 @@
# RET-11 – Nachtrag: Untersuchung `chattr +i` auf Host 204/131 (unprivilegierte LXC-Container)
Bezug: PR #309 (QA-02), #310 (QA-07), #311 (QA-05) sowie
`RET-11-PRUEFPROTOKOLL.md` (Host 131) haben wiederholt denselben Befund
dokumentiert: `lockImmutable`/`Put` in
`archive/internal/wormstore/wormstore.go` schlägt auf Host 204 und 131
mit `EPERM` fehl. Dieser Nachtrag klärt abschließend, ob das ein
behebbarer Code-/Konfigurationsfehler ist oder eine echte
Infrastrukturgrenze — damit das nicht in jedem weiteren PR erneut
untersucht werden muss.
**Ergebnis vorweg: echte Infrastrukturgrenze, kein Code-Bug. Keine
Änderung an `wormstore.go` vorgenommen.** Die bisherige Begründung in
`RET-11-PRUEFPROTOKOLL.md` ("ZFS … kein FS_IOC_SETFLAGS-Immutable-
Support in dieser Kombination") wird hier präzisiert/korrigiert: Die
Ursache ist **nicht ZFS**, sondern dass Host 204/131 **unprivilegierte**
LXC-Container sind. Das betrifft jedes Dateisystem darunter (ext4, XFS,
Btrfs eingeschlossen), siehe Prüfung 3 unten.
## Vorgehen
Frischer Klon auf Host 204 nach `/root/archive-worm-check-1792155880`
(Testartefakte nach Abschluss entfernt, keine Produktivdaten
berührt — nur synthetische Testdateien wie `probe.txt`).
## Prüfung 1: Reproduktion des Fehlers
```
$ echo "synthetic-test-content" > probe.txt
$ chattr +i probe.txt
chattr: Operation not permitted while setting flags on probe.txt
$ echo $?
1
```
Bestätigt: identischer Fehler wie in PR #309/#310/#311 und
`RET-11-PRUEFPROTOKOLL.md` dokumentiert.
## Prüfung 2: Ist es wirklich ZFS?
```
$ df -T /
Filesystem Type
rpool/data/subvol-1204-disk-0 zfs
$ mount | grep zfs
rpool/data/subvol-1204-disk-0 on / type zfs (rw,relatime,xattr,posixacl,casesensitive)
rpool/data/subvol-1204-disk-1 on /var/archivdms type zfs (...)
```
Host 204 ist tatsächlich ein LXC-Container mit ZFS-Root
(`rpool/data/subvol-1204-disk-0`), wie in den PRs beschrieben.
## Prüfung 3: Container-Privileg statt Dateisystem als eigentliche Ursache
```
$ cat /proc/self/uid_map
0 100000 65536
$ cat /proc/self/gid_map
0 100000 65536
```
→ Host 204 ist ein **unprivilegierter** Container (UID 0 im Container
= UID 100000 auf dem Proxmox-Host, eigener User-Namespace).
Gegenprobe auf einem NICHT-ZFS-Dateisystem **im selben Container**
(tmpfs, `/dev/shm`):
```
$ echo hi > /dev/shm/probe.txt
$ chattr +i /dev/shm/probe.txt
chattr: Operation not permitted while setting flags on /dev/shm/probe.txt
```
Derselbe `EPERM` — obwohl tmpfs nichts mit ZFS zu tun hat. Das
widerlegt die These "ZFS unterstützt kein `FS_IOC_SETFLAGS`": Wäre es
ein ZFS-spezifisches Nicht-Unterstützen, müsste tmpfs funktionieren.
Direkter Test des rohen Kernel-Ioctls (umgeht `chattr`-Binary und jede
Fehlermeldungs-Umdeutung durch das Tool):
```python
FS_IOC_SETFLAGS = 0x40086602
FS_IMMUTABLE_FL = 0x00000010
fcntl.ioctl(fd, FS_IOC_SETFLAGS, struct.pack("i", FS_IMMUTABLE_FL))
# -> OSError errno=1 (EPERM)
```
`capsh --print` zeigt `cap_linux_immutable` im Bounding/Effective-Set
des Containers — das ist aber irrelevant für diesen Check: Die
Kernel-Prüfung für das Setzen/Löschen des Immutable-Flags
(`vfs_ioc_setflags_prepare` / fs-spezifische `*_ioctl_setflags`, z. B.
`ext4_ioctl_setflags`) ruft `capable(CAP_LINUX_IMMUTABLE)` auf — das ist
die KLASSISCHE, NICHT namensraum-bewusste Prüfung (äquivalent zu
`ns_capable(&init_user_ns, …)`), sie prüft also IMMER gegen den
initialen (Host-)User-Namespace, unabhängig davon, was der Container in
seinem EIGENEN Namespace an Fähigkeiten zu haben glaubt.
Das ist laut Kernel-Mailingliste (2014) eine **bewusste
Sicherheitsentscheidung**, kein Versehen: Ein Patch, der genau diese
Prüfung namensraum-bewusst machen wollte (`ns_capable(current_user_ns(),
CAP_LINUX_IMMUTABLE)`), wurde von Andy Lutomirski explizit abgelehnt,
mit der Begründung, dass Prozesse sonst durch Erzeugen eines eigenen
User-Namespace das Immutable-Bit manipulieren könnten, was als
Sicherheitsrisiko eingestuft wurde.
Quelle: https://lists.linuxfoundation.org/pipermail/containers/2014-April/034973.html
**Konsequenz:** Kein Code innerhalb des Containers — unabhängig von
Sprache, Implementierung oder gewähltem Dateisystem — kann
`CAP_LINUX_IMMUTABLE` im Host-Namespace erlangen. Das ist strukturell
unmöglich, solange der Container unprivilegiert bleibt. Das betrifft
nicht nur ZFS, sondern auch ext4/XFS/Btrfs, sobald sie unter einem
unprivilegierten Container gemountet sind.
**Korrektur an `RET-11-PRUEFPROTOKOLL.md`:** Die dortige
Handlungsempfehlung ("WORM-Basisverzeichnis MUSS auf ext2/3/4, XFS oder
Btrfs liegen, NICHT auf ZFS") ist unvollständig/irreführend — das
Dateisystem ist nicht der limitierende Faktor. Richtig ist: Das
WORM-Basisverzeichnis muss auf einem Host/Container liegen, der dem
Prozess REALE (nicht nur namensraum-gemappte) Root-Rechte gibt
(privilegierter Container, VM oder Bare-Metal) — das Dateisystem
darunter ist dabei zweitrangig, sofern es `chattr`/Ext2-Flags
grundsätzlich unterstützt (ext4/XFS/Btrfs tun das; ob modernes ZFS das
ebenfalls täte, ließ sich auf einem privilegierten Host nicht
verifizieren, ist aber plausibel, siehe TrueNAS-SCALE-Hinweise unten —
irrelevant, weil auf einem privilegierten Host ohnehin auch ZFS in
Frage käme, solange OpenZFS das Flag unterstützt).
## Geprüfte Alternativen (alle unzureichend oder im Container nicht verfügbar)
| # | Alternative | Befund |
|---|---|---|
| 1 | `zfs set readonly=on` (Dataset-Ebene) | `zfs`-CLI im Container **nicht vorhanden** (`which zfs` → leer, ZFS-Verwaltung ist beim unprivilegierten LXC-Container nicht delegiert). Selbst wenn verfügbar: Dataset-weit, nicht pro-Objekt — würde auch neue `Put`-Schreibvorgänge blockieren. Wer die Berechtigung hat, es zu setzen, hat auch die, es zurückzusetzen — dieselbe strukturelle Schwäche wie chmod/ACL (siehe unten). Infrastrukturoperation auf Proxmox-Host-Ebene, nicht im Container lösbar. |
| 2 | ZFS-Snapshots / `zfs hold` | Nur Nachweis-/Erkennungsmechanismus (Snapshot zeigt nachträglich, dass sich etwas geändert hat), verhindert aber NICHT das Schreiben/Löschen der aktiven Datei selbst — erfüllt nicht die Kernanforderung "Verhinderung", nur "Erkennung". `zfs`-CLI ohnehin nicht verfügbar. |
| 3 | POSIX-ACLs / Verzeichnis-Schreibschutz (wie in QA-02 als Ersatznachweis erprobt) | Strukturell geprüft per Bind-Mount-Readonly-Test: `mount -o remount,bind,ro` verhindert Schreiben, aber derselbe Account kann sofort `mount -o remount,bind,rw` erneut ausführen und weiterschreiben — **kein Schutz gegen denselben (ggf. kompromittierten) Service-Account**, nur gegen andere Accounts. Das ist dieselbe Einschränkung, die der Code-Kommentar in `wormstore.go` (Zeilen 5-15) bereits für `chmod 0400` korrekt benennt: Löschrecht hängt vom Verzeichnis ab, nicht von der Datei, und der schreibende Account braucht zwingend Verzeichnis-Schreibrecht, um neue Objekte abzulegen — er kann sich damit jederzeit selbst wieder Rechte einräumen. **Bestätigt: taugt nur als Ersatznachweis/Fallback, nicht als vollwertiger Ersatz für echte Storage-Object-Lock.** |
| 4 | Proxmox-Container-Feature (`features: mknod=1` o.ä.) damit `chattr +i` im Container greift | Aus dem Container heraus nicht konfigurierbar (Proxmox-Host-Ebene, kein Zugriff in diesem Auftrag). Fachlich ohnehin wirkungslos: `mknod=1` betrifft `CAP_MKNOD`, nicht `CAP_LINUX_IMMUTABLE`/User-Namespace-Mapping. Einzige wirksame Proxmox-seitige Abhilfe wäre, den Container als PRIVILEGIERT zu betreiben (`unprivileged: 0`) — das hebt die Isolation durch User-Namespace-Mapping komplett auf und ist eine bewusste Sicherheits-Tradeoff-Entscheidung auf Infrastruktur-Ebene, keine Code-Lösung. |
| 5 | Datei-Capability (`setcap cap_linux_immutable+ep <helper>`) auf einen Helper-Prozess innerhalb des Containers | Datei-Capabilities wirken nur innerhalb des EIGENEN User-Namespace des Containers — sie können keine Capability im übergeordneten (Host-)Namespace verleihen, gegen den die Kernel-Prüfung tatsächlich läuft. Kein Workaround möglich. |
| 6 | Verschachtelter User-Namespace (`unshare -U`) um "Eigentümer" des Dateisystems zu werden | Wirkt nicht: Das zugrunde liegende Superblock-Objekt (ZFS-Dataset, tmpfs-Mount) gehört dem HOST-User-Namespace (Ahn-Namespace), nicht dem Container. Verschachtelung erzeugt nur weitere Nachfahren-Namespaces, keiner davon wird dadurch zum Eigentümer eines bereits bestehenden, von außen gemounteten Dateisystems. |
| 7 | FUSE-basiertes eigenes WORM-Dateisystem (Durchsetzung rein in Userspace, ganz ohne `chattr`) | Technisch der einzige denkbare Weg, eine Immutable-Garantie komplett ohne `CAP_LINUX_IMMUTABLE` umzusetzen. Aber: `/dev/fuse` ist in diesem Container **nicht vorhanden** (`ls /dev/fuse` → "No such file or directory") — FUSE-Mounts sind nicht an den Container durchgereicht. Erfordert Proxmox-Host-Konfiguration (Device-Passthrough), nicht im Container lösbar. Selbst wenn verfügbar: kein "minimaler Fix in `wormstore.go`" mehr, sondern ein komplett neuer Dienst/eigenes Dateisystem — als mögliche ZUKÜNFTIGE Architekturoption vermerkt, nicht Teil dieses Nachtrags. |
| 8 | Externes echtes Object-Lock-Backend (z. B. MinIO/S3 mit aktiviertem Object Lock Compliance-Mode) statt lokalem Dateisystem | Einzige Option, die eine zu S3 Object Lock GLEICHWERTIGE Garantie ohne Abhängigkeit von Host-Kernel-Capabilities liefern würde (die Durchsetzung läge dann beim Storage-Backend-Prozess, nicht beim Kernel-Dateisystem-Flag des Containers). Das ist aber ein Architekturwechsel (neuer Storage-Treiber statt `wormstore`-Dateisystem-Pfad), kein Fix an `wormstore.go` — außerhalb des Scopes dieses Nachtrags, als Hinweis für eine mögliche künftige Kachel vermerkt. |
## Prüfung 4: `ImmutableLockSupported` als bestehende Absicherung
Der Code verfügt bereits über exakt den richtigen Schutzmechanismus für
genau diese Situation: `ImmutableLockSupported(dir)`
(`wormstore.go:237-251`) prüft aktiv, ob das Basisverzeichnis
`chattr +i` durchsetzt, und `Put` schlägt bei fehlgeschlagenem
`lockImmutable` fail-closed fehl (Datei wird wieder entfernt, kein
WORM-Objekt ohne echte Sperre). Das ist bereits das korrekte Verhalten
für eine Infrastrukturgrenze: lieber ehrlich ablehnen/überspringen, als
eine WORM-Garantie vorzutäuschen, die nicht existiert. Hier besteht
kein Nachbesserungsbedarf.
## Fazit
Die wiederholt in PR #309/#310/#311 dokumentierte `EPERM`-Meldung ist
**keine behebbare Code- oder Konfigurationslücke**, sondern eine vom
Linux-Kernel bewusst vorgesehene Sicherheitsgrenze: Ein unprivilegierter
Container kann das Immutable-Flag nicht setzen, unabhängig vom
darunterliegenden Dateisystem. Das wurde durch direkte Reproduktion
(`chattr`, roher Ioctl-Aufruf), Gegenprobe auf einer anderen
Dateisystemart (tmpfs) im selben Container und eine bestätigte
Kernel-Design-Entscheidung (2014, LKML) zweifelsfrei nachgewiesen.
Alle acht geprüften Alternativen sind entweder im Container-Scope
technisch nicht verfügbar (ZFS-CLI, FUSE), strukturell keine
vollwertige Entsprechung (ACL/chmod/Dataset-readonly sind vom selben
Account umkehrbar), oder erfordern eine Infrastruktur-/Architektur-
entscheidung außerhalb des Container-Scopes (privilegierter Container,
externes Object-Lock-Backend).
**Keine Änderung an `wormstore.go` vorgenommen** — der bestehende
Fail-Closed-Mechanismus (`ImmutableLockSupported`, `Put` schlägt
komplett fehl statt eine Pseudo-Sperre vorzutäuschen) ist bereits die
richtige Reaktion auf diese Infrastrukturgrenze. Für einen echten
End-to-End-Nachweis von RET-11 AC1/AC2 wird weiterhin ein Testlauf auf
einem PRIVILEGIERTEN Host/Container benötigt (Dateisystem darunter ist
dabei zweitrangig) — oder ein künftiger Architekturwechsel auf ein
externes Object-Lock-fähiges Storage-Backend (siehe Alternative 8).
Dieser Nachtrag gilt als abschließende Untersuchung; eine erneute
Prüfung derselben Frage in künftigen QA-Runden ist nicht nötig, sofern
sich die Host-Konfiguration (privilegiert/unprivilegiert) nicht ändert.