From 6a1eff739240477f7ee583f51b3a8889c6fbb0b8 Mon Sep 17 00:00:00 2001 From: spektr Date: Fri, 30 Oct 2026 18:49:10 +0100 Subject: [PATCH] CT-101 Samba + PVE-Host: NFS auf den Host verlagert, Host per Ansible verwaltet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CT 101 kennt NFS nicht mehr: samba_nfs-Rolle und Ganesha-Artefakte entfernt, kein Podman, keine 2049-Firewall-Regel, kein seccomp-Sondereintrag; live restlos bereinigt und deckungsgleich mit dem Playbook (verify ok=47, Idempotenz changed=0). Dafür neue SMB-Freigabe [Encoding] (Export-Root) für die Kontrolle von anderen Geräten als ghostnet-ki. - Grund: Weder kernel-nfsd noch Ganesha (VFS-FSAL) laufen in einem unprivileged LXC — open_by_handle_at braucht CAP_DAC_READ_SEARCH in der init-userns und liefert dort immer EPERM (verifiziert auf Kernel 7.0.14-14-pve; Web-Recherche: NFS direkt auf dem PVE-Host ist gängige Praxis und anerkannte Ausnahme vom Host-Grundsatz). - Neuer Account ansible-deploy (UID 1001) auf dem PVE-Host 10.0.0.2 nach LXC-Muster: gelocktes Passwort, source-restrictierter passphrase-loser Key (nur pub im Repo), NOPASSWD-Sudoers; Host-Key-Fingerprint verifiziert. - Neues Host-Projekt proxmox/pve-host/ansible (Schema wie die CT-Projekte): preflight/site.yml/verify-production + Rollen pve_nfs_export (Kernel-NFS, /etc/exports nur 10.0.0.90/32, NFSv4-only, Export-Root 100000:970 2775, nft 2049 nur von 10.0.0.90) und pve_lxc_conf (seccomp-Leerzeile aus 101.conf entfernt). Auf dem Host grün: check changed=0, verify ok=15, Idempotenz changed=0. - Abnahme bestanden: NFSv4.2-Mount von ghostnet-ki gegen 10.0.0.2:/media/datenhdd3/Encoding (Export-Root 100000:970 2775 sichtbar), sauberer Unmount. CT 101 nach Reboot: Seccomp 2, keine failed units, 17 Shares, 10 CIFS-Mounts der Workstation hängen. - Doku aktualisiert: GNAI-017 §4.1 und GNAI-019 E2/E3/§15 revidiert, Bereichsindex ergänzt, READMEs angepasst; PVE-Lifecycle-Skripte ohne seccomp-Ausnahme (Hashes aktualisiert). Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- docs/platform/proxmox-server/README.md | 1 + .../GNAI-017-PLAN_Samba-NFS-Export.md | 52 +-- .../GNAI-019-DEC_Ansible-Migration.md | 94 +++--- .../lxc/ct-101-samba/Containerfile.ganesha | 26 -- proxmox/lxc/ct-101-samba/ansible/README.md | 52 +-- .../agent/group_vars/samba_hosts.yml | 39 +-- .../bootstrap/group_vars/samba_hosts.yml | 39 +-- .../production/group_vars/samba_hosts.yml | 39 +-- .../ansible/playbooks/audit-production.yml | 21 -- .../ansible/playbooks/preflight.yml | 26 +- .../ct-101-samba/ansible/playbooks/site.yml | 2 - .../ansible/playbooks/verify-production.yml | 158 +--------- .../ansible/roles/samba_firewall/README.md | 6 +- .../roles/samba_firewall/defaults/main.yml | 3 - .../samba_firewall/meta/argument_specs.yml | 7 - .../roles/samba_firewall/tasks/main.yml | 7 +- .../samba_firewall/templates/nftables.conf.j2 | 3 - .../roles/samba_lxc_baseline/README.md | 4 +- .../samba_lxc_baseline/defaults/main.yml | 16 - .../roles/samba_lxc_baseline/tasks/main.yml | 7 +- .../ansible/roles/samba_nfs/README.md | 44 --- .../ansible/roles/samba_nfs/defaults/main.yml | 18 -- .../ansible/roles/samba_nfs/handlers/main.yml | 10 - .../roles/samba_nfs/meta/argument_specs.yml | 47 --- .../ansible/roles/samba_nfs/tasks/main.yml | 296 ------------------ .../roles/samba_nfs/templates/ganesha.conf.j2 | 52 --- .../templates/ghostnet-samba-nfs.container.j2 | 50 --- .../ct-101-samba/ghostnet-ganesha-healthcheck | 9 - proxmox/lxc/ct-101-samba/scripts/README.md | 28 +- .../scripts/provision-ct101-samba.sh | 27 +- .../scripts/upgrade-ct101-to-debian13.sh | 28 +- proxmox/pve-host/README.md | 88 ++++++ proxmox/pve-host/ansible/ansible.cfg | 12 + .../ansible/files/ssh/pve-ansible-deploy.pub | 1 + .../production/group_vars/pve_hosts.yml | 16 + .../production/host_vars/pve-host.yml | 12 + .../ansible/inventories/production/hosts.yml | 8 + .../pve-host/ansible/playbooks/preflight.yml | 108 +++++++ proxmox/pve-host/ansible/playbooks/site.yml | 9 + .../ansible/playbooks/verify-production.yml | 140 +++++++++ .../ansible/roles/pve_lxc_conf/README.md | 12 + .../roles/pve_lxc_conf/defaults/main.yml | 6 + .../ansible/roles/pve_lxc_conf/tasks/main.yml | 9 + .../ansible/roles/pve_nfs_export/README.md | 24 ++ .../roles/pve_nfs_export/defaults/main.yml | 17 + .../roles/pve_nfs_export/tasks/main.yml | 110 +++++++ 46 files changed, 768 insertions(+), 1015 deletions(-) delete mode 100644 proxmox/lxc/ct-101-samba/Containerfile.ganesha delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/README.md delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/defaults/main.yml delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/handlers/main.yml delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/meta/argument_specs.yml delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/tasks/main.yml delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ganesha.conf.j2 delete mode 100644 proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ghostnet-samba-nfs.container.j2 delete mode 100755 proxmox/lxc/ct-101-samba/ghostnet-ganesha-healthcheck create mode 100644 proxmox/pve-host/README.md create mode 100644 proxmox/pve-host/ansible/ansible.cfg create mode 100644 proxmox/pve-host/ansible/files/ssh/pve-ansible-deploy.pub create mode 100644 proxmox/pve-host/ansible/inventories/production/group_vars/pve_hosts.yml create mode 100644 proxmox/pve-host/ansible/inventories/production/host_vars/pve-host.yml create mode 100644 proxmox/pve-host/ansible/inventories/production/hosts.yml create mode 100644 proxmox/pve-host/ansible/playbooks/preflight.yml create mode 100644 proxmox/pve-host/ansible/playbooks/site.yml create mode 100644 proxmox/pve-host/ansible/playbooks/verify-production.yml create mode 100644 proxmox/pve-host/ansible/roles/pve_lxc_conf/README.md create mode 100644 proxmox/pve-host/ansible/roles/pve_lxc_conf/defaults/main.yml create mode 100644 proxmox/pve-host/ansible/roles/pve_lxc_conf/tasks/main.yml create mode 100644 proxmox/pve-host/ansible/roles/pve_nfs_export/README.md create mode 100644 proxmox/pve-host/ansible/roles/pve_nfs_export/defaults/main.yml create mode 100644 proxmox/pve-host/ansible/roles/pve_nfs_export/tasks/main.yml diff --git a/docs/platform/proxmox-server/README.md b/docs/platform/proxmox-server/README.md index 7df4667..0fc20b8 100644 --- a/docs/platform/proxmox-server/README.md +++ b/docs/platform/proxmox-server/README.md @@ -18,6 +18,7 @@ Hypervisors selbst. | [GNAI-018 / CT 101](lxc/ct-101-samba/GNAI-018-REQ_Encoding-Pipeline-Integration.md) | Requirements der Encoding-Pipeline an CT 101 für den Ansible-Rebuild (Übergabe) | | [GNAI-019 / CT 101](lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md) | Entwurfsentscheidungen der Ansible-Migration: natives Samba, NFS-Ganesha als Quadlet, Fresh-State-Rebuild | | [CT-101-Automation](../../../proxmox/lxc/ct-101-samba/README.md) | produktives Samba-Ansible-Projekt und CT-spezifische PVE-Helfer | +| [PVE-Host-Automation](../../../proxmox/pve-host/README.md) | Ansible-Verwaltung des PVE-Hosts selbst (`10.0.0.2`): Deploy-Account, Kernel-NFS-Export, LXC-Conf | Die versionierte Quelle für den optionalen, secretfreien Community-Scripts-Post-install-Hook liegt unter diff --git a/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md b/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md index eced3de..bb661eb 100644 --- a/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md +++ b/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md @@ -59,29 +59,37 @@ guests = yes`, `obey pam restrictions = yes`. ### 4.1 NFS-Export -Ursprünglich war `nfs-kernel-server` mit `/etc/exports` geplant. **Überholt -durch GNAI-019 Entscheidung 2 und 3:** Der Kernel-NFS-Server ist in einem -unprivileged LXC nicht betreibbar; der Export läuft stattdessen als -**NFS-Ganesha** in einem rootful Podman-Quadlet-Container -(`ghostnet-samba-nfs.service`, Host-Networking, NFSv4-only). Dafür wird in -`/etc/pve/lxc/101.conf` ein leerer `lxc.seccomp.profile:`-Eintrag gesetzt -(Ganesha braucht `open_by_handle_at`). Umsetzung: Rolle `samba_nfs` plus die -PVE-Skripte `scripts/upgrade-ct101-to-debian13.sh` (Adoption) und -`scripts/provision-ct101-samba.sh` (Fresh-State-Rebuild). +**Korrektur 30.10.2026:** Der Export läuft als **Kernel-NFS +(`nfs-kernel-server`) auf dem PVE-Host** — umgesetzt via +`Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)` (idempotent, versioniert im Repo). +Zuvor war Ganesha-in-CT-101 geplant; das ist in einem unprivileged LXC nicht +möglich (`open_by_handle_at` braucht CAP_DAC_READ_SEARCH in der init-userns +und liefert dort immer EPERM — verifiziert 30.10.2026 auf Kernel +7.0.14-14-pve; der leere `lxc.seccomp.profile:`-Eintrag war dafür nicht +ausreichend). Web-Recherche zu Proxmox-Best-Practices (30.10.2026): NFS direkt +auf dem Host ist gängige Praxis zum Teilen von Host-Storage und eine +anerkannte Ausnahme vom Grundsatz „keine Extra-Services auf dem Host"; +Reproduzierbarkeit bleibt durch das versionierte Script gewährleistet. -- Export ausschließlich für `ghostnet-ki` (10.0.0.90), NFSv4.2-only (TCP+UDP), - `SecType = sys`, Standard-Squash bleibt aktiv (`Squash = rootid`): Die - rootless Container schreiben als UID 982/983, nicht als root. Kein NFSv3, - kein mountd/rpcbind — `showmount` gilt nicht; Abnahme ist ein realer - NFSv4.2-Mount von `ghostnet-ki` (GNAI-018 Abschnitt 4 Punkt 2, angepasst). -- Mount auf `ghostnet-ki`: `/var/lib/ghostnet-encoding` (NFSv4.2, `hard`, - `_netdev`, `noatime`) — verwaltet vom Encoding-Ansible (GNENC-001 Abschnitt 6). -- Vorab wird nur das Export-Verzeichnis `/media/datenhdd3/Encoding` angelegt - (`root:gn-media 2775` — Abweichung von root:root 0755: Die Pipeline erzeugt - die Top-Level-Verzeichnisse via NFS als UID 982/983 und braucht group-write - plus setgid auf dem Export-Root); die Pipeline-Unterverzeichnisse werden via - NFS von `ghostnet-ki` durch die Ansible-Rolle angelegt (Owner/Group gemäß - GNENC-002 Abschnitt 4). +- Export ausschließlich für `ghostnet-ki` (`10.0.0.90/32`) via `/etc/exports` + (`rw,sync,no_subtree_check,root_squash`), **NFSv4-only** (NFSv2/v3 auf dem + Host abgeschaltet). `showmount` gilt nicht; Abnahme ist ein realer + NFSv4.2-Mount von `ghostnet-ki` gegen die PVE-Host-IP. +- Mount auf `ghostnet-ki`: `/var/lib/ghostnet-encoding` + (`mount -t nfs -o nfsvers=4.2,hard,noatime :/media/datenhdd3/Encoding …`) + — verwaltet vom Encoding-Ansible (GNENC-001 Abschnitt 6; die Mount-Quelle + ändert sich von `10.0.0.9` auf die PVE-Host-IP). +- Export-Root `/media/datenhdd3/Encoding` auf dem Host: `100000:970 2775` + (100000 = CT-101-Gast-root, damit die SMB-Freigabe `[Encoding]` aus CT 101 + schreiben kann; GID 970 = `gn-media`, group-write + setgid für die Pipeline- + Identitäten via AUTH_SYS). Die Pipeline-Unterverzeichnisse entstehen via NFS + von `ghostnet-ki` (Owner/Group gemäß GNENC-002 Abschnitt 4). +- Host-Firewall: 2049 tcp+udp nur von 10.0.0.90 (nft-Tabelle + `ghostnet_host_nfs`, sofern pve-firewall den Host nicht verwaltet). +- CT 101 kennt NFS nicht mehr: keine `samba_nfs`-Rolle, kein Podman, keine + 2049-Regel in der CT-Firewall, kein seccomp-Eintrag in `/etc/pve/lxc/101.conf`. + Dafür SMB-Freigaben `[Encode]`, `[Encode-In]` und `[Encoding]` (Root) für die + Pipeline und die Kontrolle von anderen Geräten. ### 4.2 Gruppen- und UID-Abgleich diff --git a/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md b/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md index ca2a2d1..ab6dc5a 100644 --- a/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md +++ b/docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md @@ -66,59 +66,48 @@ ist als Jinja2-Template im Repository gepflegt (Entscheidung 8). Konfigurationsänderung; Post-State-Verifikation per `testparm`, `pdbedit -L` und anonymer `smbclient -L`-Abfrage. -## 4. Entscheidung 2: NFS-Export via NFS-Ganesha als rootful Podman-Quadlet mit Host-Networking (statt `nfs-kernel-server`) +## 4. Entscheidung 2: NFS-Export als Kernel-NFS auf dem PVE-Host (revidiert 30.10.2026) -**Entscheidung:** Der NFS-Export für die Encoding-Pipeline (GNAI-018 R1) wird -nicht mit `nfs-kernel-server`, sondern mit **NFS-Ganesha** als **rootful** -Podman-Quadlet-Container (`ghostnet-samba-nfs.service`) mit **Host-Networking** -umgesetzt. Dafür wird in `/etc/pve/lxc/101.conf` ein **leerer -`lxc.seccomp.profile:`**-Eintrag gesetzt (Deaktivierung des LXC-Seccomp-Filters -für den gesamten CT). +**Entscheidung (revidiert):** Der NFS-Export für die Encoding-Pipeline +(GNAI-018 R1) läuft als **Kernel-NFS (`nfs-kernel-server`) auf dem +Proxmox-Host**, exportiert `/media/datenhdd3/Encoding` exklusiv für +`ghostnet-ki` (`10.0.0.90/32`), NFSv4-only — umgesetzt durch das idempotente, +versionierte Script `Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)` (siehe +`scripts/README.md`). CT 101 kennt NFS nicht mehr: keine `samba_nfs`-Rolle, +kein Podman, keine 2049-Firewall-Regel, kein `lxc.seccomp.profile:`-Eintrag. +Dafür stellt CT 101 die SMB-Freigaben `[Encode]`, `[Encode-In]` und +`[Encoding]` (Root) für die Pipeline bzw. die Kontrolle von anderen Geräten. -**Begründung:** +**Begründung:** Weder Kernel-nfsd noch NFS-Ganesha (VFS-FSAL) können in einem +unprivileged LXC laufen: `open_by_handle_at` — für Ganesha zwingend nötig — +erfordert `CAP_DAC_READ_SEARCH` in der init-userns und liefert im unprivileged +LXC immer EPERM (verifiziert 30.10.2026 auf Kernel 7.0.14-14-pve; der leere +Seccomp-Eintrag der ursprünglichen Planung war dafür nicht ausreichend). +Web-Recherche zu Proxmox-Best-Practices (30.10.2026): NFS direkt auf dem +PVE-Host ist gängige Praxis zum Teilen von Host-Storage und eine anerkannte +Ausnahme vom Grundsatz „keine Extra-Services auf dem Host“; die +Reproduzierbarkeit des Host-Änderung ist durch das versionierte Script im +Repository gegeben. virtiofs/9p scheiden aus, weil `ghostnet-ki` eine eigene +physikalische Maschine im LAN ist. -- GNAI-018 R1 verlangte `nfs-kernel-server`. Der Kernel-NFS-Server kann in - einem unprivileged LXC nicht betrieben werden (kein Zugriff auf das - Kernel-NFS-Subsystem des Hosts). Ganesha ist ein Userspace-NFS-Server (VFS - FSAL) und läuft in einem unprivileged LXC. -- **Rootful** ist fachlich nötig: Der VFS-FSAL wendet die AUTH_SYS-Credentials - der Pipeline (UID 982/983) pro Request an; das erfordert `CAP_SETUID`/ - `CAP_SETGID` innerhalb der LXC-User-Namespace, was ein rootless Container - nicht leisten kann. Standard-Squash bleibt aktiv (`Squash = rootid`, R1). -- **Host-Networking** bindet den Listener 2049 im Netz-Namespace von CT 101; - die nftables-Input-Chain deckt ihn ab. Die Quadlet-Unit trägt - `Requires=nftables.service` — ohne Firewall startet der Export nicht - (fail closed). -- **Leerer Seccomp-Eintrag:** Ganesha benötigt `open_by_handle_at` (VFS FSAL), - das das PVE-Standard-Seccomp-Profil blockiert. Der leere Eintrag deaktiviert - den Filter für den gesamten CT. Das ist vertretbar, weil der CT unprivileged - bleibt und Samba nativ keine privilegierten Syscalls jenseits der - User-Namespace benötigt. Beide PVE-Skripte setzen bzw. verifizieren den - leeren Eintrag (`scripts/upgrade-ct101-to-debian13.sh`, - `scripts/provision-ct101-samba.sh`). -- **Abweichung vom Export-Root aus R1:** Statt `root:root 0755` wird - `/media/datenhdd3/Encoding` als `root:gn-media 2775` angelegt. Die Pipeline - erzeugt die Top-Level-Verzeichnisse via NFS als UID 982/983 und benötigt - group-write plus setgid auf dem Export-Root (GNAI-017 Abschnitt 4.4); die - Pipeline-Unterverzeichnisse selbst entstehen weiterhin via NFS durch die - Rolle `video_encoding` auf `ghostnet-ki`. - -**Konsequenz:** PVE-seitige Änderung am produktiven CT (Seccomp) — im -Upgrade-Skript durch einen `vzdump --mode stop` abgesichert. Die -Quadlet-Unit folgt dem CT-100/102-Pattern: digest-gepinntes Debian-13-Basisimage -(`debian:trixie-slim`), sha256-gepinntes `Containerfile.ganesha`, -`Pull=never`, Runtime-Tag `localhost/ghostnet/ganesha:trixie-ansible1`, -Healthcheck, read-only rootfs mit tmpfs `/run` und `/tmp`. Ohne dbus logged -Ganesha eine nicht-fatale Admin-Interface-Warnung (dokumentiert in -`roles/samba_nfs/README.md`). +**Konsequenz:** Host-seitige Änderung (Paket `nfs-kernel-server`, +`/etc/exports`, ggf. Host-Firewall 2049 nur von 10.0.0.90), abgesichert durch +eine Confirmation-Env-Variable; CT-Upgrades bleiben durch `vzdump` gesichert. +Export-Root auf dem Host: `100000:970 2775` (100000 = CT-101-Gast-root für den +SMB-Zugriff aus CT 101; GID 970 = `gn-media` group-write + setgid für die +Pipeline-Identitäten via AUTH_SYS). Der Ganesha-Ära-Eintrag +`lxc.seccomp.profile:` in `/etc/pve/lxc/101.conf` wird entfernt — CT 101 läuft +wieder mit Default-Seccomp wie CT 100/102/103. Mount auf `ghostnet-ki`: +`/var/lib/ghostnet-encoding` via `:/media/datenhdd3/Encoding` +(NFSv4.2; die Mount-Quelle wechselt von `10.0.0.9` auf die Host-IP). ## 5. Entscheidung 3: NFSv4-only — kein NFSv3, kein `showmount` -**Entscheidung:** Der Export ist NFSv4-only (`Protocols = 4`, kein NLM, kein -RQUOTA, kein mountd/rpcbind), Transport TCP+UDP, `SecType = sys`, -`Squash = rootid`, client-beschränkt auf `10.0.0.90`, `Pseudo` identisch zum -Export-Pfad (dokumentierter Mount-Pfad `10.0.0.9:/media/datenhdd3/Encoding` -bleibt unverändert). +**Entscheidung:** Der Export ist NFSv4-only (NFSv2/v3 auf dem Host +abgeschaltet, kein mountd/rpcbind), Transport TCP+UDP, `root_squash` (Default), +client-beschränkt auf `10.0.0.90/32` in `/etc/exports`. Mount-Pfad auf +`ghostnet-ki`: `:/media/datenhdd3/Encoding` (host-seitig seit der +Revision 30.10.2026, nicht mehr `10.0.0.9`). **Begründung:** NFSv4 benötigt keinen Portmapper und kein mountd — weniger Angriffsfläche und keine Legacy-Protokolle. Die Pipeline mountet NFSv4.2 @@ -126,8 +115,9 @@ Angriffsfläche und keine Legacy-Protokolle. Die Pipeline mountet NFSv4.2 **Konsequenz:** `showmount -e` ist kein zulässiges Abnahmekriterium (NFSv4-only). GNAI-018 Abschnitt 4 Punkt 2 wird als **realer NFSv4.2-Mount-Test -von `ghostnet-ki`** interpretiert; dasselbe gilt für Punkt 8 (Dienste nach -Reboot: `ghostnet-samba-nfs.service` statt `nfs-kernel-server`). +von `ghostnet-ki` gegen den PVE-Host** interpretiert; dasselbe gilt für Punkt 8 +(Dienste nach Reboot: `nfs-kernel-server` auf dem PVE-Host statt +`ghostnet-samba-nfs.service` in CT 101). ## 6. Entscheidung 4: Identität — `gn-media` GID 970, aufgezeichnete Medien-UIDs, Mitgliedschaft nur `ghost` @@ -342,6 +332,12 @@ dieselbe Person auf Medien-/SMB-Ebene ist nicht erforderlich. `smb.conf` (inkl. `admin users` auf elf Shares, `write list` auf `[Dokumente]`/`[Extern]`; `[Serien2]` ohne `force user/group`), damit die Konvergenz keine produktiven Share-Rechte ändert. +- **Revision 30.10.2026:** Entscheidung 2/3 revidiert — der NFS-Export läuft + als Kernel-NFS auf dem PVE-Host (`Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)`), + nicht als Ganesha-Quadlet in CT 101 (`open_by_handle_at` → EPERM im + unprivileged LXC, verifiziert). GNAI-017 Abschnitt 4.1 entsprechend + aktualisiert; `samba_nfs`-Rolle, Podman, 2049-Regel und seccomp-Eintrag aus + CT 101 entfernt; SMB-Freigabe `[Encoding]` (Root) ergänzt. ## 16. Offene Punkte diff --git a/proxmox/lxc/ct-101-samba/Containerfile.ganesha b/proxmox/lxc/ct-101-samba/Containerfile.ganesha deleted file mode 100644 index 0c11ff6..0000000 --- a/proxmox/lxc/ct-101-samba/Containerfile.ganesha +++ /dev/null @@ -1,26 +0,0 @@ -FROM docker.io/library/debian:trixie-slim@sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483 - -ENV DEBIAN_FRONTEND=noninteractive - -# NFS-Ganesha is the userspace NFSv4 server for the unprivileged CT 101 LXC -# (GNAI-019): the kernel nfsd cannot run inside an unprivileged container. -# The healthcheck helper is deliberately not baked in; the Quadlet mounts it -# read-only from /etc/ghostnet/samba/ganesha (CT 102 pattern). -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - bash \ - nfs-ganesha \ - nfs-ganesha-vfs \ - && rm -rf /var/lib/apt/lists/* - -RUN mkdir -p /var/lib/nfs-ganesha /etc/ganesha - -LABEL eu.selfhost.ghostnet.stack="samba" \ - eu.selfhost.ghostnet.role="nfs-export" \ - eu.selfhost.ghostnet.base-digest="sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483" - -# -p /run/ganesha.pid: the container runs ReadOnly with /run as a rw tmpfs, so -# the compiled-in default /var/run/ganesha/ganesha.pid is not writable there -# (the directory does not exist -> ENOENT at startup). /run itself exists and -# is writable. Verified with ganesha.nfsd -p on 2026-10-30. -CMD ["/usr/bin/ganesha.nfsd", "-F", "-f", "/etc/ganesha/ganesha.conf", "-L", "/dev/stderr", "-p", "/run/ganesha.pid"] diff --git a/proxmox/lxc/ct-101-samba/ansible/README.md b/proxmox/lxc/ct-101-samba/ansible/README.md index 5791281..2a8c228 100644 --- a/proxmox/lxc/ct-101-samba/ansible/README.md +++ b/proxmox/lxc/ct-101-samba/ansible/README.md @@ -2,17 +2,18 @@ Self-contained host project for the production Samba file server LXC (CT 101, `10.0.0.9`). It follows the CT 100/102 project schema and uses the -repository-wide pinned control environment plus the shared `podman_runtime` -role; all CT 101 inventories, playbooks, public keys and service roles stay -inside the CT 101 root. +repository-wide pinned control environment; all CT 101 inventories, +playbooks, public keys and service roles stay inside the CT 101 root. Design decisions (GNAI-019): Samba stays **native** (Debian package, fully -templated `smb.conf` with a `testparm` gate), NFS-Ganesha runs as a **rootful -Podman/Quadlet container with host networking**, the firewall is the shared +templated `smb.conf` with a `testparm` gate), the firewall is the shared nftables pattern, and the media identity (`gn-media` GID 970, recorded UIDs) -is declared in the inventory. All configuration files are Jinja2 templates in -this repository; Ansible renders and installs them — there is no hand-edited -state on the target. +is declared in the inventory. The encoding-pipeline NFS export is served by +the **PVE host** (kernel NFS, `Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)`), not by +this CT — NFS (kernel or Ganesha) cannot run in an unprivileged LXC because +`open_by_handle_at` requires CAP_DAC_READ_SEARCH in the init user namespace. +All configuration files are Jinja2 templates in this repository; Ansible +renders and installs them — there is no hand-edited state on the target. Create the repository-level control environment once: @@ -71,7 +72,7 @@ role argument specs reject them before any task runs). `preflight.yml` on the | `bootstrap.yml` | `bootstrap` | adopts the access boundary: `ansible-deploy`, hardened keys, sudoers, SSH policy (runs for real, not in check mode) | | `verify-deploy-access.yml` | `production` | proves the deployment account and its sudo path | | `backup.yml` | `production` | one-shot pre-migration backup of `/etc/samba` and `/var/lib/samba` (adoption only) | -| `site.yml` | `production` | converges the full declared state (roles in order: `samba_lxc_baseline`, `samba_access`, `podman_runtime`, `samba_firewall`, `samba_media`, `samba_smb`, `samba_nfs`) | +| `site.yml` | `production` | converges the full declared state (roles in order: `samba_lxc_baseline`, `samba_access`, `samba_firewall`, `samba_media`, `samba_smb`) | | `verify-production.yml` | `production` | read-only verification of the converged state, including template-drift checks | `site.yml --check` is clean: post-state assertions that depend on freshly @@ -86,11 +87,10 @@ keeps the healthy file server and its Samba passdb intact: 1. Generate the three keypairs; commit only `files/ssh/*.pub`. 2. On the Proxmox host, run `scripts/upgrade-ct101-to-debian13.sh` (see `scripts/README.md`): in-place Debian 12 → 13 dist-upgrade of the existing - CT 101 with a pre-upgrade `vzdump` and the empty `lxc.seccomp.profile`. - When the dist-upgrade was performed manually instead of via the script, - ensure the empty `lxc.seccomp.profile:` line is present in - `/etc/pve/lxc/101.conf` and reboot the CT — the Ganesha container needs it - (GNAI-019 decision 2). + CT 101 with a pre-upgrade `vzdump`. No `lxc.seccomp.profile` change is + needed anymore — that exception existed only for the Ganesha NFS container + (GNAI-019 decision 2, reverted 2026-10-30: the NFS export now runs on the + PVE host). 3. On the PVE console of CT 101: capture the productive `ghost` UID, pick a free UID for `ghost-agent`, then run the shared baseline `ghostnet-lxc-baseline.sh 101` with `GHOSTNET_AGENT_UID`/`GHOSTNET_ADMIN_UID` @@ -114,13 +114,14 @@ keeps the healthy file server and its Samba passdb intact: 9. `backup.yml` on the `production` inventory (one-shot; the passdb must be backed up before the first convergence, GNAI-018 R5). 10. `site.yml --check --diff` on the `production` inventory previews the - convergence (templated `smb.conf`, nftables, SSH drop-in, Ganesha Quadlet). + convergence (templated `smb.conf`, nftables, SSH drop-in). 11. `site.yml` converges. The Samba passdb is preserved untouched. 12. `verify-production.yml` on the `production` inventory. 13. `site.yml` again: expect `changed=0` (idempotence). 14. Operator reboot test of CT 101 (GNAI-016 section 7, LXC-boot_id caveat). -15. NFSv4.2 mount test from `ghostnet-ki` (`10.0.0.90`) against the export; - `showmount` does not apply (NFSv4-only, GNAI-019). +15. NFSv4.2 mount test from `ghostnet-ki` (`10.0.0.90`) against the PVE-host + export (`Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)`); `showmount` does not + apply (NFSv4-only, GNAI-019). ## Fresh-state rebuild runbook @@ -171,17 +172,20 @@ and installs; the only state that cannot be declared is secret material. 8. `verify-production.yml` with `-e samba_verify_require_premigration_backup=false` (the pre-migration backup check is adoption-only; a fresh system has nothing to back up). -9. NFSv4.2 mount test from `ghostnet-ki` (`10.0.0.90`). +9. NFSv4.2 mount test from `ghostnet-ki` (`10.0.0.90`) against the PVE-host + export. The rebuild reproduces the declared state: media accounts with the recorded -UIDs, `gn-media` GID 970 with the declared membership, the 14 shares from the -templated `smb.conf`, the nftables policy, and the Ganesha NFS export as a -rootful Quadlet container. The SMB passwords are the single deliberate -operator step — they exist only in the passdb on CT 101. +UIDs, `gn-media` GID 970 with the declared membership, the 15 shares from the +templated `smb.conf` (incl. `[Encode]`/`[Encode-In]`/`[Encoding]` for the +pipeline), and the nftables policy. The pipeline's NFS export is served by the +PVE host (`Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)`), not by this CT. The SMB +passwords are the single deliberate operator step — they exist only in the +passdb on CT 101. ## Secrets policy -Do not store SMB passwords, the Samba passdb, the `ghost` password, private -SSH keys or Podman secret contents in this repository, the inventory or +Do not store SMB passwords, the Samba passdb, the `ghost` password or private +SSH keys in this repository, the inventory or Ansible output. `pdbedit -L` lists usernames only; `backup.yml` writes root-only copies on the target under `/var/backups/ghostnet/samba/`. diff --git a/proxmox/lxc/ct-101-samba/ansible/inventories/agent/group_vars/samba_hosts.yml b/proxmox/lxc/ct-101-samba/ansible/inventories/agent/group_vars/samba_hosts.yml index 3a18ef6..dfb7539 100644 --- a/proxmox/lxc/ct-101-samba/ansible/inventories/agent/group_vars/samba_hosts.yml +++ b/proxmox/lxc/ct-101-samba/ansible/inventories/agent/group_vars/samba_hosts.yml @@ -12,8 +12,6 @@ samba_expected_distribution_major_version: "13" # disabled, so the live server does not answer NetBIOS names): samba_expected_hostname: Samba samba_expected_virtualization_type: lxc -# Expected Podman version after the dist-upgrade (trixie), analog to CT 100/102: -samba_expected_podman_version: "5.4.2" # Resource minimums (recorded 2026-10-27; round values at/below the live # capacity, CT 100/102 convention): samba_minimum_root_size_bytes: 4000000000 @@ -28,10 +26,6 @@ samba_firewall_smb_tcp_ports: samba_firewall_smb_udp_ports: - 137 - 138 -samba_firewall_nfs_client_ipv4: 10.0.0.90 -samba_firewall_nfs_ports: - - 2049 - samba_access_source_networks: 10.0.0.0/24,10.8.0.0/24 # Mirrored from the samba_access role defaults so that verify-production.yml # runs with a self-contained inventory (CT 100/102 convention): @@ -252,23 +246,16 @@ samba_smb_media_shares: force_group: gn-media create_mask: "0664" directory_mask: "0775" - -samba_nfs_export_path: /media/datenhdd3/Encoding -samba_nfs_client_ipv4: 10.0.0.90 -samba_nfs_export_id: 101 -samba_nfs_container_name: ghostnet-samba-nfs -samba_nfs_service_unit: ghostnet-samba-nfs.service -# Digest-pinned Debian 13 base image (amd64), resolved on 2026-10-20: -samba_nfs_source_image: >- - docker.io/library/debian:trixie-slim@sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483 -samba_nfs_runtime_tag: localhost/ghostnet/ganesha:trixie-ansible1 -samba_nfs_expected_architecture: amd64 -# Paths and Containerfile checksum mirrored from the role defaults so that -# verify-production.yml can render the templates via lookup and compare them -# with the live files (self-contained inventory, CT 100/102 convention): -samba_nfs_quadlet_directory: /etc/containers/systemd -samba_nfs_build_directory: /var/lib/ghostnet/samba/build -samba_nfs_config_directory: /etc/ghostnet/samba/ganesha -samba_nfs_state_directory: /var/lib/ghostnet/samba/ganesha -samba_nfs_containerfile_sha256: >- - 54cf9cae60b3a3849d61213e5892f8ec14a2c8bdffeed2c8586ff727c73aef73 + - name: Encoding + comment: "Encoding-Pipeline: Root (Verwaltung/Kontrolle per SMB, z.B. von anderen Geraeten)" + path: /media/datenhdd3/Encoding + read_only: "no" + valid_users: + - ghost + write_list: + - ghost + admin_users: + - ghost + force_group: gn-media + create_mask: "0664" + directory_mask: "0775" diff --git a/proxmox/lxc/ct-101-samba/ansible/inventories/bootstrap/group_vars/samba_hosts.yml b/proxmox/lxc/ct-101-samba/ansible/inventories/bootstrap/group_vars/samba_hosts.yml index 3a18ef6..dfb7539 100644 --- a/proxmox/lxc/ct-101-samba/ansible/inventories/bootstrap/group_vars/samba_hosts.yml +++ b/proxmox/lxc/ct-101-samba/ansible/inventories/bootstrap/group_vars/samba_hosts.yml @@ -12,8 +12,6 @@ samba_expected_distribution_major_version: "13" # disabled, so the live server does not answer NetBIOS names): samba_expected_hostname: Samba samba_expected_virtualization_type: lxc -# Expected Podman version after the dist-upgrade (trixie), analog to CT 100/102: -samba_expected_podman_version: "5.4.2" # Resource minimums (recorded 2026-10-27; round values at/below the live # capacity, CT 100/102 convention): samba_minimum_root_size_bytes: 4000000000 @@ -28,10 +26,6 @@ samba_firewall_smb_tcp_ports: samba_firewall_smb_udp_ports: - 137 - 138 -samba_firewall_nfs_client_ipv4: 10.0.0.90 -samba_firewall_nfs_ports: - - 2049 - samba_access_source_networks: 10.0.0.0/24,10.8.0.0/24 # Mirrored from the samba_access role defaults so that verify-production.yml # runs with a self-contained inventory (CT 100/102 convention): @@ -252,23 +246,16 @@ samba_smb_media_shares: force_group: gn-media create_mask: "0664" directory_mask: "0775" - -samba_nfs_export_path: /media/datenhdd3/Encoding -samba_nfs_client_ipv4: 10.0.0.90 -samba_nfs_export_id: 101 -samba_nfs_container_name: ghostnet-samba-nfs -samba_nfs_service_unit: ghostnet-samba-nfs.service -# Digest-pinned Debian 13 base image (amd64), resolved on 2026-10-20: -samba_nfs_source_image: >- - docker.io/library/debian:trixie-slim@sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483 -samba_nfs_runtime_tag: localhost/ghostnet/ganesha:trixie-ansible1 -samba_nfs_expected_architecture: amd64 -# Paths and Containerfile checksum mirrored from the role defaults so that -# verify-production.yml can render the templates via lookup and compare them -# with the live files (self-contained inventory, CT 100/102 convention): -samba_nfs_quadlet_directory: /etc/containers/systemd -samba_nfs_build_directory: /var/lib/ghostnet/samba/build -samba_nfs_config_directory: /etc/ghostnet/samba/ganesha -samba_nfs_state_directory: /var/lib/ghostnet/samba/ganesha -samba_nfs_containerfile_sha256: >- - 54cf9cae60b3a3849d61213e5892f8ec14a2c8bdffeed2c8586ff727c73aef73 + - name: Encoding + comment: "Encoding-Pipeline: Root (Verwaltung/Kontrolle per SMB, z.B. von anderen Geraeten)" + path: /media/datenhdd3/Encoding + read_only: "no" + valid_users: + - ghost + write_list: + - ghost + admin_users: + - ghost + force_group: gn-media + create_mask: "0664" + directory_mask: "0775" diff --git a/proxmox/lxc/ct-101-samba/ansible/inventories/production/group_vars/samba_hosts.yml b/proxmox/lxc/ct-101-samba/ansible/inventories/production/group_vars/samba_hosts.yml index 3a18ef6..dfb7539 100644 --- a/proxmox/lxc/ct-101-samba/ansible/inventories/production/group_vars/samba_hosts.yml +++ b/proxmox/lxc/ct-101-samba/ansible/inventories/production/group_vars/samba_hosts.yml @@ -12,8 +12,6 @@ samba_expected_distribution_major_version: "13" # disabled, so the live server does not answer NetBIOS names): samba_expected_hostname: Samba samba_expected_virtualization_type: lxc -# Expected Podman version after the dist-upgrade (trixie), analog to CT 100/102: -samba_expected_podman_version: "5.4.2" # Resource minimums (recorded 2026-10-27; round values at/below the live # capacity, CT 100/102 convention): samba_minimum_root_size_bytes: 4000000000 @@ -28,10 +26,6 @@ samba_firewall_smb_tcp_ports: samba_firewall_smb_udp_ports: - 137 - 138 -samba_firewall_nfs_client_ipv4: 10.0.0.90 -samba_firewall_nfs_ports: - - 2049 - samba_access_source_networks: 10.0.0.0/24,10.8.0.0/24 # Mirrored from the samba_access role defaults so that verify-production.yml # runs with a self-contained inventory (CT 100/102 convention): @@ -252,23 +246,16 @@ samba_smb_media_shares: force_group: gn-media create_mask: "0664" directory_mask: "0775" - -samba_nfs_export_path: /media/datenhdd3/Encoding -samba_nfs_client_ipv4: 10.0.0.90 -samba_nfs_export_id: 101 -samba_nfs_container_name: ghostnet-samba-nfs -samba_nfs_service_unit: ghostnet-samba-nfs.service -# Digest-pinned Debian 13 base image (amd64), resolved on 2026-10-20: -samba_nfs_source_image: >- - docker.io/library/debian:trixie-slim@sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483 -samba_nfs_runtime_tag: localhost/ghostnet/ganesha:trixie-ansible1 -samba_nfs_expected_architecture: amd64 -# Paths and Containerfile checksum mirrored from the role defaults so that -# verify-production.yml can render the templates via lookup and compare them -# with the live files (self-contained inventory, CT 100/102 convention): -samba_nfs_quadlet_directory: /etc/containers/systemd -samba_nfs_build_directory: /var/lib/ghostnet/samba/build -samba_nfs_config_directory: /etc/ghostnet/samba/ganesha -samba_nfs_state_directory: /var/lib/ghostnet/samba/ganesha -samba_nfs_containerfile_sha256: >- - 54cf9cae60b3a3849d61213e5892f8ec14a2c8bdffeed2c8586ff727c73aef73 + - name: Encoding + comment: "Encoding-Pipeline: Root (Verwaltung/Kontrolle per SMB, z.B. von anderen Geraeten)" + path: /media/datenhdd3/Encoding + read_only: "no" + valid_users: + - ghost + write_list: + - ghost + admin_users: + - ghost + force_group: gn-media + create_mask: "0664" + directory_mask: "0775" diff --git a/proxmox/lxc/ct-101-samba/ansible/playbooks/audit-production.yml b/proxmox/lxc/ct-101-samba/ansible/playbooks/audit-production.yml index 435e983..5cbb0d8 100644 --- a/proxmox/lxc/ct-101-samba/ansible/playbooks/audit-production.yml +++ b/proxmox/lxc/ct-101-samba/ansible/playbooks/audit-production.yml @@ -50,7 +50,6 @@ - /etc/default/samba - /var/lib/samba - /var/lib/samba/private/passdb.tdb - - /etc/exports - /etc/ssh/sshd_config - /etc/ssh/sshd_config.d - /etc/sudoers.d @@ -154,26 +153,6 @@ failed_when: false register: samba_audit_nft - - name: Require the NFS kernel server to be absent - ansible.builtin.command: - argv: - - /usr/bin/dpkg-query - - -W - - -f=${Status} - - nfs-kernel-server - check_mode: false - changed_when: false - failed_when: false - register: samba_audit_nfs_kernel - - - name: Verify the NFS kernel server is not installed - ansible.builtin.assert: - that: - - samba_audit_nfs_kernel.stdout is not search('install ok installed') - fail_msg: >- - nfs-kernel-server is installed. The export is implemented with - NFS-Ganesha (GNAI-019); review the package state before converging. - - name: Read the media mount options ansible.builtin.command: argv: diff --git a/proxmox/lxc/ct-101-samba/ansible/playbooks/preflight.yml b/proxmox/lxc/ct-101-samba/ansible/playbooks/preflight.yml index 4bb6ecc..b5c2a7b 100644 --- a/proxmox/lxc/ct-101-samba/ansible/playbooks/preflight.yml +++ b/proxmox/lxc/ct-101-samba/ansible/playbooks/preflight.yml @@ -20,7 +20,6 @@ - samba_expected_distribution is defined - samba_expected_distribution_major_version is defined - samba_expected_virtualization_type is defined - - samba_expected_podman_version is defined - samba_expected_hostname is defined - samba_minimum_root_size_bytes is defined - samba_minimum_root_available_bytes is defined @@ -99,9 +98,8 @@ - samba_preflight_uid_map.stdout_lines | length > 0 - samba_preflight_uid_map.stdout_lines[0] != "0 0 4294967295" fail_msg: >- - CT 101 must stay an unprivileged LXC (GNAI-015); the NFS-Ganesha - export and the gn-media alignment depend on the standard Proxmox ID - mapping. + CT 101 must stay an unprivileged LXC (GNAI-015); the gn-media + alignment depends on the standard Proxmox ID mapping. - name: Read the unprivileged SSH identity ansible.builtin.command: @@ -205,10 +203,9 @@ - samba_preflight_listeners.stdout is search(':22 ') - samba_preflight_listeners.stdout is search(':139 ') - samba_preflight_listeners.stdout is search(':445 ') - - samba_preflight_listeners.stdout is not search(':2049 ') fail_msg: >- The pre-migration listener set is unexpected: SSH and SMB must be - served and NFS (2049) must not be active yet. + served. - name: Verify the declared media mount points are mounted ansible.builtin.assert: @@ -218,8 +215,8 @@ superset(samba_media_mount_points) fail_msg: >- Not all declared media mount points are mounted on CT 101. The Samba - shares and the NFS export depend on the complete media library; stop - the adoption until the storage topology is complete. + shares depend on the complete media library; stop the adoption until + the storage topology is complete. success_msg: All declared media mount points are mounted. - name: Read the Samba server version @@ -389,16 +386,6 @@ changed_when: false register: samba_preflight_memory_max - - name: Read the Podman presence - ansible.builtin.command: - argv: - - /usr/bin/podman - - --version - check_mode: false - changed_when: false - failed_when: false - register: samba_preflight_podman - - name: Report the CT 101 preflight baseline ansible.builtin.debug: msg: @@ -446,9 +433,6 @@ agent_uid: "{{ samba_preflight_agent_uid.stdout }}" all_accounts: "{{ samba_preflight_all_accounts.stdout }}" all_groups: "{{ samba_preflight_all_groups.stdout }}" - podman_version: >- - {{ samba_preflight_podman.stdout - if samba_preflight_podman.rc == 0 else 'not installed' }} trusted_ssh_fingerprint: >- {{ samba_expected_ssh_host_key_fingerprint }} placeholder_sources: >- diff --git a/proxmox/lxc/ct-101-samba/ansible/playbooks/site.yml b/proxmox/lxc/ct-101-samba/ansible/playbooks/site.yml index a44a9fb..d23c3f4 100644 --- a/proxmox/lxc/ct-101-samba/ansible/playbooks/site.yml +++ b/proxmox/lxc/ct-101-samba/ansible/playbooks/site.yml @@ -21,8 +21,6 @@ samba_lxc_baseline_expected_virtualization_type: >- {{ samba_expected_virtualization_type }} - role: samba_access - - role: podman_runtime - role: samba_firewall - role: samba_media - role: samba_smb - - role: samba_nfs diff --git a/proxmox/lxc/ct-101-samba/ansible/playbooks/verify-production.yml b/proxmox/lxc/ct-101-samba/ansible/playbooks/verify-production.yml index a632f33..2748dd6 100644 --- a/proxmox/lxc/ct-101-samba/ansible/playbooks/verify-production.yml +++ b/proxmox/lxc/ct-101-samba/ansible/playbooks/verify-production.yml @@ -194,10 +194,6 @@ - >- samba_production_nft_table.stdout is search('SMB over TCP from the LAN') - - samba_production_nft_table.stdout is search('NFSv4 from ghostnet-ki') - - >- - samba_production_nft_table.stdout is - search(samba_firewall_nfs_client_ipv4) fail_msg: >- The active nftables policy does not match the declared CT 101 Samba baseline. @@ -415,129 +411,6 @@ loop_control: label: "{{ item }}" - - name: Read the deployed Ganesha configuration - ansible.builtin.slurp: - src: "{{ samba_nfs_config_directory }}/ganesha.conf" - register: samba_production_ganesha_conf - - - name: Verify the deployed Ganesha configuration matches the template - ansible.builtin.assert: - that: - - >- - samba_production_ganesha_conf['content'] | b64decode == - lookup('ansible.builtin.template', - playbook_dir ~ '/../roles/samba_nfs/templates/ganesha.conf.j2') - fail_msg: The deployed ganesha.conf differs from the declared template. - - - name: Read the runtime image identity - ansible.builtin.command: - argv: - - /usr/bin/podman - - image - - inspect - - --format=json - - "{{ samba_nfs_runtime_tag }}" - check_mode: false - changed_when: false - register: samba_production_image_inspect - - - name: Verify the optimized Ganesha image - ansible.builtin.assert: - that: - - >- - (samba_production_image_inspect.stdout | from_json)[0].Architecture == - samba_nfs_expected_architecture - - (samba_production_image_inspect.stdout | from_json)[0].Os == "linux" - - >- - ( - (samba_production_image_inspect.stdout | from_json)[0] - ).Config.Labels['eu.selfhost.ghostnet.containerfile-sha256'] == - samba_nfs_containerfile_sha256 - fail_msg: >- - The optimized Ganesha image does not match the reviewed Containerfile - checksum. - - - name: Read the deployed Ganesha Quadlet - ansible.builtin.slurp: - src: "{{ samba_nfs_quadlet_directory }}/ghostnet-samba-nfs.container" - register: samba_production_quadlet - - - name: Verify the deployed Ganesha Quadlet matches the template - ansible.builtin.assert: - that: - - >- - samba_production_quadlet['content'] | b64decode == - lookup('ansible.builtin.template', - playbook_dir ~ '/../roles/samba_nfs/templates/ghostnet-samba-nfs.container.j2') - fail_msg: The Ganesha Quadlet differs from the declared template. - vars: - samba_nfs_runtime_image_id: >- - {{ (samba_production_image_inspect.stdout | from_json)[0].Id }} - - - name: Check the generated Ganesha boot target link - ansible.builtin.stat: - path: >- - /run/systemd/generator/multi-user.target.wants/{{ samba_nfs_service_unit }} - follow: false - register: samba_production_boot_link - - - name: Verify the Ganesha boot policy - ansible.builtin.assert: - that: - - samba_production_boot_link.stat.exists - - samba_production_boot_link.stat.islnk - fail_msg: >- - The Ganesha service is not linked into the generated multi-user - target; the export would not survive a reboot. - - - name: Stat the NFS export root - ansible.builtin.stat: - path: "{{ samba_nfs_export_path }}" - register: samba_production_export_root - - - name: Verify the NFS export root ownership and mode - ansible.builtin.assert: - that: - - samba_production_export_root.stat.exists - - samba_production_export_root.stat.isdir - - samba_production_export_root.stat.pw_name == "root" - - >- - samba_production_export_root.stat.gr_name == - samba_media_group_name - - >- - '%04o' % (samba_production_export_root.stat.mode | int(0, 8)) == - '2775' - fail_msg: >- - The export root must be root:gn-media 2775 so the pipeline can create - its top-level directories via NFS (GNAI-019). - - - name: Read the Ganesha container state - ansible.builtin.command: - argv: - - /usr/bin/podman - - inspect - - --format=json - - "{{ samba_nfs_container_name }}" - check_mode: false - changed_when: false - register: samba_production_container - - - name: Verify the Ganesha container is healthy and rootful - ansible.builtin.assert: - that: - - >- - (samba_production_container.stdout | from_json)[0].State.Status == - "running" - - >- - (samba_production_container.stdout | from_json)[0].State.Health.Status == - "healthy" - - >- - (samba_production_container.stdout | from_json)[0].Config.User == - "0:0" - fail_msg: >- - The Ganesha container is not running and healthy as root; check - 'podman logs {{ samba_nfs_container_name }}'. - - name: Read required service states ansible.builtin.command: argv: @@ -553,7 +426,6 @@ - smbd.service - nmbd.service - unattended-upgrades.service - - "{{ samba_nfs_service_unit }}" register: samba_production_services - name: Require every production service to be active @@ -574,34 +446,14 @@ changed_when: false register: samba_production_listeners - - name: Require the SSH, SMB and NFS listeners + - name: Require the SSH and SMB listeners ansible.builtin.assert: that: - samba_production_listeners.stdout is search(':22 ') - samba_production_listeners.stdout is search(':139 ') - samba_production_listeners.stdout is search(':445 ') - - samba_production_listeners.stdout is search(':2049 ') fail_msg: >- - The CT 101 service listeners for SSH, SMB and NFS are incomplete. - - - name: Read the installed Podman version - ansible.builtin.command: - argv: - - /usr/bin/podman - - --version - check_mode: false - changed_when: false - register: samba_production_podman_version - - - name: Verify the expected Podman version - ansible.builtin.assert: - that: - - >- - samba_production_podman_version.stdout == - "podman version " ~ samba_expected_podman_version - fail_msg: >- - The installed Podman version differs from the expected Debian 13 - runtime. + The CT 101 service listeners for SSH and SMB are incomplete. - name: Read the host cgroup filesystem type ansible.builtin.command: @@ -618,7 +470,7 @@ ansible.builtin.assert: that: - samba_production_cgroup.stdout == "cgroup2fs" - fail_msg: The Podman runtime requires cgroup v2. + fail_msg: The cgroup filesystem must be cgroup v2 (cgroup2fs). - name: Read the pre-migration backup directories ansible.builtin.command: @@ -658,9 +510,5 @@ map(attribute='stdout') | unique | list }} smb_users: "{{ samba_production_pdbedit.stdout_lines }}" pre_migration_backups: "{{ samba_production_backups.stdout_lines }}" - ganesha_container: >- - {{ (samba_production_container.stdout | from_json)[0].State.Status }} - / - {{ (samba_production_container.stdout | from_json)[0].State.Health.Status }} trusted_ssh_fingerprint: >- {{ samba_expected_ssh_host_key_fingerprint }} diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/README.md b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/README.md index 3e21310..29d109d 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/README.md +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/README.md @@ -6,9 +6,9 @@ Declaratively manages the CT 101 nftables boundary (GNAI-018 R7): accepted. - SSH (`22/tcp`) and SMB (`139,445/tcp` plus `137,138/udp` for nmbd) are accepted from the management LAN only (`samba_firewall_management_ipv4`). -- NFS (`2049/tcp+udp`) is accepted exclusively from the encoding-pipeline host - `ghostnet-ki` (`samba_firewall_nfs_client_ipv4`), because the NFS-Ganesha - container runs with host networking inside CT 101. +- NFS is not opened on CT 101 at all: the encoding-pipeline export + (`/media/datenhdd3/Encoding`) is served by the PVE host itself (kernel NFS, + see `Host-Projekt `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)`), so no 2049 rule exists here. - Forward policy `drop` without allow rules; output policy `accept`. The policy lives in the Ghostnet-owned table `inet ghostnet_filter`. The table diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/defaults/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/defaults/main.yml index 4389597..18732c8 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/defaults/main.yml +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/defaults/main.yml @@ -8,6 +8,3 @@ samba_firewall_smb_tcp_ports: samba_firewall_smb_udp_ports: - 137 - 138 -samba_firewall_nfs_client_ipv4: 10.0.0.90 -samba_firewall_nfs_ports: - - 2049 diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/meta/argument_specs.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/meta/argument_specs.yml index 6937a18..4723372 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/meta/argument_specs.yml +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/meta/argument_specs.yml @@ -18,10 +18,3 @@ argument_specs: type: list elements: int required: true - samba_firewall_nfs_client_ipv4: - type: str - required: true - samba_firewall_nfs_ports: - type: list - elements: int - required: true diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/tasks/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/tasks/main.yml index 905dbd5..fa54a57 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/tasks/main.yml +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/tasks/main.yml @@ -5,8 +5,6 @@ - samba_firewall_management_ipv4 | length > 0 - samba_firewall_smb_tcp_ports | length > 0 - samba_firewall_smb_udp_ports | length > 0 - - samba_firewall_nfs_client_ipv4 | length > 0 - - samba_firewall_nfs_ports | length > 0 fail_msg: The firewall allowlists must be explicit and non-empty. - name: Read the active Ansible SSH transport endpoints @@ -86,10 +84,7 @@ - samba_firewall_active_table.stdout is search('policy drop') - samba_firewall_active_table.stdout is search('SSH management') - samba_firewall_active_table.stdout is search('SMB over TCP from the LAN') - - samba_firewall_active_table.stdout is search('NFSv4 from ghostnet-ki') - - samba_firewall_active_table.stdout is search(samba_firewall_nfs_client_ipv4) fail_msg: >- The active nftables policy does not match the declared CT 101 Samba - baseline (input/forward drop, SSH+SMB from the LAN, NFS only from - ghostnet-ki). + baseline (input/forward drop, SSH+SMB from the LAN). when: not ansible_check_mode diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/templates/nftables.conf.j2 b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/templates/nftables.conf.j2 index 6276030..f016139 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/templates/nftables.conf.j2 +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_firewall/templates/nftables.conf.j2 @@ -26,9 +26,6 @@ table inet ghostnet_filter { ip saddr @management_ipv4 tcp dport { {{ samba_firewall_smb_tcp_ports | join(', ') }} } ct state new counter accept comment "SMB over TCP from the LAN" ip saddr @management_ipv4 udp dport { {{ samba_firewall_smb_udp_ports | join(', ') }} } counter accept comment "SMB name service from the LAN" - ip saddr {{ samba_firewall_nfs_client_ipv4 }} tcp dport { {{ samba_firewall_nfs_ports | join(', ') }} } ct state new counter accept comment "NFSv4 from ghostnet-ki" - ip saddr {{ samba_firewall_nfs_client_ipv4 }} udp dport { {{ samba_firewall_nfs_ports | join(', ') }} } counter accept comment "NFSv4 datagrams from ghostnet-ki" - counter drop comment "default input deny" } diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/README.md b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/README.md index 0de7047..8857d0a 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/README.md +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/README.md @@ -14,7 +14,7 @@ The role refuses to run while the inventory still contains `REPLACE-WITH-*` placeholders; `playbooks/preflight.yml` on the `agent` inventory reports the values to copy into the selected inventory. -The unprivileged assertion is load-bearing: the NFS-Ganesha export and the -`gn-media` UID/GID alignment with `ghostnet-ki` (GNAI-019) assume the standard +The unprivileged assertion is load-bearing: the `gn-media` UID/GID alignment +with `ghostnet-ki` (GNAI-019) assumes the standard Proxmox ID mapping. A privileged conversion of CT 101 changes the on-disk UID/GID semantics of the media library and must not happen silently. diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/defaults/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/defaults/main.yml index 250b13a..607ae40 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/defaults/main.yml +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/defaults/main.yml @@ -23,26 +23,10 @@ samba_lxc_baseline_directories: owner: root group: root mode: "0700" - - path: /etc/ghostnet/samba/ganesha - owner: root - group: root - mode: "0700" - path: /var/lib/ghostnet owner: root group: root mode: "0755" - - path: /var/lib/ghostnet/samba - owner: root - group: root - mode: "0700" - - path: /var/lib/ghostnet/samba/build - owner: root - group: root - mode: "0700" - - path: /var/lib/ghostnet/samba/ganesha - owner: root - group: root - mode: "0700" - path: /var/backups/ghostnet owner: root group: root diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/tasks/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/tasks/main.yml index 2d446aa..9c23dde 100644 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/tasks/main.yml +++ b/proxmox/lxc/ct-101-samba/ansible/roles/samba_lxc_baseline/tasks/main.yml @@ -52,9 +52,10 @@ - samba_lxc_baseline_uid_map.stdout_lines | length > 0 - samba_lxc_baseline_uid_map.stdout_lines[0] != "0 0 4294967295" fail_msg: >- - CT 101 must stay an unprivileged LXC. The NFS-Ganesha export design - (GNAI-019) depends on the standard Proxmox ID mapping; a privileged - conversion changes the UID/GID semantics of the media library. + CT 101 must stay an unprivileged LXC. The gn-media UID/GID alignment + with ghostnet-ki depends on the standard Proxmox ID mapping; a + privileged conversion changes the UID/GID semantics of the media + library. - name: Mask the configfs mount unit (configfs is unavailable in unprivileged LXC) ansible.builtin.systemd_service: diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/README.md b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/README.md deleted file mode 100644 index 6d76327..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# `samba_nfs` - -Declaratively manages the NFS export of `/media/datenhdd3/Encoding` for the -encoding pipeline on `ghostnet-ki` (GNAI-018 R1, implemented with NFS-Ganesha -instead of `nfs-kernel-server` — see GNAI-019 decision 2 for the evidence: -the kernel nfsd cannot run in an unprivileged LXC). - -- Creates the export root `/media/datenhdd3/Encoding` as `root:gn-media 2775` - (approved deviation from R1's `root:root 0755`: the pipeline creates the - top-level directories via NFS as UID 982/983 and needs group-write plus - setgid on the export root, GNAI-017 section 4.4). The `gn-media` group - (GID 970) must exist first — the role fails closed otherwise. -- Verifies the reviewed `Containerfile.ganesha` checksum on the control node - before anything is built, installs the build context - (`0600`) and the healthcheck helper (`0555`). -- Pulls the digest-pinned Debian 13 base image, builds - `localhost/ghostnet/ganesha:trixie-ansible1` with `--pull=never - --network=host --timestamp=0` and a `containerfile-sha256` label, and - rebuilds only when the image is missing or the label diverges. -- Installs the fully templated `ganesha.conf` (`0600`): NFSv4.2 only, - AUTH_SYS, `Squash = rootid` (standard squash stays, R1), client-restricted - to `10.0.0.90`, `Pseudo` identical to the export path so the documented - mount path `10.0.0.9:/media/datenhdd3/Encoding` is unchanged. -- Installs the Quadlet `ghostnet-samba-nfs.container`: **rootful** (the VFS - FSAL applies the AUTH_SYS credentials per request, which needs CAP_SETUID), - **host networking** (listener 2049 in the CT namespace, covered by the - `samba_firewall` input chain), `Requires=nftables.service` (fail closed), - read-only rootfs with tmpfs `/run`+`​/tmp`, journald logging, conmon notify, - TCP healthcheck against 2049. -- Enables and starts `ghostnet-samba-nfs.service`, verifies the generated - boot link, waits for the 2049 listener and asserts the container is - running, healthy and `Config.User == "0:0"`. - -Ganesha has no config-test flag that is safe to rely on, so the configuration -syntax is verified by the service start and the health/until-loop asserts; -on a crash-looping container, `podman logs ghostnet-samba-nfs` shows the -ganesha parser error. Ganesha without dbus in the container logs a non-fatal -admin-interface warning (documented in GNAI-019). - -Prerequisites on the Proxmox host (operator, before the first converge): -the LXC seccomp profile must allow `open_by_handle_at` — an empty -`lxc.seccomp.profile:` line in `/etc/pve/lxc/101.conf` plus a CT reboot. The -one-shot `scripts/upgrade-ct101-to-debian13.sh` performs both the Debian 13 -dist-upgrade and this prerequisite. diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/defaults/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/defaults/main.yml deleted file mode 100644 index 45b9133..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/defaults/main.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -samba_nfs_artifact_source_root: "{{ playbook_dir }}/../.." -samba_nfs_quadlet_directory: /etc/containers/systemd -samba_nfs_build_directory: /var/lib/ghostnet/samba/build -samba_nfs_config_directory: /etc/ghostnet/samba/ganesha -samba_nfs_state_directory: /var/lib/ghostnet/samba/ganesha -# Reviewed checksum of ../../Containerfile.ganesha (sha256sum, 2026-10-30): -samba_nfs_containerfile_sha256: >- - 54cf9cae60b3a3849d61213e5892f8ec14a2c8bdffeed2c8586ff727c73aef73 -samba_nfs_source_image: >- - docker.io/library/debian:trixie-slim@sha256:918311b7b6c4c6f68b232ba516584925f6c78ad82b6fd534b98979df6438e483 -samba_nfs_runtime_tag: localhost/ghostnet/ganesha:trixie-ansible1 -samba_nfs_expected_architecture: amd64 -samba_nfs_container_name: ghostnet-samba-nfs -samba_nfs_service_unit: ghostnet-samba-nfs.service -samba_nfs_export_path: /media/datenhdd3/Encoding -samba_nfs_client_ipv4: 10.0.0.90 -samba_nfs_export_id: 101 diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/handlers/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/handlers/main.yml deleted file mode 100644 index 5b15a14..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/handlers/main.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -- name: Reload systemd for the Ganesha Quadlet - ansible.builtin.systemd_service: - daemon_reload: true - -- name: Restart the Ganesha NFS export - ansible.builtin.systemd_service: - name: ghostnet-samba-nfs.service - state: restarted - when: not ansible_check_mode diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/meta/argument_specs.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/meta/argument_specs.yml deleted file mode 100644 index cb7d7bd..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/meta/argument_specs.yml +++ /dev/null @@ -1,47 +0,0 @@ ---- -argument_specs: - main: - short_description: Build and run the CT 101 NFS-Ganesha export container - options: - samba_nfs_artifact_source_root: - type: path - required: true - samba_nfs_quadlet_directory: - type: path - required: true - samba_nfs_build_directory: - type: path - required: true - samba_nfs_config_directory: - type: path - required: true - samba_nfs_state_directory: - type: path - required: true - samba_nfs_containerfile_sha256: - type: str - required: true - samba_nfs_source_image: - type: str - required: true - samba_nfs_runtime_tag: - type: str - required: true - samba_nfs_expected_architecture: - type: str - required: true - samba_nfs_container_name: - type: str - required: true - samba_nfs_service_unit: - type: str - required: true - samba_nfs_export_path: - type: path - required: true - samba_nfs_client_ipv4: - type: str - required: true - samba_nfs_export_id: - type: int - required: true diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/tasks/main.yml b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/tasks/main.yml deleted file mode 100644 index b586bbe..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/tasks/main.yml +++ /dev/null @@ -1,296 +0,0 @@ ---- -- name: Require the NFS export declarations - ansible.builtin.assert: - that: - - samba_nfs_export_path | length > 0 - - samba_nfs_client_ipv4 | length > 0 - - samba_nfs_export_id | int > 0 - - samba_nfs_source_image | length > 0 - - samba_nfs_runtime_tag | length > 0 - - samba_nfs_container_name | length > 0 - - samba_nfs_service_unit | length > 0 - fail_msg: The NFS export declarations must be explicit and non-empty. - -- name: Require the media group before creating the export root - ansible.builtin.command: - argv: - - /usr/bin/getent - - group - - "{{ samba_media_group_name }}" - check_mode: false - changed_when: false - failed_when: false - register: samba_nfs_media_group - -- name: Fail closed when the media group is missing - ansible.builtin.assert: - that: - - samba_nfs_media_group.rc == 0 - - >- - samba_nfs_media_group.stdout.split(':')[2] | int == - samba_media_group_gid | int - fail_msg: >- - The group {{ samba_media_group_name }} (GID {{ samba_media_group_gid }}) - is missing or carries a different GID. Run the samba_media role first; - the export root must be group-writable for the pipeline identities. - when: not ansible_check_mode or not samba_media_group_task.changed | default(false) | bool - -- name: Create the NFS export root with the pipeline group - ansible.builtin.file: - path: "{{ samba_nfs_export_path }}" - state: directory - owner: root - group: "{{ samba_media_group_name }}" - mode: "2775" - -- name: Check the reviewed Containerfile checksum on the control node - become: false - ansible.builtin.stat: - path: "{{ samba_nfs_artifact_source_root }}/Containerfile.ganesha" - checksum_algorithm: sha256 - delegate_to: localhost - register: samba_nfs_source_containerfile - -- name: Require the pinned reviewed Containerfile - ansible.builtin.assert: - that: - - samba_nfs_source_containerfile.stat.exists - - >- - samba_nfs_source_containerfile.stat.checksum == - samba_nfs_containerfile_sha256 - fail_msg: The Ganesha Containerfile differs from its reviewed checksum. - -- name: Install the dedicated Ganesha build context - ansible.builtin.copy: - src: "{{ samba_nfs_artifact_source_root }}/Containerfile.ganesha" - dest: "{{ samba_nfs_build_directory }}/Containerfile.ganesha" - owner: root - group: root - mode: "0600" - -- name: Install the Ganesha healthcheck helper - ansible.builtin.copy: - src: "{{ samba_nfs_artifact_source_root }}/ghostnet-ganesha-healthcheck" - dest: "{{ samba_nfs_config_directory }}/ghostnet-ganesha-healthcheck" - owner: root - group: root - mode: "0555" - -- name: Check the pinned Ganesha source image - ansible.builtin.command: - argv: - - /usr/bin/podman - - image - - exists - - "{{ samba_nfs_source_image }}" - check_mode: false - changed_when: false - failed_when: false - register: samba_nfs_source_image_exists - -- name: Pull the pinned Ganesha source image - ansible.builtin.command: - argv: - - /usr/bin/podman - - pull - - --platform - - "linux/{{ samba_nfs_expected_architecture }}" - - "{{ samba_nfs_source_image }}" - changed_when: true - when: - - not ansible_check_mode - - samba_nfs_source_image_exists.rc != 0 - -- name: Check the local optimized Ganesha image - ansible.builtin.command: - argv: - - /usr/bin/podman - - image - - exists - - "{{ samba_nfs_runtime_tag }}" - check_mode: false - changed_when: false - failed_when: false - register: samba_nfs_runtime_image_exists - -- name: Inspect the existing optimized Ganesha image - ansible.builtin.command: - argv: - - /usr/bin/podman - - image - - inspect - - --format=json - - "{{ samba_nfs_runtime_tag }}" - check_mode: false - changed_when: false - register: samba_nfs_existing_runtime_inspect - when: samba_nfs_runtime_image_exists.rc == 0 - -- name: Decide whether the optimized Ganesha image needs rebuilding - ansible.builtin.set_fact: - samba_nfs_rebuild_required: >- - {{ - samba_nfs_runtime_image_exists.rc != 0 or - ( - ( - (samba_nfs_existing_runtime_inspect.stdout | from_json)[0] - ).Config.Labels['eu.selfhost.ghostnet.containerfile-sha256'] | - default('') - ) != samba_nfs_containerfile_sha256 - }} - -- name: Build the optimized Ganesha image from pinned local inputs - ansible.builtin.command: - argv: - - /usr/bin/podman - - build - - --pull=never - - --network=host - - --timestamp=0 - - --label - - >- - eu.selfhost.ghostnet.containerfile-sha256={{ samba_nfs_containerfile_sha256 }} - - --tag - - "{{ samba_nfs_runtime_tag }}" - - --file - - "{{ samba_nfs_build_directory }}/Containerfile.ganesha" - - "{{ samba_nfs_build_directory }}" - changed_when: true - when: - - not ansible_check_mode - - samba_nfs_rebuild_required - -- name: Inspect the selected optimized Ganesha image - ansible.builtin.command: - argv: - - /usr/bin/podman - - image - - inspect - - --format=json - - "{{ samba_nfs_runtime_tag }}" - check_mode: false - changed_when: false - register: samba_nfs_runtime_inspect - when: - - not ansible_check_mode or samba_nfs_runtime_image_exists.rc == 0 - -- name: Select the immutable runtime image ID - ansible.builtin.set_fact: - samba_nfs_runtime_image_id: >- - {{ (samba_nfs_runtime_inspect.stdout | from_json)[0].Id }} - when: - - not ansible_check_mode or samba_nfs_runtime_image_exists.rc == 0 - -- name: Use a non-runnable placeholder image ID only for an empty check-mode target - ansible.builtin.set_fact: - samba_nfs_runtime_image_id: >- - sha256:0000000000000000000000000000000000000000000000000000000000000000 - when: - - ansible_check_mode - - samba_nfs_runtime_image_exists.rc != 0 - -- name: Verify the optimized Ganesha image - ansible.builtin.assert: - that: - - >- - (samba_nfs_runtime_inspect.stdout | from_json)[0].Architecture == - samba_nfs_expected_architecture - - (samba_nfs_runtime_inspect.stdout | from_json)[0].Os == "linux" - - >- - ( - (samba_nfs_runtime_inspect.stdout | from_json)[0] - ).Config.Labels['eu.selfhost.ghostnet.containerfile-sha256'] == - samba_nfs_containerfile_sha256 - when: - - not ansible_check_mode or samba_nfs_runtime_image_exists.rc == 0 - -- name: Install the Ganesha export configuration - ansible.builtin.template: - src: ganesha.conf.j2 - dest: "{{ samba_nfs_config_directory }}/ganesha.conf" - owner: root - group: root - mode: "0600" - notify: Restart the Ganesha NFS export - -- name: Install the Ganesha NFS export Quadlet - ansible.builtin.template: - src: ghostnet-samba-nfs.container.j2 - dest: "{{ samba_nfs_quadlet_directory }}/ghostnet-samba-nfs.container" - owner: root - group: root - mode: "0644" - notify: Reload systemd for the Ganesha Quadlet - -- name: Flush the Ganesha handlers before state management - ansible.builtin.meta: flush_handlers - -- name: Enable and start the Ganesha NFS export service - ansible.builtin.systemd_service: - name: "{{ samba_nfs_service_unit }}" - enabled: true - state: started - when: not ansible_check_mode - -- name: Check the generated Ganesha boot target link - ansible.builtin.stat: - path: >- - /run/systemd/generator/multi-user.target.wants/{{ samba_nfs_service_unit }} - follow: false - register: samba_nfs_boot_link - when: not ansible_check_mode - -- name: Verify the declared Ganesha boot policy - ansible.builtin.assert: - that: - - samba_nfs_boot_link.stat.exists - - samba_nfs_boot_link.stat.islnk - fail_msg: >- - The Ganesha NFS export service is not linked into the generated - multi-user target; the export would not survive a reboot. - when: not ansible_check_mode - -- name: Wait for the NFSv4 listener - ansible.builtin.command: - argv: - - /usr/bin/ss - - -H - - -lnt - - ( sport = :2049 ) - check_mode: false - changed_when: false - register: samba_nfs_listener - until: samba_nfs_listener.stdout | length > 0 - retries: 12 - delay: 5 - when: not ansible_check_mode - -- name: Inspect the Ganesha NFS export container - ansible.builtin.command: - argv: - - /usr/bin/podman - - inspect - - --format=json - - "{{ samba_nfs_container_name }}" - check_mode: false - changed_when: false - register: samba_nfs_container_inspect - when: not ansible_check_mode - -- name: Verify the Ganesha NFS export container is healthy and rootful - ansible.builtin.assert: - that: - - >- - (samba_nfs_container_inspect.stdout | from_json)[0].State.Status == - "running" - - >- - (samba_nfs_container_inspect.stdout | from_json)[0].State.Health.Status == - "healthy" - - >- - (samba_nfs_container_inspect.stdout | from_json)[0].Config.User == - "0:0" - fail_msg: >- - The Ganesha container is not running and healthy as root. Check - 'podman logs {{ samba_nfs_container_name }}' for ganesha configuration - or startup errors. - when: not ansible_check_mode diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ganesha.conf.j2 b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ganesha.conf.j2 deleted file mode 100644 index 65db83d..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ganesha.conf.j2 +++ /dev/null @@ -1,52 +0,0 @@ -# Managed by Ansible role samba_nfs (CT 101, GNAI-017/GNAI-018, GNAI-019). -# Userspace NFSv4.2 export for the encoding pipeline on ghostnet-ki. -# NFSv4 only: no mountd/rpcbind, therefore no `showmount` (see GNAI-019); -# the authoritative check is a real NFSv4.2 mount from ghostnet-ki. -NFS_CORE_PARAM { - Protocols = 4; - Enable_NLM = false; - Enable_RQUOTA = false; - # Unprivileged LXC: PR_SET_IO_FLUSHER needs CAP_SYS_ADMIN in the init user - # namespace, which a guest root does not have; allow the failure (verified - # with ganesha.nfsd in the image on 2026-10-30). - Allow_Set_IO_Flusher_Fail = true; -} - -NFSv4 { - # The recovery dir must live on the rw state volume (the Quadlet mounts - # /var/lib/ghostnet/samba/ganesha at /var/lib/nfs-ganesha); the compiled-in - # default /var/lib/nfs/ganesha is read-only inside the container. - RecoveryRoot = /var/lib/nfs-ganesha; -} - -EXPORT_DEFAULTS { - Access_Type = None; -} - -# Client allowlist: only ghostnet-ki gets RW (GNAI-017 section 4.1). Ganesha -# 6.5 syntax: CLIENT block with lowercase `clients` parameter, and it must -# appear BEFORE the EXPORT block (parser is order-sensitive; verified against -# the 6.5 parser on 2026-10-30). Additionally the nftables policy only admits -# 2049 from 10.0.0.90 (samba_firewall, asserted by verify-production.yml). -CLIENT { - clients = {{ samba_nfs_client_ipv4 }}; - Access_Type = RW; -} - -EXPORT { - Export_Id = {{ samba_nfs_export_id }}; - Path = {{ samba_nfs_export_path }}; - Pseudo = {{ samba_nfs_export_path }}; - Access_Type = None; - Squash = rootid; - SecType = sys; - Protocols = 4; - Transports = TCP,UDP; - FSAL { - Name = VFS; - } -} - -LOG { - Default_Log_Level = INFO; -} diff --git a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ghostnet-samba-nfs.container.j2 b/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ghostnet-samba-nfs.container.j2 deleted file mode 100644 index 1b7aa84..0000000 --- a/proxmox/lxc/ct-101-samba/ansible/roles/samba_nfs/templates/ghostnet-samba-nfs.container.j2 +++ /dev/null @@ -1,50 +0,0 @@ -[Unit] -Description=Ghostnet Samba NFS-Ganesha export for the encoding pipeline -Wants=network-online.target -After=network-online.target nftables.service -# Fail closed: without the nftables policy the export must not start. -Requires=nftables.service - -[Container] -Image={{ samba_nfs_runtime_image_id }} -Pull=never -ContainerName={{ samba_nfs_container_name }} - -# Host networking: the export listener (2049) is bound in the CT 101 network -# namespace and covered by the samba_firewall input chain. -Network=host - -Volume={{ samba_nfs_config_directory }}/ganesha.conf:/etc/ganesha/ganesha.conf:ro -Volume={{ samba_nfs_config_directory }}/ghostnet-ganesha-healthcheck:/usr/local/bin/ghostnet-ganesha-healthcheck:ro -Volume={{ samba_nfs_export_path }}:{{ samba_nfs_export_path }}:rw -Volume={{ samba_nfs_state_directory }}:/var/lib/nfs-ganesha:rw - -# Rootful on purpose: the VFS FSAL applies the AUTH_SYS credentials of the -# pipeline (UID 982/983) per request, which requires CAP_SETUID inside the -# unprivileged LXC user namespace. -User=0 -Group=0 -ReadOnly=true -Tmpfs=/run:rw -Tmpfs=/tmp:rw -LogDriver=journald - -HealthCmd=/usr/local/bin/ghostnet-ganesha-healthcheck -HealthInterval=10s -HealthTimeout=5s -HealthRetries=6 -HealthStartPeriod=30s -Notify=conmon -StopSignal=SIGTERM -StopTimeout=30 - -Label=eu.selfhost.ghostnet.stack=samba -Label=eu.selfhost.ghostnet.role=nfs-export - -[Service] -Restart=on-failure -RestartSec=5s -TimeoutStartSec=120s - -[Install] -WantedBy=multi-user.target diff --git a/proxmox/lxc/ct-101-samba/ghostnet-ganesha-healthcheck b/proxmox/lxc/ct-101-samba/ghostnet-ganesha-healthcheck deleted file mode 100755 index 553f09a..0000000 --- a/proxmox/lxc/ct-101-samba/ghostnet-ganesha-healthcheck +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Liveness probe for the rootful NFS-Ganesha container (CT 101, GNAI-019). -# A bare TCP connect to the NFSv4 listener proves that ganesha.nfsd is -# serving; there is no HTTP endpoint to probe. - -set -euo pipefail - -exec 3<>/dev/tcp/127.0.0.1/2049 diff --git a/proxmox/lxc/ct-101-samba/scripts/README.md b/proxmox/lxc/ct-101-samba/scripts/README.md index 228a92c..038ef18 100644 --- a/proxmox/lxc/ct-101-samba/scripts/README.md +++ b/proxmox/lxc/ct-101-samba/scripts/README.md @@ -20,17 +20,19 @@ productive `smb.conf`). Sequence: IPv4 matches. 2. `vzdump 101 --mode stop` as a safety net (storage and dump directory are configurable). -3. Add an empty `lxc.seccomp.profile:` to `/etc/pve/lxc/101.conf` — the rootful - NFS-Ganesha container needs `open_by_handle_at`, which the default seccomp - profile blocks (GNAI-019 decision 2). +3. No `lxc.seccomp.profile` change is needed anymore: that exception existed + only for the Ganesha NFS container inside CT 101. Since 2026-10-30 the NFS + export runs on the PVE host (see `proxmox/pve-host/ansible/` (Rolle `pve_nfs_export`)); if the + empty line is still present from the earlier Ganesha setup, the script + removes it again (the NFS export can no longer run in an unprivileged LXC: + `open_by_handle_at` needs CAP_DAC_READ_SEARCH in the init user namespace). 4. In the guest: `bookworm` → `trixie` in the apt sources (deb822 and one-line style), `apt-get update`, `dist-upgrade` with `Dpkg::Options::=--force-confold`, `autoremove`, then the known dangling `wtmpdb-rotate.timer` cleanup rule from `ghostnet-lxc-baseline.sh`. 5. Reboot and post-verify: Debian 13, `ssh`/`smbd`/`nmbd` active, no failed units, listeners 22/139/445, anonymous `smbclient -L` query successful, - unprivileged uid_map intact, seccomp filter disabled, all six media - mounts mounted. + unprivileged uid_map intact, all six media mounts mounted. Before the run, all media mounts in the productive `pct config 101` must carry the `backup=no` option. Installation and invocation: @@ -51,13 +53,13 @@ file-server service; the host key is unchanged, so the workstation does not have to replace a known host key. The reviewed repository copy has SHA-256 -`99f34cd991143d7d11dc1f64a49d29e966dee9db23737d081b06b67c1e409aac`. +`7671722cbd5107b0453ec5d016d8ffed82f36ec34d959f50507c21dc953c00a4`. ## `provision-ct101-samba.sh` (fresh-state rebuild) Creates a fresh CT 101 from a Debian 13 template with the recorded profile, -attaches the six media mounts, disables the LXC seccomp filter and then runs -the reviewed baseline hook with the recorded account UIDs. After the hook it +attaches the six media mounts and then runs the reviewed baseline hook with +the recorded account UIDs (no seccomp change — see above). After the hook it masks `sys-kernel-config.mount` (configfs is unavailable in unprivileged LXC; CT 100/102/103 convention) and clears a stale failed state, so the fresh CT passes the no-failed-units gate of `playbooks/preflight.yml`. The script @@ -73,7 +75,6 @@ setting explicitly. | Rootfs | 4 GiB on `local-lvm` (recorded profile, adjustable via env) | | Network | `vmbr0`, `10.0.0.9/24`, gateway `10.0.0.1`, DNS `10.0.0.14`, no IPv6 | | Boot / timezone | `onboot: 1`, `Europe/Berlin` | -| Seccomp | empty `lxc.seccomp.profile:` (Ganesha `open_by_handle_at`) | | Media mounts | `mp0`–`mp5` → `/media/daten1`, `/media/daten2`, `/media/daten3`, `/media/datenhdd1`, `/media/datenhdd2/extern`, `/media/datenhdd3`, each with `backup=no` | | Baseline hook | `/usr/local/sbin/ghostnet-lxc-baseline.sh` (SHA-256 pinned) | @@ -115,8 +116,15 @@ steps (set the ghost password, install the public keys from placeholders, rebuild sequence from `ansible/README.md`). The reviewed repository copy has SHA-256 -`f40f3da836d3b0b31773c8293fa245eb970fae079dfe6189c91c699a48c21306`. +`74447ae18ab294fae08845490a214959e8e2a629c6aaf798351b0669e2f91823`. +## NFS-Export auf dem PVE-Host + +Der Kernel-NFS-Export von `/media/datenhdd3/Encoding` läuft nicht mehr als +manuelles Script, sondern deklarativ über das Host-Projekt +[`proxmox/pve-host/ansible/`](../../../pve-host/README.md) (Rolle `pve_nfs_export`, +Playbook `site.yml` auf dem PVE-Host via `ansible-deploy`). Siehe dort für +Account-Prep und Runbook. ## `ghostnet-lxc-baseline.sh` The versioned source lives under diff --git a/proxmox/lxc/ct-101-samba/scripts/provision-ct101-samba.sh b/proxmox/lxc/ct-101-samba/scripts/provision-ct101-samba.sh index f9fd970..214ed0c 100644 --- a/proxmox/lxc/ct-101-samba/scripts/provision-ct101-samba.sh +++ b/proxmox/lxc/ct-101-samba/scripts/provision-ct101-samba.sh @@ -2,11 +2,11 @@ # Fresh-state rebuild of CT 101 (Samba file server) on the Proxmox VE host. # Creates the container from a Debian 13 template with the recorded profile, -# attaches the six media mounts, disables the LXC seccomp filter (required -# by the rootful NFS-Ganesha container, GNAI-019) and runs the reviewed -# generic LXC baseline hook with the recorded account UIDs. It then masks the -# configfs mount unit (configfs is unavailable in unprivileged LXC; CT -# 100/102/103 convention) and clears a stale failed state. +# attaches the six media mounts and runs the reviewed generic LXC baseline +# hook with the recorded account UIDs. It then masks the configfs mount unit +# (configfs is unavailable in unprivileged LXC; CT 100/102/103 convention) +# and clears a stale failed state. No lxc.seccomp.profile change: the NFS +# export runs on the PVE host since 2026-10-30 (GNAI-019 decision 2 reverted). # # The script never replaces or destroys an existing CT 101. It contains no # password, private key or SMB secret. The SMB passwords are set by the @@ -177,17 +177,6 @@ for index in "${!MOUNT_GUEST_PATHS[@]}"; do die "pct set --mp${index} failed" done -# An empty lxc.seccomp.profile disables the LXC seccomp filter for the -# container. The rootful NFS-Ganesha container needs open_by_handle_at, which -# the default profile blocks (GNAI-019 decision 2). Append after all pct set -# operations because pct rewrites the configuration file. -conf_file="/etc/pve/lxc/${CTID}.conf" -if grep -Eq '^lxc\.seccomp\.profile:.+' "$conf_file"; then - die "CT ${CTID} already has a non-empty lxc.seccomp.profile; review manually" -fi -grep -qx 'lxc.seccomp.profile:' "$conf_file" || - printf 'lxc.seccomp.profile:\n' >>"$conf_file" - config="$(pct config "$CTID")" grep -qx "cores: ${CORES}" <<<"$config" || die 'unexpected CPU allocation' grep -qx "memory: ${MEMORY_MB}" <<<"$config" || die 'unexpected memory allocation' @@ -202,8 +191,6 @@ grep -Eq '^features: .*nesting=1' <<<"$config" || die 'nesting is not enabled' grep -Eq '^features: .*keyctl=1' <<<"$config" || die 'keyctl is not enabled' grep -Eq "^rootfs: ${CONTAINER_STORAGE}:.*,size=${ROOTFS_GB}G(,|\$)" <<<"$config" || die 'unexpected root filesystem allocation' -grep -qx 'lxc.seccomp.profile:' <<<"$config" || - die 'the empty seccomp profile line is missing' net0="$(grep '^net0: ' <<<"$config")" for expected_net_field in \ 'name=eth0' \ @@ -284,13 +271,9 @@ for mount_point in "${mounts[@]}"; do awk -v target="$mount_point" '$2 == target { found = 1 } END { exit(found ? 0 : 1) }' /proc/mounts || die "media mount point is not mounted: $mount_point" done -# The empty lxc.seccomp.profile must disable the container seccomp filter. -test "$(awk '$1 == "Seccomp:" { print $2 }' /proc/self/status)" = '0' - log "guest: $(hostname), Debian ${VERSION_ID}, $(systemd-detect-virt)" log "ghost-agent UID ${agent_uid}, ghost UID ${admin_uid}, ssh.service active" log "media mounts verified: ${mounts[*]}" -log 'seccomp filter disabled (open_by_handle_at available for Ganesha)' GUEST_VERIFY log "CT ${CTID} fresh-state provisioning completed and verified" diff --git a/proxmox/lxc/ct-101-samba/scripts/upgrade-ct101-to-debian13.sh b/proxmox/lxc/ct-101-samba/scripts/upgrade-ct101-to-debian13.sh index 47ccda4..6dd20a8 100644 --- a/proxmox/lxc/ct-101-samba/scripts/upgrade-ct101-to-debian13.sh +++ b/proxmox/lxc/ct-101-samba/scripts/upgrade-ct101-to-debian13.sh @@ -4,9 +4,10 @@ # (Samba file server) on the Proxmox VE host, as the first step of the # Ansible adoption. The existing LXC is kept; this script never recreates it. # -# Sequence: guest preflight, pre-upgrade vzdump (mode stop), empty -# lxc.seccomp.profile (required later by the rootful NFS-Ganesha container, -# GNAI-019), in-guest bookworm -> trixie dist-upgrade, reboot, post-verify. +# Sequence: guest preflight, pre-upgrade vzdump (mode stop), in-guest +# bookworm -> trixie dist-upgrade, reboot, post-verify. If the Ganesha-era +# empty lxc.seccomp.profile line is present, it is removed again (the NFS +# export runs on the PVE host since 2026-10-30; GNAI-019 decision 2 reverted). # # The script contains no password or private key. The SMB passdb and the # productive smb.conf survive the upgrade (conffile kept via @@ -136,17 +137,14 @@ for _ in $(seq 1 60); do done [[ "$responsive" == 'yes' ]] || die 'CT 101 did not become responsive after the vzdump' -# An empty lxc.seccomp.profile disables the LXC seccomp filter for the -# container. The rootful NFS-Ganesha container needs open_by_handle_at, which -# the default profile blocks (GNAI-019 decision 2). The change applies at the -# reboot below. +# Revert the Ganesha-era seccomp exception: the NFS export now runs on the PVE +# host, so CT 101 uses the default seccomp profile again (like CT 100/102/103). +# Applies at the reboot below. conf_file="/etc/pve/lxc/${CTID}.conf" -if grep -Eq '^lxc\.seccomp\.profile:.+' "$conf_file"; then - die "CT ${CTID} already has a non-empty lxc.seccomp.profile; review manually" +if grep -qx 'lxc.seccomp.profile:' "$conf_file"; then + sed -i '/^lxc\.seccomp\.profile:$/d' "$conf_file" + log 'empty lxc.seccomp.profile removed (Ganesha exception reverted)' fi -grep -qx 'lxc.seccomp.profile:' "$conf_file" || - printf 'lxc.seccomp.profile:\n' >>"$conf_file" -log 'empty lxc.seccomp.profile recorded in the container configuration' log 'running the in-guest bookworm -> trixie dist-upgrade (this takes a while)' pct exec "$CTID" -- /bin/bash -s <<'GUEST_UPGRADE' || @@ -200,7 +198,7 @@ log "dist-upgrade complete: Debian ${VERSION_ID}" log "samba now: $(/usr/sbin/smbd --version)" GUEST_UPGRADE -log 'rebooting CT 101 to complete the transition and apply the seccomp change' +log 'rebooting CT 101 to complete the transition' pct reboot "$CTID" 2>/dev/null || pct start "$CTID" || die 'reboot failed; run pct start 101 manually' responsive='no' @@ -254,8 +252,6 @@ grep -q ':445 ' <<<"$listeners" || die 'SMB listener 445 is missing' die 'the anonymous Samba share listing failed after the upgrade' [[ "$(head -n 1 /proc/self/uid_map)" != '0 0 4294967295' ]] || die 'guest lost its unprivileged user-namespace mapping' -test "$(awk '$1 == "Seccomp:" { print $2 }' /proc/self/status)" = '0' || - die 'the container seccomp filter is still active' for mount_point in "${mounts[@]}"; do awk -v target="$mount_point" '$2 == target { found = 1 } END { exit(found ? 0 : 1) }' /proc/mounts || die "media mount is not mounted: ${mount_point}" @@ -264,7 +260,7 @@ done log "Debian ${VERSION_ID}, hostname $(hostname)" log "samba: $(/usr/sbin/smbd --version)" log 'ssh, smbd and nmbd are active; listeners 22/139/445 are present' -log 'unprivileged mapping and disabled seccomp filter verified' +log 'unprivileged mapping verified' log "media mounts verified: ${mounts[*]}" GUEST_POSTVERIFY diff --git a/proxmox/pve-host/README.md b/proxmox/pve-host/README.md new file mode 100644 index 0000000..2ab6b2e --- /dev/null +++ b/proxmox/pve-host/README.md @@ -0,0 +1,88 @@ +# PVE-Host (`10.0.0.2`) — Ansible-Verwaltung + +Projekt zur deklarativen Verwaltung des **Proxmox-VE-Hosts selbst** +(`pve`, `10.0.0.2`). Bis 2026-10-30 liefen Host-Änderungen als manuelle +Root-Skripte auf der Konsole; seit der NFS-Umstellung (GNAI-019 Entscheidung 2, +revidiert) wird persistente Host-State über dieses Ansible-Projekt gepflegt — +reproduzierbar und versioniert im Repo. GNAI-011 §1 sieht ein eigenes +Host-Verfahren vor („Proxmox-Hostzugang … benötigen ein eigenes, späteres +Verfahren“); dieses Projekt ist der Anfang davon. Die **SSH-Härtung des Hosts +selbst** bleibt davon unberührt (GNAI-011 gilt nur für die LXCs) und ist ein +eigenes, späteres Vorhaben. + +Aktuell verwaltete Host-State: + +- **Kernel-NFS-Export** `/media/datenhdd3/Encoding` → `ghostnet-ki` + (`10.0.0.90/32`), NFSv4-only — Rolle `pve_nfs_export` (Ersatz für das + frühere manuelle Script; CT 101 kann NFS nicht selbst serve, siehe GNAI-019). +- **LXC-Configs**: Entfernen der Ganesha-Ära-`lxc.seccomp.profile:`-Leerzeile + aus `/etc/pve/lxc/101.conf` — Rolle `pve_lxc_conf` (wirksam beim nächsten + CT-Start/Reboot). + +Die CT-Lebenszyklus-Skripte (`proxmox/lxc//scripts/`: `pct create`, +Dist-Upgrade) bleiben Bootstrap-Skripte und laufen einmalig manuell als +root@pve — sie erzeugen die CTs, bevor Ansible sie erreichen kann. + +## Zugang: dedizierter Deploy-Account auf dem Host + +Muster wie auf den LXCs/ghostnet-ki: eigener Account, gelocktes Passwort, +passphrase-loser Ed25519-Key mit Source-Restrictions, NOPASSWD-Sudoers. Der +Key ist **root-äquivalent** — nur der public Key liegt im Repo +(`ansible/files/ssh/pve-ansible-deploy.pub`), der private Key nur in `~/.ssh` +(mode 0600). + +Einmalig auf der PVE-Konsole (root@pve) ausführen: + +```bash +# freie UID >= 1000 wählen, z.B. 1000: +getent passwd | awk -F: '$3>=1000 {print $1, $3}' | sort -k2 -n | head +useradd -m -u -s /bin/bash ansible-deploy +usermod -L ansible-deploy # Passwort sperren +mkdir -p /home/ansible-deploy/.ssh && chmod 700 /home/ansible-deploy/.ssh +# public Key aus ansible/files/ssh/pve-ansible-deploy.pub eintragen: +cat >> /home/ansible-deploy/.ssh/authorized_keys <<'EOF' +from="10.0.0.0/24,10.8.0.0/24",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFtneJFtlwj7D284Yar7ROTflZqKxi/IEyMcIoqazIDD ansible-deploy@ghostnet-pve +EOF +chown -R ansible-deploy:ansible-deploy /home/ansible-deploy/.ssh +chmod 600 /home/ansible-deploy/.ssh/authorized_keys +cat > /etc/sudoers.d/90-ghostnet-ansible-deploy <<'EOF' +ansible-deploy ALL=(ALL) NOPASSWD: ALL +EOF +chmod 0440 /etc/sudoers.d/90-ghostnet-ansible-deploy +visudo -c +# Host-Key-Fingerprint für die Workstation notieren (siehe unten): +ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub +``` + +Auf der Workstation: Host-Key-Fingerprint gegen die PVE-Konsole prüfen +(GNAI-011 Abschnitt 3.3), dann `known_hosts` befüllen und testen: + +```bash +ssh-keyscan -t ed25519 10.0.0.2 >> ~/.ssh/known_hosts_ghostnet_pve # erst nach Fingerprint-Prüfung! +ssh -o BatchMode=yes ghostnet-pve-ansible-deploy 'id; hostname; sudo -n id -u' +``` + +ssh-Alias auf der Workstation (bereits in `~/.ssh/config`): +`ghostnet-pve-ansible-deploy` → `ansible-deploy@10.0.0.2` mit +`~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy` (passphrase-los) und +`UserKnownHostsFile=~/.ssh/known_hosts_ghostnet_pve`. + +## Runbook + +Alle Befehle aus `proxmox/pve-host/ansible/` mit dem venv der Repo-Wurzel: + +```bash +cd proxmox/pve-host/ansible +../../../.venv/bin/ansible-playbook -i inventories/production/hosts.yml playbooks/preflight.yml --private-key ~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy +../../../.venv/bin/ansible-playbook -i inventories/production/hosts.yml playbooks/site.yml --check --diff --private-key ~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy +../../../.venv/bin/ansible-playbook -i inventories/production/hosts.yml playbooks/site.yml --private-key ~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy +../../../.venv/bin/ansible-playbook -i inventories/production/hosts.yml playbooks/verify-production.yml --private-key ~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy +../../../.venv/bin/ansible-playbook -i inventories/production/hosts.yml playbooks/site.yml --private-key ~/.ssh/id_ed25519_ghostnet_pve_ansible_deploy # changed=0 +``` + +## Secrets-Policy + +Keine Passwörter, privaten Schlüssel oder Secrets in Repo/Inventory/Logs. Der +private Deploy-Key liegt nur in `~/.ssh` (0600). Der Account hat NOPASSWD +sudo — der Key ist root-äquivalent und wird nur für autorisierte +Infrastruktur-Änderungen genutzt. diff --git a/proxmox/pve-host/ansible/ansible.cfg b/proxmox/pve-host/ansible/ansible.cfg new file mode 100644 index 0000000..b13fcf0 --- /dev/null +++ b/proxmox/pve-host/ansible/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +home = .ansible +host_key_checking = True +interpreter_python = auto_silent +local_tmp = .ansible/tmp +retry_files_enabled = False +roles_path = roles:../../../shared/ansible/roles +timeout = 15 + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s diff --git a/proxmox/pve-host/ansible/files/ssh/pve-ansible-deploy.pub b/proxmox/pve-host/ansible/files/ssh/pve-ansible-deploy.pub new file mode 100644 index 0000000..8c74d41 --- /dev/null +++ b/proxmox/pve-host/ansible/files/ssh/pve-ansible-deploy.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFtneJFtlwj7D284Yar7ROTflZqKxi/IEyMcIoqazIDD ansible-deploy@ghostnet-pve diff --git a/proxmox/pve-host/ansible/inventories/production/group_vars/pve_hosts.yml b/proxmox/pve-host/ansible/inventories/production/group_vars/pve_hosts.yml new file mode 100644 index 0000000..17fb2be --- /dev/null +++ b/proxmox/pve-host/ansible/inventories/production/group_vars/pve_hosts.yml @@ -0,0 +1,16 @@ +--- +ansible_become_method: sudo +ansible_python_interpreter: /usr/bin/python3 +ansible_ssh_common_args: >- + -o IdentitiesOnly=yes + -o StrictHostKeyChecking=yes + -o UserKnownHostsFile=~/.ssh/known_hosts_ghostnet_pve + +# Mirrored from the pve_nfs_export role defaults so that verify-production.yml +# runs with a self-contained inventory (CT 100/102 convention): +pve_nfs_export_path: /media/datenhdd3/Encoding +pve_nfs_client_ipv4: 10.0.0.90 +pve_nfs_export_owner_uid: 100000 +pve_nfs_export_owner_gid: 970 +pve_nfs_export_mode: "2775" +pve_nfs_service_unit: nfs-kernel-server.service diff --git a/proxmox/pve-host/ansible/inventories/production/host_vars/pve-host.yml b/proxmox/pve-host/ansible/inventories/production/host_vars/pve-host.yml new file mode 100644 index 0000000..c46da8f --- /dev/null +++ b/proxmox/pve-host/ansible/inventories/production/host_vars/pve-host.yml @@ -0,0 +1,12 @@ +--- +pve_expected_ipv4: 10.0.0.2 +pve_expected_hostname: pve +pve_expected_distribution: Debian +pve_expected_distribution_major_version: "13" +# ED25519 host key, captured on the PVE console 2026-10-30 (GNAI-011 3.3) and +# verified against ssh-keyscan from the admin workstation: +pve_expected_ssh_host_key_fingerprint: >- + SHA256:0oMpG9ik9JkThVwI1OE7MlMv1yYCwob6/w45RRd+Asg +# ansible-deploy account on the host (prepared 2026-10-30, locked password, +# NOPASSWD sudo, source-restricted key): +pve_deploy_uid: 1001 diff --git a/proxmox/pve-host/ansible/inventories/production/hosts.yml b/proxmox/pve-host/ansible/inventories/production/hosts.yml new file mode 100644 index 0000000..5dcb06b --- /dev/null +++ b/proxmox/pve-host/ansible/inventories/production/hosts.yml @@ -0,0 +1,8 @@ +--- +all: + children: + pve_hosts: + hosts: + pve-host: + ansible_host: 10.0.0.2 + ansible_user: ansible-deploy diff --git a/proxmox/pve-host/ansible/playbooks/preflight.yml b/proxmox/pve-host/ansible/playbooks/preflight.yml new file mode 100644 index 0000000..6e9a481 --- /dev/null +++ b/proxmox/pve-host/ansible/playbooks/preflight.yml @@ -0,0 +1,108 @@ +--- +- name: Read-only preflight of the Proxmox VE host + hosts: pve_hosts + become: false + gather_facts: true + gather_subset: + - min + - hardware + - network + + tasks: + - name: Verify the expected host identity + ansible.builtin.assert: + that: + - ansible_facts["hostname"] == pve_expected_hostname + - ansible_facts["distribution"] == pve_expected_distribution + - >- + ansible_facts["default_ipv4"]["address"] == pve_expected_ipv4 + fail_msg: >- + The connected system is not the declared Proxmox VE host + ({{ pve_expected_hostname }}, {{ pve_expected_ipv4 }}). + + - name: Read the PVE version + ansible.builtin.command: + argv: + - /usr/bin/pveversion + check_mode: false + changed_when: false + register: pve_preflight_version + + - name: Read the deployment account identity + ansible.builtin.command: + argv: + - /usr/bin/id + check_mode: false + changed_when: false + register: pve_preflight_id + + - name: Read the kernel NFS server package state + ansible.builtin.command: + argv: + - /usr/bin/dpkg-query + - -W + - -f=${Status} + - nfs-kernel-server + check_mode: false + changed_when: false + failed_when: false + register: pve_preflight_nfs_pkg + + - name: Read the current exports file + ansible.builtin.slurp: + src: /etc/exports + register: pve_preflight_exports + failed_when: false + + - name: Read the PVE firewall status + ansible.builtin.command: + argv: + - /usr/sbin/pve-firewall + - status + check_mode: false + changed_when: false + failed_when: false + register: pve_preflight_fw + + - name: Read the CT 101 config seccomp lines + ansible.builtin.command: + argv: + - /usr/bin/grep + - -n + - lxc.seccomp.profile + - /etc/pve/lxc/101.conf + check_mode: false + changed_when: false + failed_when: false + register: pve_preflight_seccomp + + - name: Read the NFS listener state + ansible.builtin.command: + argv: + - /usr/bin/ss + - -tln + check_mode: false + changed_when: false + register: pve_preflight_listeners + + - name: Report the PVE host preflight baseline + ansible.builtin.debug: + msg: + inventory_host: "{{ inventory_hostname }}" + hostname: "{{ ansible_facts['hostname'] }}" + distribution: >- + {{ ansible_facts['distribution'] }} + {{ ansible_facts['distribution_version'] }} + ipv4: "{{ ansible_facts['default_ipv4']['address'] }}" + pveversion: "{{ pve_preflight_version.stdout }}" + deploy_identity: "{{ pve_preflight_id.stdout }}" + nfs_kernel_server_pkg: >- + {{ pve_preflight_nfs_pkg.stdout | default('not installed') }} + exports_file: >- + {{ (pve_preflight_exports.content | default('') | b64decode) + if pve_preflight_exports.content is defined else '' }} + pve_firewall_status: "{{ pve_preflight_fw.stdout | default('') }}" + ct101_seccomp_lines: >- + {{ pve_preflight_seccomp.stdout_lines | default([]) }} + nfs_listener_2049: >- + {{ ':2049 ' in pve_preflight_listeners.stdout }} diff --git a/proxmox/pve-host/ansible/playbooks/site.yml b/proxmox/pve-host/ansible/playbooks/site.yml new file mode 100644 index 0000000..100f1bb --- /dev/null +++ b/proxmox/pve-host/ansible/playbooks/site.yml @@ -0,0 +1,9 @@ +--- +- name: Converge the Proxmox VE host on the declared state + hosts: pve_hosts + become: true + gather_facts: true + + roles: + - role: pve_lxc_conf + - role: pve_nfs_export diff --git a/proxmox/pve-host/ansible/playbooks/verify-production.yml b/proxmox/pve-host/ansible/playbooks/verify-production.yml new file mode 100644 index 0000000..28d04f9 --- /dev/null +++ b/proxmox/pve-host/ansible/playbooks/verify-production.yml @@ -0,0 +1,140 @@ +--- +- name: Read-only verification of the converged Proxmox VE host state + hosts: pve_hosts + become: true + gather_facts: true + gather_subset: + - min + - hardware + - network + + tasks: + - name: Verify the expected host identity + ansible.builtin.assert: + that: + - ansible_facts["hostname"] == pve_expected_hostname + - ansible_facts["distribution"] == pve_expected_distribution + - >- + ansible_facts["default_ipv4"]["address"] == pve_expected_ipv4 + fail_msg: The connected system is not the declared Proxmox VE host. + + - name: Read the kernel NFS server package state + ansible.builtin.command: + argv: + - /usr/bin/dpkg-query + - -W + - -f=${Status} + - nfs-kernel-server + check_mode: false + changed_when: false + register: pve_verify_nfs_pkg + + - name: Require the kernel NFS server to be installed + ansible.builtin.assert: + that: + - pve_verify_nfs_pkg.stdout is search('install ok installed') + fail_msg: nfs-kernel-server is not installed on the PVE host. + + - name: Read the NFS service state + ansible.builtin.command: + argv: + - /usr/bin/systemctl + - is-active + - "{{ pve_nfs_service_unit }}" + check_mode: false + changed_when: false + failed_when: false + register: pve_verify_nfs_service + + - name: Require the kernel NFS server to be active + ansible.builtin.assert: + that: + - pve_verify_nfs_service.stdout == 'active' + fail_msg: nfs-kernel-server is not active on the PVE host. + + - name: Read the applied export table + ansible.builtin.command: + argv: + - /usr/sbin/exportfs + - -s + check_mode: false + changed_when: false + register: pve_verify_exportfs + + - name: Require the pipeline export in the applied export table + ansible.builtin.assert: + that: + - pve_verify_exportfs.stdout is + search(pve_nfs_export_path ~ '\\s+' ~ pve_nfs_client_ipv4) + fail_msg: >- + The encoding export is missing from exportfs -s; expected + {{ pve_nfs_export_path }} for {{ pve_nfs_client_ipv4 }}. + + - name: Stat the export root + ansible.builtin.stat: + path: "{{ pve_nfs_export_path }}" + register: pve_verify_export_root + + - name: Verify the export root ownership and mode + ansible.builtin.assert: + that: + - pve_verify_export_root.stat.exists + - pve_verify_export_root.stat.isdir + - pve_verify_export_root.stat.uid == pve_nfs_export_owner_uid | int + - pve_verify_export_root.stat.gid == pve_nfs_export_owner_gid | int + - >- + '%04o' % (pve_verify_export_root.stat.mode | int(0, 8)) == + pve_nfs_export_mode + fail_msg: >- + The export root must be + {{ pve_nfs_export_owner_uid }}:{{ pve_nfs_export_owner_gid }} + {{ pve_nfs_export_mode }} (CT 101 guest root for the SMB share, + gn-media GID for the pipeline via AUTH_SYS). + + - name: Read the NFSv4 listener + ansible.builtin.command: + argv: + - /usr/bin/ss + - -tln + check_mode: false + changed_when: false + register: pve_verify_listeners + + - name: Require the NFSv4 listener on 2049 + ansible.builtin.assert: + that: + - pve_verify_listeners.stdout is search(':2049 ') + fail_msg: The PVE host does not listen on 2049 (NFSv4) yet. + + - name: Read the CT 101 config seccomp lines + ansible.builtin.command: + argv: + - /usr/bin/grep + - -c + - '^lxc\.seccomp\.profile:\s*$' + - /etc/pve/lxc/101.conf + check_mode: false + changed_when: false + failed_when: false + register: pve_verify_seccomp + + - name: Require no empty seccomp profile line in the CT 101 config + ansible.builtin.assert: + that: + - (pve_verify_seccomp.stdout | trim) == '0' + fail_msg: >- + /etc/pve/lxc/101.conf still carries the empty Ganesha-era + lxc.seccomp.profile line (applies at the next CT start). + + - name: Report the verified PVE host production state + ansible.builtin.debug: + msg: + hostname: "{{ ansible_facts['hostname'] }}" + ipv4: "{{ ansible_facts['default_ipv4']['address'] }}" + nfs_service: "{{ pve_verify_nfs_service.stdout }}" + export: "{{ pve_nfs_export_path }} -> {{ pve_nfs_client_ipv4 }}/32" + export_root: >- + {{ pve_verify_export_root.stat.uid }}:{{ pve_verify_export_root.stat.gid }} + {{ '%04o' % (pve_verify_export_root.stat.mode | int(0, 8)) }} + {{ pve_nfs_export_path }} + listener_2049: true diff --git a/proxmox/pve-host/ansible/roles/pve_lxc_conf/README.md b/proxmox/pve-host/ansible/roles/pve_lxc_conf/README.md new file mode 100644 index 0000000..69e8891 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_lxc_conf/README.md @@ -0,0 +1,12 @@ +# `pve_lxc_conf` + +Manages the LXC container configs in `/etc/pve/lxc/` on the PVE host that this +project owns: currently it removes the **empty `lxc.seccomp.profile:` line** +from the listed CT configs. That line was added for the Ganesha NFS container +inside CT 101 (GNAI-019 decision 2, revised 2026-10-30); with the export on the +host, the CTs run with the default seccomp profile again, like CT 100/102/103. + +The change applies at the next CT start/reboot (`pct reboot `), not +live. `pct set` rewrites the config file, so direct edits here are safe for +lines pct does not manage; the CT-101 PVE scripts handle the same line during +provisioning/upgrade. diff --git a/proxmox/pve-host/ansible/roles/pve_lxc_conf/defaults/main.yml b/proxmox/pve-host/ansible/roles/pve_lxc_conf/defaults/main.yml new file mode 100644 index 0000000..a7b81b2 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_lxc_conf/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# LXC configs on the PVE host whose Ganesha-era empty seccomp profile line is +# removed again (the NFS export now runs on the host). Takes effect at the +# next CT start/reboot. +pve_lxc_conf_ctids: + - 101 diff --git a/proxmox/pve-host/ansible/roles/pve_lxc_conf/tasks/main.yml b/proxmox/pve-host/ansible/roles/pve_lxc_conf/tasks/main.yml new file mode 100644 index 0000000..edd0982 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_lxc_conf/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: Remove the Ganesha-era empty seccomp profile line from the CT configs + ansible.builtin.lineinfile: + path: "/etc/pve/lxc/{{ item }}.conf" + regexp: '^lxc\.seccomp\.profile:\s*$' + state: absent + loop: "{{ pve_lxc_conf_ctids }}" + loop_control: + label: "CT {{ item }}" diff --git a/proxmox/pve-host/ansible/roles/pve_nfs_export/README.md b/proxmox/pve-host/ansible/roles/pve_nfs_export/README.md new file mode 100644 index 0000000..56a8976 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_nfs_export/README.md @@ -0,0 +1,24 @@ +# `pve_nfs_export` + +Serves the encoding-pipeline directory `/media/datenhdd3/Encoding` from the +**Proxmox VE host** via kernel NFS (NFSv4-only) to `ghostnet-ki` +(`10.0.0.90/32`), replacing the reverted Ganesha-in-CT-101 design (GNAI-019 +decision 2, revised 2026-10-30: NFS cannot run in an unprivileged LXC — +`open_by_handle_at` needs CAP_DAC_READ_SEARCH in the init user namespace). + +- Installs `nfs-kernel-server`. +- Disables NFSv2/v3 in `/etc/default/nfs-kernel-server` (`RPCNFSDARGS`). +- Manages a marked block in `/etc/exports` (idempotent `blockinfile`): + ` /32(rw,sync,no_subtree_check,root_squash)`. +- Sets the export root to `100000:970 2775`: UID 100000 is the CT 101 guest + root (so the SMB `[Encoding]` share inside CT 101 can write), GID 970 is + `gn-media` (group-write + setgid for the pipeline identities arriving via + AUTH_SYS from ghostnet-ki; `root_squash` stays active). +- Applies `exportfs -ra` and enables/starts `nfs-kernel-server.service`. +- Firewall: when `pve-firewall` is **not** active on the host, allows + 2049/tcp+udp from the client only, in a dedicated nftables table + `inet ghostnet_host_nfs`; when pve-firewall **is** active, the role only + prints the required PVE-firewall rule instead of touching nftables. + +Post-state verification lives in `playbooks/verify-production.yml` +(package, service, `exportfs -s`, export-root ownership/mode, listener 2049). diff --git a/proxmox/pve-host/ansible/roles/pve_nfs_export/defaults/main.yml b/proxmox/pve-host/ansible/roles/pve_nfs_export/defaults/main.yml new file mode 100644 index 0000000..cbd9c17 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_nfs_export/defaults/main.yml @@ -0,0 +1,17 @@ +--- +# Kernel-NFS export of the encoding-pipeline directory on the PVE host. +# CT 101 cannot serve NFS itself (unprivileged LXC: open_by_handle_at needs +# CAP_DAC_READ_SEARCH in the init user namespace -> always EPERM there). +pve_nfs_export_path: /media/datenhdd3/Encoding +pve_nfs_client_ipv4: 10.0.0.90 +# Export-root ownership on the host: 100000 = CT 101 guest root (so the SMB +# [Encoding] share inside CT 101 can write), 970 = gn-media (group-write for +# the pipeline identities arriving via AUTH_SYS from ghostnet-ki). +pve_nfs_export_owner_uid: 100000 +pve_nfs_export_owner_gid: 970 +pve_nfs_export_mode: "2775" +pve_nfs_packages: + - nfs-kernel-server +pve_nfs_service_unit: nfs-kernel-server.service +pve_nfs_firewall_table: ghostnet_host_nfs +pve_nfs_firewall_port: 2049 diff --git a/proxmox/pve-host/ansible/roles/pve_nfs_export/tasks/main.yml b/proxmox/pve-host/ansible/roles/pve_nfs_export/tasks/main.yml new file mode 100644 index 0000000..12d2e42 --- /dev/null +++ b/proxmox/pve-host/ansible/roles/pve_nfs_export/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: Require the NFS export declarations + ansible.builtin.assert: + that: + - pve_nfs_export_path | length > 0 + - pve_nfs_client_ipv4 | length > 0 + - pve_nfs_service_unit | length > 0 + fail_msg: The NFS export path, client and service unit must be declared. + +- name: Require the export root to exist + ansible.builtin.stat: + path: "{{ pve_nfs_export_path }}" + register: pve_nfs_export_root_probe + +- name: Fail closed when the export root is missing + ansible.builtin.assert: + that: + - pve_nfs_export_root_probe.stat.exists + - pve_nfs_export_root_probe.stat.isdir + fail_msg: >- + {{ pve_nfs_export_path }} does not exist on the PVE host. The media + mount must be present before the export converges. + +- name: Install the kernel NFS server + ansible.builtin.apt: + name: "{{ pve_nfs_packages }}" + state: present + update_cache: true + cache_valid_time: 3600 + install_recommends: false + +- name: Disable NFSv2/v3 in the kernel server defaults (NFSv4-only) + ansible.builtin.lineinfile: + path: /etc/default/nfs-kernel-server + create: true + owner: root + group: root + mode: "0644" + regexp: '^RPCNFSDARGS=' + line: 'RPCNFSDARGS="--no-nfs-version 2 --no-nfs-version 3"' + +- name: Manage the marked export block in /etc/exports + ansible.builtin.blockinfile: + path: /etc/exports + create: true + owner: root + group: root + mode: "0644" + marker: "# {mark} ghostnet CT-101 encoding export (managed by role pve_nfs_export)" + block: | + {{ pve_nfs_export_path }} {{ pve_nfs_client_ipv4 }}/32(rw,sync,no_subtree_check,root_squash) + +- name: Set the export root ownership and mode + ansible.builtin.file: + path: "{{ pve_nfs_export_path }}" + state: directory + owner: "{{ pve_nfs_export_owner_uid }}" + group: "{{ pve_nfs_export_owner_gid }}" + mode: "{{ pve_nfs_export_mode }}" + +- name: Apply the export table + ansible.builtin.command: + argv: + - /usr/sbin/exportfs + - -ra + changed_when: false + +- name: Enable and start the kernel NFS server + ansible.builtin.systemd_service: + name: "{{ pve_nfs_service_unit }}" + enabled: true + state: started + +- name: Read the PVE firewall status + ansible.builtin.command: + argv: + - /usr/sbin/pve-firewall + - status + check_mode: false + changed_when: false + failed_when: false + register: pve_nfs_firewall_status + +- name: Allow 2049 from the pipeline client in a dedicated nftables table + ansible.builtin.shell: | + set -euo pipefail + changed=0 + nft list table inet {{ pve_nfs_firewall_table }} >/dev/null 2>&1 || + { nft add table inet {{ pve_nfs_firewall_table }}; changed=1; } + nft list chain inet {{ pve_nfs_firewall_table }} input >/dev/null 2>&1 || + { nft add chain inet {{ pve_nfs_firewall_table }} input '{ type filter hook input priority 10; policy accept; }'; changed=1; } + nft list ruleset | grep -q 'ip saddr {{ pve_nfs_client_ipv4 }} tcp dport {{ pve_nfs_firewall_port }}' || + { nft add rule inet {{ pve_nfs_firewall_table }} input ip saddr {{ pve_nfs_client_ipv4 }} tcp dport {{ pve_nfs_firewall_port }} accept; changed=1; } + nft list ruleset | grep -q 'ip saddr {{ pve_nfs_client_ipv4 }} udp dport {{ pve_nfs_firewall_port }}' || + { nft add rule inet {{ pve_nfs_firewall_table }} input ip saddr {{ pve_nfs_client_ipv4 }} udp dport {{ pve_nfs_firewall_port }} accept; changed=1; } + if [ "$changed" = 1 ]; then echo CHANGED; fi + args: + executable: /bin/bash + when: not (pve_nfs_firewall_status.stdout | lower is search('enabled')) + register: pve_nfs_nft_rules + changed_when: "'CHANGED' in pve_nfs_nft_rules.stdout" + +- name: Note when pve-firewall manages the host + ansible.builtin.debug: + msg: >- + pve-firewall is active on the host — add the allow rule there instead + (host/cluster firewall: input, source {{ pve_nfs_client_ipv4 }}, ports + {{ pve_nfs_firewall_port }}/tcp+udp). The dedicated nft table + {{ pve_nfs_firewall_table }} was not touched. + when: pve_nfs_firewall_status.stdout | lower is search('enabled')