CT-101 Samba + PVE-Host: NFS auf den Host verlagert, Host per Ansible verwaltet
- 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 <vibe@mistral.ai>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 <pve-host-ip>:/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
|
||||
|
||||
|
||||
@@ -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 `<pve-host-ip>:/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`: `<pve-host-ip>:/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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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/`.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+13
-26
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: >-
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
-50
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/<ct>/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 <UID> -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFtneJFtlwj7D284Yar7ROTflZqKxi/IEyMcIoqazIDD ansible-deploy@ghostnet-pve
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
pve_hosts:
|
||||
hosts:
|
||||
pve-host:
|
||||
ansible_host: 10.0.0.2
|
||||
ansible_user: ansible-deploy
|
||||
@@ -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 '<missing>' }}
|
||||
pve_firewall_status: "{{ pve_preflight_fw.stdout | default('<none>') }}"
|
||||
ct101_seccomp_lines: >-
|
||||
{{ pve_preflight_seccomp.stdout_lines | default([]) }}
|
||||
nfs_listener_2049: >-
|
||||
{{ ':2049 ' in pve_preflight_listeners.stdout }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 <ctid>`), 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.
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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`):
|
||||
`<path> <client>/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).
|
||||
@@ -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
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user