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:
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.
|
||||
Reference in new issue
Block a user