CT-101 Samba: Ansible-Migration (GNAI-017/018/019) mit PVE-Skripten, Runbooks und Fresh-State-Rebuild aufgenommen
- Ansible-Projekt proxmox/lxc/ct-101-samba/ansible: 7 Playbooks, 6 Rollen, 3 Inventories (agent/bootstrap/production), 11 fail-closed Platzhalter - Design GNAI-019: Samba nativ (templated smb.conf mit testparm-Gate), NFS-Ganesha als rootful Quadlet mit Host-Networking und leerem lxc.seccomp.profile, NFSv4-only, gn-media GID 970, passdb-Erhalt - PVE-Skripte: upgrade-ct101-to-debian13.sh (Adoption), provision-ct101-samba.sh (Fresh-State-Rebuild), sha256-gepinnt - Runbooks: Adoption (15 Schritte) und Fresh-State-Rebuild (9 Schritte) - Dokumentation: GNAI-019 (DEC), GNAI-017 aktualisiert, Index-Einträge; Befund Dist-Upgrade 21.10.2026: smb.conf-Verlust aus vzdump-Backup wiederhergestellt und verifiziert - Secrets (SMB-Passwörter, passdb, private Schlüssel) nicht im Repository Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Ghostnet.AI – Dokumentationsindex und Projektstand
|
||||
|
||||
**Stand:** 2026-10-14
|
||||
**Stand:** 2026-10-21
|
||||
|
||||
**Status:** Plattformgrundlage vorhanden; Sicherheitsabschluss und LLM-Serving offen
|
||||
|
||||
@@ -38,6 +38,9 @@ jeweiligen Fachdokumenten. Die priorisierte weitere Umsetzung beschreibt der
|
||||
| [VPN-Client-Verwaltung](platform/proxmox-server/lxc/ct-105-wireguard/GNAI-007-SEC_VPN_Client-Management.md) | Ausgabe, Änderung und Widerruf von Geräteprofilen |
|
||||
| [Windows 11](platform/proxmox-server/lxc/ct-105-wireguard/GNAI-007-SEC_VPN_Clients_Windows11.md) / [Linux](platform/proxmox-server/lxc/ct-105-wireguard/GNAI-007-SEC_VPN_Clients_Linux.md) | Client-SOPs |
|
||||
| [VPN-Implementation-Log](platform/proxmox-server/lxc/ct-105-wireguard/GNAI-007-SEC_VPN_Implementation-Log.md) | historische Umsetzung und datierte Evidenz |
|
||||
| [CT 101 / GNAI-017](platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md) | Samba-Dateiserver `10.0.0.9`: Aufnahme ins Repository, NFS-Export und Encoding-Shares |
|
||||
| [CT 101 / GNAI-018](platform/proxmox-server/lxc/ct-101-samba/GNAI-018-REQ_Encoding-Pipeline-Integration.md) | Requirements der Encoding-Pipeline an CT 101 — Übergabe an den Ansible-Rebuild |
|
||||
| [CT 101 / GNAI-019](platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md) | Entwurfsentscheidungen der CT-101-Ansible-Migration: natives Samba, NFS-Ganesha als Quadlet, Fresh-State-Rebuild |
|
||||
|
||||
### KI und Deployment
|
||||
|
||||
@@ -51,13 +54,17 @@ jeweiligen Fachdokumenten. Die priorisierte weitere Umsetzung beschreibt der
|
||||
| [LLM-Server-Host-Ansible](../llm-server/host/ansible/README.md) | ausführbare Host-/ROCm-Playbooks und Rollen für `10.0.0.90` |
|
||||
| [Encoding-Ansible](../llm-server/encoding/ansible/README.md) | ausführbares Ansible-Projekt für den CPU-Encoding-Workload auf `10.0.0.90` |
|
||||
| [Keycloak-Ansible](../proxmox/lxc/ct-102-keycloak/ansible/README.md) | ausführbare Provisionierungs-, Restore- und Prüfautomation für CT 102 |
|
||||
| [Samba-Ansible](../proxmox/lxc/ct-101-samba/ansible/README.md) | ausführbare Provisionierungs-, Backup- und Prüfautomation für CT 101 |
|
||||
|
||||
### Video-Encoding
|
||||
### Video-Encoding und Monitoring
|
||||
|
||||
| Dokument | Verbindlicher Inhalt |
|
||||
| --- | --- |
|
||||
| [GNENC-000](platform/llm-server/encoding/GNENC-000-DOC_Index.md) | Bereichsstatus und offene Festlegungen |
|
||||
| [GNENC-001](platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md) | vorläufige Architektur des CPU-Encoding-Workloads |
|
||||
| [GNENC-001](platform/llm-server/encoding/GNENC-001-PLAN_Architecture.md) | finale Architektur des CPU-Encoding-Workloads |
|
||||
| [GNENC-002](platform/llm-server/encoding/GNENC-002-PLAN_Pipeline.md) | operative Encoding-Pipeline: Komponenten, Verzeichnislayout, Validierung, Statusmodell |
|
||||
| [GNMON-000](platform/llm-server/monitoring/GNMON-000-DOC_Index.md) | Monitoring-Bereichsstatus (Metriken, Dashboard) |
|
||||
| [GNMON-001](platform/llm-server/monitoring/GNMON-001-PLAN_Architecture.md) | Host-Monitoring: Metrics-Collector, Dashboard, Anbindung Encoding |
|
||||
|
||||
Die Dokumente wurden am 21.09.2026 neu geordnet: Die Roadmap belegt
|
||||
`GNAI-001`; die zuvor bestehenden Dokumente `GNAI-001` bis `GNAI-009` wurden
|
||||
@@ -84,7 +91,8 @@ die Dokumentation folgt derselben Dreiteilung.
|
||||
| DNS / Pi-hole | CT 100 unter `10.0.0.14` mit digest-gepinntem Pi-hole 2026.09.0 als Podman-Quadlet; SSH/nftables, UDP/TCP-DNS, Split-DNS, Blocking/Allowlist, FRITZ!Box-Cutover, LXC-Reboot und Stop-Mode-Backup am 01.10.2026 abgenommen | vollständige App-/OIDC-Regression, praktischer FRITZ!Box-Rollback, mobile DuckDuckGo-/IPv6-Strategie, WireGuard-Profile und hostunabhängiger Restore-Test |
|
||||
| Reverse Proxy | CT-103-Zugänge, SSH-/Firewall-Baseline, Reboot und lokales Backup abgenommen; NPM 2.15.1 routet öffentlichen OIDC- und internen Admin-vHost getrennt; öffentliche Keycloak-Administration liefert 403 | Fail2Ban-Jails/Bans und Wiederanlauf; hostunabhängige Backupkopie/Restore-Test |
|
||||
| Keycloak | CT 102 unter `10.0.0.4` reproduzierbar per Ansible aufgebaut; gepinntes Keycloak 26.2.4 und PostgreSQL 16.15, Restore von CT 104, Container-/DB-Härtung, NPM-Cutover, interner Adminzugang, öffentliche Admin-Sperre, realer Vaultwarden-OIDC-Login und LXC-Neustart abgenommen | 14-tägige Stabilisierungsphase, praktischer NPM-Rückfall auf CT 104, Betriebs-/Backup-/Restore-SOP und anschließende Rückfallbereinigung |
|
||||
| Encoding | Konto, Pfade, eigener rootless Store und CPU-Isolation per Ansible verwaltet und verifiziert | Workload, Zeitsteuerung, Ressourcenlimits und Lasttest |
|
||||
| Encoding | Konto, Pfade, eigener rootless Store und CPU-Isolation per Ansible verwaltet und verifiziert; Planungsabschluss 19.10.2026 (Pipeline GNENC-002, Monitoring GNMON-001, Speicher auf CT-101) | Implementierung (NFS-Mount, Images, Quadlets, Skripte, Worker, Dashboard, Collector); Lasttest und Limit-Feintuning |
|
||||
| Samba (CT 101) | Share-Bestand in GNAI-017 dokumentiert; Ansible-Projekt nach GNAI-019 gebaut und lokal verifiziert; Runbooks vorhanden; In-Place-Upgrade auf Debian 13 am 21.10.2026; smb.conf-Verlust beim Upgrade aus vzdump-Backup wiederhergestellt, Media-Shares und Workstation-Mounts verifiziert | produktive Adoption (Platzhalter, Schlüssel, Convergence, Verifikation), NFS-Export und Encoding-Shares produktiv, UID/GID-Abgleich produktiv, Backup-/Restore-Test |
|
||||
|
||||
Ein Zielbild, eine Betreiberangabe oder ein erfolgreicher Teiltest gilt nicht als
|
||||
vollständige Abnahme. Datierte Details stehen im zuständigen Fachdokument.
|
||||
|
||||
@@ -14,6 +14,10 @@ Hypervisors selbst.
|
||||
| [CT-104-Rückfallartefakte](../../../proxmox/lxc/ct-104-keycloak/README.md) | erhaltene Blue/Green-Referenz unter `10.0.0.16` |
|
||||
| [GNAI-006 / CT 103](lxc/ct-103-reverse-proxy/GNAI-006-SEC_Reverse-Proxy.md) | `10.0.0.10`: Reverse Proxy, Keycloak-Adminpfad und Fail2Ban |
|
||||
| [GNAI-007 / CT 105](lxc/ct-105-wireguard/GNAI-007-SEC_VPN.md) | `10.0.0.15`: WireGuard-Gateway, Rollen-Firewall und Clientverfahren |
|
||||
| [GNAI-017 / CT 101](lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md) | `10.0.0.9`: Samba-Dateiserver, NFS-Export und Encoding-Shares |
|
||||
| [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 |
|
||||
|
||||
Die versionierte Quelle für den optionalen, secretfreien
|
||||
Community-Scripts-Post-install-Hook liegt unter
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# CT 101 – Samba-Dateiserver
|
||||
|
||||
**Dokumenten-ID:** GNAI-017-PLAN
|
||||
**Titel:** CT 101 Samba: Aufnahme ins Repository, NFS-Export und Shares für die Encoding-Pipeline
|
||||
**Stand:** 2026-10-21
|
||||
**Status:** Plan; bestehende Shares produktiv und bislang manuell verwaltet; Ansible-Projekt gebaut (GNAI-019), Adoption des produktiven LXC ausstehend
|
||||
**Referenzen:** [GNENC-001](../../../llm-server/encoding/GNENC-001-PLAN_Architecture.md), [GNENC-002](../../../llm-server/encoding/GNENC-002-PLAN_Pipeline.md), [GNMON-001](../../../llm-server/monitoring/GNMON-001-PLAN_Architecture.md), [GNAI-011](../../GNAI-011-SEC_LXC-SSH-Architecture.md), [GNAI-018](GNAI-018-REQ_Encoding-Pipeline-Integration.md), [GNAI-019](GNAI-019-DEC_Ansible-Migration.md)
|
||||
|
||||
## 1. Zweck und Geltungsbereich
|
||||
|
||||
CT 101 (`10.0.0.9`) ist der Samba-Dateiserver des Proxmox-Kontexts und trägt
|
||||
den Medienspeicher (Filme, Serien, Anime, Musik, Software, Downloads) auf
|
||||
mehreren Platten (`/media/daten1`, `/media/daten2`, `/media/daten3`,
|
||||
`/media/datenhdd2`, `/media/datenhdd3`). Bislang ist die LXC nicht im
|
||||
Infrastructure-Repository verwaltet; am 19.10.2026 wurde beschlossen, sie
|
||||
aufzunehmen (Betreiberentscheidung).
|
||||
|
||||
Dieses Dokument hält den aktuellen Share-Bestand, die geplanten Änderungen für
|
||||
die Encoding-Pipeline (NFS-Export, zwei Shares, Gruppen-Abgleich) und die
|
||||
offenen Punkte fest. Bestehende Shares bleiben unberührt.
|
||||
|
||||
## 2. System-Baseline
|
||||
|
||||
| Parameter | Wert |
|
||||
| --- | --- |
|
||||
| Container-ID | CT 101 |
|
||||
| IPv4 | 10.0.0.9 |
|
||||
| Samba-Rolle | standalone server, Workgroup `Ghost.net` (aus `smb.conf`) |
|
||||
| SMB-Benutzer | `ghost`, `daidalaw`, `debugofen` |
|
||||
| Betriebssystem | Debian 13 (trixie) seit In-Place-Upgrade durch den Betreiber am 21.10.2026 (manuell; nicht via `scripts/upgrade-ct101-to-debian13.sh`) |
|
||||
| Hostname | nicht erfasst — `preflight.yml` meldet ihn vor der Adoption (Inventory-Platzhalter `samba_expected_hostname`) |
|
||||
|
||||
## 3. Bestand: Shares (aus `smb.conf`, Stand 19.10.2026)
|
||||
|
||||
| Share | Pfad | Zugriff | Besonderheiten |
|
||||
| --- | --- | --- | --- |
|
||||
| `[homes]` | Home-Verzeichnisse | read-only, `valid users = %S` | Debian-Standard |
|
||||
| `[printers]` | `/var/tmp` | printable, read-only | **Abschnitt doppelt vorhanden** (siehe Abschnitt 6) |
|
||||
| `[print$]` | `/var/lib/samba/printers` | read-only | Druckertreiber |
|
||||
| `[Dokumente]` | `/media/daten1/Dokumente` | RO, nur `ghost` | — |
|
||||
| `[Filme]` | `/media/daten1/Filme` | RO, `ghost`/`daidalaw`/`debugofen`, write `ghost` | — |
|
||||
| `[Filme2]` | `/media/datenhdd3/Filme` | wie `[Filme]` | — |
|
||||
| `[Serien]` | `/media/daten2/Serien` | RO, alle drei, write `ghost` | `force user/group = root` |
|
||||
| `[Serien2]` | `/media/daten3/Serien` | wie `[Serien]` | — |
|
||||
| `[Animation]` | `/media/daten1/Animation` | RO, alle drei, write `ghost` | — |
|
||||
| `[Anime]` | `/media/daten3/Anime` | RO, alle drei, write `ghost` | — |
|
||||
| `[Extern]` | `/media/datenhdd2/extern` | RW, nur `ghost` | — |
|
||||
| `[Musik]` | `/media/daten1/Musik` | RO, alle drei, write `ghost` | — |
|
||||
| `[Audiobooks]` | `/media/daten1/Audiobooks` | RO, alle drei, write `ghost` | — |
|
||||
| `[Software]` | `/media/datenhdd3/Software` | RO, alle drei, write `ghost`/`daidalaw` | — |
|
||||
| `[Downloads]` | `/media/datenhdd3/Downloads` | RW, alle drei | bleibt unberührt; `.dlc`-Drop läuft künftig über `[Encode-In]` |
|
||||
|
||||
Globale Einstellungen (Auszug): `map to guest = bad user`, `usershare allow
|
||||
guests = yes`, `obey pam restrictions = yes`.
|
||||
|
||||
## 4. Geplante Änderungen für die Encoding-Pipeline
|
||||
|
||||
### 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).
|
||||
|
||||
- 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).
|
||||
|
||||
### 4.2 Gruppen- und UID-Abgleich
|
||||
|
||||
- Gruppe `gn-media` mit **GID 970** auf CT-101 anlegen (identisch zu
|
||||
`ghostnet-ki`).
|
||||
- Benutzer `ghost` in `gn-media` aufnehmen: Schreib-/Löschrecht auf den
|
||||
Pipeline-Verzeichnissen, damit die Abholung per `mv` aus `[Encode]`
|
||||
funktioniert. `daidalaw`/`debugofen` optional für reinen Lesezugriff.
|
||||
- Die Platte `datenhdd3` erscheint im Monitoring über den NFS-Mount auf
|
||||
`ghostnet-ki` (Disk-Metrik des NFS-Exports, GNMON-001).
|
||||
|
||||
### 4.3 Shares
|
||||
|
||||
```ini
|
||||
[Encode]
|
||||
comment = Encoding-Pipeline: fertige Encodes (Abholung per mv)
|
||||
path = /media/datenhdd3/Encoding/output
|
||||
public = no
|
||||
writeable = yes
|
||||
valid users = ghost, daidalaw, debugofen
|
||||
admin users = ghost
|
||||
write list = ghost
|
||||
force group = gn-media
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
|
||||
[Encode-In]
|
||||
comment = Encoding-Pipeline: .dlc-Drop (input)
|
||||
path = /media/datenhdd3/Encoding/input
|
||||
public = no
|
||||
writeable = yes
|
||||
valid users = ghost
|
||||
admin users = ghost
|
||||
write list = ghost
|
||||
force group = gn-media
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
```
|
||||
|
||||
- `[Encode]`: RW für `ghost` — die Entnahme per `mv` in die Bibliotheks-Shares
|
||||
(Filme/Serien/Anime, teils andere Platten → Kopie+Verschieben auf CT-101)
|
||||
räumt `output/` auf. `force group` stellt sicher, dass auch über SMB
|
||||
angelegte Dateien der Gruppe `gn-media` angehören.
|
||||
- `[Encode-In]`: RW für `ghost` — `.dlc`-Dateien werden in
|
||||
`Encode-In\<Profilname>\` abgelegt (Profil = Preset-Name); jDownloader2
|
||||
räumt sie nach der Verarbeitung nach `jd/dlc_done/` bzw. `jd/failed/` auf.
|
||||
- Vor Produktivsetzung: `testparm` laufen lassen (siehe Abschnitt 6).
|
||||
|
||||
### 4.4 Verzeichnisstruktur
|
||||
|
||||
`/media/datenhdd3/Encoding` ist die Pipeline-Wurzel (vollständiges Layout in
|
||||
GNENC-002 Abschnitt 4): `input/`, `jd/`, `queue/`, `work/`, `output/`,
|
||||
`state/`. Anlage und Rechte erfolgen via NFS durch die Ansible-Rolle
|
||||
`video_encoding` auf `ghostnet-ki`.
|
||||
|
||||
## 5. Abholung und Clean-Job
|
||||
|
||||
- Abholung: Entnahme per `mv` aus `[Encode]`; das `mv` räumt `output/` auf.
|
||||
- Optionaler Clean-Job (Worker-Housekeeping auf `ghostnet-ki`, GNENC-002
|
||||
Abschnitt 6): löscht Dateien in `output/` älter als 30 Tage.
|
||||
|
||||
## 6. Bekannte Befunde und offene Punkte
|
||||
|
||||
1. **Ansible-Rebuild inkl. Pipeline-Anbindung** — erledigt: Das Projekt
|
||||
`proxmox/lxc/ct-101-samba/` ist gebaut (Design in
|
||||
[GNAI-019](GNAI-019-DEC_Ansible-Migration.md), Requirements in
|
||||
[GNAI-018](GNAI-018-REQ_Encoding-Pipeline-Integration.md)) und lokal
|
||||
verifiziert (Syntax-Checks, Check-Mode-Smoke). **Ausstehend:** Adoption des
|
||||
produktiven LXC gemäß Runbook in `ansible/README.md` (Dist-Upgrade auf
|
||||
Debian 13, elf Inventory-Platzhalter, Schlüssel, Convergence, Verifikation).
|
||||
2. **`[printers]` doppelt** in der `smb.conf` — erledigt: Das Duplikat (inkl.
|
||||
Kommentarblöcke) ist im templated `smb.conf` bereinigt; `testparm` ist als
|
||||
`validate:`-Gate im Rollen-Flow verankert (GNAI-019 Entscheidung 7).
|
||||
3. **`usershare allow guests = yes`** — entschieden: beibehalten und im
|
||||
Template dokumentiert; Änderung nur nach Betreiberentscheidung (GNAI-018 R4,
|
||||
GNAI-019 Entscheidung 7).
|
||||
4. **Backup** — entschieden: `backup.yml` (one-shot) sichert `/etc/samba` und
|
||||
`/var/lib/samba` nach `/var/backups/ghostnet/samba/` vor der ersten
|
||||
Convergence; alle fünf Medien-Mounts tragen `backup=no`; das Upgrade-Skript
|
||||
fährt vor dem Dist-Upgrade einen `vzdump --mode stop` (GNAI-019 Entscheidung
|
||||
6). **Offen:** hostunabhängiger Restore-Test (GNAI-000 Abschnitt 4 Punkt 4).
|
||||
5. **System-Baseline** — erfasst: OS-Version (Debian 13 nach Upgrade, GNAI-019
|
||||
Entscheidung 10), Hostname und Ressourcen-Mindestwerte über die
|
||||
preflight-Platzhalter im Inventory (`samba_expected_hostname`,
|
||||
`samba_expected_processor_vcpus`, `samba_minimum_memory_mb`,
|
||||
`samba_minimum_root_size_bytes`, `samba_minimum_root_available_bytes`).
|
||||
Plattengrößen/Kapazität siehe Punkt 6.
|
||||
6. **Kapazitäts-Monitoring** der daten-Platten auf CT-101 — offen: heute nur
|
||||
indirekt über den NFS-Mount auf ghostnet-ki sichtbar (GNMON-001); eigener
|
||||
CT-101-Collector später.
|
||||
7. **smb.conf-Verlust beim Dist-Upgrade (21.10.2026)** — erledigt: Beim
|
||||
manuellen In-Place-Upgrade auf Debian 13 wurde die produktive `smb.conf`
|
||||
durch die Paket-Default ersetzt; die 12 Media-Shares fehlten vorübergehend
|
||||
(`smbclient -L` zeigte nur `homes`/`printers`/`print$`; die Media-Shares
|
||||
antworteten `NT_STATUS_BAD_NETWORK_NAME`). Die passdb blieb intakt. Der
|
||||
Betreiber stellte die produktive `smb.conf` am 21.10.2026 aus dem
|
||||
vzdump-Backup vor dem Upgrade wieder her. Verifiziert am 21.10.2026: alle 12
|
||||
Media-Shares antworten, die 10 CIFS-Mounts der Admin-Workstation
|
||||
(`/etc/fstab`, `//10.0.0.9/…`) sind gehängt und lesbar, keine failed
|
||||
`mnt-*.mount`-Units. Das bereitgestellte
|
||||
`scripts/upgrade-ct101-to-debian13.sh` verwendet `--force-confold` und hätte
|
||||
den Verlust verhindert. Bei der kommenden Ansible-Adoption wird die
|
||||
`smb.conf` planmäßig durch das Template ersetzt (zusätzlich
|
||||
`[Encode]`/`[Encode-In]`, `[printers]` bereinigt).
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
# Übergabe: Requirements CT 101 (Samba/NFS) für die Encoding-Pipeline
|
||||
|
||||
**Dokumenten-ID:** GNAI-018-REQ
|
||||
**Titel:** Requirements der LLM-Server-Encoding-Pipeline an CT 101 — Spezifikation für den Ansible-Rebuild
|
||||
**Stand:** 2026-10-19
|
||||
**Status:** Übergabedokument; verbindliche Anforderungsliste für den ausführenden Agenten
|
||||
**Adressat:** Ansible-Rebuild CT 101 (ausführendes Projekt: `proxmox/lxc/ct-101-samba/ansible/`)
|
||||
**Quellen:** [GNAI-017](GNAI-017-PLAN_Samba-NFS-Export.md), [GNENC-001](../../../llm-server/encoding/GNENC-001-PLAN_Architecture.md), [GNENC-002](../../../llm-server/encoding/GNENC-002-PLAN_Pipeline.md), [GNMON-001](../../../llm-server/monitoring/GNMON-001-PLAN_Architecture.md)
|
||||
|
||||
## 1. Zweck und Adressat
|
||||
|
||||
CT 101 (`10.0.0.9`) ist der Samba-Dateiserver mit der Medienbibliothek und wird
|
||||
derzeit manuell verwaltet. Betreiberentscheidung 19.10.2026: Die Umstellung auf
|
||||
ein reproduzierbares Ansible-Projekt und die Anbindung der Encoding-Pipeline
|
||||
werden **in einem Schritt** durchgeführt.
|
||||
|
||||
Dieses Dokument ist die verbindliche Requirements-Liste aus Sicht der
|
||||
Encoding-Pipeline auf `ghostnet-ki` (`10.0.0.90`, Spezifikation GNENC-002). Der
|
||||
ausführende Agent baut das Ansible-Projekt und setzt die Pipeline-Anbindung um.
|
||||
Die Pipeline selbst (Mount, Container, Worker auf `ghostnet-ki`) ist **nicht**
|
||||
Bestandteil dieses Auftrags.
|
||||
|
||||
**Sequenzhinweis:** Dieser Rebuild ist Voraussetzung für die
|
||||
Encoding-Implementierung auf `ghostnet-ki`. Die gemeinsame Verifikation der
|
||||
Pipeline erfolgt nach Fertigstellung durch den Betreiber.
|
||||
|
||||
## 2. Kontext
|
||||
|
||||
- Die Encoding-Pipeline arbeitet vollständig auf einem NFS-Mount:
|
||||
`/var/lib/ghostnet-encoding` auf `ghostnet-ki` ← Export
|
||||
`/media/datenhdd3/Encoding` von CT-101.
|
||||
- Verzeichnislayout auf dem Export (GNENC-002 Abschnitt 4): `input/<Profil>/`,
|
||||
`jd/`, `queue/<Profil>/<Release>/`, `work/`, `output/` (inkl. `_failed/`),
|
||||
`state/`.
|
||||
- Die Pipeline schreibt via NFS als UID 982 (`gn-encoding`) bzw. UID 983
|
||||
(`gn-download`); Dateien entstehen mit umask 022 (0644/0755, Verzeichnisse
|
||||
2770/2750 mit setgid, Gruppe `gn-media`).
|
||||
- Abholung: Benutzer `ghost` entnimmt fertige Encodes per `mv` aus dem Share
|
||||
`[Encode]` (räumt `output/` auf) und legt `.dlc`-Dateien in `[Encode-In]` ab.
|
||||
|
||||
## 3. Anforderungen (Muss)
|
||||
|
||||
### R1 – NFS-Export für `ghostnet-ki`
|
||||
|
||||
- Paket `nfs-kernel-server` installieren, Dienst aktivieren und starten.
|
||||
- `/etc/exports` (genau diese Zeile, Export nur für `ghostnet-ki`):
|
||||
|
||||
```text
|
||||
/media/datenhdd3/Encoding 10.0.0.90(rw,sync,no_subtree_check)
|
||||
```
|
||||
|
||||
- Export-Root `/media/datenhdd3/Encoding` anlegen (root:root, 0755).
|
||||
**Die Pipeline-Unterverzeichnisse werden nicht hier angelegt** — sie
|
||||
entstehen via NFS durch die Ansible-Rolle `video_encoding` auf
|
||||
`ghostnet-ki`.
|
||||
- Kein `no_root_squash` erforderlich (rootless Container schreiben als
|
||||
UID 982/983, nicht als root); Standard-Squash bleibt.
|
||||
- `exportfs -ra` ausführen; Persistenz über Dienst-Unit sicherstellen.
|
||||
|
||||
### R2 – Samba-Shares für die Pipeline
|
||||
|
||||
Zwei neue Shares, exakt wie folgt (bestehende Shares unberührt):
|
||||
|
||||
```ini
|
||||
[Encode]
|
||||
comment = Encoding-Pipeline: fertige Encodes (Abholung per mv)
|
||||
path = /media/datenhdd3/Encoding/output
|
||||
public = no
|
||||
writeable = yes
|
||||
valid users = ghost, daidalaw, debugofen
|
||||
admin users = ghost
|
||||
write list = ghost
|
||||
force group = gn-media
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
|
||||
[Encode-In]
|
||||
comment = Encoding-Pipeline: .dlc-Drop (input)
|
||||
path = /media/datenhdd3/Encoding/input
|
||||
public = no
|
||||
writeable = yes
|
||||
valid users = ghost
|
||||
admin users = ghost
|
||||
write list = ghost
|
||||
force group = gn-media
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
```
|
||||
|
||||
### R3 – Gruppe und Benutzer-Abgleich
|
||||
|
||||
- Gruppe `gn-media` mit **GID 970** anlegen (identisch zu `ghostnet-ki`,
|
||||
siehe GNAI-017 Abschnitt 4.2).
|
||||
- Benutzer `ghost` in `gn-media` aufnehmen — erforderlich für das Löschen
|
||||
(mv-Abholung) in `output/` und das Anlegen/Rename in `input/`, da die
|
||||
Pipeline-Verzeichnisse Gruppe `gn-media` mit Mode 2770 tragen.
|
||||
- `daidalaw`/`debugofen` benötigen keine Gruppenmitgliedschaft (reiner
|
||||
Lesezugriff über world-read der Dateien).
|
||||
|
||||
### R4 – Bestandserhalt
|
||||
|
||||
- Alle bestehenden Shares bleiben funktionsgleich erhalten: `[homes]`,
|
||||
`[printers]`, `[print$]`, `[Dokumente]`, `[Filme]`, `[Filme2]`, `[Serien]`,
|
||||
`[Serien2]`, `[Animation]`, `[Anime]`, `[Extern]`, `[Musik]`, `[Audiobooks]`,
|
||||
`[Software]`, `[Downloads]` (Bestandsliste in GNAI-017 Abschnitt 3).
|
||||
- Vor der Umstellung `smb.conf` und den Samba-State (`/var/lib/samba`,
|
||||
insb. `private/passdb.tdb`) sichern (Backup-Pfad dokumentieren).
|
||||
- Das doppelte `[printers]`-Abschnitt in der bestehenden `smb.conf` bereinigen.
|
||||
- `usershare allow guests = yes` dokumentieren; Änderung nur nach
|
||||
Betreiberentscheidung (offen, siehe Abschnitt 7).
|
||||
|
||||
### R5 – Samba-Benutzer und Secrets
|
||||
|
||||
- Bestehende SMB-Benutzer `ghost`, `daidalaw`, `debugofen` samt Passwörtern
|
||||
dürfen nicht verloren gehen (kein Lockout der produktiven Nutzung).
|
||||
- passdb-Strategie festlegen: Erhalt des bestehenden passdb (Backup/Restore)
|
||||
oder Neuvergabe mit server-lokal generierten Secrets. **Secrets werden auf
|
||||
dem Ziel generiert und nie über Inventory oder git übergeben** (Konvention
|
||||
analog CT-102-Ansible-Projekt).
|
||||
|
||||
### R6 – Idempotentes Ansible-Projekt
|
||||
|
||||
- Projekt unter `proxmox/lxc/ct-101-samba/ansible/`, analog
|
||||
`proxmox/lxc/ct-102-keycloak/ansible/`: `ansible.cfg`,
|
||||
`inventories/<env>/hosts.yml`, `playbooks/`, `roles/`, `files/`.
|
||||
- Repo-weites Control-Venv (`shared/ansible/requirements-control.txt`) nutzen.
|
||||
- Deploy-Zugang nach GNAI-011 (SSH-Key, z. B.
|
||||
`~/.ssh/id_ed25519_ghostnet_ct101_ansible_deploy`); Key liegt nicht im Repo.
|
||||
- Check-Mode (`--check --diff`) muss für das Site-Playbook funktionieren;
|
||||
zwei aufeinanderfolgende Läufe müssen `changed=0` melden (Idempotenz).
|
||||
- `testparm` als Verifikationsschritt im Rollen-Flow (fail bei Fehlern).
|
||||
- Die `smb.conf` wird als Template gepflegt (vollständige Datei, nicht
|
||||
snippets appended) — der Sollzustand ist reproduzierbar.
|
||||
|
||||
### R7 – Netzwerk und Firewall
|
||||
|
||||
- NFS (2049/tcp+udp) nur von `10.0.0.90` erreichbar; SMB (139/445) wie bisher
|
||||
aus dem LAN. Ist-Zustand der Firewall dokumentieren, Sollzustand herstellen.
|
||||
- CT-101 muss nach dem Rebuild booten und Dienste starten (Reboot-Test).
|
||||
|
||||
## 4. Abnahmekriterien
|
||||
|
||||
Der Rebuild gilt als abgenommen, wenn alles Folgende nachgewiesen ist
|
||||
(datierte Nachweise in GNAI-017):
|
||||
|
||||
1. `testparm` fehlerfrei (inkl. bereinigtem `[printers]`-Duplikat).
|
||||
2. `showmount -e localhost` zeigt `/media/datenhdd3/Encoding`.
|
||||
3. `smbclient -L localhost` zeigt `[Encode]`, `[Encode-In]` und alle
|
||||
Bestands-Shares.
|
||||
4. Von `ghostnet-ki` (10.0.0.90) via NFS: Schreiben/Lesen im Export als
|
||||
UID 982 und UID 983 erfolgreich (z. B. nach dem Mount auf
|
||||
`ghostnet-ki` oder ein gezielter Mount-Test von CT-101 aus).
|
||||
5. `ghost` kann in `[Encode-In]` Dateien anlegen und in `[Encode]` löschen
|
||||
(mv-Simulation), dank `gn-media`-Mitgliedschaft.
|
||||
6. `testparm`-Vergleich vor/nach: bestehende Shares funktional unverändert.
|
||||
7. Idempotenz: zweiter Ansible-Lauf meldet `changed=0`.
|
||||
8. Reboot-Test: Dienste (smbd, nfs-kernel-server) starten nach Neustart.
|
||||
9. GNAI-017 (Abschnitt 6) und dieser Index aktualisiert.
|
||||
|
||||
## 5. Grenzen (Out of Scope)
|
||||
|
||||
- `ghostnet-ki`-Seite: NFS-Mount, Konten, Container, Worker, Dashboard —
|
||||
anderes Projekt (GNENC/GNMON).
|
||||
- Anlage der Pipeline-Unterverzeichnisse — erfolgt via NFS durch die Rolle
|
||||
`video_encoding` auf `ghostnet-ki` (R1: nur Export-Root).
|
||||
- Passwort-Neuvergabe für bestehende Benutzer ohne Betreiberentscheidung.
|
||||
- Monitoring auf CT-101 (Plattenkapazität ist über den NFS-Mount auf
|
||||
`ghostnet-ki` sichtbar, GNMON-001; eigener CT-101-Collector später).
|
||||
- LLM-Metriken.
|
||||
|
||||
## 6. Referenzen
|
||||
|
||||
- [GNAI-017](GNAI-017-PLAN_Samba-NFS-Export.md) — Bestand, Plan, smb.conf-Schnipsel
|
||||
- [GNENC-001](../../../llm-server/encoding/GNENC-001-PLAN_Architecture.md) — Architektur, Entscheidungen
|
||||
- [GNENC-002](../../../llm-server/encoding/GNENC-002-PLAN_Pipeline.md) — Pipeline, Verzeichnislayout, Rechte
|
||||
- [GNMON-001](../../../llm-server/monitoring/GNMON-001-PLAN_Architecture.md) — Monitoring-Anbindung
|
||||
- [GNAI-011](../../GNAI-011-SEC_LXC-SSH-Architecture.md) — SSH-Zugang der LXCs
|
||||
- [`proxmox/lxc/ct-102-keycloak/ansible/README.md`](../../../../../../proxmox/lxc/ct-102-keycloak/ansible/README.md) — Konventionen des Ansible-Projekts
|
||||
|
||||
## 7. Offene Punkte für den Rebuild
|
||||
|
||||
1. passdb-Strategie (Erhalt vs. Neuvergabe) — Entscheidung vor der Migration.
|
||||
2. Backup-Ziel für `smb.conf` und Samba-State (Pfad, Aufbewahrung).
|
||||
3. `usershare allow guests = yes` — beibehalten oder härten?
|
||||
4. System-Baseline erfassen: Hostname, OS-Version, Platten/Kapazität (GNAI-017
|
||||
Abschnitt 6).
|
||||
@@ -0,0 +1,323 @@
|
||||
# CT 101 – Samba-Dateiserver: Entwurfsentscheidungen der Ansible-Migration
|
||||
|
||||
**Dokumenten-ID:** GNAI-019-DEC
|
||||
**Titel:** CT 101 Samba: Design-Entscheidungen für die Ansible-Migration (natives Samba, NFS-Ganesha als Quadlet, Fresh-State-Rebuild)
|
||||
**Stand:** 2026-10-21
|
||||
**Status:** Entwurfsentscheidungen verbindlich; im Projekt `proxmox/lxc/ct-101-samba/` umgesetzt und lokal verifiziert (Syntax-Checks, Check-Mode-Smoke); die Adoption des produktiven LXC steht aus und erfordert Betreiber-Autorisierung
|
||||
**Referenzen:** [GNAI-017](GNAI-017-PLAN_Samba-NFS-Export.md), [GNAI-018](GNAI-018-REQ_Encoding-Pipeline-Integration.md), [GNAI-011](../../GNAI-011-SEC_LXC-SSH-Architecture.md), [GNAI-016](../ct-102-keycloak/GNAI-016-PLAN_Keycloak-Ansible-Rebuild.md), [GNENC-001](../../../llm-server/encoding/GNENC-001-PLAN_Architecture.md), [GNENC-002](../../../llm-server/encoding/GNENC-002-PLAN_Pipeline.md), [GNMON-001](../../../llm-server/monitoring/GNMON-001-PLAN_Architecture.md); Projektkonventionen: `proxmox/lxc/ct-100-dns/` und `proxmox/lxc/ct-102-keycloak/`
|
||||
|
||||
## 1. Zweck und Geltungsbereich
|
||||
|
||||
CT 101 (`10.0.0.9`) ist der produktive Samba-Dateiserver und bislang manuell
|
||||
verwaltet. Betreiberentscheidungen vom 19.10.2026 (GNAI-017/GNAI-018): Aufnahme
|
||||
ins Repository und Anbindung der Encoding-Pipeline in einem Schritt. Dieses
|
||||
Dokument hält die verbindlichen Design-Entscheidungen für die Ansible-Migration
|
||||
fest — insbesondere die Antworten auf die offene Frage „Samba nativ oder im
|
||||
Podman-Container?“ — und begründet die Abweichungen von GNAI-017 Abschnitt 4.1
|
||||
und GNAI-018 R1. Es ist das Referenzdokument für das ausführende Projekt
|
||||
`proxmox/lxc/ct-101-samba/` (Rollen, Playbooks, PVE-Skripte, Runbooks).
|
||||
|
||||
## 2. Ausgangslage und Randbedingungen
|
||||
|
||||
- Produktiver, unprivileged LXC (Proxmox VE), Debian 12 (bookworm), manuell
|
||||
verwaltet; Samba-Dateiserver mit 14 Shares auf fünf Host-Mounts
|
||||
(`/media/daten1`, `/media/daten2`, `/media/daten3`, `/media/datenhdd2`,
|
||||
`/media/datenhdd3`).
|
||||
- Produktiver Samba-State: `smb.conf` plus `/var/lib/samba` inkl.
|
||||
`private/passdb.tdb`; SMB-Benutzer `ghost`, `daidalaw`, `debugofen` mit
|
||||
produktiven Passwörtern.
|
||||
- Die Encoding-Pipeline auf `ghostnet-ki` (`10.0.0.90`) schreibt via NFS als
|
||||
UID 982 (`gn-encoding`) bzw. UID 983 (`gn-download`); die Gruppe `gn-media`
|
||||
(GID 970) muss auf beiden Seiten numerisch identisch sein (GNAI-018 R3,
|
||||
GNENC-002).
|
||||
- Verbindliche Anforderungen: GNAI-018 R1–R7. Projektkonventionen: CT 100
|
||||
(Pi-hole-Quadlet, nftables-Pattern, Inventories agent/bootstrap/production)
|
||||
und CT 102 (Keycloak-Ansible-Projekt, `podman_runtime`-Rolle, Quadlet-Pattern
|
||||
mit digest-gepinntem Basisimage und sha256-gepinntem Containerfile).
|
||||
- Secrets (SMB-Passwörter, passdb, `ghost`-Passwort, private SSH-Schlüssel,
|
||||
Podman-Secrets) werden nie im Repository, im Inventory oder in Logs abgelegt
|
||||
(Konvention analog CT 102).
|
||||
|
||||
## 3. Entscheidung 1: Samba bleibt nativ (Debian-Paket)
|
||||
|
||||
**Entscheidung:** Samba wird nicht containerisiert. Der Dienst läuft als
|
||||
natives Debian-Paket (`samba`, `samba-common`, `samba-common-bin`, `smbclient`)
|
||||
mit `smbd`/`nmbd` als systemd-Dienste in CT 101. Die vollständige `smb.conf`
|
||||
ist als Jinja2-Template im Repository gepflegt (Entscheidung 8).
|
||||
|
||||
**Begründung:**
|
||||
|
||||
- Der produktive Bestand umfasst gewachsene POSIX-Rechte auf fünf Host-Mounts
|
||||
und einen Samba-State (tdb/passdb). Ein Container-Wechsel würde die
|
||||
Datei- und ID-Mapping-Semantik verändern, ohne einen Sicherheitsgewinn zu
|
||||
bringen: Der LXC bleibt unprivileged, und Samba ist der Dienst, der die
|
||||
Mounts direkt sehen muss.
|
||||
- Image-Pinning und Container-Isolation (die sonst typische Begründung für
|
||||
Podman, siehe CT 100/102) betreffen hier einen einzelnen, langlebigen
|
||||
Systemdienst auf dem eigenen Host — der Mehrwert ist gering, die Komplexität
|
||||
(State-Verlagerung, Druckerspool, ID-Mapping) hoch.
|
||||
- Debian-Paketpflege ist über `unattended-upgrades` etabliert
|
||||
(Rolle `samba_lxc_baseline`).
|
||||
- Reproduzierbarkeit wird über das vollständig templated `smb.conf` plus
|
||||
`testparm`-Gate hergestellt, nicht über ein Container-Image.
|
||||
|
||||
**Konsequenz:** Rolle `samba_smb`; native Dienste mit Handler-Neustart bei
|
||||
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`)
|
||||
|
||||
**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).
|
||||
|
||||
**Begründung:**
|
||||
|
||||
- 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`).
|
||||
|
||||
## 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).
|
||||
|
||||
**Begründung:** NFSv4 benötigt keinen Portmapper und kein mountd — weniger
|
||||
Angriffsfläche und keine Legacy-Protokolle. Die Pipeline mountet NFSv4.2
|
||||
(`hard`, `_netdev`, `noatime`; GNENC-001 Abschnitt 6).
|
||||
|
||||
**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`).
|
||||
|
||||
## 6. Entscheidung 4: Identität — `gn-media` GID 970, aufgezeichnete Medien-UIDs, Mitgliedschaft nur `ghost`
|
||||
|
||||
**Entscheidung:**
|
||||
|
||||
- Gruppe `gn-media` mit GID 970 (identisch zu `ghostnet-ki`). Die Rolle
|
||||
`samba_media` fails closed, wenn der GID-Slot oder der Gruppenname von einer
|
||||
anderen Identität belegt ist.
|
||||
- Die Medien-Konten `ghost`, `daidalaw`, `debugofen` werden mit ihren
|
||||
**produktiv aufgezeichneten UIDs** im Inventory deklariert
|
||||
(`samba_media_users`; UID-Platzhalter stoppen die Rollen-Validierung fail
|
||||
closed, bis die Ist-Werte eingetragen sind). Bestehende Konten werden nur
|
||||
auf die UID validiert und nie verändert; fehlende Konten werden im
|
||||
Fresh-State-Rebuild mit exakt diesen UIDs angelegt (primäre Gruppe
|
||||
`gid=uid`, `password_lock: true`, `daidalaw`/`debugofen` mit
|
||||
`/usr/sbin/nologin` und ohne Home-Anlage).
|
||||
- Mitgliedschaft in `gn-media`: nur `ghost` (GNAI-018 R3). `daidalaw` und
|
||||
`debugofen` benötigen keine Mitgliedschaft — reiner Lesezugriff genügt.
|
||||
|
||||
**Begründung:** Die UID-Stabilität ist Voraussetzung der Pipeline
|
||||
(AUTH_SYS-Credentials, `mv`-Abholung aus `[Encode]`, `.dlc`-Drop in
|
||||
`[Encode-In]` über Gruppe `gn-media` mit Mode 2770). Fail-closed verhindert
|
||||
Drift zwischen Soll- und Ist-Identität.
|
||||
|
||||
## 7. Entscheidung 5: passdb-Erhalt bei der Adoption; Secrets nie im Repository
|
||||
|
||||
**Entscheidung:**
|
||||
|
||||
- **Adoption (Default):** Die produktive passdb bleibt erhalten. Die Rolle
|
||||
`samba_smb` assertet fail-closed, dass `/var/lib/samba/private/passdb.tdb`
|
||||
vor der Convergence existiert, dass die `pdbedit -L`-Userliste vor und nach
|
||||
der Convergence identisch ist und dass `ghost`, `daidalaw`, `debugofen`
|
||||
enthalten sind (`samba_smb_preserve_passdb: true`).
|
||||
- **Fresh-State-Rebuild:** Der erste `site.yml`-Lauf erfolgt einmalig mit
|
||||
`-e samba_smb_preserve_passdb=false` (eine frische passdb existiert noch
|
||||
nicht). Anschließend setzt der Operator die SMB-Passwörter interaktiv per
|
||||
`smbpasswd -a ghost/daidalaw/debugofen` auf CT 101; der zweite Lauf mit dem
|
||||
Default validiert die gefüllte passdb mit den vollen Guardrails.
|
||||
- Secrets (SMB-Passwörter, passdb, `ghost`-Passwort, private SSH-Schlüssel,
|
||||
Podman-Secrets) werden nie im Repository, im Inventory oder in Ansible-Logs
|
||||
abgelegt; `pdbedit -L` listet nur Benutzernamen.
|
||||
|
||||
**Begründung:** GNAI-018 R5 (kein Lockout der produktiven Nutzung) und R4
|
||||
(Backup vor der Umstellung); Konvention analog CT-102 (Secrets werden auf dem
|
||||
Ziel generiert bzw. operator-seitig gesetzt).
|
||||
|
||||
## 8. Entscheidung 6: Backup-Strategie
|
||||
|
||||
**Entscheidung:**
|
||||
|
||||
- **Adoption:** `playbooks/backup.yml` (one-shot) sichert vor der ersten
|
||||
Convergence `/etc/samba` und `/var/lib/samba` nach
|
||||
`/var/backups/ghostnet/samba/` (root-only, `0700`). `verify-production.yml`
|
||||
assertet das Vorhandensein dieses pre-migration Backups
|
||||
(`samba_verify_require_premigration_backup: true`; Fresh-State-Rebuild:
|
||||
`-e samba_verify_require_premigration_backup=false`).
|
||||
- **Medien-Mounts:** Alle fünf Mounts tragen `backup=no` — `vzdump` archiviert
|
||||
die Medienbibliothek nicht.
|
||||
- **Upgrade:** `scripts/upgrade-ct101-to-debian13.sh` fährt vor dem
|
||||
Dist-Upgrade einen `vzdump --mode stop --storage local --compress zstd` als
|
||||
Sicherheitsnetz und sichert den laufenden Zustand (leerer Seccomp-Eintrag).
|
||||
- **Restore-Pfad:** vzdump-Restore des CT plus erneuter `site.yml`-Convergence
|
||||
(idempotent) bzw. das pre-migration Backup für den Samba-State.
|
||||
|
||||
**Offen (GNAI-017 Abschnitt 6 Punkt 4, GNAI-000 Abschnitt 4 Punkt 4):** Ein
|
||||
hostunabhängiger Backup-/Restore-Test für CT 101 steht noch aus (analog zu den
|
||||
anderen LXCs).
|
||||
|
||||
## 9. Entscheidung 7: `usershare allow guests = yes` beibehalten; `[printers]`-Duplikat bereinigt
|
||||
|
||||
**Entscheidung:**
|
||||
|
||||
- `usershare allow guests = yes` bleibt im templated `smb.conf` erhalten und
|
||||
ist dort dokumentiert. Eine Änderung erfolgt nur nach ausdrücklicher
|
||||
Betreiberentscheidung (GNAI-018 R4 und Abschnitt 7 Punkt 3).
|
||||
- Der doppelte `[printers]`-Abschnitt aus dem Live-Bestand (inkl. duplizierter
|
||||
Kommentarblöcke) ist im Template bereinigt (GNAI-018 R4).
|
||||
|
||||
**Begründung:** Bestandserhalt (R4) — eine Änderung des Guest-Verhaltens ohne
|
||||
Betreiberentscheidung wäre ein funktionaler Eingriff in den produktiven
|
||||
Betrieb.
|
||||
|
||||
## 10. Entscheidung 8: Config-as-Code — alle Konfigurationen als Repo-Templates
|
||||
|
||||
**Entscheidung:** Jede Konfigurationsdatei auf CT 101 ist ein Jinja2-Template
|
||||
im Repository, das Ansible rendert und installiert; es gibt keinen
|
||||
hand-editierten Zustand auf dem Ziel:
|
||||
|
||||
- `smb.conf` (vollständige Datei, 14 Shares: 12 Bestand-Shares + `[Encode]` und
|
||||
`[Encode-In]` exakt nach GNAI-018 R2) — validiert per `testparm -s` vor der
|
||||
Installation (`validate:`-Gate, GNAI-018 R6).
|
||||
- `sshd` security drop-in (`/etc/ssh/sshd_config.d/99-security.conf`,
|
||||
GNAI-011-Baseline ohne `AllowUsers` — siehe Template-Kommentar).
|
||||
- `nftables.conf` plus systemd-Drop-in (Ghostnet-eigene Tabelle
|
||||
`inet ghostnet_filter`, Netavark-Tabellen bleiben unberührt; `nft --check`
|
||||
vor Installation).
|
||||
- `ganesha.conf` (NFSv4-only, siehe Entscheidung 3).
|
||||
- Quadlet `ghostnet-samba-nfs.container` (siehe Entscheidung 2).
|
||||
|
||||
`verify-production.yml` rendert die Templates per `lookup` erneut und
|
||||
vergleicht sie mit den Live-Dateien (Drift-Erkennung).
|
||||
|
||||
## 11. Entscheidung 9: Fresh-State-Rebuild-Fähigkeit
|
||||
|
||||
**Entscheidung:** Die Playbooks bauen CT 101 von einem frischen Zustand
|
||||
genau so wieder auf. Der dokumentierte Ablauf (Runbook in
|
||||
`ansible/README.md`):
|
||||
|
||||
1. `scripts/provision-ct101-samba.sh` auf dem Proxmox-Host: frischer CT 101 aus
|
||||
einer Debian-13-Vorlage mit dem aufgezeichneten Profil, fünf Medien-Mounts
|
||||
(`backup=no`), leerer Seccomp-Eintrag, Baseline-Hook mit den
|
||||
aufgezeichneten Konto-UIDs.
|
||||
2. Operator auf der PVE-Konsole: `passwd ghost`, öffentliche Schlüssel aus
|
||||
`ansible/files/ssh/` eintragen; Workstation: ssh-Aliase und frischer
|
||||
known_hosts-Eintrag (Fingerprint gegenprüfen, GNAI-011 Abschnitt 3.3).
|
||||
3. Erster `site.yml`-Lauf mit `-e samba_smb_preserve_passdb=false`
|
||||
(`samba_media` legt fehlende Konten mit den aufgezeichneten UIDs an,
|
||||
`samba_smb` installiert Samba und die templated `smb.conf`).
|
||||
4. Operator: `smbpasswd -a` für `ghost`, `daidalaw`, `debugofen` (Secrets nie
|
||||
im Repo).
|
||||
5. Zweiter `site.yml`-Lauf mit Default: volle passdb-Guardrails, erwartet
|
||||
`changed=0` (Idempotenz).
|
||||
6. `verify-production.yml` mit
|
||||
`-e samba_verify_require_premigration_backup=false`.
|
||||
7. NFSv4.2-Mount-Test von `ghostnet-ki` (`10.0.0.90`).
|
||||
|
||||
Die einzige nicht deklarierbare State-Größe sind die Secrets (Schritt 4) —
|
||||
sie existieren ausschließlich in der passdb auf CT 101.
|
||||
|
||||
## 12. Entscheidung 10: In-Place-Dist-Upgrade auf Debian 13 vor der Adoption
|
||||
|
||||
**Entscheidung:** Der bestehende produktive LXC wird vor der Übernahme per
|
||||
`scripts/upgrade-ct101-to-debian13.sh` in-place von Debian 12 (bookworm) auf
|
||||
Debian 13 (trixie) gehoben (Betreiberentscheidung: bestehender LXC bleibt
|
||||
erhalten, zieht mit CT 100/102 gleich).
|
||||
|
||||
**Begründung:** Gleicher OS-Stand wie die übrigen Ansible-LXCs (CT 100/102 auf
|
||||
Debian 13); frische Paketbasis für die Migration; die Baseline-Rolle erwartet
|
||||
Debian 13.
|
||||
|
||||
**Ablauf (Skript):** Guards (CT läuft, unprivileged, Gast Debian 12, Dienste
|
||||
aktiv, IPv4, uid_map, `backup=no` auf allen fünf Mounts) → `vzdump --mode stop`
|
||||
→ laufender Zustand gesichert (leerer Seccomp-Eintrag) → In-Guest-Upgrade
|
||||
(`bookworm` → `trixie` in deb822- und One-Line-Quellen, `dist-upgrade` mit
|
||||
`--force-confold` — hält die produktive `smb.conf`, `autoremove`,
|
||||
wtmpdb-Cleanup analog Baseline) → Reboot → Post-Verify (Debian 13, Dienste,
|
||||
keine failed units, Listener 22/139/445, anonyme `smbclient -L`-Abfrage,
|
||||
uid_map intakt, Seccomp deaktiviert, fünf Mounts eingehängt).
|
||||
|
||||
## 13. Verworfene Alternativen
|
||||
|
||||
| Alternative | Grund der Ablehnung |
|
||||
| --- | --- |
|
||||
| Samba im Podman-Container (analog CT 100/102) | Kein Sicherheitsgewinn bei unprivileged LXC; verändert ID-Mapping/State der gewachsenen Mounts; höherer Aufwand (Entscheidung 1) |
|
||||
| `nfs-kernel-server` nativ (GNAI-018 R1) | In einem unprivileged LXC nicht betreibbar; Ganesha ist die Userspace-Alternative (Entscheidung 2) |
|
||||
| NFSv3 mit `showmount`-Abnahme | Zusätzliche Legacy-Dienste (mountd/rpcbind); `showmount` ist kein verlässlicher Indikator; NFSv4-only (Entscheidung 3) |
|
||||
| passdb-Neuvergebung als Default | Lockout-Risiko für produktive Nutzer (GNAI-018 R5); Erhalt mit Guardrails (Entscheidung 5) |
|
||||
| SMB auch aus dem Admin-VPN (`10.8.0.0/24`) öffnen | Medien-Clients leben im LAN; Erweiterung ist ausdrückliche Betreiberentscheidung (GNAI-018 R7) |
|
||||
| Medien-Mounts mit `backup=yes` | `vzdump` würde die Medienbibliothek archivieren (Ressourcen, Datenschutz); `backup=no` plus gezieltes State-Backup (Entscheidung 6) |
|
||||
|
||||
## 14. Auswirkungen auf GNAI-017 und GNAI-018
|
||||
|
||||
- **GNAI-017 Abschnitt 4.1** (`nfs-kernel-server`, `/etc/exports`) ist durch
|
||||
Entscheidung 2 und 3 überholt und wird entsprechend aktualisiert.
|
||||
- **GNAI-017 Abschnitt 2** (System-Baseline) wird um OS-Version (Debian 13 nach
|
||||
Upgrade) und Hostname (Erfassung per `preflight.yml`) ergänzt; Abschnitt 6
|
||||
(offene Punkte) wird um die Entscheidungen dieses Dokuments aktualisiert.
|
||||
- **GNAI-018 R1** wird revidiert: NFS-Ganesha-Quadlet statt
|
||||
`nfs-kernel-server`; Export-Root `root:gn-media 2775` statt `root:root 0755`
|
||||
(begründete Abweichung, siehe Entscheidung 2).
|
||||
- **GNAI-018 Abschnitt 4** (Abnahme): Punkt 2 (`showmount`) wird als realer
|
||||
NFSv4.2-Mount-Test von `ghostnet-ki` interpretiert (Entscheidung 3); Punkt 8
|
||||
nennt `ghostnet-samba-nfs.service` statt `nfs-kernel-server`.
|
||||
- **GNAI-018 Abschnitt 7** (offene Punkte) ist entschieden: passdb-Erhalt
|
||||
(Entscheidung 5), Backup-Ziel `/var/backups/ghostnet/samba/` plus vzdump im
|
||||
Upgrade-Skript (Entscheidung 6), `usershare allow guests` beibehalten
|
||||
(Entscheidung 7), System-Baseline via preflight-Platzhalter (Entscheidung 9).
|
||||
|
||||
## 15. Offene Punkte
|
||||
|
||||
1. Adoption des produktiven CT 101 gemäß Runbook in
|
||||
`proxmox/lxc/ct-101-samba/ansible/README.md` (erfordert Betreiber-Autorisierung).
|
||||
2. Elf Inventory-Platzhalter mit den Ist-Werten füllen (`preflight.yml` /
|
||||
`audit-production.yml` liefern sie).
|
||||
3. Drei Ed25519-Schlüsselpaare generieren (Operator); nur die öffentlichen
|
||||
Schlüssel liegen im Repo unter `ansible/files/ssh/`.
|
||||
4. Dist-Upgrade auf Debian 13 ausführen (`scripts/upgrade-ct101-to-debian13.sh`).
|
||||
5. Reboot-Test nach der Convergence (GNAI-016 Abschnitt 7, LXC-boot_id-Caveat).
|
||||
6. Hostunabhängiger Backup-/Restore-Test (GNAI-000 Abschnitt 4 Punkt 4).
|
||||
7. Kapazitäts-Monitoring der daten-Platten auf CT 101 (GNAI-017 Abschnitt 6
|
||||
Punkt 6; heute nur indirekt über den NFS-Mount auf `ghostnet-ki` sichtbar,
|
||||
GNMON-001; eigener Collector später).
|
||||
8. Erweiterung der SMB-Allowlist auf das Admin-VPN (`10.8.0.0/24`) —
|
||||
Betreiberentscheidung, aktuell nicht geöffnet.
|
||||
@@ -0,0 +1,22 @@
|
||||
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"
|
||||
|
||||
CMD ["/usr/bin/ganesha.nfsd", "-F", "-f", "/etc/ganesha/ganesha.conf", "-L", "/dev/stderr"]
|
||||
@@ -0,0 +1,32 @@
|
||||
# CT 101 – Samba file server
|
||||
|
||||
Host root for the Samba file server LXC at `10.0.0.9`. The live service is
|
||||
stable and was managed manually until 2026-10-19; the LXC is being imported
|
||||
into this repository step by step.
|
||||
|
||||
The declared state now lives in the CT-scoped Ansible project
|
||||
[`ansible/`](ansible/). The design (GNAI-019) keeps Samba **native** (Debian
|
||||
package, fully templated `smb.conf` with a `testparm` gate) and runs
|
||||
NFS-Ganesha as a **rootful Podman/Quadlet container with host networking**;
|
||||
the nftables firewall, the `gn-media` group identity (GID 970) and the
|
||||
recorded media account UIDs are declared in the inventory. Every configuration
|
||||
file is a repository template that Ansible renders and installs, so the LXC can
|
||||
be rebuilt from a fresh state to exactly this state.
|
||||
|
||||
The project was prepared for the adoption of the productive LXC: the PVE-side
|
||||
dist-upgrade to Debian 13 and the fresh-state provisioning are scripted under
|
||||
[`scripts/`](scripts/), and both runbooks (adoption of the productive CT 101
|
||||
and rebuild from a fresh container) are sequenced in
|
||||
[`ansible/README.md`](ansible/README.md). The privileged adoption steps still
|
||||
require explicit operator authorization.
|
||||
|
||||
Planning and requirements are documented in
|
||||
[`GNAI-017`](../../../docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-017-PLAN_Samba-NFS-Export.md)
|
||||
(share inventory and NFS plan),
|
||||
[`GNAI-018`](../../../docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-018-REQ_Encoding-Pipeline-Integration.md)
|
||||
(encoding-pipeline handover requirements) and
|
||||
[`GNAI-019`](../../../docs/platform/proxmox-server/lxc/ct-101-samba/GNAI-019-DEC_Ansible-Migration.md)
|
||||
(design decisions for the Ansible migration).
|
||||
|
||||
Do not store SMB passwords, the Samba passdb, the `ghost` password, private
|
||||
SSH keys or Podman secret contents in this repository.
|
||||
@@ -0,0 +1,179 @@
|
||||
# CT 101 Samba Ansible project
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Create the repository-level control environment once:
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
.venv/bin/python -m pip install --upgrade pip
|
||||
.venv/bin/python -m pip install -r shared/ansible/requirements-control.txt
|
||||
```
|
||||
|
||||
Run commands from this directory and always select an inventory explicitly:
|
||||
|
||||
```bash
|
||||
cd proxmox/lxc/ct-101-samba/ansible
|
||||
../../../../.venv/bin/ansible-playbook \
|
||||
--inventory inventories/production/hosts.yml \
|
||||
playbooks/verify-production.yml \
|
||||
--private-key ~/.ssh/id_ed25519_ghostnet_ct101_ansible_deploy
|
||||
```
|
||||
|
||||
## Inventories and access paths
|
||||
|
||||
| Inventory | Account | Key | Become | Use |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent` | `ghost-agent` | `~/.ssh/id_ed25519_ghostnet_ct101_agent` | no | read-only preflight |
|
||||
| `bootstrap` | `ghost` | `~/.ssh/id_ed25519_ghostnet_ct101_ghost` | `--ask-become-pass` (sudo) | audit, access bootstrap |
|
||||
| `production` | `ansible-deploy` | `~/.ssh/id_ed25519_ghostnet_ct101_ansible_deploy` | NOPASSWD sudo | convergence and verification |
|
||||
|
||||
The operator generates the three Ed25519 keypairs (`agent` and
|
||||
`ansible-deploy` without a passphrase, `ghost` with the operator passphrase)
|
||||
and commits only the public halves under `files/ssh/`. Private keys stay in
|
||||
`~/.ssh` with mode `0600` and are never committed. The `ghost` login password
|
||||
is set interactively (`passwd ghost` on the PVE console) and the SMB passwords
|
||||
are set interactively (`smbpasswd -a <user>`); neither ever enters the
|
||||
repository, the inventory, Ansible logs or this README.
|
||||
|
||||
## Placeholders
|
||||
|
||||
The inventory carries `REPLACE-WITH-*` placeholders that fail closed (the
|
||||
role argument specs reject them before any task runs). `preflight.yml` on the
|
||||
`agent` inventory reports the live values to copy:
|
||||
|
||||
- `samba_expected_hostname`, `samba_expected_processor_vcpus`,
|
||||
`samba_minimum_memory_mb`, `samba_minimum_root_size_bytes`,
|
||||
`samba_minimum_root_available_bytes` (group_vars)
|
||||
- `samba_access_admin_uid`, `samba_access_agent_uid`,
|
||||
`samba_access_deploy_uid` (host_vars)
|
||||
- `samba_media_users[ghost|daidalaw|debugofen].uid` (group_vars; the recorded
|
||||
productive UIDs — the pipeline depends on them, GNAI-018 R1)
|
||||
|
||||
## Playbooks
|
||||
|
||||
| Playbook | Inventory | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `preflight.yml` | `agent` | read-only identity, health, listener and placeholder-source report |
|
||||
| `audit-production.yml` | `bootstrap` | read-only deep pre-migration audit (file stats, `testparm` dump, `pdbedit`, sshd drift, mounts, account/group databases) |
|
||||
| `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`) |
|
||||
| `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
|
||||
created state are guarded. `bootstrap.yml` is executed for real during the
|
||||
adoption; only `site.yml` must be check-mode clean.
|
||||
|
||||
## Adoption runbook for the productive CT 101
|
||||
|
||||
The LXC exists and serves production traffic. The deliberate adoption path
|
||||
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`.
|
||||
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`
|
||||
(validates the unprivileged mapping, creates the locked `ghost-agent`,
|
||||
prepares `.ssh`, adds `ghost` to `sudo`, disables `ssh.socket`). Then set
|
||||
`passwd ghost` and paste the `ghost-agent` and `ghost` public keys into the
|
||||
respective `authorized_keys`.
|
||||
4. On the workstation: add the `ghostnet-ct101-agent` / `ghostnet-ct101-ghost`
|
||||
ssh aliases and seed `~/.ssh/known_hosts_ghostnet_ct101` (verify the
|
||||
fingerprint on the PVE console first, GNAI-011 section 3.3).
|
||||
5. `preflight.yml` on the `agent` inventory; copy the reported values into all
|
||||
three inventories (the 11 placeholders above).
|
||||
6. `audit-production.yml` on the `bootstrap` inventory (`--ask-become-pass`).
|
||||
7. `bootstrap.yml` on the `bootstrap` inventory (`--ask-become-pass`); keep a
|
||||
second session or the PVE console open while the SSH policy changes.
|
||||
8. `verify-deploy-access.yml` on the `production` inventory.
|
||||
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).
|
||||
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).
|
||||
|
||||
## Fresh-state rebuild runbook
|
||||
|
||||
The playbooks rebuild CT 101 from a fresh container to exactly the declared
|
||||
state. Every configuration file is a repository template that Ansible renders
|
||||
and installs; the only state that cannot be declared is secret material.
|
||||
|
||||
1. Prerequisites: the inventory is fully filled (all 11 placeholders carry the
|
||||
recorded productive values), the three `.pub` files are committed, and the
|
||||
five media host paths are known.
|
||||
2. On the Proxmox host, run `scripts/provision-ct101-samba.sh` with
|
||||
`GHOSTNET_AGENT_UID`, `GHOSTNET_ADMIN_UID` and `GHOSTNET_MP0_HOST` …
|
||||
`GHOSTNET_MP4_HOST` (see `scripts/README.md`). This creates CT 101 from a
|
||||
Debian 13 template with the recorded profile, attaches the five media
|
||||
mounts (`backup=no`), disables the LXC seccomp filter and runs the baseline
|
||||
hook, which creates `ghost-agent` and `ghost` with the recorded UIDs.
|
||||
3. Operator: `pct exec 101 -- passwd ghost`, then install the `ghost-agent` and
|
||||
`ghost` public keys from `files/ssh/` into the prepared `authorized_keys`.
|
||||
4. On the workstation: ssh aliases plus a **fresh** `known_hosts_ghostnet_ct101`
|
||||
entry — verify the new host-key fingerprint on the PVE console first
|
||||
(GNAI-011 section 3.3).
|
||||
5. First convergence on the `production` inventory:
|
||||
|
||||
```bash
|
||||
../../../../.venv/bin/ansible-playbook \
|
||||
--inventory inventories/production/hosts.yml \
|
||||
playbooks/site.yml \
|
||||
--private-key ~/.ssh/id_ed25519_ghostnet_ct101_ansible_deploy \
|
||||
-e samba_smb_preserve_passdb=false
|
||||
```
|
||||
|
||||
`samba_media` creates the missing media accounts with their recorded UIDs
|
||||
(`ghost` already exists from the baseline and is only validated), `samba_smb`
|
||||
installs Samba and the templated `smb.conf`. The passdb preservation guard
|
||||
is disabled for this first run only, because a fresh passdb does not exist
|
||||
yet.
|
||||
6. Operator, on CT 101 (secrets never enter the repository):
|
||||
|
||||
```bash
|
||||
smbpasswd -a ghost
|
||||
smbpasswd -a daidalaw
|
||||
smbpasswd -a debugofen
|
||||
```
|
||||
|
||||
7. Second `site.yml` run with the default `samba_smb_preserve_passdb=true`:
|
||||
the passdb now exists, so the full preservation guardrails apply; expect
|
||||
`changed=0`.
|
||||
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`).
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
Ansible output. `pdbedit -L` lists usernames only; `backup.yml` writes
|
||||
root-only copies on the target under `/var/backups/ghostnet/samba/`.
|
||||
@@ -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 AAAAC3NzaC1lZDI1NTE5AAAAIJ4uaHiZpbqy1Gnp5+TrYLH1VfpUPbMNPW9IXLm/UFA5 ansible-deploy@ghostnet-ct101
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhjaSvQXVYWdBxEwZi4cD9iOB7R6teWKZvV5o3iOtiV ghost-agent@ghostnet-ct101
|
||||
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICBlIsYEO2YSZVsWvLg891kh6JbMyS1eU+ypSVqXOId+ ghost@ghostnet-ct101
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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_ct101
|
||||
|
||||
samba_expected_distribution: Debian
|
||||
samba_expected_distribution_major_version: "13"
|
||||
# Live hostname of CT 101; copy from preflight.yml (SMB1 is disabled, so the
|
||||
# live server does not answer NetBIOS names):
|
||||
samba_expected_hostname: REPLACE-WITH-PREFLIGHT-HOSTNAME
|
||||
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; copy from preflight.yml:
|
||||
samba_minimum_root_size_bytes: REPLACE-WITH-PREFLIGHT-ROOT-SIZE
|
||||
samba_minimum_root_available_bytes: REPLACE-WITH-PREFLIGHT-ROOT-AVAIL
|
||||
|
||||
samba_firewall_management_ipv4:
|
||||
- 10.0.0.0/24
|
||||
samba_firewall_ssh_port: 22
|
||||
samba_firewall_smb_tcp_ports:
|
||||
- 139
|
||||
- 445
|
||||
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
|
||||
|
||||
samba_media_group_name: gn-media
|
||||
samba_media_group_gid: 970
|
||||
# Media accounts with their productive UIDs; copy from preflight.yml /
|
||||
# audit-production.yml (fresh-state rebuild: missing accounts are created with
|
||||
# exactly these UIDs; existing accounts are only validated on the UID):
|
||||
samba_media_users:
|
||||
- name: ghost
|
||||
uid: REPLACE-WITH-AUDIT-UID-GHOST
|
||||
shell: /bin/bash
|
||||
home: /home/ghost
|
||||
create_home: true
|
||||
- name: daidalaw
|
||||
uid: REPLACE-WITH-AUDIT-UID-DAIDALAW
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/daidalaw
|
||||
create_home: false
|
||||
- name: debugofen
|
||||
uid: REPLACE-WITH-AUDIT-UID-DEBUGOFEN
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/debugofen
|
||||
create_home: false
|
||||
samba_media_group_members:
|
||||
- ghost
|
||||
samba_media_mount_points:
|
||||
- /media/daten1
|
||||
- /media/daten2
|
||||
- /media/daten3
|
||||
- /media/datenhdd2
|
||||
- /media/datenhdd3
|
||||
|
||||
samba_smb_workgroup: Ghost.net
|
||||
samba_smb_service_units:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
# Complete share target state from GNAI-017 section 3 plus [Encode] and
|
||||
# [Encode-In] from GNAI-018 R2. Comments only where the live system shows them
|
||||
# (Debian defaults and the new pipeline shares); the media shares had empty
|
||||
# comments in the anonymous listing and stay without comments.
|
||||
samba_smb_media_shares:
|
||||
- name: Dokumente
|
||||
path: /media/daten1/Dokumente
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Filme
|
||||
path: /media/daten1/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Filme2
|
||||
path: /media/datenhdd3/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Serien
|
||||
path: /media/daten2/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Serien2
|
||||
path: /media/daten3/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Animation
|
||||
path: /media/daten1/Animation
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Anime
|
||||
path: /media/daten3/Anime
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Extern
|
||||
path: /media/datenhdd2/extern
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Musik
|
||||
path: /media/daten1/Musik
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Audiobooks
|
||||
path: /media/daten1/Audiobooks
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Software
|
||||
path: /media/datenhdd3/Software
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- name: Downloads
|
||||
path: /media/datenhdd3/Downloads
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
- name: Encode
|
||||
comment: "Encoding-Pipeline: fertige Encodes (Abholung per mv)"
|
||||
path: /media/datenhdd3/Encoding/output
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
force_group: gn-media
|
||||
create_mask: "0664"
|
||||
directory_mask: "0775"
|
||||
- name: Encode-In
|
||||
comment: "Encoding-Pipeline: .dlc-Drop (input)"
|
||||
path: /media/datenhdd3/Encoding/input
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
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: >-
|
||||
0fdbe6b5fe542f22ed291c4cabe505e0da472024f0186e2414fd002b751f784f
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
samba_ctid: 101
|
||||
samba_expected_ipv4: 10.0.0.9
|
||||
# ED25519 host key, captured via ssh-keyscan on 2026-10-19 from the admin
|
||||
# workstation; before the first trust, verify it again on the PVE console
|
||||
# (GNAI-011 section 3.3):
|
||||
samba_expected_ssh_host_key_fingerprint: >-
|
||||
SHA256:+MUO4J4x3TCYLc1iei95UcPTzkPwDPOzovfaIieuOKs
|
||||
# Live values; copy from preflight.yml:
|
||||
samba_expected_processor_vcpus: REPLACE-WITH-PREFLIGHT-VCPUS
|
||||
samba_minimum_memory_mb: REPLACE-WITH-PREFLIGHT-MEMORY-MB
|
||||
# UID of the existing ghost account; copy from audit-production.yml:
|
||||
samba_access_admin_uid: REPLACE-WITH-AUDIT-GHOST-UID
|
||||
# Free UIDs to pick from audit-production.yml (ghost-agent / ansible-deploy):
|
||||
samba_access_agent_uid: REPLACE-WITH-FREE-AGENT-UID
|
||||
samba_access_deploy_uid: REPLACE-WITH-FREE-DEPLOY-UID
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
samba_hosts:
|
||||
hosts:
|
||||
samba-ct101:
|
||||
ansible_host: 10.0.0.9
|
||||
ansible_user: ghost-agent
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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_ct101
|
||||
|
||||
samba_expected_distribution: Debian
|
||||
samba_expected_distribution_major_version: "13"
|
||||
# Live hostname of CT 101; copy from preflight.yml (SMB1 is disabled, so the
|
||||
# live server does not answer NetBIOS names):
|
||||
samba_expected_hostname: REPLACE-WITH-PREFLIGHT-HOSTNAME
|
||||
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; copy from preflight.yml:
|
||||
samba_minimum_root_size_bytes: REPLACE-WITH-PREFLIGHT-ROOT-SIZE
|
||||
samba_minimum_root_available_bytes: REPLACE-WITH-PREFLIGHT-ROOT-AVAIL
|
||||
|
||||
samba_firewall_management_ipv4:
|
||||
- 10.0.0.0/24
|
||||
samba_firewall_ssh_port: 22
|
||||
samba_firewall_smb_tcp_ports:
|
||||
- 139
|
||||
- 445
|
||||
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
|
||||
|
||||
samba_media_group_name: gn-media
|
||||
samba_media_group_gid: 970
|
||||
# Media accounts with their productive UIDs; copy from preflight.yml /
|
||||
# audit-production.yml (fresh-state rebuild: missing accounts are created with
|
||||
# exactly these UIDs; existing accounts are only validated on the UID):
|
||||
samba_media_users:
|
||||
- name: ghost
|
||||
uid: REPLACE-WITH-AUDIT-UID-GHOST
|
||||
shell: /bin/bash
|
||||
home: /home/ghost
|
||||
create_home: true
|
||||
- name: daidalaw
|
||||
uid: REPLACE-WITH-AUDIT-UID-DAIDALAW
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/daidalaw
|
||||
create_home: false
|
||||
- name: debugofen
|
||||
uid: REPLACE-WITH-AUDIT-UID-DEBUGOFEN
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/debugofen
|
||||
create_home: false
|
||||
samba_media_group_members:
|
||||
- ghost
|
||||
samba_media_mount_points:
|
||||
- /media/daten1
|
||||
- /media/daten2
|
||||
- /media/daten3
|
||||
- /media/datenhdd2
|
||||
- /media/datenhdd3
|
||||
|
||||
samba_smb_workgroup: Ghost.net
|
||||
samba_smb_service_units:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
# Complete share target state from GNAI-017 section 3 plus [Encode] and
|
||||
# [Encode-In] from GNAI-018 R2. Comments only where the live system shows them
|
||||
# (Debian defaults and the new pipeline shares); the media shares had empty
|
||||
# comments in the anonymous listing and stay without comments.
|
||||
samba_smb_media_shares:
|
||||
- name: Dokumente
|
||||
path: /media/daten1/Dokumente
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Filme
|
||||
path: /media/daten1/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Filme2
|
||||
path: /media/datenhdd3/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Serien
|
||||
path: /media/daten2/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Serien2
|
||||
path: /media/daten3/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Animation
|
||||
path: /media/daten1/Animation
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Anime
|
||||
path: /media/daten3/Anime
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Extern
|
||||
path: /media/datenhdd2/extern
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Musik
|
||||
path: /media/daten1/Musik
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Audiobooks
|
||||
path: /media/daten1/Audiobooks
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Software
|
||||
path: /media/datenhdd3/Software
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- name: Downloads
|
||||
path: /media/datenhdd3/Downloads
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
- name: Encode
|
||||
comment: "Encoding-Pipeline: fertige Encodes (Abholung per mv)"
|
||||
path: /media/datenhdd3/Encoding/output
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
force_group: gn-media
|
||||
create_mask: "0664"
|
||||
directory_mask: "0775"
|
||||
- name: Encode-In
|
||||
comment: "Encoding-Pipeline: .dlc-Drop (input)"
|
||||
path: /media/datenhdd3/Encoding/input
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
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: >-
|
||||
0fdbe6b5fe542f22ed291c4cabe505e0da472024f0186e2414fd002b751f784f
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
samba_ctid: 101
|
||||
samba_expected_ipv4: 10.0.0.9
|
||||
# ED25519 host key, captured via ssh-keyscan on 2026-10-19 from the admin
|
||||
# workstation; before the first trust, verify it again on the PVE console
|
||||
# (GNAI-011 section 3.3):
|
||||
samba_expected_ssh_host_key_fingerprint: >-
|
||||
SHA256:+MUO4J4x3TCYLc1iei95UcPTzkPwDPOzovfaIieuOKs
|
||||
# Live values; copy from preflight.yml:
|
||||
samba_expected_processor_vcpus: REPLACE-WITH-PREFLIGHT-VCPUS
|
||||
samba_minimum_memory_mb: REPLACE-WITH-PREFLIGHT-MEMORY-MB
|
||||
# UID of the existing ghost account; copy from audit-production.yml:
|
||||
samba_access_admin_uid: REPLACE-WITH-AUDIT-GHOST-UID
|
||||
# Free UIDs to pick from audit-production.yml (ghost-agent / ansible-deploy):
|
||||
samba_access_agent_uid: REPLACE-WITH-FREE-AGENT-UID
|
||||
samba_access_deploy_uid: REPLACE-WITH-FREE-DEPLOY-UID
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
samba_hosts:
|
||||
hosts:
|
||||
samba-ct101:
|
||||
ansible_host: 10.0.0.9
|
||||
ansible_user: ghost
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
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_ct101
|
||||
|
||||
samba_expected_distribution: Debian
|
||||
samba_expected_distribution_major_version: "13"
|
||||
# Live hostname of CT 101; copy from preflight.yml (SMB1 is disabled, so the
|
||||
# live server does not answer NetBIOS names):
|
||||
samba_expected_hostname: REPLACE-WITH-PREFLIGHT-HOSTNAME
|
||||
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; copy from preflight.yml:
|
||||
samba_minimum_root_size_bytes: REPLACE-WITH-PREFLIGHT-ROOT-SIZE
|
||||
samba_minimum_root_available_bytes: REPLACE-WITH-PREFLIGHT-ROOT-AVAIL
|
||||
|
||||
samba_firewall_management_ipv4:
|
||||
- 10.0.0.0/24
|
||||
samba_firewall_ssh_port: 22
|
||||
samba_firewall_smb_tcp_ports:
|
||||
- 139
|
||||
- 445
|
||||
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
|
||||
|
||||
samba_media_group_name: gn-media
|
||||
samba_media_group_gid: 970
|
||||
# Media accounts with their productive UIDs; copy from preflight.yml /
|
||||
# audit-production.yml (fresh-state rebuild: missing accounts are created with
|
||||
# exactly these UIDs; existing accounts are only validated on the UID):
|
||||
samba_media_users:
|
||||
- name: ghost
|
||||
uid: REPLACE-WITH-AUDIT-UID-GHOST
|
||||
shell: /bin/bash
|
||||
home: /home/ghost
|
||||
create_home: true
|
||||
- name: daidalaw
|
||||
uid: REPLACE-WITH-AUDIT-UID-DAIDALAW
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/daidalaw
|
||||
create_home: false
|
||||
- name: debugofen
|
||||
uid: REPLACE-WITH-AUDIT-UID-DEBUGOFEN
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/debugofen
|
||||
create_home: false
|
||||
samba_media_group_members:
|
||||
- ghost
|
||||
samba_media_mount_points:
|
||||
- /media/daten1
|
||||
- /media/daten2
|
||||
- /media/daten3
|
||||
- /media/datenhdd2
|
||||
- /media/datenhdd3
|
||||
|
||||
samba_smb_workgroup: Ghost.net
|
||||
samba_smb_service_units:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
# Complete share target state from GNAI-017 section 3 plus [Encode] and
|
||||
# [Encode-In] from GNAI-018 R2. Comments only where the live system shows them
|
||||
# (Debian defaults and the new pipeline shares); the media shares had empty
|
||||
# comments in the anonymous listing and stay without comments.
|
||||
samba_smb_media_shares:
|
||||
- name: Dokumente
|
||||
path: /media/daten1/Dokumente
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Filme
|
||||
path: /media/daten1/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Filme2
|
||||
path: /media/datenhdd3/Filme
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Serien
|
||||
path: /media/daten2/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Serien2
|
||||
path: /media/daten3/Serien
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
force_user: root
|
||||
force_group: root
|
||||
- name: Animation
|
||||
path: /media/daten1/Animation
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Anime
|
||||
path: /media/daten3/Anime
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Extern
|
||||
path: /media/datenhdd2/extern
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- name: Musik
|
||||
path: /media/daten1/Musik
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Audiobooks
|
||||
path: /media/daten1/Audiobooks
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- name: Software
|
||||
path: /media/datenhdd3/Software
|
||||
read_only: "yes"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
write_list:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- name: Downloads
|
||||
path: /media/datenhdd3/Downloads
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
- name: Encode
|
||||
comment: "Encoding-Pipeline: fertige Encodes (Abholung per mv)"
|
||||
path: /media/datenhdd3/Encoding/output
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
force_group: gn-media
|
||||
create_mask: "0664"
|
||||
directory_mask: "0775"
|
||||
- name: Encode-In
|
||||
comment: "Encoding-Pipeline: .dlc-Drop (input)"
|
||||
path: /media/datenhdd3/Encoding/input
|
||||
read_only: "no"
|
||||
valid_users:
|
||||
- ghost
|
||||
admin_users:
|
||||
- ghost
|
||||
write_list:
|
||||
- ghost
|
||||
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: >-
|
||||
0fdbe6b5fe542f22ed291c4cabe505e0da472024f0186e2414fd002b751f784f
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
samba_ctid: 101
|
||||
samba_expected_ipv4: 10.0.0.9
|
||||
# ED25519 host key, captured via ssh-keyscan on 2026-10-19 from the admin
|
||||
# workstation; before the first trust, verify it again on the PVE console
|
||||
# (GNAI-011 section 3.3):
|
||||
samba_expected_ssh_host_key_fingerprint: >-
|
||||
SHA256:+MUO4J4x3TCYLc1iei95UcPTzkPwDPOzovfaIieuOKs
|
||||
# Live values; copy from preflight.yml:
|
||||
samba_expected_processor_vcpus: REPLACE-WITH-PREFLIGHT-VCPUS
|
||||
samba_minimum_memory_mb: REPLACE-WITH-PREFLIGHT-MEMORY-MB
|
||||
# UID of the existing ghost account; copy from audit-production.yml:
|
||||
samba_access_admin_uid: REPLACE-WITH-AUDIT-GHOST-UID
|
||||
# Free UIDs to pick from audit-production.yml (ghost-agent / ansible-deploy):
|
||||
samba_access_agent_uid: REPLACE-WITH-FREE-AGENT-UID
|
||||
samba_access_deploy_uid: REPLACE-WITH-FREE-DEPLOY-UID
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
samba_hosts:
|
||||
hosts:
|
||||
samba-ct101:
|
||||
ansible_host: 10.0.0.9
|
||||
ansible_user: ansible-deploy
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
- name: Read-only deep audit of the pre-migration CT 101 Samba state
|
||||
hosts: samba_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
gather_subset:
|
||||
- min
|
||||
- network
|
||||
- virtual
|
||||
|
||||
tasks:
|
||||
- name: Verify the expected guest identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- inventory_hostname == "samba-ct101"
|
||||
- samba_ctid | int == 101
|
||||
- ansible_facts["distribution"] == samba_expected_distribution
|
||||
- >-
|
||||
ansible_facts["distribution_major_version"] ==
|
||||
samba_expected_distribution_major_version
|
||||
- ansible_facts["default_ipv4"]["address"] == samba_expected_ipv4
|
||||
- ansible_facts["virtualization_type"] == "lxc"
|
||||
- ansible_facts["virtualization_role"] == "guest"
|
||||
fail_msg: The connected machine is not the declared CT 101 Samba LXC.
|
||||
|
||||
- name: Read the audit transport identity
|
||||
become: false
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -un
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_identity
|
||||
|
||||
- name: Verify the bootstrap transport account
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_audit_identity.stdout == "ghost"
|
||||
fail_msg: >-
|
||||
The audit must use the personal administrator account ghost with
|
||||
password-gated sudo (bootstrap inventory, --ask-become-pass).
|
||||
|
||||
- name: Stat the Samba and access state files
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /etc/samba
|
||||
- /etc/samba/smb.conf
|
||||
- /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
|
||||
- /etc/nftables.conf
|
||||
- /home/ghost/.ssh/authorized_keys
|
||||
- /home/ghost-agent/.ssh/authorized_keys
|
||||
- /media/datenhdd3/Encoding
|
||||
register: samba_audit_stats
|
||||
|
||||
- name: Report the observed file states
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ item.item }}: exists={{ item.stat.exists }}
|
||||
mode={{ '%04o' % (item.stat.mode | default(0) | int(0, 8)) }}
|
||||
owner={{ item.stat.uid | default('-') }}:{{ item.stat.gid | default('-') }}
|
||||
size={{ item.stat.size | default('-') }}
|
||||
loop: "{{ samba_audit_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
|
||||
- name: Read the live smb.conf checksum
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/sha256sum
|
||||
- /etc/samba/smb.conf
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_smb_conf
|
||||
|
||||
- name: Read the live testparm dump for the pre-migration record
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/testparm
|
||||
- -s
|
||||
- /etc/samba/smb.conf
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_testparm
|
||||
|
||||
- name: Read the productive Samba user database
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/pdbedit
|
||||
- -L
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_pdbedit
|
||||
|
||||
- name: Require the productive Samba users to be present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_audit_pdbedit.rc == 0
|
||||
- >-
|
||||
samba_media_users | map(attribute='name') | list is
|
||||
subset(samba_audit_pdbedit.stdout_lines)
|
||||
fail_msg: >-
|
||||
The productive SMB users ghost, daidalaw and debugofen are missing
|
||||
from the passdb. Stop the adoption; the passdb must not be at risk.
|
||||
|
||||
- name: Validate the effective SSH policy
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/sshd
|
||||
- -T
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_sshd
|
||||
|
||||
- name: Report the legacy SSH drift against the GNAI-011 baseline
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
permitrootlogin: >-
|
||||
{{ (samba_audit_sshd.stdout_lines |
|
||||
select('match', '^permitrootlogin ') | list | first)
|
||||
| default('permitrootlogin <unset>') }}
|
||||
passwordauthentication: >-
|
||||
{{ (samba_audit_sshd.stdout_lines |
|
||||
select('match', '^passwordauthentication ') | list | first)
|
||||
| default('passwordauthentication <unset>') }}
|
||||
kbdinteractiveauthentication: >-
|
||||
{{ (samba_audit_sshd.stdout_lines |
|
||||
select('match', '^kbdinteractiveauthentication ') | list | first)
|
||||
| default('kbdinteractiveauthentication <unset>') }}
|
||||
disableforwarding: >-
|
||||
{{ (samba_audit_sshd.stdout_lines |
|
||||
select('match', '^disableforwarding ') | list | first)
|
||||
| default('disableforwarding <unset>') }}
|
||||
note: >-
|
||||
The legacy policy is closed by samba_access during bootstrap.yml;
|
||||
this report documents the pre-migration drift.
|
||||
|
||||
- name: Read the live nftables ruleset
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/nft
|
||||
- list
|
||||
- ruleset
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
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:
|
||||
- /usr/bin/findmnt
|
||||
- --noheadings
|
||||
- --output
|
||||
- SOURCE,FSTYPE,OPTIONS
|
||||
- --target
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_mount_points }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_audit_mounts
|
||||
|
||||
- name: Read the media account password states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/passwd
|
||||
- -S
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
register: samba_audit_password_states
|
||||
|
||||
- name: Read the complete account database for UID slot picking
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_all_accounts
|
||||
|
||||
- name: Read the complete group database for GID slot picking
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_all_groups
|
||||
|
||||
- name: Stat the media account authorized keys
|
||||
ansible.builtin.stat:
|
||||
path: "/home/{{ item }}/.ssh/authorized_keys"
|
||||
loop:
|
||||
- ghost
|
||||
- daidalaw
|
||||
- debugofen
|
||||
register: samba_audit_keys_stats
|
||||
|
||||
- name: Read the existing authorized keys
|
||||
ansible.builtin.slurp:
|
||||
src: "/home/{{ item.item }}/.ssh/authorized_keys"
|
||||
loop: "{{ samba_audit_keys_stats.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: item.stat.exists
|
||||
register: samba_audit_keys
|
||||
|
||||
- name: Read the sudoers drop-ins
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/ls
|
||||
- -la
|
||||
- /etc/sudoers.d
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_sudoers
|
||||
|
||||
- name: Read failed systemd units
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- --failed
|
||||
- --no-legend
|
||||
- --plain
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_audit_failed_units
|
||||
|
||||
- name: Require no failed units
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_audit_failed_units.stdout | trim == ""
|
||||
|
||||
- name: Read the service states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- is-active
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- ssh.service
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
- nftables.service
|
||||
- unattended-upgrades.service
|
||||
register: samba_audit_services
|
||||
|
||||
- name: Report the complete pre-migration audit
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
ctid: "{{ samba_ctid }}"
|
||||
smb_conf_sha256: "{{ samba_audit_smb_conf.stdout.split()[0] }}"
|
||||
testparm_dump: "{{ samba_audit_testparm.stdout }}"
|
||||
smb_users: "{{ samba_audit_pdbedit.stdout_lines }}"
|
||||
nft_ruleset: "{{ samba_audit_nft.stdout }}"
|
||||
media_mounts: >-
|
||||
{{ samba_audit_mounts.results | map(attribute='stdout') | list }}
|
||||
media_password_states: >-
|
||||
{{ samba_audit_password_states.results |
|
||||
map(attribute='stdout') | list }}
|
||||
all_accounts: "{{ samba_audit_all_accounts.stdout }}"
|
||||
all_groups: "{{ samba_audit_all_groups.stdout }}"
|
||||
authorized_keys: >-
|
||||
{{ samba_audit_keys.results |
|
||||
selectattr('content', 'defined') |
|
||||
map(attribute='content') | map('b64decode') | list }}
|
||||
sudoers_d: "{{ samba_audit_sudoers.stdout }}"
|
||||
service_states: >-
|
||||
{{ samba_audit_services.results | map(attribute='stdout') | list }}
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
- name: Create the one-shot pre-migration Samba backup on CT 101
|
||||
hosts: samba_hosts
|
||||
become: true
|
||||
gather_facts: false
|
||||
|
||||
tasks:
|
||||
- name: Require the productive Samba passdb before the backup
|
||||
ansible.builtin.stat:
|
||||
path: "{{ samba_smb_passdb_file }}"
|
||||
register: samba_backup_passdb
|
||||
|
||||
- name: Fail closed when the passdb is missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_backup_passdb.stat.exists
|
||||
fail_msg: >-
|
||||
The productive passdb is missing; there is nothing safe to back up.
|
||||
Investigate before running this one-shot playbook.
|
||||
|
||||
- name: Read the current UTC timestamp
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/date
|
||||
- -u
|
||||
- +%Y%m%dT%H%M%SZ
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_backup_timestamp
|
||||
|
||||
- name: Define the backup directory
|
||||
ansible.builtin.set_fact:
|
||||
samba_backup_directory: >-
|
||||
/var/backups/ghostnet/samba/pre-migration-{{ samba_backup_timestamp.stdout }}
|
||||
|
||||
- name: Require the backup directory to be absent
|
||||
ansible.builtin.stat:
|
||||
path: "{{ samba_backup_directory }}"
|
||||
register: samba_backup_existing
|
||||
|
||||
- name: Fail when the backup directory already exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not samba_backup_existing.stat.exists
|
||||
fail_msg: >-
|
||||
The backup directory already exists; this playbook is one-shot and
|
||||
never overwrites a previous pre-migration backup.
|
||||
|
||||
- name: Create the protected backup directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ samba_backup_directory }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Check for the Samba defaults file
|
||||
ansible.builtin.stat:
|
||||
path: /etc/default/samba
|
||||
register: samba_backup_default_samba
|
||||
|
||||
- name: Copy the Samba configuration and state with services stopped
|
||||
block:
|
||||
- name: Stop the Samba services for a consistent copy
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
loop:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Copy the Samba configuration tree
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cp
|
||||
- -a
|
||||
- /etc/samba
|
||||
- "{{ samba_backup_directory }}/etc-samba"
|
||||
changed_when: true
|
||||
|
||||
- name: Copy the Samba state tree including the passdb
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cp
|
||||
- -a
|
||||
- /var/lib/samba
|
||||
- "{{ samba_backup_directory }}/var-lib-samba"
|
||||
changed_when: true
|
||||
|
||||
- name: Copy the Samba defaults when present
|
||||
ansible.builtin.copy:
|
||||
src: /etc/default/samba
|
||||
dest: "{{ samba_backup_directory }}/etc-default-samba"
|
||||
remote_src: true
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: samba_backup_default_samba.stat.exists
|
||||
|
||||
- name: Read the testparm dump of the live configuration
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/testparm
|
||||
- -s
|
||||
- /etc/samba/smb.conf
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_backup_testparm
|
||||
|
||||
- name: Write the testparm dump into the backup
|
||||
ansible.builtin.copy:
|
||||
content: "{{ samba_backup_testparm.stdout }}\n"
|
||||
dest: "{{ samba_backup_directory }}/testparm-live.txt"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Read the Samba user list
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/pdbedit
|
||||
- -L
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_backup_pdbedit
|
||||
|
||||
- name: Write the Samba user list into the backup
|
||||
ansible.builtin.copy:
|
||||
content: "{{ samba_backup_pdbedit.stdout }}"
|
||||
dest: "{{ samba_backup_directory }}/pdbedit-users.txt"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Read the backup file checksums
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/find
|
||||
- "{{ samba_backup_directory }}"
|
||||
- -type
|
||||
- f
|
||||
- -exec
|
||||
- /usr/bin/sha256sum
|
||||
- "{}"
|
||||
- +
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_backup_checksums
|
||||
|
||||
- name: Write the checksum manifest into the backup
|
||||
ansible.builtin.copy:
|
||||
content: "{{ samba_backup_checksums.stdout }}"
|
||||
dest: "{{ samba_backup_directory }}/SHA256SUMS"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Write the backup manifest
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
# CT 101 Samba pre-migration backup (one-shot, playbooks/backup.yml)
|
||||
created_utc={{ samba_backup_timestamp.stdout }}
|
||||
host={{ inventory_hostname }} ({{ samba_expected_ipv4 }})
|
||||
source_etc_samba=/etc/samba
|
||||
source_var_lib_samba=/var/lib/samba
|
||||
source_etc_default_samba={{ '/etc/default/samba' if samba_backup_default_samba.stat.exists else 'absent' }}
|
||||
passdb_file={{ samba_smb_passdb_file }}
|
||||
smb_users={{ samba_backup_pdbedit.stdout_lines | join(',') }}
|
||||
verify_checksums=sha256sum -c /var/backups/ghostnet/samba/<this-directory>/SHA256SUMS
|
||||
restore_hint=systemctl stop smbd nmbd && cp -a <this-directory>/etc-samba/. /etc/samba/ && cp -a <this-directory>/var-lib-samba/. /var/lib/samba/ && systemctl start smbd nmbd
|
||||
dest: "{{ samba_backup_directory }}/MANIFEST"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
rescue:
|
||||
- name: Report the failed backup
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
The pre-migration backup failed; the always section restarts the
|
||||
Samba services. Investigate before retrying.
|
||||
always:
|
||||
- name: Ensure the Samba services are started
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
loop:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Report the created backup
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
backup_directory: "{{ samba_backup_directory }}"
|
||||
created_utc: "{{ samba_backup_timestamp.stdout }}"
|
||||
smb_users: "{{ samba_backup_pdbedit.stdout_lines }}"
|
||||
next_step: >-
|
||||
site.yml --check --diff, then site.yml, then verify-production.yml
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: Adopt the CT 101 access bootstrap declaratively
|
||||
hosts: samba_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
gather_subset:
|
||||
- min
|
||||
- network
|
||||
- virtual
|
||||
|
||||
roles:
|
||||
- role: samba_access
|
||||
@@ -0,0 +1,400 @@
|
||||
---
|
||||
- name: Read-only preflight of the adopted CT 101 Samba LXC
|
||||
hosts: samba_hosts
|
||||
become: false
|
||||
gather_facts: true
|
||||
gather_subset:
|
||||
- min
|
||||
- hardware
|
||||
- network
|
||||
- virtual
|
||||
|
||||
tasks:
|
||||
- name: Verify required inventory safeguards are defined
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_ctid is defined
|
||||
- samba_expected_ipv4 is defined
|
||||
- samba_expected_ssh_host_key_fingerprint is defined
|
||||
- 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
|
||||
- samba_expected_processor_vcpus is defined
|
||||
- samba_minimum_memory_mb is defined
|
||||
fail_msg: >-
|
||||
The selected inventory lacks mandatory CT 101 identity safeguards.
|
||||
Stop before running any configuration playbook.
|
||||
|
||||
- name: Verify the expected post-upgrade guest identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- inventory_hostname == "samba-ct101"
|
||||
- samba_ctid | int == 101
|
||||
- ansible_facts["distribution"] == samba_expected_distribution
|
||||
- >-
|
||||
ansible_facts["distribution_major_version"] ==
|
||||
samba_expected_distribution_major_version
|
||||
- ansible_facts["default_ipv4"]["address"] == samba_expected_ipv4
|
||||
- >-
|
||||
ansible_facts["virtualization_type"] ==
|
||||
samba_expected_virtualization_type
|
||||
- ansible_facts["virtualization_role"] == "guest"
|
||||
- ansible_facts["service_mgr"] == "systemd"
|
||||
- ansible_facts["python"]["version"]["major"] | int >= 3
|
||||
fail_msg: >-
|
||||
The connected machine is not the expected CT 101 Samba LXC after the
|
||||
Debian 13 dist-upgrade. Run scripts/upgrade-ct101-to-debian13.sh on
|
||||
the Proxmox host first.
|
||||
success_msg: The expected Debian 13 CT 101 Samba LXC is connected.
|
||||
|
||||
- name: Verify the hostname when the placeholder is already replaced
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts["hostname"] == samba_expected_hostname
|
||||
when: samba_expected_hostname is not match('^REPLACE-')
|
||||
|
||||
- name: Read the guest user-namespace ID mapping
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cat
|
||||
- /proc/self/uid_map
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_uid_map
|
||||
|
||||
- name: Require the unprivileged LXC identity mapping
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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.
|
||||
|
||||
- name: Read the unprivileged SSH identity
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -un
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_ssh_identity
|
||||
|
||||
- name: Verify the agent transport account
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_preflight_ssh_identity.stdout == "ghost-agent"
|
||||
fail_msg: The agent inventory must connect through ghost-agent.
|
||||
|
||||
- name: Read the agent UID for the inventory placeholder
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -u
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_agent_uid
|
||||
|
||||
- name: Read the ED25519 host key fingerprint
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/ssh-keygen
|
||||
- -lf
|
||||
- /etc/ssh/ssh_host_ed25519_key.pub
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_host_key
|
||||
|
||||
- name: Verify the trusted SSH host key fingerprint
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_preflight_host_key.stdout is
|
||||
search(samba_expected_ssh_host_key_fingerprint | regex_escape)
|
||||
fail_msg: >-
|
||||
The ED25519 host key does not match the trusted fingerprint. Verify
|
||||
the fingerprint on the PVE console (GNAI-011 section 3.3) before
|
||||
trusting this host.
|
||||
|
||||
- name: Read failed systemd units
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- --failed
|
||||
- --no-legend
|
||||
- --plain
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_failed_units
|
||||
|
||||
- name: Require no failed units
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_preflight_failed_units.stdout | trim == ""
|
||||
|
||||
- name: Read the productive service states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- is-active
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- ssh.service
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
register: samba_preflight_services
|
||||
|
||||
- name: Require the productive services to be active
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_preflight_services.results |
|
||||
map(attribute='stdout') | unique | list == ['active']
|
||||
fail_msg: >-
|
||||
ssh, smbd or nmbd is not active. The productive Samba file server
|
||||
must be healthy before the adoption continues.
|
||||
|
||||
- name: Read the service listeners
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/ss
|
||||
- -H
|
||||
- -tuln
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_listeners
|
||||
|
||||
- name: Require the pre-migration listener set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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.
|
||||
|
||||
- name: Read the Samba server version
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/smbd
|
||||
- --version
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_samba_version
|
||||
|
||||
- name: Read the live smb.conf checksum
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/sha256sum
|
||||
- /etc/samba/smb.conf
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_smb_conf
|
||||
|
||||
- name: Read the live anonymous Samba share listing
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/smbclient
|
||||
- -L
|
||||
- localhost
|
||||
- -N
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_shares
|
||||
|
||||
- name: Read the media account entries
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: samba_preflight_media_accounts
|
||||
|
||||
- name: Read the media account UIDs for the inventory placeholders
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -u
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: samba_preflight_media_uids
|
||||
|
||||
- name: Read the media group entry for the inventory placeholder
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ samba_media_group_name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_media_group
|
||||
|
||||
- name: Read the complete account database for UID slot picking
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_all_accounts
|
||||
|
||||
- name: Read the complete group database for GID slot picking
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_all_groups
|
||||
|
||||
- name: Read the root filesystem capacity
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/findmnt
|
||||
- --bytes
|
||||
- --noheadings
|
||||
- --output
|
||||
- SIZE,AVAIL
|
||||
- --target
|
||||
- /
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_root_capacity
|
||||
|
||||
- name: Read the media mount capacities
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/findmnt
|
||||
- --bytes
|
||||
- --noheadings
|
||||
- --output
|
||||
- SIZE,AVAIL
|
||||
- --target
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_mount_points }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_preflight_media_capacity
|
||||
|
||||
- name: Read the media mount table
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/findmnt
|
||||
- --noheadings
|
||||
- --output
|
||||
- SOURCE,FSTYPE,OPTIONS
|
||||
- --target
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_mount_points }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_preflight_media_mounts
|
||||
|
||||
- name: Read the effective cgroup CPU limit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cat
|
||||
- /sys/fs/cgroup/cpu.max
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_preflight_cpu_max
|
||||
|
||||
- name: Read the effective cgroup memory limit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cat
|
||||
- /sys/fs/cgroup/memory.max
|
||||
check_mode: false
|
||||
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:
|
||||
inventory_host: "{{ inventory_hostname }}"
|
||||
ctid: "{{ samba_ctid }}"
|
||||
hostname: "{{ ansible_facts['hostname'] }}"
|
||||
operating_system: >-
|
||||
{{ ansible_facts['distribution'] }}
|
||||
{{ ansible_facts['distribution_version'] }}
|
||||
default_ipv4: "{{ ansible_facts['default_ipv4']['address'] }}"
|
||||
virtualization: >-
|
||||
{{ ansible_facts['virtualization_type'] }}
|
||||
{{ ansible_facts['virtualization_role'] }}
|
||||
host_visible_processor_vcpus: "{{ ansible_facts['processor_vcpus'] }}"
|
||||
memory_mib: "{{ ansible_facts['memtotal_mb'] }}"
|
||||
cgroup_cpu_max: "{{ samba_preflight_cpu_max.stdout }}"
|
||||
cgroup_memory_max: "{{ samba_preflight_memory_max.stdout }}"
|
||||
root_size_bytes: "{{ samba_preflight_root_capacity.stdout.split()[0] }}"
|
||||
root_available_bytes: "{{ samba_preflight_root_capacity.stdout.split()[1] }}"
|
||||
media_mounts: >-
|
||||
{{ samba_preflight_media_mounts.results |
|
||||
map(attribute='stdout') | list }}
|
||||
media_capacities_bytes: >-
|
||||
{{ samba_preflight_media_capacity.results |
|
||||
map(attribute='stdout') | list }}
|
||||
samba_version: "{{ samba_preflight_samba_version.stdout }}"
|
||||
smb_conf_sha256: "{{ samba_preflight_smb_conf.stdout.split()[0] }}"
|
||||
smb_share_listing: "{{ samba_preflight_shares.stdout }}"
|
||||
media_accounts: >-
|
||||
{{ samba_preflight_media_accounts.results |
|
||||
map(attribute='stdout') | list }}
|
||||
media_user_uids: >-
|
||||
{{ samba_preflight_media_uids.results |
|
||||
map(attribute='item.name') |
|
||||
zip(samba_preflight_media_uids.results | map(attribute='stdout')) |
|
||||
list }}
|
||||
media_group: "{{ samba_preflight_media_group.stdout }}"
|
||||
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: >-
|
||||
samba_expected_hostname=hostname,
|
||||
samba_expected_processor_vcpus=host_visible_processor_vcpus,
|
||||
samba_minimum_memory_mb=memory_mib,
|
||||
samba_minimum_root_size_bytes=root_size_bytes,
|
||||
samba_minimum_root_available_bytes=root_available_bytes,
|
||||
samba_access_admin_uid=UID of ghost in media_accounts,
|
||||
samba_access_agent_uid=agent_uid,
|
||||
samba_access_deploy_uid=free UID from all_accounts,
|
||||
samba_media_users[ghost].uid=uid of ghost in media_user_uids,
|
||||
samba_media_users[daidalaw].uid=uid of daidalaw in media_user_uids,
|
||||
samba_media_users[debugofen].uid=uid of debugofen in media_user_uids,
|
||||
samba_media_group_gid=gid of gn-media in media_group
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Converge the CT 101 Samba LXC on the declared state
|
||||
hosts: samba_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
gather_subset:
|
||||
- min
|
||||
- hardware
|
||||
- network
|
||||
- virtual
|
||||
|
||||
roles:
|
||||
- role: samba_lxc_baseline
|
||||
vars:
|
||||
samba_lxc_baseline_expected_hostname: "{{ samba_expected_hostname }}"
|
||||
samba_lxc_baseline_expected_distribution: >-
|
||||
{{ samba_expected_distribution }}
|
||||
samba_lxc_baseline_expected_distribution_major_version: >-
|
||||
{{ samba_expected_distribution_major_version }}
|
||||
samba_lxc_baseline_expected_ipv4: "{{ samba_expected_ipv4 }}"
|
||||
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
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
- name: Verify the CT 101 Ansible deployment boundary
|
||||
hosts: samba_hosts
|
||||
become: false
|
||||
gather_facts: false
|
||||
|
||||
tasks:
|
||||
- name: Verify Ansible module execution without privilege escalation
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Read the SSH account identity
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -un
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_deploy_account_identity
|
||||
|
||||
- name: Verify the expected SSH account
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_deploy_account_identity.stdout == "ansible-deploy"
|
||||
fail_msg: The SSH connection did not use ansible-deploy.
|
||||
|
||||
- name: Read the effective identity through sudo
|
||||
become: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -u
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_deploy_root_identity
|
||||
|
||||
- name: Verify non-interactive root escalation
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_deploy_root_identity.stdout == "0"
|
||||
fail_msg: Non-interactive sudo did not produce effective UID 0.
|
||||
success_msg: >-
|
||||
The CT 101 deployment account can connect and escalate privileges
|
||||
non-interactively.
|
||||
@@ -0,0 +1,659 @@
|
||||
---
|
||||
- name: Read-only verification of the converged CT 101 production state
|
||||
hosts: samba_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
gather_subset:
|
||||
- min
|
||||
- hardware
|
||||
- network
|
||||
- virtual
|
||||
vars:
|
||||
# Adoption: the one-shot pre-migration backup must exist. Fresh-state
|
||||
# rebuilds have nothing to back up and pass
|
||||
# -e samba_verify_require_premigration_backup=false.
|
||||
samba_verify_require_premigration_backup: true
|
||||
|
||||
tasks:
|
||||
- name: Require the production inventory safeguards
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_ctid | int == 101
|
||||
- inventory_hostname == "samba-ct101"
|
||||
- >-
|
||||
samba_expected_ssh_host_key_fingerprint is
|
||||
match('^SHA256:[A-Za-z0-9+/]{43}$')
|
||||
|
||||
- name: Require the inventory placeholders to be replaced
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_expected_hostname is not match('^REPLACE-')
|
||||
- samba_minimum_root_size_bytes is not match('^REPLACE-')
|
||||
- samba_minimum_root_available_bytes is not match('^REPLACE-')
|
||||
- samba_expected_processor_vcpus is not match('^REPLACE-')
|
||||
- samba_minimum_memory_mb is not match('^REPLACE-')
|
||||
- samba_access_admin_uid is not match('^REPLACE-')
|
||||
- samba_access_agent_uid is not match('^REPLACE-')
|
||||
- samba_access_deploy_uid is not match('^REPLACE-')
|
||||
- >-
|
||||
(samba_media_users | map(attribute='uid') | list |
|
||||
select('number') | list | length) == samba_media_users | length
|
||||
fail_msg: >-
|
||||
The inventory still contains REPLACE-WITH placeholders. Run
|
||||
playbooks/preflight.yml on the agent inventory and copy the reported
|
||||
values into the selected inventory before verifying production.
|
||||
|
||||
- name: Verify the expected guest identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts["hostname"] == samba_expected_hostname
|
||||
- ansible_facts["distribution"] == samba_expected_distribution
|
||||
- >-
|
||||
ansible_facts["distribution_major_version"] ==
|
||||
samba_expected_distribution_major_version
|
||||
- ansible_facts["default_ipv4"]["address"] == samba_expected_ipv4
|
||||
- ansible_facts["virtualization_type"] == "lxc"
|
||||
- ansible_facts["virtualization_role"] == "guest"
|
||||
- ansible_facts["service_mgr"] == "systemd"
|
||||
fail_msg: The connected machine is not the declared CT 101.
|
||||
|
||||
- name: Read the ED25519 host key fingerprint
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/ssh-keygen
|
||||
- -lf
|
||||
- /etc/ssh/ssh_host_ed25519_key.pub
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_host_key
|
||||
|
||||
- name: Verify the trusted SSH host key fingerprint
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_host_key.stdout is
|
||||
search(samba_expected_ssh_host_key_fingerprint | regex_escape)
|
||||
|
||||
- name: Read failed systemd units
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- --failed
|
||||
- --no-legend
|
||||
- --plain
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_failed_units
|
||||
|
||||
- name: Require no failed units
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_failed_units.stdout | trim == ""
|
||||
|
||||
- name: Read the deployment account identity
|
||||
become: false
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -un
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_account
|
||||
|
||||
- name: Require the ansible-deploy transport
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_account.stdout == "ansible-deploy"
|
||||
|
||||
- name: Read account password states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/passwd
|
||||
- -S
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- ghost-agent
|
||||
- ghost
|
||||
- ansible-deploy
|
||||
register: samba_production_password_states
|
||||
|
||||
- name: Verify account password states
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_password_states.results[0].stdout.split()[1] == "L"
|
||||
- samba_production_password_states.results[1].stdout.split()[1] == "P"
|
||||
- samba_production_password_states.results[2].stdout.split()[1] == "L"
|
||||
fail_msg: >-
|
||||
ghost-agent and ansible-deploy must remain locked, while ghost must
|
||||
keep the independently assigned password used only for sudo.
|
||||
|
||||
- name: Validate the effective SSH policy
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/sshd
|
||||
- -T
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_sshd
|
||||
|
||||
- name: Verify the complete effective SSH baseline
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'permitrootlogin no' in samba_production_sshd.stdout_lines"
|
||||
- "'passwordauthentication no' in samba_production_sshd.stdout_lines"
|
||||
- "'kbdinteractiveauthentication no' in samba_production_sshd.stdout_lines"
|
||||
- "'pubkeyauthentication yes' in samba_production_sshd.stdout_lines"
|
||||
- "'authenticationmethods publickey' in samba_production_sshd.stdout_lines"
|
||||
- "'permitemptypasswords no' in samba_production_sshd.stdout_lines"
|
||||
- "'x11forwarding no' in samba_production_sshd.stdout_lines"
|
||||
- "'allowagentforwarding no' in samba_production_sshd.stdout_lines"
|
||||
- "'allowtcpforwarding no' in samba_production_sshd.stdout_lines"
|
||||
- "'allowstreamlocalforwarding no' in samba_production_sshd.stdout_lines"
|
||||
- "'gatewayports no' in samba_production_sshd.stdout_lines"
|
||||
- "'permittunnel no' in samba_production_sshd.stdout_lines"
|
||||
- "'disableforwarding yes' in samba_production_sshd.stdout_lines"
|
||||
- "'maxauthtries 3' in samba_production_sshd.stdout_lines"
|
||||
- "'logingracetime 30' in samba_production_sshd.stdout_lines"
|
||||
- "'loglevel VERBOSE' in samba_production_sshd.stdout_lines"
|
||||
fail_msg: >-
|
||||
The effective SSH policy does not match the full GNAI-011 baseline.
|
||||
|
||||
- name: Read the deployed SSH drop-in
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ samba_access_ssh_dropin }}"
|
||||
register: samba_production_sshd_dropin
|
||||
|
||||
- name: Verify the deployed SSH drop-in matches the template
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_sshd_dropin['content'] | b64decode ==
|
||||
lookup('ansible.builtin.template',
|
||||
playbook_dir ~ '/../roles/samba_access/templates/sshd.conf.j2')
|
||||
fail_msg: The SSH drop-in differs from the declared template.
|
||||
|
||||
- name: Read the active Ghostnet nftables table
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/nft
|
||||
- list
|
||||
- table
|
||||
- inet
|
||||
- ghostnet_filter
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_nft_table
|
||||
|
||||
- name: Verify the active firewall policy
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_nft_table.stdout is search('policy drop')
|
||||
- samba_production_nft_table.stdout is search('SSH management')
|
||||
- >-
|
||||
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.
|
||||
|
||||
- name: Read the deployed nftables configuration
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/nftables.conf
|
||||
register: samba_production_nft_config
|
||||
|
||||
- name: Verify the deployed nftables file matches the template
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_nft_config['content'] | b64decode ==
|
||||
lookup('ansible.builtin.template',
|
||||
playbook_dir ~ '/../roles/samba_firewall/templates/nftables.conf.j2')
|
||||
fail_msg: /etc/nftables.conf differs from the declared template.
|
||||
|
||||
- name: Read the nftables service override
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/systemd/system/nftables.service.d/10-ghostnet-preserve-netavark.conf
|
||||
register: samba_production_nft_override
|
||||
|
||||
- name: Verify the nftables service override
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_nft_override['content'] | b64decode is
|
||||
search('ExecStop=/usr/sbin/nft destroy table inet ghostnet_filter')
|
||||
fail_msg: The nftables service override is missing or incomplete.
|
||||
|
||||
- name: Read the media mount table
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/findmnt
|
||||
- --noheadings
|
||||
- --output
|
||||
- SOURCE,FSTYPE,OPTIONS
|
||||
- --target
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_mount_points }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_production_mounts
|
||||
|
||||
- name: Require every declared media mount to be mounted
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_mounts.results | map(attribute='rc') | unique | list == [0]
|
||||
fail_msg: Not all declared media mount points are mounted.
|
||||
|
||||
- name: Read the declared media accounts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
index_var: account_index
|
||||
register: samba_production_media_accounts
|
||||
|
||||
- name: Require the declared media accounts to carry their recorded UIDs
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_media_accounts.results[account_index].rc == 0
|
||||
- >-
|
||||
samba_production_media_accounts.results[account_index].stdout.split(':')[0]
|
||||
== item.name
|
||||
- >-
|
||||
samba_production_media_accounts.results[account_index].stdout.split(':')[2]
|
||||
| int == item.uid | int
|
||||
- >-
|
||||
item.name != samba_access_admin_user or
|
||||
item.uid | int == samba_access_admin_uid | int
|
||||
fail_msg: >-
|
||||
The media account {{ item.name }} is missing or does not carry the
|
||||
recorded UID {{ item.uid }}. The encoding pipeline depends on stable
|
||||
UIDs (GNAI-018 R1), and the administrator declaration must stay
|
||||
consistent with samba_access_admin_uid.
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
index_var: account_index
|
||||
|
||||
- name: Read the media group entry
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ samba_media_group_name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_media_group
|
||||
|
||||
- name: Verify the media group identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_media_group.rc == 0
|
||||
- >-
|
||||
samba_production_media_group.stdout.split(':')[0] ==
|
||||
samba_media_group_name
|
||||
- >-
|
||||
samba_production_media_group.stdout.split(':')[2] | int ==
|
||||
samba_media_group_gid | int
|
||||
fail_msg: The media group does not carry the pipeline GID 970.
|
||||
|
||||
- name: Read the declared media group memberships
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -Gn
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_media_group_members }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_production_member_groups
|
||||
|
||||
- name: Verify the declared media group memberships
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_media_group_name in item.stdout.split()
|
||||
fail_msg: >-
|
||||
{{ item.item }} is not a member of {{ samba_media_group_name }}; the
|
||||
mv pickup from [Encode] and the .dlc drop into [Encode-In] would
|
||||
fail (GNAI-018 R3).
|
||||
loop: "{{ samba_production_member_groups.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
|
||||
- name: Read the deployed Samba configuration
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ samba_smb_config_file }}"
|
||||
register: samba_production_smb_conf
|
||||
|
||||
- name: Verify the deployed Samba configuration matches the template
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_smb_conf['content'] | b64decode ==
|
||||
lookup('ansible.builtin.template',
|
||||
playbook_dir ~ '/../roles/samba_smb/templates/smb.conf.j2')
|
||||
fail_msg: The deployed smb.conf differs from the declared template.
|
||||
|
||||
- name: Validate the deployed Samba configuration
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/testparm
|
||||
- -s
|
||||
- "{{ samba_smb_config_file }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_testparm
|
||||
|
||||
- name: Require a clean testparm result
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_testparm.rc == 0
|
||||
fail_msg: testparm rejected the deployed smb.conf.
|
||||
|
||||
- name: Read the Samba user database
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/pdbedit
|
||||
- -L
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_pdbedit
|
||||
|
||||
- name: Verify the productive Samba users survived
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_media_users | map(attribute='name') | list is
|
||||
subset(samba_production_pdbedit.stdout_lines)
|
||||
fail_msg: >-
|
||||
The productive SMB users ghost, daidalaw and debugofen are missing
|
||||
from the passdb (GNAI-018 R5). On a fresh-state rebuild, set them
|
||||
with smbpasswd -a <user> after the first convergence.
|
||||
|
||||
- name: Read the anonymous Samba share listing
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/smbclient
|
||||
- -L
|
||||
- localhost
|
||||
- -N
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_shares
|
||||
|
||||
- name: Verify the visible Samba shares
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_shares.stdout is
|
||||
search('(?m)^\\s*' ~ (item | regex_escape) ~ '\\s+Disk')
|
||||
fail_msg: >-
|
||||
The share {{ item }} is not visible in the anonymous share listing.
|
||||
loop: >-
|
||||
{{ samba_smb_media_shares | map(attribute='name') | list + ['print$'] }}
|
||||
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:
|
||||
- /usr/bin/systemctl
|
||||
- is-active
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- ssh.service
|
||||
- nftables.service
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
- unattended-upgrades.service
|
||||
- "{{ samba_nfs_service_unit }}"
|
||||
register: samba_production_services
|
||||
|
||||
- name: Require every production service to be active
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_production_services.results |
|
||||
map(attribute='stdout') | unique | list == ['active']
|
||||
fail_msg: Not every CT 101 production service is active.
|
||||
|
||||
- name: Read the service listeners
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/ss
|
||||
- -H
|
||||
- -tuln
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_listeners
|
||||
|
||||
- name: Require the SSH, SMB and NFS 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.
|
||||
|
||||
- name: Read the host cgroup filesystem type
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/stat
|
||||
- -fc
|
||||
- "%T"
|
||||
- /sys/fs/cgroup
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_cgroup
|
||||
|
||||
- name: Require cgroup v2
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_cgroup.stdout == "cgroup2fs"
|
||||
fail_msg: The Podman runtime requires cgroup v2.
|
||||
|
||||
- name: Read the pre-migration backup directories
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/find
|
||||
- /var/backups/ghostnet/samba
|
||||
- -maxdepth
|
||||
- "1"
|
||||
- -type
|
||||
- d
|
||||
- -name
|
||||
- pre-migration-*
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_production_backups
|
||||
|
||||
- name: Require the one-shot pre-migration backup to exist
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_production_backups.stdout | trim | length > 0
|
||||
fail_msg: >-
|
||||
No pre-migration backup exists under /var/backups/ghostnet/samba;
|
||||
run playbooks/backup.yml before the first convergence.
|
||||
when: samba_verify_require_premigration_backup | bool
|
||||
|
||||
- name: Report the verified CT 101 production state
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
ctid: "{{ samba_ctid }}"
|
||||
hostname: "{{ ansible_facts['hostname'] }}"
|
||||
operating_system: >-
|
||||
{{ ansible_facts['distribution'] }}
|
||||
{{ ansible_facts['distribution_version'] }}
|
||||
ipv4: "{{ ansible_facts['default_ipv4']['address'] }}"
|
||||
services: >-
|
||||
{{ samba_production_services.results |
|
||||
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 }}
|
||||
@@ -0,0 +1,24 @@
|
||||
# `samba_access`
|
||||
|
||||
Declaratively adopts the CT 101 access and SSH boundary after the console
|
||||
bootstrap:
|
||||
|
||||
- `ghost` — the pre-existing personal administrator account of the legacy
|
||||
LXC. The role only validates its productive UID against
|
||||
`samba_access_admin_uid`, grants password-gated `sudo` and installs the
|
||||
restricted personal key. The account is never recreated.
|
||||
- `ghost-agent` — locked, without sudo, created with the fixed
|
||||
`samba_access_agent_uid` after the slot was verified free.
|
||||
- `ansible-deploy` — locked, source-restricted key, validated non-interactive
|
||||
`NOPASSWD` sudo, created with the fixed `samba_access_deploy_uid` after the
|
||||
slot was verified free.
|
||||
|
||||
Each account receives a separate public key bound to the declared LAN and
|
||||
administration-VPN source networks. The GNAI-011 SSH baseline is installed as
|
||||
`/etc/ssh/sshd_config.d/99-security.conf` **without** `AllowUsers` (see the
|
||||
template comment); SSH socket activation is disabled in favour of the classic
|
||||
service.
|
||||
|
||||
Private keys and the personal `ghost` sudo password are never managed by this
|
||||
role or stored in the repository. Inputs are documented in
|
||||
`meta/argument_specs.yml`.
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
samba_access_source_networks: 10.0.0.0/24,10.8.0.0/24
|
||||
samba_access_public_key_root: "{{ playbook_dir }}/../files/ssh"
|
||||
|
||||
samba_access_agent_user: ghost-agent
|
||||
samba_access_agent_uid: 1000
|
||||
samba_access_agent_public_key_file: >-
|
||||
{{ samba_access_public_key_root }}/samba-ghost-agent.pub
|
||||
|
||||
samba_access_admin_user: ghost
|
||||
samba_access_admin_uid: 1001
|
||||
samba_access_admin_public_key_file: >-
|
||||
{{ samba_access_public_key_root }}/samba-ghost.pub
|
||||
|
||||
samba_access_deploy_user: ansible-deploy
|
||||
samba_access_deploy_uid: 1002
|
||||
samba_access_deploy_public_key_file: >-
|
||||
{{ samba_access_public_key_root }}/samba-ansible-deploy.pub
|
||||
|
||||
samba_access_ssh_dropin: /etc/ssh/sshd_config.d/99-security.conf
|
||||
samba_access_sudoers_file: /etc/sudoers.d/90-ghostnet-ansible-deploy
|
||||
|
||||
# CT 101 is an adopted legacy LXC: the personal administrator account ghost
|
||||
# already exists with its productive UID and is only validated here, never
|
||||
# recreated. ghost-agent and ansible-deploy are created with fixed UIDs that
|
||||
# the operator picks from the audit output because the media accounts
|
||||
# ghost/daidalaw/debugofen already occupy the low UID range.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart the classic SSH service
|
||||
ansible.builtin.systemd_service:
|
||||
name: ssh.service
|
||||
state: restarted
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage the CT 101 Samba LXC access and SSH boundary
|
||||
options:
|
||||
samba_access_source_networks:
|
||||
type: str
|
||||
required: true
|
||||
samba_access_public_key_root:
|
||||
type: path
|
||||
required: true
|
||||
samba_access_agent_user:
|
||||
type: str
|
||||
required: true
|
||||
samba_access_agent_uid:
|
||||
type: int
|
||||
required: true
|
||||
samba_access_agent_public_key_file:
|
||||
type: path
|
||||
required: true
|
||||
samba_access_admin_user:
|
||||
type: str
|
||||
required: true
|
||||
samba_access_admin_uid:
|
||||
type: int
|
||||
required: true
|
||||
samba_access_admin_public_key_file:
|
||||
type: path
|
||||
required: true
|
||||
samba_access_deploy_user:
|
||||
type: str
|
||||
required: true
|
||||
samba_access_deploy_uid:
|
||||
type: int
|
||||
required: true
|
||||
samba_access_deploy_public_key_file:
|
||||
type: path
|
||||
required: true
|
||||
samba_access_ssh_dropin:
|
||||
type: path
|
||||
required: true
|
||||
samba_access_sudoers_file:
|
||||
type: path
|
||||
required: true
|
||||
@@ -0,0 +1,351 @@
|
||||
---
|
||||
- name: Read the existing access accounts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop:
|
||||
- "{{ samba_access_agent_user }}"
|
||||
- "{{ samba_access_admin_user }}"
|
||||
- "{{ samba_access_deploy_user }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_access_account_probes
|
||||
|
||||
- name: Probe the UID/GID slots for the automation accounts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- "{{ item.database }}"
|
||||
- "{{ item.key | string }}"
|
||||
loop:
|
||||
- database: passwd
|
||||
key: "{{ samba_access_agent_uid }}"
|
||||
- database: group
|
||||
key: "{{ samba_access_agent_uid }}"
|
||||
- database: group
|
||||
key: "{{ samba_access_agent_user }}"
|
||||
- database: passwd
|
||||
key: "{{ samba_access_deploy_uid }}"
|
||||
- database: group
|
||||
key: "{{ samba_access_deploy_uid }}"
|
||||
- database: group
|
||||
key: "{{ samba_access_deploy_user }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
register: samba_access_slot_probes
|
||||
loop_control:
|
||||
label: "{{ item.database }} {{ item.key }}"
|
||||
|
||||
- name: Determine the existing access account states
|
||||
ansible.builtin.set_fact:
|
||||
samba_access_agent_exists: >-
|
||||
{{ samba_access_account_probes.results[0].rc == 0 }}
|
||||
samba_access_admin_exists: >-
|
||||
{{ samba_access_account_probes.results[1].rc == 0 }}
|
||||
samba_access_deploy_exists: >-
|
||||
{{ samba_access_account_probes.results[2].rc == 0 }}
|
||||
|
||||
- name: Require the existing administrator account to match the declared UID
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_access_admin_exists | bool
|
||||
- >-
|
||||
samba_access_account_probes.results[1].stdout.split(':')[2] | int ==
|
||||
samba_access_admin_uid | int
|
||||
fail_msg: >-
|
||||
The existing account ghost must keep its productive UID. Copy the UID
|
||||
reported by audit-production.yml into samba_access_admin_uid; the role
|
||||
never recreates the administrator account.
|
||||
|
||||
- name: Verify the agent account state against the declared UID
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_access_account_probes.results[0].stdout.split(':')[2] | int ==
|
||||
samba_access_agent_uid | int
|
||||
fail_msg: >-
|
||||
ghost-agent exists with an unexpected UID. Align samba_access_agent_uid
|
||||
with the existing account or remove the account deliberately.
|
||||
when: samba_access_agent_exists | bool
|
||||
|
||||
- name: Verify the agent UID/GID slot is free when the account is missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_access_slot_probes.results[0].rc == 2
|
||||
- samba_access_slot_probes.results[1].rc == 2
|
||||
- samba_access_slot_probes.results[2].rc == 2
|
||||
fail_msg: >-
|
||||
The UID/GID slot for ghost-agent is occupied by another identity. Choose
|
||||
a free UID from the audit output and update samba_access_agent_uid.
|
||||
when: not samba_access_agent_exists | bool
|
||||
|
||||
- name: Verify the deployment account state against the declared UID
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_access_account_probes.results[2].stdout.split(':')[2] | int ==
|
||||
samba_access_deploy_uid | int
|
||||
fail_msg: >-
|
||||
ansible-deploy exists with an unexpected UID. Align
|
||||
samba_access_deploy_uid with the existing account or remove the account
|
||||
deliberately.
|
||||
when: samba_access_deploy_exists | bool
|
||||
|
||||
- name: Verify the deployment UID/GID slot is free when the account is missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_access_slot_probes.results[3].rc == 2
|
||||
- samba_access_slot_probes.results[4].rc == 2
|
||||
- samba_access_slot_probes.results[5].rc == 2
|
||||
fail_msg: >-
|
||||
The UID/GID slot for ansible-deploy is occupied by another identity.
|
||||
Choose a free UID from the audit output and update
|
||||
samba_access_deploy_uid.
|
||||
when: not samba_access_deploy_exists | bool
|
||||
|
||||
- name: Create the fixed primary group for the read-only agent
|
||||
ansible.builtin.group:
|
||||
name: "{{ samba_access_agent_user }}"
|
||||
gid: "{{ samba_access_agent_uid }}"
|
||||
state: present
|
||||
when: not samba_access_agent_exists | bool
|
||||
|
||||
- name: Create the fixed primary group for the deployment account
|
||||
ansible.builtin.group:
|
||||
name: "{{ samba_access_deploy_user }}"
|
||||
gid: "{{ samba_access_deploy_uid }}"
|
||||
state: present
|
||||
when: not samba_access_deploy_exists | bool
|
||||
|
||||
- name: Create the fixed automation accounts
|
||||
ansible.builtin.user:
|
||||
name: "{{ item.name }}"
|
||||
uid: "{{ item.uid }}"
|
||||
group: "{{ item.name }}"
|
||||
home: "/home/{{ item.name }}"
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
state: present
|
||||
loop:
|
||||
- name: "{{ samba_access_agent_user }}"
|
||||
uid: "{{ samba_access_agent_uid }}"
|
||||
missing: "{{ not samba_access_agent_exists | bool }}"
|
||||
- name: "{{ samba_access_deploy_user }}"
|
||||
uid: "{{ samba_access_deploy_uid }}"
|
||||
missing: "{{ not samba_access_deploy_exists | bool }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: item.missing | bool
|
||||
|
||||
- name: Keep the read-only agent outside supplementary groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ samba_access_agent_user }}"
|
||||
groups: ""
|
||||
append: false
|
||||
password_lock: true
|
||||
|
||||
- name: Keep the deployment account outside supplementary groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ samba_access_deploy_user }}"
|
||||
groups: ""
|
||||
append: false
|
||||
password_lock: true
|
||||
|
||||
- name: Grant password-gated sudo membership to the administrator
|
||||
ansible.builtin.user:
|
||||
name: "{{ samba_access_admin_user }}"
|
||||
groups:
|
||||
- sudo
|
||||
append: true
|
||||
|
||||
- name: Create protected SSH directories
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ item }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ item }}"
|
||||
group: "{{ item }}"
|
||||
mode: "0700"
|
||||
loop:
|
||||
- "{{ samba_access_agent_user }}"
|
||||
- "{{ samba_access_admin_user }}"
|
||||
- "{{ samba_access_deploy_user }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Install the restricted read-only agent key
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
restrict,from="{{ samba_access_source_networks }}" {{ lookup('ansible.builtin.file', samba_access_agent_public_key_file) }}
|
||||
dest: "/home/{{ samba_access_agent_user }}/.ssh/authorized_keys"
|
||||
owner: "{{ samba_access_agent_user }}"
|
||||
group: "{{ samba_access_agent_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Install the restricted personal administrator key
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
from="{{ samba_access_source_networks }}",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-user-rc {{ lookup('ansible.builtin.file', samba_access_admin_public_key_file) }}
|
||||
dest: "/home/{{ samba_access_admin_user }}/.ssh/authorized_keys"
|
||||
owner: "{{ samba_access_admin_user }}"
|
||||
group: "{{ samba_access_admin_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Install the restricted deployment key
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
restrict,from="{{ samba_access_source_networks }}" {{ lookup('ansible.builtin.file', samba_access_deploy_public_key_file) }}
|
||||
dest: "/home/{{ samba_access_deploy_user }}/.ssh/authorized_keys"
|
||||
owner: "{{ samba_access_deploy_user }}"
|
||||
group: "{{ samba_access_deploy_user }}"
|
||||
mode: "0600"
|
||||
|
||||
- name: Install validated deployment sudo policy
|
||||
ansible.builtin.copy:
|
||||
content: |
|
||||
{{ samba_access_deploy_user }} ALL=(root) NOPASSWD: ALL
|
||||
dest: "{{ samba_access_sudoers_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0440"
|
||||
validate: /usr/sbin/visudo -cf %s
|
||||
|
||||
- name: Ensure root has no SSH authorization material
|
||||
ansible.builtin.file:
|
||||
path: /root/.ssh
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
|
||||
- name: Keep the root authorized keys file empty
|
||||
ansible.builtin.copy:
|
||||
content: ""
|
||||
dest: /root/.ssh/authorized_keys
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
|
||||
- name: Install the hardened SSH policy
|
||||
ansible.builtin.template:
|
||||
src: sshd.conf.j2
|
||||
dest: "{{ samba_access_ssh_dropin }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
validate: /usr/sbin/sshd -t -f %s
|
||||
notify: Restart the classic SSH service
|
||||
|
||||
- name: Disable SSH socket activation
|
||||
ansible.builtin.systemd_service:
|
||||
name: ssh.socket
|
||||
enabled: false
|
||||
state: stopped
|
||||
|
||||
- name: Enable the classic SSH service
|
||||
ansible.builtin.systemd_service:
|
||||
name: ssh.service
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
- name: Read account password states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/passwd
|
||||
- -S
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- "{{ samba_access_agent_user }}"
|
||||
- "{{ samba_access_admin_user }}"
|
||||
- "{{ samba_access_deploy_user }}"
|
||||
register: samba_access_password_states
|
||||
|
||||
- name: Verify account password states
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_access_password_states.results[0].stdout.split()[1] == "L"
|
||||
- samba_access_password_states.results[1].stdout.split()[1] == "P"
|
||||
- samba_access_password_states.results[2].stdout.split()[1] == "L"
|
||||
fail_msg: >-
|
||||
ghost-agent and ansible-deploy must remain locked, while ghost must keep
|
||||
the independently assigned password used only for sudo.
|
||||
|
||||
- name: Read effective supplementary groups
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -Gn
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop:
|
||||
- "{{ samba_access_agent_user }}"
|
||||
- "{{ samba_access_deploy_user }}"
|
||||
register: samba_access_restricted_groups
|
||||
|
||||
- name: Verify restricted accounts have no supplementary groups
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_access_restricted_groups.results[0].stdout ==
|
||||
samba_access_agent_user
|
||||
- >-
|
||||
samba_access_restricted_groups.results[1].stdout ==
|
||||
samba_access_deploy_user
|
||||
|
||||
- name: Verify non-interactive deployment sudo
|
||||
become: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -u
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_access_deploy_root_identity
|
||||
|
||||
- name: Require effective root through the deployment account
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_access_deploy_root_identity.stdout == "0"
|
||||
|
||||
- name: Flush SSH policy handlers before final validation
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Validate the effective SSH policy
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/sshd
|
||||
- -T
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_access_effective_sshd
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Verify the complete effective SSH baseline
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'permitrootlogin no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'passwordauthentication no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'kbdinteractiveauthentication no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'pubkeyauthentication yes' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'authenticationmethods publickey' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'permitemptypasswords no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'x11forwarding no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'allowagentforwarding no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'allowtcpforwarding no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'allowstreamlocalforwarding no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'gatewayports no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'permittunnel no' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'disableforwarding yes' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'maxauthtries 3' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'logingracetime 30' in samba_access_effective_sshd.stdout_lines"
|
||||
- "'loglevel VERBOSE' in samba_access_effective_sshd.stdout_lines"
|
||||
fail_msg: The effective SSH policy differs from the GNAI-011 baseline.
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,24 @@
|
||||
# Managed by Ansible role samba_access.
|
||||
# GNAI-011 baseline for CT 101. AllowUsers is deliberately not set: CT 101 is
|
||||
# an adopted legacy LXC and the media accounts daidalaw/debugofen may hold
|
||||
# existing SSH access; a blanket AllowUsers could lock out operational or
|
||||
# recovery accounts (GNAI-011 section 4). Tightening is an explicit
|
||||
# operator decision after the audit review.
|
||||
PermitRootLogin no
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PermitEmptyPasswords no
|
||||
AuthenticationMethods publickey
|
||||
|
||||
X11Forwarding no
|
||||
AllowAgentForwarding no
|
||||
AllowTcpForwarding no
|
||||
AllowStreamLocalForwarding no
|
||||
GatewayPorts no
|
||||
PermitTunnel no
|
||||
DisableForwarding yes
|
||||
|
||||
MaxAuthTries 3
|
||||
LoginGraceTime 30
|
||||
LogLevel VERBOSE
|
||||
@@ -0,0 +1,27 @@
|
||||
# `samba_firewall`
|
||||
|
||||
Declaratively manages the CT 101 nftables boundary (GNAI-018 R7):
|
||||
|
||||
- Input policy `drop`; loopback, established/related and ICMP diagnostics are
|
||||
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.
|
||||
- Forward policy `drop` without allow rules; output policy `accept`.
|
||||
|
||||
The policy lives in the Ghostnet-owned table `inet ghostnet_filter`. The table
|
||||
is replaced atomically (`destroy table` + re-add) and a systemd drop-in keeps
|
||||
`nftables.service` from destroying Netavark's dynamic tables on stop, matching
|
||||
the CT 102 Keycloak pattern. The rendered ruleset is validated with
|
||||
`nft --check` before it is installed, and the active table is verified after
|
||||
the service restart.
|
||||
|
||||
Before the policy is installed, the role verifies that the Ansible SSH
|
||||
connection originates from the management allowlist, so a converged firewall
|
||||
can never lock out the controlling session.
|
||||
|
||||
SMB is deliberately not opened towards the administration VPN
|
||||
(`10.8.0.0/24`); media clients live in the LAN. Widening the SMB allowlist is
|
||||
an explicit operator decision (see GNAI-019).
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
samba_firewall_management_ipv4:
|
||||
- 10.0.0.0/24
|
||||
samba_firewall_ssh_port: 22
|
||||
samba_firewall_smb_tcp_ports:
|
||||
- 139
|
||||
- 445
|
||||
samba_firewall_smb_udp_ports:
|
||||
- 137
|
||||
- 138
|
||||
samba_firewall_nfs_client_ipv4: 10.0.0.90
|
||||
samba_firewall_nfs_ports:
|
||||
- 2049
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Reload systemd for the Samba firewall
|
||||
ansible.builtin.systemd_service:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart the Samba firewall
|
||||
ansible.builtin.systemd_service:
|
||||
name: nftables.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage the CT 101 Samba nftables firewall
|
||||
options:
|
||||
samba_firewall_management_ipv4:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
samba_firewall_ssh_port:
|
||||
type: int
|
||||
required: true
|
||||
samba_firewall_smb_tcp_ports:
|
||||
type: list
|
||||
elements: int
|
||||
required: true
|
||||
samba_firewall_smb_udp_ports:
|
||||
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
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
- name: Require explicit non-empty firewall allowlists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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
|
||||
become: false
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/printenv
|
||||
- SSH_CONNECTION
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_firewall_ssh_connection
|
||||
|
||||
- name: Require the active Ansible SSH source in the management allowlist
|
||||
ansible.builtin.command:
|
||||
argv: >-
|
||||
{{
|
||||
[
|
||||
'/usr/bin/python3',
|
||||
'-c',
|
||||
'import ipaddress,sys; address=ipaddress.ip_address(sys.argv[1]); raise SystemExit(0 if any(address in ipaddress.ip_network(network) for network in sys.argv[2:]) else 1)',
|
||||
samba_firewall_ssh_connection.stdout.split()[0]
|
||||
] + samba_firewall_management_ipv4
|
||||
}}
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
vars:
|
||||
ansible_command_timeout: 5
|
||||
|
||||
- name: Create the nftables service override directory
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/system/nftables.service.d
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Install the scoped nftables stop override
|
||||
ansible.builtin.template:
|
||||
src: nftables.service-override.conf.j2
|
||||
dest: /etc/systemd/system/nftables.service.d/10-ghostnet-preserve-netavark.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
notify:
|
||||
- Reload systemd for the Samba firewall
|
||||
- Restart the Samba firewall
|
||||
|
||||
- name: Install and validate the Samba nftables policy
|
||||
ansible.builtin.template:
|
||||
src: nftables.conf.j2
|
||||
dest: /etc/nftables.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
validate: /usr/sbin/nft --check --file %s
|
||||
notify: Restart the Samba firewall
|
||||
|
||||
- name: Flush firewall handlers before verification
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Read the active Ghostnet nftables table
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/nft
|
||||
- list
|
||||
- table
|
||||
- inet
|
||||
- ghostnet_filter
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_firewall_active_table
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Verify the active firewall policy and service
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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).
|
||||
when: not ansible_check_mode
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/sbin/nft -f
|
||||
|
||||
# Managed by Ansible role samba_firewall.
|
||||
# Replace only the Ghostnet-owned table. Netavark manages separate tables.
|
||||
destroy table inet ghostnet_filter
|
||||
|
||||
table inet ghostnet_filter {
|
||||
set management_ipv4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
elements = { {{ samba_firewall_management_ipv4 | join(', ') }} }
|
||||
}
|
||||
|
||||
chain input {
|
||||
type filter hook input priority 10; policy drop;
|
||||
|
||||
iifname "lo" counter accept comment "local loopback"
|
||||
ct state established,related counter accept
|
||||
ct state invalid counter drop
|
||||
|
||||
meta l4proto icmp counter accept comment "IPv4 diagnostics"
|
||||
meta l4proto ipv6-icmp counter accept comment "IPv6 neighbor discovery"
|
||||
|
||||
ip saddr @management_ipv4 tcp dport {{ samba_firewall_ssh_port }} ct state new counter accept comment "SSH management"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority 10; policy drop;
|
||||
|
||||
ct state established,related counter accept
|
||||
ct state invalid counter drop
|
||||
|
||||
counter drop comment "default forward deny"
|
||||
}
|
||||
|
||||
chain output {
|
||||
type filter hook output priority 10; policy accept;
|
||||
}
|
||||
}
|
||||
|
||||
{{ '' }}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[Service]
|
||||
# Preserve Netavark's dynamic tables when nftables.service is stopped.
|
||||
ExecStop=
|
||||
ExecStop=/usr/sbin/nft destroy table inet ghostnet_filter
|
||||
{{ '\n' }}
|
||||
@@ -0,0 +1,15 @@
|
||||
# `samba_lxc_baseline`
|
||||
|
||||
Declaratively manages the CT 101 Samba LXC baseline: guest identity
|
||||
(hostname, Debian 13, `10.0.0.9`, LXC guest, systemd), the unprivileged
|
||||
ID-mapping premise, baseline administration packages, the `Europe/Berlin`
|
||||
timezone and the protected Ghostnet configuration/data/backup directories.
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
samba_lxc_baseline_expected_hostname: samba
|
||||
samba_lxc_baseline_expected_distribution: Debian
|
||||
samba_lxc_baseline_expected_distribution_major_version: "13"
|
||||
samba_lxc_baseline_expected_ipv4: 10.0.0.9
|
||||
samba_lxc_baseline_expected_virtualization_type: lxc
|
||||
samba_lxc_baseline_timezone: Europe/Berlin
|
||||
|
||||
samba_lxc_baseline_packages:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- jq
|
||||
- nftables
|
||||
- openssl
|
||||
- unattended-upgrades
|
||||
|
||||
samba_lxc_baseline_directories:
|
||||
- path: /etc/ghostnet
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
- path: /etc/ghostnet/samba
|
||||
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
|
||||
mode: "0700"
|
||||
- path: /var/backups/ghostnet/samba
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0700"
|
||||
@@ -0,0 +1 @@
|
||||
---
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage the CT 101 Samba LXC baseline
|
||||
options:
|
||||
samba_lxc_baseline_expected_hostname:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_expected_distribution:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_expected_distribution_major_version:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_expected_ipv4:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_expected_virtualization_type:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_timezone:
|
||||
type: str
|
||||
required: true
|
||||
samba_lxc_baseline_packages:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
samba_lxc_baseline_directories:
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
- name: Require the inventory placeholders to be replaced before converging
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_expected_hostname is not match('^REPLACE-')
|
||||
- samba_minimum_root_size_bytes is not match('^REPLACE-')
|
||||
- samba_minimum_root_available_bytes is not match('^REPLACE-')
|
||||
- samba_expected_processor_vcpus is not match('^REPLACE-')
|
||||
- samba_minimum_memory_mb is not match('^REPLACE-')
|
||||
- samba_access_admin_uid is not match('^REPLACE-')
|
||||
- samba_access_agent_uid is not match('^REPLACE-')
|
||||
- samba_access_deploy_uid is not match('^REPLACE-')
|
||||
fail_msg: >-
|
||||
The inventory still contains REPLACE-WITH placeholders. Run
|
||||
playbooks/preflight.yml on the agent inventory and copy the reported
|
||||
values into the selected inventory before converging.
|
||||
|
||||
- name: Verify the CT 101 Samba LXC identity before changing baseline state
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts["hostname"] == samba_lxc_baseline_expected_hostname
|
||||
- >-
|
||||
ansible_facts["distribution"] ==
|
||||
samba_lxc_baseline_expected_distribution
|
||||
- >-
|
||||
ansible_facts["distribution_major_version"] ==
|
||||
samba_lxc_baseline_expected_distribution_major_version
|
||||
- >-
|
||||
ansible_facts["default_ipv4"]["address"] ==
|
||||
samba_lxc_baseline_expected_ipv4
|
||||
- >-
|
||||
ansible_facts["virtualization_type"] ==
|
||||
samba_lxc_baseline_expected_virtualization_type
|
||||
- ansible_facts["virtualization_role"] == "guest"
|
||||
- ansible_facts["service_mgr"] == "systemd"
|
||||
fail_msg: >-
|
||||
The connected system is not the declared CT 101 Samba LXC. No baseline
|
||||
changes may be applied.
|
||||
|
||||
- name: Read the guest user-namespace ID mapping
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/cat
|
||||
- /proc/self/uid_map
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_lxc_baseline_uid_map
|
||||
|
||||
- name: Require the unprivileged LXC identity mapping
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- 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.
|
||||
|
||||
- name: Install baseline administration packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ samba_lxc_baseline_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
install_recommends: false
|
||||
|
||||
- name: Set the local timezone database link
|
||||
ansible.builtin.file:
|
||||
src: "/usr/share/zoneinfo/{{ samba_lxc_baseline_timezone }}"
|
||||
dest: /etc/localtime
|
||||
state: link
|
||||
owner: root
|
||||
group: root
|
||||
force: true
|
||||
|
||||
- name: Record the configured timezone
|
||||
ansible.builtin.copy:
|
||||
content: "{{ samba_lxc_baseline_timezone }}\n"
|
||||
dest: /etc/timezone
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Create protected Samba configuration and data paths
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: directory
|
||||
owner: "{{ item.owner }}"
|
||||
group: "{{ item.group }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop: "{{ samba_lxc_baseline_directories }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
@@ -0,0 +1,28 @@
|
||||
# `samba_media`
|
||||
|
||||
Declaratively manages the CT 101 media prerequisites for the Samba shares and
|
||||
the NFS export (GNAI-018 R1/R3):
|
||||
|
||||
- Verifies that all declared media mount points (`/media/daten1`,
|
||||
`/media/daten2`, `/media/daten3`, `/media/datenhdd2`, `/media/datenhdd3`)
|
||||
are actually mounted. Mount facts are gathered on demand when the playbook
|
||||
fact subset omits them.
|
||||
- Validates the productive media accounts `ghost`, `daidalaw` and `debugofen`
|
||||
against their recorded UIDs (`samba_media_users`; the `REPLACE-WITH-*` UID
|
||||
placeholders fail closed in the argument spec before any task runs).
|
||||
Existing accounts are only validated, never modified. On a fresh-state
|
||||
rebuild, missing accounts are created with exactly the declared UIDs
|
||||
(primary group `gid=uid`, `password_lock: true`; shell/home/create_home come
|
||||
from the inventory), so the playbooks rebuild CT 101 from a fresh state to
|
||||
exactly this state (GNAI-019 decisions 4 and 9).
|
||||
- Creates the group `gn-media` with the pipeline GID **970** — the numeric
|
||||
identity must match `ghostnet-ki` (GNENC-002). The role fails closed when
|
||||
the GID slot or the group name is occupied by a different identity.
|
||||
- Adds `ghost` to `gn-media` (required for the `mv` pickup from `[Encode]` and
|
||||
the `.dlc` drop into `[Encode-In]`, because the pipeline directories carry
|
||||
group `gn-media` with mode 2770). `daidalaw`/`debugofen` deliberately get no
|
||||
membership (read-only world access is sufficient, GNAI-018 R3).
|
||||
|
||||
Post-state verification (converged group entry, effective memberships) runs
|
||||
on real executions and on check-mode reruns of an already converged host;
|
||||
on a first check-mode run the mutations are only previewed.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
samba_media_group_name: gn-media
|
||||
samba_media_group_gid: 970
|
||||
# Media accounts with their productive UIDs (source: preflight.yml /
|
||||
# audit-production.yml). shell/home/create_home are only used when creating
|
||||
# missing accounts (fresh-state rebuild); existing accounts are only validated
|
||||
# on the UID and never modified. The UID placeholders stop the role
|
||||
# validation fail-closed until the live values are entered.
|
||||
samba_media_users:
|
||||
- name: ghost
|
||||
uid: REPLACE-WITH-AUDIT-UID-GHOST
|
||||
shell: /bin/bash
|
||||
home: /home/ghost
|
||||
create_home: true
|
||||
- name: daidalaw
|
||||
uid: REPLACE-WITH-AUDIT-UID-DAIDALAW
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/daidalaw
|
||||
create_home: false
|
||||
- name: debugofen
|
||||
uid: REPLACE-WITH-AUDIT-UID-DEBUGOFEN
|
||||
shell: /usr/sbin/nologin
|
||||
home: /home/debugofen
|
||||
create_home: false
|
||||
samba_media_group_members:
|
||||
- ghost
|
||||
samba_media_mount_points:
|
||||
- /media/daten1
|
||||
- /media/daten2
|
||||
- /media/daten3
|
||||
- /media/datenhdd2
|
||||
- /media/datenhdd3
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage the CT 101 media accounts, group, members and mount prerequisites
|
||||
options:
|
||||
samba_media_group_name:
|
||||
type: str
|
||||
required: true
|
||||
samba_media_group_gid:
|
||||
type: int
|
||||
required: true
|
||||
samba_media_users:
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
options:
|
||||
name:
|
||||
type: str
|
||||
required: true
|
||||
uid:
|
||||
type: int
|
||||
required: true
|
||||
shell:
|
||||
type: str
|
||||
required: true
|
||||
home:
|
||||
type: str
|
||||
required: true
|
||||
create_home:
|
||||
type: bool
|
||||
required: true
|
||||
samba_media_group_members:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
samba_media_mount_points:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
@@ -0,0 +1,316 @@
|
||||
---
|
||||
- name: Require explicit media group, user and mount point declarations
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_media_group_name | length > 0
|
||||
- samba_media_users | length > 0
|
||||
- samba_media_group_members | length > 0
|
||||
- samba_media_mount_points | length > 0
|
||||
fail_msg: >-
|
||||
The media group, member, user and mount point declarations must be
|
||||
explicit and non-empty.
|
||||
|
||||
- name: Gather the mount facts when the playbook subset omits them
|
||||
ansible.builtin.setup:
|
||||
gather_subset:
|
||||
- "!all"
|
||||
- "!min"
|
||||
- mounts
|
||||
when: ansible_facts["mounts"] is not defined
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
|
||||
- name: Verify the declared media mount points are mounted
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
ansible_facts["mounts"] | map(attribute="mount") | list is
|
||||
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.
|
||||
|
||||
- name: Read the existing media accounts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: samba_media_account_probes
|
||||
|
||||
- name: Probe the declared media UID slots in the account database
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item.uid | string }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }} ({{ item.uid }})"
|
||||
register: samba_media_uid_slot_probes
|
||||
|
||||
- name: Probe the declared media UID slots in the group database
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ item.uid | string }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }} ({{ item.uid }})"
|
||||
register: samba_media_gid_slot_probes
|
||||
|
||||
- name: Probe the declared media group names
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: samba_media_group_name_slot_probes
|
||||
|
||||
- name: Determine the media account states
|
||||
ansible.builtin.set_fact:
|
||||
samba_media_account_states: >-
|
||||
{{ samba_media_account_states | default([]) + [{
|
||||
'name': item.name,
|
||||
'uid': item.uid | int,
|
||||
'shell': item.shell,
|
||||
'home': item.home,
|
||||
'create_home': item.create_home,
|
||||
'exists': samba_media_account_probes.results[account_index].rc == 0,
|
||||
'account_uid': (samba_media_account_probes.results[account_index].stdout.split(':')[2] | int) if samba_media_account_probes.results[account_index].rc == 0 else -1,
|
||||
'uid_slot_free': samba_media_uid_slot_probes.results[account_index].rc == 2,
|
||||
'gid_slot_free': samba_media_gid_slot_probes.results[account_index].rc == 2,
|
||||
'group_name_free': samba_media_group_name_slot_probes.results[account_index].rc == 2,
|
||||
}] }}
|
||||
loop: "{{ samba_media_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
index_var: account_index
|
||||
|
||||
- name: Verify the declared media account identities
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(item.exists and item.account_uid == item.uid) or
|
||||
(not item.exists and item.uid_slot_free and item.gid_slot_free and
|
||||
item.group_name_free)
|
||||
fail_msg: >-
|
||||
The media account {{ item.name }} does not match its declaration. An
|
||||
existing account must keep the recorded UID (copy the UID reported by
|
||||
preflight.yml or audit-production.yml into the inventory); a missing
|
||||
account needs the declared UID/GID slots to be free so that a rebuild
|
||||
can create it with the recorded identity (GNAI-018 R1).
|
||||
loop: "{{ samba_media_account_states }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Create the fixed primary groups for missing media accounts
|
||||
ansible.builtin.group:
|
||||
name: "{{ item.name }}"
|
||||
gid: "{{ item.uid }}"
|
||||
state: present
|
||||
loop: "{{ samba_media_account_states }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: not item.exists
|
||||
register: samba_media_primary_group_tasks
|
||||
|
||||
- name: Create missing media accounts with their recorded identities
|
||||
ansible.builtin.user:
|
||||
name: "{{ item.name }}"
|
||||
uid: "{{ item.uid }}"
|
||||
group: "{{ item.name }}"
|
||||
home: "{{ item.home }}"
|
||||
shell: "{{ item.shell }}"
|
||||
create_home: "{{ item.create_home }}"
|
||||
password_lock: true
|
||||
state: present
|
||||
loop: "{{ samba_media_account_states }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: not item.exists
|
||||
register: samba_media_account_tasks
|
||||
|
||||
- name: Record whether media account mutations were required
|
||||
ansible.builtin.set_fact:
|
||||
samba_media_account_creation_required: >-
|
||||
{{ (samba_media_primary_group_tasks.results | selectattr('changed') | list | length > 0) or
|
||||
(samba_media_account_tasks.results | selectattr('changed') | list | length > 0) }}
|
||||
|
||||
- name: Read the converged media accounts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- passwd
|
||||
- "{{ item.name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_account_states }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: samba_media_converged_accounts
|
||||
|
||||
- name: Verify the converged media accounts carry the recorded UIDs
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_media_converged_accounts.results[account_index].rc == 0
|
||||
- >-
|
||||
samba_media_converged_accounts.results[account_index].stdout.split(':')[2]
|
||||
| int == item.uid
|
||||
fail_msg: >-
|
||||
The media account {{ item.name }} does not carry the recorded UID
|
||||
{{ item.uid }} after the convergence.
|
||||
loop: "{{ samba_media_account_states }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
index_var: account_index
|
||||
when: >-
|
||||
not ansible_check_mode or
|
||||
not samba_media_account_creation_required | bool
|
||||
|
||||
- name: Probe the declared media group by name
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ samba_media_group_name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
register: samba_media_group_name_probe
|
||||
|
||||
- name: Probe the declared media GID slot
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ samba_media_group_gid | string }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
register: samba_media_gid_slot
|
||||
|
||||
- name: Determine the media group state
|
||||
ansible.builtin.set_fact:
|
||||
samba_media_group_exists: >-
|
||||
{{ samba_media_group_name_probe.rc == 0 }}
|
||||
samba_media_gid_occupied: >-
|
||||
{{ samba_media_gid_slot.rc == 0 }}
|
||||
samba_media_gid_slot_name: >-
|
||||
{{ samba_media_gid_slot.stdout.split(':')[0] if samba_media_gid_slot.rc == 0 else '' }}
|
||||
|
||||
- name: Require a consistent media group identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
not samba_media_group_exists | bool or
|
||||
samba_media_group_name_probe.stdout.split(':')[2] | int ==
|
||||
samba_media_group_gid | int
|
||||
- >-
|
||||
not samba_media_gid_occupied | bool or
|
||||
samba_media_gid_slot_name == samba_media_group_name
|
||||
fail_msg: >-
|
||||
The GID {{ samba_media_group_gid }} or the group name
|
||||
{{ samba_media_group_name }} is occupied by a different identity. The
|
||||
encoding pipeline requires GID {{ samba_media_group_gid }} to mean
|
||||
{{ samba_media_group_name }} on CT 101 and ghostnet-ki alike; resolve
|
||||
the collision deliberately.
|
||||
|
||||
- name: Create the media group with the pipeline GID
|
||||
ansible.builtin.group:
|
||||
name: "{{ samba_media_group_name }}"
|
||||
gid: "{{ samba_media_group_gid }}"
|
||||
state: present
|
||||
register: samba_media_group_task
|
||||
|
||||
- name: Add the declared members to the media group
|
||||
ansible.builtin.user:
|
||||
name: "{{ item }}"
|
||||
groups: "{{ samba_media_group_name }}"
|
||||
append: true
|
||||
loop: "{{ samba_media_group_members }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
when: >-
|
||||
not ansible_check_mode or not samba_media_group_task.changed | bool
|
||||
register: samba_media_membership_task
|
||||
|
||||
- name: Record whether the media group mutations were required
|
||||
ansible.builtin.set_fact:
|
||||
samba_media_mutation_required: >-
|
||||
{{ samba_media_group_task.changed | bool or
|
||||
(samba_media_membership_task.results | selectattr('changed') | list | length > 0) }}
|
||||
|
||||
- name: Read the converged media group entry
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/getent
|
||||
- group
|
||||
- "{{ samba_media_group_name }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_media_group_entry
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Verify the converged media group identity
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_media_group_entry.rc == 0
|
||||
- >-
|
||||
samba_media_group_entry.stdout.split(':')[0] ==
|
||||
samba_media_group_name
|
||||
- >-
|
||||
samba_media_group_entry.stdout.split(':')[2] | int ==
|
||||
samba_media_group_gid | int
|
||||
fail_msg: The media group does not carry the pipeline GID.
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Read the effective media group memberships
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/id
|
||||
- -Gn
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
loop: "{{ samba_media_group_members }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_media_memberships
|
||||
|
||||
- name: Verify the declared media group memberships
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_media_group_name in item.stdout.split()
|
||||
fail_msg: >-
|
||||
{{ item.item }} is not a member of {{ samba_media_group_name }}. The
|
||||
membership is required for the mv pickup from [Encode] and the .dlc
|
||||
drop into [Encode-In] (GNAI-018 R3).
|
||||
loop: "{{ samba_media_memberships.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when: >-
|
||||
not ansible_check_mode or
|
||||
(not samba_media_mutation_required | bool and
|
||||
not samba_media_account_creation_required | bool)
|
||||
@@ -0,0 +1,44 @@
|
||||
# `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.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
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-20):
|
||||
samba_nfs_containerfile_sha256: >-
|
||||
0fdbe6b5fe542f22ed291c4cabe505e0da472024f0186e2414fd002b751f784f
|
||||
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
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
- 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
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,294 @@
|
||||
---
|
||||
- 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
|
||||
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.
|
||||
|
||||
- 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
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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;
|
||||
}
|
||||
|
||||
EXPORT_DEFAULTS {
|
||||
AccessType = None;
|
||||
}
|
||||
|
||||
EXPORT {
|
||||
Export_Id = {{ samba_nfs_export_id }};
|
||||
Path = {{ samba_nfs_export_path }};
|
||||
Pseudo = {{ samba_nfs_export_path }};
|
||||
AccessType = RW;
|
||||
Squash = rootid;
|
||||
SecType = sys;
|
||||
Protocols = 4;
|
||||
Transports = TCP,UDP;
|
||||
Clients = {{ samba_nfs_client_ipv4 }};
|
||||
FSAL {
|
||||
Name = VFS;
|
||||
}
|
||||
}
|
||||
|
||||
LOG {
|
||||
Default_Log_Level = INFO;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
[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
|
||||
@@ -0,0 +1,34 @@
|
||||
# `samba_smb`
|
||||
|
||||
Declaratively manages the **native** Samba server of CT 101 (GNAI-019
|
||||
decision 1: Debian package, no container — see there for the rationale).
|
||||
|
||||
- Fail-closed guard (GNAI-018 R5, passdb strategy: preserve): with the default
|
||||
`samba_smb_preserve_passdb: true`, `/var/lib/samba/private/passdb.tdb` must
|
||||
exist before anything converges, the `pdbedit -L` user list is read before
|
||||
and after the convergence and must be identical, and the productive users
|
||||
`ghost`, `daidalaw`, `debugofen` must be present. On a fresh-state rebuild
|
||||
the first `site.yml` run passes `-e samba_smb_preserve_passdb=false` (no
|
||||
passdb exists yet); the operator then sets the SMB passwords interactively
|
||||
(`smbpasswd -a`), and the second run validates the populated passdb with the
|
||||
default (GNAI-019 decision 5).
|
||||
- Installs `samba`, `samba-common`, `samba-common-bin`, `smbclient`.
|
||||
- Installs the **fully templated** `smb.conf` (`templates/smb.conf.j2`) and
|
||||
validates the staged file with `testparm -s` before it is installed
|
||||
(`validate:` gate, GNAI-018 R6). The template contains the Debian default
|
||||
globals including `usershare allow guests = yes` (documented, kept per
|
||||
GNAI-018 R4), the `[homes]`, `[printers]` (once — the duplicate from the
|
||||
live system is cleaned up, R4) and `[print$]` sections as well as all
|
||||
14 media shares from the inventory (12 existing shares without comments,
|
||||
exactly as in the live system; `[Encode]`/`[Encode-In]` exactly per
|
||||
GNAI-018 R2).
|
||||
- `smbd` and `nmbd` are enabled and started; a configuration change restarts
|
||||
both via handlers.
|
||||
- Post-state verification: `testparm -s` on the installed file, `pdbedit -L`
|
||||
comparison, anonymous `smbclient -L localhost -N` must list every declared
|
||||
media share plus `print$`, and both services must be active. These checks
|
||||
run on real executions and on check-mode reruns of a converged host; on a
|
||||
first check-mode run the configuration change is only previewed.
|
||||
|
||||
Secrets (SMB passwords, passdb) are never read into logs, inventory or Git —
|
||||
`pdbedit -L` lists usernames only.
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
samba_smb_packages:
|
||||
- samba
|
||||
- samba-common
|
||||
- samba-common-bin
|
||||
- smbclient
|
||||
samba_smb_workgroup: Ghost.net
|
||||
samba_smb_config_file: /etc/samba/smb.conf
|
||||
samba_smb_passdb_file: /var/lib/samba/private/passdb.tdb
|
||||
# Adoption (default): the productive passdb must exist and survive the
|
||||
# convergence (GNAI-018 R5). Fresh-state rebuilds run site.yml once with
|
||||
# -e samba_smb_preserve_passdb=false; afterwards the operator sets the SMB
|
||||
# passwords via smbpasswd, and the second site.yml run validates the populated
|
||||
# passdb with the default.
|
||||
samba_smb_preserve_passdb: true
|
||||
samba_smb_service_units:
|
||||
- smbd.service
|
||||
- nmbd.service
|
||||
# Complete share target state; overridden from the inventory (group_vars). The
|
||||
# role default list is intentionally empty so that a missing inventory stops
|
||||
# fail-closed.
|
||||
samba_smb_media_shares: []
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Restart the Samba smbd service
|
||||
ansible.builtin.systemd_service:
|
||||
name: smbd.service
|
||||
state: restarted
|
||||
|
||||
- name: Restart the Samba nmbd service
|
||||
ansible.builtin.systemd_service:
|
||||
name: nmbd.service
|
||||
state: restarted
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
argument_specs:
|
||||
main:
|
||||
short_description: Manage the CT 101 native Samba server and its shares
|
||||
options:
|
||||
samba_smb_packages:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
samba_smb_workgroup:
|
||||
type: str
|
||||
required: true
|
||||
samba_smb_config_file:
|
||||
type: path
|
||||
required: true
|
||||
samba_smb_passdb_file:
|
||||
type: path
|
||||
required: true
|
||||
samba_smb_preserve_passdb:
|
||||
type: bool
|
||||
required: true
|
||||
samba_smb_service_units:
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
samba_smb_media_shares:
|
||||
type: list
|
||||
elements: dict
|
||||
required: true
|
||||
@@ -0,0 +1,166 @@
|
||||
---
|
||||
- name: Require the Samba service and share declarations
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_smb_workgroup | length > 0
|
||||
- samba_smb_config_file | length > 0
|
||||
- samba_smb_service_units | length > 0
|
||||
- samba_smb_media_shares | length > 0
|
||||
fail_msg: The Samba workgroup, service and share declarations must be explicit.
|
||||
|
||||
- name: Require the productive Samba passdb before any convergence
|
||||
ansible.builtin.stat:
|
||||
path: "{{ samba_smb_passdb_file }}"
|
||||
register: samba_smb_passdb
|
||||
|
||||
- name: Fail closed when the productive passdb is missing
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_smb_passdb.stat.exists
|
||||
fail_msg: >-
|
||||
{{ samba_smb_passdb_file }} is missing. CT 101 is an adopted productive
|
||||
file server; the passdb carries the SMB passwords of ghost, daidalaw
|
||||
and debugofen and must be preserved (GNAI-018 R5). Run
|
||||
playbooks/backup.yml first and investigate the missing state before
|
||||
converging the shares. Fresh-state rebuilds only: run site.yml with
|
||||
-e samba_smb_preserve_passdb=false and set the SMB passwords with
|
||||
smbpasswd before the verification.
|
||||
when: samba_smb_preserve_passdb | bool
|
||||
|
||||
- name: Read the Samba user database before convergence
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/pdbedit
|
||||
- -L
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_smb_passdb_before
|
||||
when: samba_smb_preserve_passdb | bool
|
||||
|
||||
- name: Install the Samba server and client packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ samba_smb_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
install_recommends: false
|
||||
|
||||
- name: Install the fully templated Samba configuration
|
||||
ansible.builtin.template:
|
||||
src: smb.conf.j2
|
||||
dest: "{{ samba_smb_config_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
validate: /usr/bin/testparm -s %s
|
||||
notify:
|
||||
- Restart the Samba smbd service
|
||||
- Restart the Samba nmbd service
|
||||
register: samba_smb_config
|
||||
|
||||
- name: Flush Samba handlers before service management
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
- name: Enable and start the Samba services
|
||||
ansible.builtin.systemd_service:
|
||||
name: "{{ item }}"
|
||||
enabled: true
|
||||
state: started
|
||||
loop: "{{ samba_smb_service_units }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Validate the installed Samba configuration
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/testparm
|
||||
- -s
|
||||
- "{{ samba_smb_config_file }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_smb_testparm
|
||||
when: >-
|
||||
not ansible_check_mode or not samba_smb_config.changed | bool
|
||||
|
||||
- name: Require a clean testparm result for the installed configuration
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_smb_testparm.rc == 0
|
||||
fail_msg: >-
|
||||
testparm rejected the installed smb.conf. The template gate passed the
|
||||
staged file, but the installed file differs; investigate before
|
||||
continuing.
|
||||
when: >-
|
||||
not ansible_check_mode or not samba_smb_config.changed | bool
|
||||
|
||||
- name: Read the Samba user database after convergence
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/sbin/pdbedit
|
||||
- -L
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_smb_passdb_after
|
||||
when: samba_smb_preserve_passdb | bool
|
||||
|
||||
- name: Verify the productive Samba users survived the convergence
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_smb_passdb_after.stdout_lines | sort ==
|
||||
samba_smb_passdb_before.stdout_lines | sort
|
||||
- >-
|
||||
samba_media_users | map(attribute='name') | list is
|
||||
subset(samba_smb_passdb_after.stdout_lines)
|
||||
fail_msg: >-
|
||||
The Samba user database changed during convergence. The productive
|
||||
users ghost, daidalaw and debugofen must be preserved (GNAI-018 R5).
|
||||
when: samba_smb_preserve_passdb | bool
|
||||
|
||||
- name: Read the anonymous Samba share listing
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/smbclient
|
||||
- -L
|
||||
- localhost
|
||||
- -N
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
register: samba_smb_share_listing
|
||||
when: >-
|
||||
not ansible_check_mode or not samba_smb_config.changed | bool
|
||||
|
||||
- name: Verify the visible Samba shares
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
samba_smb_share_listing.stdout is
|
||||
search('(?m)^\\s*' ~ (item | regex_escape) ~ '\\s+Disk')
|
||||
fail_msg: >-
|
||||
The share {{ item }} is not visible in the anonymous share listing of
|
||||
the converged smb.conf.
|
||||
loop: >-
|
||||
{{ samba_smb_media_shares | map(attribute='name') | list + ['print$'] }}
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
when: >-
|
||||
not ansible_check_mode or not samba_smb_config.changed | bool
|
||||
|
||||
- name: Read the Samba service states
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/systemctl
|
||||
- is-active
|
||||
- "{{ item }}"
|
||||
check_mode: false
|
||||
changed_when: false
|
||||
loop: "{{ samba_smb_service_units }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
register: samba_smb_service_states
|
||||
|
||||
- name: Require the Samba services to be active
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- samba_smb_service_states.results | map(attribute='stdout') | unique | list == ['active']
|
||||
fail_msg: The Samba services are not active after the convergence.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Managed by Ansible role samba_smb (CT 101, GNAI-017/GNAI-018).
|
||||
# Complete target file; the documented live state is in GNAI-017 section 3,
|
||||
# deviations and their rationale in GNAI-019. The file is validated with
|
||||
# testparm before installation.
|
||||
[global]
|
||||
workgroup = {{ samba_smb_workgroup }}
|
||||
server string = %h server (Samba, Debian)
|
||||
dns proxy = no
|
||||
log file = /var/log/samba/log.%m
|
||||
max log size = 1000
|
||||
syslog = 0
|
||||
panic action = /usr/share/samba/panic-action %d
|
||||
server role = standalone server
|
||||
obey pam restrictions = yes
|
||||
unix password sync = yes
|
||||
passwd program = /usr/bin/passwd %u
|
||||
passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
|
||||
pam password change = yes
|
||||
map to guest = bad user
|
||||
usershare allow guests = yes
|
||||
load printers = yes
|
||||
printing = bsd
|
||||
printcap name = /dev/null
|
||||
disable spoolss = yes
|
||||
|
||||
[homes]
|
||||
comment = Home Directories
|
||||
browseable = no
|
||||
read only = yes
|
||||
valid users = %S
|
||||
create mask = 0700
|
||||
directory mask = 0700
|
||||
|
||||
[printers]
|
||||
comment = All Printers
|
||||
path = /var/tmp
|
||||
printable = yes
|
||||
guest ok = no
|
||||
read only = yes
|
||||
create mask = 0700
|
||||
|
||||
[print$]
|
||||
comment = Printer Drivers
|
||||
path = /var/lib/samba/printers
|
||||
browseable = yes
|
||||
read only = yes
|
||||
guest ok = no
|
||||
|
||||
{% for share in samba_smb_media_shares %}
|
||||
[{{ share.name }}]
|
||||
{% if share.comment is defined %}
|
||||
comment = {{ share.comment }}
|
||||
{% endif %}
|
||||
path = {{ share.path }}
|
||||
read only = {{ share.read_only }}
|
||||
valid users = {{ share.valid_users | join(', ') }}
|
||||
{% if share.write_list is defined %}
|
||||
write list = {{ share.write_list | join(', ') }}
|
||||
{% endif %}
|
||||
{% if share.admin_users is defined %}
|
||||
admin users = {{ share.admin_users | join(', ') }}
|
||||
{% endif %}
|
||||
{% if share.force_user is defined %}
|
||||
force user = {{ share.force_user }}
|
||||
{% endif %}
|
||||
{% if share.force_group is defined %}
|
||||
force group = {{ share.force_group }}
|
||||
{% endif %}
|
||||
{% if share.create_mask is defined %}
|
||||
create mask = {{ share.create_mask }}
|
||||
{% endif %}
|
||||
{% if share.directory_mask is defined %}
|
||||
directory mask = {{ share.directory_mask }}
|
||||
{% endif %}
|
||||
guest ok = no
|
||||
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,136 @@
|
||||
# Proxmox automation (CT 101 Samba)
|
||||
|
||||
Two PVE-side scripts for CT 101 (`10.0.0.9`, Samba file server,
|
||||
GNAI-017/GNAI-018): the in-place dist-upgrade of the productive, adopted LXC to
|
||||
Debian 13 (adoption, step 1) and the provisioning of a fresh CT 101 for the
|
||||
documented fresh-state rebuild. Both scripts run as root on the Proxmox host,
|
||||
contain no password, no private key and no SMB secret, and never destroy or
|
||||
replace an existing CT 101.
|
||||
|
||||
## `upgrade-ct101-to-debian13.sh` (adoption, step 1)
|
||||
|
||||
Upgrades the productive CT 101 from Debian 12 (bookworm) to Debian 13
|
||||
(trixie) in place. The existing LXC is preserved; the Samba passdb and the
|
||||
productive `smb.conf` survive the upgrade (`--force-confold` keeps the
|
||||
productive `smb.conf`). Sequence:
|
||||
|
||||
1. Preflight: CT 101 is running, is unprivileged (GNAI-015), all five media
|
||||
mounts carry `backup=no` (otherwise `vzdump` would archive the media
|
||||
library), and in the guest Debian 12, `ssh`/`smbd`/`nmbd` are active and the
|
||||
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).
|
||||
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 five media
|
||||
mounts mounted.
|
||||
|
||||
Before the run, all media mounts in the productive `pct config 101` must carry
|
||||
the `backup=no` option. Installation and invocation:
|
||||
|
||||
```bash
|
||||
install -o root -g root -m 0700 \
|
||||
upgrade-ct101-to-debian13.sh \
|
||||
/usr/local/sbin/upgrade-ct101-to-debian13.sh
|
||||
|
||||
GHOSTNET_CT101_UPGRADE_CONFIRMATION=DIST-UPGRADE_CT101_TO_DEBIAN_13 \
|
||||
/usr/local/sbin/upgrade-ct101-to-debian13.sh
|
||||
```
|
||||
|
||||
Environment variables: `GHOSTNET_BACKUP_STORAGE` (default `local`),
|
||||
`GHOSTNET_BACKUP_DUMP_DIR` (default `/var/lib/vz/dump`, only needed for
|
||||
non-local backup storage). The confirmed downtime affects only the
|
||||
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
|
||||
`f7adfbde7378708620975a64c30bb3de8727b96915ad8c58100893a52c2c2d6e`.
|
||||
|
||||
## `provision-ct101-samba.sh` (fresh-state rebuild)
|
||||
|
||||
Creates a fresh CT 101 from a Debian 13 template with the recorded profile,
|
||||
attaches the five media mounts, disables the LXC seccomp filter and then runs
|
||||
the reviewed baseline hook with the recorded account UIDs. The script refuses
|
||||
to run when CT 101 already exists, and it names every material setting
|
||||
explicitly.
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| CT ID / hostname | `101` / `samba` (overridable via `GHOSTNET_CT101_HOSTNAME`) |
|
||||
| OS | Debian 13, amd64 (newest `debian-13-*amd64*` template in the local cache, otherwise `GHOSTNET_OS_TEMPLATE`) |
|
||||
| Container type | unprivileged LXC with `nesting=1,keyctl=1` |
|
||||
| CPU / RAM / swap | 2 cores / 2048 MiB / 1024 MiB (adjustable via env) |
|
||||
| Rootfs | 32 GiB on `local-lvm` (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`–`mp4` → `/media/daten1`, `/media/daten2`, `/media/daten3`, `/media/datenhdd2`, `/media/datenhdd3`, each with `backup=no` |
|
||||
| Baseline hook | `/usr/local/sbin/ghostnet-lxc-baseline.sh` (SHA-256 pinned) |
|
||||
|
||||
The resource defaults are conservative starting values; for an exact rebuild of
|
||||
the productive profile, set the values from `pct config 101` of the productive
|
||||
CT 101 via the env variables.
|
||||
|
||||
Mandatory environment variables (the recorded values from the Ansible inventory
|
||||
or the productive `pct config 101`):
|
||||
|
||||
| Variable | Meaning |
|
||||
| --- | --- |
|
||||
| `GHOSTNET_AGENT_UID` | UID of `ghost-agent` (must match `samba_access_agent_uid`) |
|
||||
| `GHOSTNET_ADMIN_UID` | UID of `ghost` (must match `samba_access_admin_uid`) |
|
||||
| `GHOSTNET_MP0_HOST` … `GHOSTNET_MP4_HOST` | Host paths of the five media mounts (must exist) |
|
||||
|
||||
Optional variables: `GHOSTNET_CT101_HOSTNAME`, `GHOSTNET_CT101_CORES`,
|
||||
`GHOSTNET_CT101_MEMORY_MB`, `GHOSTNET_CT101_ROOTFS_GB`, `GHOSTNET_CT101_SWAP_MB`,
|
||||
`GHOSTNET_OS_TEMPLATE`, `GHOSTNET_BASELINE_HOOK`.
|
||||
|
||||
Installation and invocation:
|
||||
|
||||
```bash
|
||||
install -o root -g root -m 0700 \
|
||||
provision-ct101-samba.sh \
|
||||
/usr/local/sbin/provision-ct101-samba.sh
|
||||
|
||||
GHOSTNET_AGENT_UID=<uid> GHOSTNET_ADMIN_UID=<uid> \
|
||||
GHOSTNET_MP0_HOST=/path/daten1 GHOSTNET_MP1_HOST=/path/daten2 \
|
||||
GHOSTNET_MP2_HOST=/path/daten3 GHOSTNET_MP3_HOST=/path/datenhdd2 \
|
||||
GHOSTNET_MP4_HOST=/path/datenhdd3 \
|
||||
/usr/local/sbin/provision-ct101-samba.sh
|
||||
```
|
||||
|
||||
The script sets neither passwords nor keys and configures neither Samba nor
|
||||
the firewall — the Ansible roles do that. The final output lists the operator
|
||||
steps (set the ghost password, install the public keys from
|
||||
`ansible/files/ssh/`, configure the workstation SSH, fill the inventory
|
||||
placeholders, rebuild sequence from `ansible/README.md`).
|
||||
|
||||
The reviewed repository copy has SHA-256
|
||||
`ac5d613fb3c3526135c96f02efff9fd7d8dc01952e3d4701b9b5a4a511fff1cd`.
|
||||
|
||||
## `ghostnet-lxc-baseline.sh`
|
||||
|
||||
The versioned source lives under
|
||||
`../../../shared/scripts/ghostnet-lxc-baseline.sh`. Both CT 101 scripts call
|
||||
this reviewed baseline (adoption: manually after the upgrade; fresh rebuild:
|
||||
automatically via `provision-ct101-samba.sh`). Installation on the Proxmox
|
||||
host:
|
||||
|
||||
```bash
|
||||
install -o root -g root -m 0750 \
|
||||
ghostnet-lxc-baseline.sh \
|
||||
/usr/local/sbin/ghostnet-lxc-baseline.sh
|
||||
```
|
||||
|
||||
The pinned SHA-256 of the repository copy is
|
||||
`111f1b70e47e3a2f6a1bcce22cb0ec50aacd8fbcff37d87fbcf853a318381b10`;
|
||||
the scripts verify the host copy before executing it. The baseline creates
|
||||
`ghost-agent` and `ghost` with the passed UIDs (or validates existing
|
||||
accounts), prepares the `.ssh` directories, adds `ghost` to `sudo` and ensures
|
||||
classic `ssh.service` operation. It installs no keys and sets no passwords.
|
||||
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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 five 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.
|
||||
#
|
||||
# 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
|
||||
# operator with smbpasswd after the first Ansible convergence.
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
readonly CTID='101'
|
||||
readonly HOSTNAME="${GHOSTNET_CT101_HOSTNAME:-samba}"
|
||||
readonly IPV4_CIDR='10.0.0.9/24'
|
||||
readonly GATEWAY='10.0.0.1'
|
||||
readonly NAMESERVER='10.0.0.14'
|
||||
readonly BRIDGE='vmbr0'
|
||||
readonly TEMPLATE_STORAGE='local'
|
||||
readonly CONTAINER_STORAGE='local-lvm'
|
||||
readonly BASELINE_HOOK="${GHOSTNET_BASELINE_HOOK:-/usr/local/sbin/ghostnet-lxc-baseline.sh}"
|
||||
# Reviewed repository copy of proxmox/shared/scripts/ghostnet-lxc-baseline.sh:
|
||||
readonly BASELINE_HOOK_SHA256='111f1b70e47e3a2f6a1bcce22cb0ec50aacd8fbcff37d87fbcf853a318381b10'
|
||||
readonly CORES="${GHOSTNET_CT101_CORES:-2}"
|
||||
readonly MEMORY_MB="${GHOSTNET_CT101_MEMORY_MB:-2048}"
|
||||
readonly ROOTFS_GB="${GHOSTNET_CT101_ROOTFS_GB:-32}"
|
||||
readonly SWAP_MB="${GHOSTNET_CT101_SWAP_MB:-1024}"
|
||||
# Empty: auto-detect the newest debian-13 amd64 template in the local cache.
|
||||
readonly OS_TEMPLATE="${GHOSTNET_OS_TEMPLATE:-}"
|
||||
# The recorded account UIDs must match samba_access_agent_uid and
|
||||
# samba_access_admin_uid in the Ansible inventory.
|
||||
readonly AGENT_UID="${GHOSTNET_AGENT_UID:-}"
|
||||
readonly ADMIN_UID="${GHOSTNET_ADMIN_UID:-}"
|
||||
|
||||
# Guest mount points of the media library (GNAI-017 section 2); the host
|
||||
# paths are supplied per environment variable and must already exist.
|
||||
readonly -a MOUNT_GUEST_PATHS=(
|
||||
'/media/daten1'
|
||||
'/media/daten2'
|
||||
'/media/daten3'
|
||||
'/media/datenhdd2'
|
||||
'/media/datenhdd3'
|
||||
)
|
||||
readonly -a MOUNT_HOST_PATHS=(
|
||||
"${GHOSTNET_MP0_HOST:-}"
|
||||
"${GHOSTNET_MP1_HOST:-}"
|
||||
"${GHOSTNET_MP2_HOST:-}"
|
||||
"${GHOSTNET_MP3_HOST:-}"
|
||||
"${GHOSTNET_MP4_HOST:-}"
|
||||
)
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-provision] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-provision] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
||||
}
|
||||
|
||||
require_file_sha256() {
|
||||
local expected="$1"
|
||||
local path="$2"
|
||||
|
||||
[[ -f "$path" ]] || die "required file is missing: ${path}"
|
||||
printf '%s %s\n' "$expected" "$path" | sha256sum --check --status - ||
|
||||
die "SHA-256 mismatch: ${path}"
|
||||
}
|
||||
|
||||
storage_is_active() {
|
||||
local storage="$1"
|
||||
pvesm status | awk -v storage="$storage" '
|
||||
$1 == storage && $3 == "active" { found = 1 }
|
||||
END { exit(found ? 0 : 1) }
|
||||
'
|
||||
}
|
||||
|
||||
require_numeric() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
|
||||
[[ "$value" =~ ^[0-9]+$ ]] || die "${name} must be numeric (got: ${value:-<unset>})"
|
||||
}
|
||||
|
||||
[[ "${EUID}" -eq 0 ]] || die 'must run as root on the Proxmox VE host'
|
||||
for command_name in awk grep ls pct pvesm pveversion seq sha256sum sort; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
|
||||
pveversion >/dev/null 2>&1 || die 'this does not appear to be a Proxmox VE host'
|
||||
[[ ! -e "/etc/pve/lxc/${CTID}.conf" ]] ||
|
||||
die "CT ${CTID} still exists; this script never replaces or destroys a container"
|
||||
if pct status "$CTID" >/dev/null 2>&1; then
|
||||
die "CT ${CTID} is still registered"
|
||||
fi
|
||||
|
||||
[[ "$HOSTNAME" =~ ^[a-z0-9][a-z0-9-]*$ ]] ||
|
||||
die "invalid hostname: ${HOSTNAME}"
|
||||
require_numeric GHOSTNET_CT101_CORES "$CORES"
|
||||
require_numeric GHOSTNET_CT101_MEMORY_MB "$MEMORY_MB"
|
||||
require_numeric GHOSTNET_CT101_ROOTFS_GB "$ROOTFS_GB"
|
||||
require_numeric GHOSTNET_CT101_SWAP_MB "$SWAP_MB"
|
||||
require_numeric GHOSTNET_AGENT_UID "$AGENT_UID"
|
||||
require_numeric GHOSTNET_ADMIN_UID "$ADMIN_UID"
|
||||
[[ "$AGENT_UID" != "$ADMIN_UID" ]] ||
|
||||
die 'GHOSTNET_AGENT_UID and GHOSTNET_ADMIN_UID must differ'
|
||||
|
||||
for index in "${!MOUNT_HOST_PATHS[@]}"; do
|
||||
[[ -n "${MOUNT_HOST_PATHS[$index]}" ]] ||
|
||||
die "GHOSTNET_MP${index}_HOST is not set; all five media mount host paths are required"
|
||||
[[ -d "${MOUNT_HOST_PATHS[$index]}" ]] ||
|
||||
die "media mount host path does not exist: ${MOUNT_HOST_PATHS[$index]}"
|
||||
done
|
||||
|
||||
storage_is_active "$TEMPLATE_STORAGE" ||
|
||||
die "template storage ${TEMPLATE_STORAGE} is not active"
|
||||
storage_is_active "$CONTAINER_STORAGE" ||
|
||||
die "container storage ${CONTAINER_STORAGE} is not active"
|
||||
|
||||
[[ -x "$BASELINE_HOOK" ]] || die "baseline hook is not executable: ${BASELINE_HOOK}"
|
||||
require_file_sha256 "$BASELINE_HOOK_SHA256" "$BASELINE_HOOK"
|
||||
|
||||
resolved_template="$OS_TEMPLATE"
|
||||
if [[ -z "$resolved_template" ]]; then
|
||||
[[ "$TEMPLATE_STORAGE" == 'local' ]] ||
|
||||
die 'template auto-detection requires the local template storage; set GHOSTNET_OS_TEMPLATE'
|
||||
template_cache='/var/lib/vz/template/cache'
|
||||
resolved_template="$(
|
||||
ls -1 "${template_cache}"/debian-13-*amd64*.tar.* 2>/dev/null | sort -V | tail -n 1 || true
|
||||
)"
|
||||
[[ -n "$resolved_template" ]] ||
|
||||
die "no debian-13 amd64 template in ${template_cache}; set GHOSTNET_OS_TEMPLATE"
|
||||
resolved_template="${TEMPLATE_STORAGE}:vztmpl/$(basename "$resolved_template")"
|
||||
fi
|
||||
[[ "$resolved_template" =~ ^[A-Za-z0-9_.-]+:vztmpl/.+\.tar\.(zst|gz|xz)$ ]] ||
|
||||
die "GHOSTNET_OS_TEMPLATE must be a template volume ID like local:vztmpl/debian-13-standard_13.1-1_amd64.tar.zst (got: ${resolved_template})"
|
||||
template_storage="${resolved_template%%:*}"
|
||||
storage_is_active "$template_storage" ||
|
||||
die "template storage ${template_storage} is not active"
|
||||
pvesm list "$template_storage" | awk '{print $1}' |
|
||||
grep -qx "$resolved_template" ||
|
||||
die "template not found in storage ${template_storage}: ${resolved_template}"
|
||||
|
||||
log "creating CT ${CTID} (${HOSTNAME}, ${IPV4_CIDR}) from ${resolved_template}"
|
||||
log "profile: ${CORES} cores, ${MEMORY_MB} MiB RAM, ${SWAP_MB} MiB swap, ${ROOTFS_GB} GiB rootfs on ${CONTAINER_STORAGE}"
|
||||
pct create "$CTID" "$resolved_template" \
|
||||
--hostname "$HOSTNAME" \
|
||||
--cores "$CORES" \
|
||||
--memory "$MEMORY_MB" \
|
||||
--swap "$SWAP_MB" \
|
||||
--rootfs "${CONTAINER_STORAGE}:${ROOTFS_GB}" \
|
||||
--net0 "name=eth0,bridge=${BRIDGE},gw=${GATEWAY},ip=${IPV4_CIDR},type=veth" \
|
||||
--nameserver "$NAMESERVER" \
|
||||
--timezone 'Europe/Berlin' \
|
||||
--ostype debian \
|
||||
--unprivileged 1 \
|
||||
--onboot 1 \
|
||||
--features 'nesting=1,keyctl=1' ||
|
||||
die 'pct create failed'
|
||||
|
||||
for index in "${!MOUNT_GUEST_PATHS[@]}"; do
|
||||
log "attaching media mount mp${index}: ${MOUNT_HOST_PATHS[$index]} -> ${MOUNT_GUEST_PATHS[$index]} (backup=no)"
|
||||
pct set "$CTID" --mp"$index" \
|
||||
"${MOUNT_HOST_PATHS[$index]},mp=${MOUNT_GUEST_PATHS[$index]},backup=no" ||
|
||||
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'
|
||||
grep -qx "swap: ${SWAP_MB}" <<<"$config" || die 'unexpected swap allocation'
|
||||
grep -qx "hostname: ${HOSTNAME}" <<<"$config" || die 'unexpected hostname'
|
||||
grep -qx "nameserver: ${NAMESERVER}" <<<"$config" || die 'unexpected nameserver'
|
||||
grep -qx 'onboot: 1' <<<"$config" || die 'onboot is not enabled'
|
||||
grep -qx 'unprivileged: 1' <<<"$config" || die 'container is not unprivileged'
|
||||
grep -qx 'ostype: debian' <<<"$config" || die 'unexpected os type'
|
||||
grep -qx 'timezone: Europe/Berlin' <<<"$config" || die 'unexpected timezone'
|
||||
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' \
|
||||
"bridge=${BRIDGE}" \
|
||||
"gw=${GATEWAY}" \
|
||||
"ip=${IPV4_CIDR}" \
|
||||
'type=veth'; do
|
||||
[[ "$net0" == *"$expected_net_field"* ]] ||
|
||||
die "net0 is missing ${expected_net_field}"
|
||||
done
|
||||
for index in "${!MOUNT_GUEST_PATHS[@]}"; do
|
||||
mp_line="$(grep "^mp${index}: " <<<"$config")"
|
||||
[[ "$mp_line" == *"mp=${MOUNT_GUEST_PATHS[$index]}"* ]] ||
|
||||
die "mp${index} is missing mp=${MOUNT_GUEST_PATHS[$index]}"
|
||||
[[ "$mp_line" == *'backup=no'* ]] ||
|
||||
die "mp${index} is missing backup=no"
|
||||
[[ "$mp_line" == *"${MOUNT_HOST_PATHS[$index]}"* ]] ||
|
||||
die "mp${index} does not reference ${MOUNT_HOST_PATHS[$index]}"
|
||||
done
|
||||
|
||||
log 'starting CT 101'
|
||||
pct start "$CTID" || die 'pct start failed'
|
||||
responsive='no'
|
||||
for _ in $(seq 1 60); do
|
||||
if pct exec "$CTID" -- /bin/true 2>/dev/null; then
|
||||
responsive='yes'
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "$responsive" == 'yes' ]] || die 'CT 101 did not become responsive after start'
|
||||
|
||||
log 'running the reviewed generic LXC baseline with the recorded account UIDs'
|
||||
GHOSTNET_AGENT_UID="$AGENT_UID" GHOSTNET_ADMIN_UID="$ADMIN_UID" \
|
||||
"$BASELINE_HOOK" "$CTID" ||
|
||||
die 'the baseline hook failed; inspect the output above before any rerun'
|
||||
|
||||
pct exec "$CTID" -- /bin/bash -s -- \
|
||||
"$HOSTNAME" "${IPV4_CIDR%/*}" "$AGENT_UID" "$ADMIN_UID" \
|
||||
"${MOUNT_GUEST_PATHS[@]}" <<'GUEST_VERIFY' ||
|
||||
die 'guest verification failed'
|
||||
set -Eeuo pipefail
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-provision/guest] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-provision/guest] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
hostname="$1"
|
||||
ipv4="$2"
|
||||
agent_uid="$3"
|
||||
admin_uid="$4"
|
||||
shift 4
|
||||
mounts=("$@")
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
[[ "${ID:-}" == 'debian' ]] || die "unsupported guest OS: ${ID:-unknown}"
|
||||
[[ "${VERSION_ID:-}" == '13' ]] ||
|
||||
die "guest is Debian ${VERSION_ID:-unknown}, expected 13 (trixie)"
|
||||
test "$(hostname --short)" = "$hostname"
|
||||
ip -o -4 address show scope global | grep -Eq "[[:space:]]inet[[:space:]]+${ipv4//./\\.}/"
|
||||
test "$(id -u ghost-agent)" = "$agent_uid"
|
||||
test "$(id -u ghost)" = "$admin_uid"
|
||||
systemctl is-active --quiet ssh.service
|
||||
for mount_point in "${mounts[@]}"; do
|
||||
findmnt --noheadings --target "$mount_point" >/dev/null
|
||||
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"
|
||||
log 'operator actions before the Ansible rebuild sequence:'
|
||||
log " 1. set the ghost sudo password: pct exec ${CTID} -- passwd ghost"
|
||||
log ' 2. install the agent and ghost public keys from ansible/files/ssh/ into'
|
||||
log ' /home/ghost-agent/.ssh/authorized_keys and /home/ghost/.ssh/authorized_keys'
|
||||
log ' (the baseline hook created both files empty and protected)'
|
||||
log ' 3. on the workstation: add the ssh aliases and seed known_hosts_ghostnet_ct101'
|
||||
log ' 4. fill every REPLACE-WITH placeholder in the Ansible inventory'
|
||||
log ' (preflight.yml reports the missing values on the agent inventory)'
|
||||
log ' 5. run the rebuild sequence from ansible/README.md:'
|
||||
log ' site.yml -e samba_smb_preserve_passdb=false, then smbpasswd -a for'
|
||||
log ' ghost, daidalaw and debugofen, then site.yml again and'
|
||||
log ' verify-production.yml -e samba_verify_require_premigration_backup=false'
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# In-place Debian 12 -> 13 dist-upgrade of the adopted, productive CT 101
|
||||
# (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.
|
||||
#
|
||||
# The script contains no password or private key. The SMB passdb and the
|
||||
# productive smb.conf survive the upgrade (conffile kept via
|
||||
# --force-confold); playbooks/backup.yml still runs before the first
|
||||
# Ansible convergence.
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
readonly CTID='101'
|
||||
readonly EXPECTED_IPV4='10.0.0.9'
|
||||
readonly CONFIRMATION_PHRASE='DIST-UPGRADE_CT101_TO_DEBIAN_13'
|
||||
readonly BACKUP_STORAGE="${GHOSTNET_BACKUP_STORAGE:-local}"
|
||||
readonly BACKUP_DUMP_DIR="${GHOSTNET_BACKUP_DUMP_DIR:-/var/lib/vz/dump}"
|
||||
readonly -a MEDIA_MOUNT_POINTS=(
|
||||
'/media/daten1'
|
||||
'/media/daten2'
|
||||
'/media/daten3'
|
||||
'/media/datenhdd2'
|
||||
'/media/datenhdd3'
|
||||
)
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-upgrade] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-upgrade] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
||||
}
|
||||
|
||||
[[ "${EUID}" -eq 0 ]] || die 'must run as root on the Proxmox VE host'
|
||||
[[ "${GHOSTNET_CT101_UPGRADE_CONFIRMATION:-}" == "$CONFIRMATION_PHRASE" ]] ||
|
||||
die "set GHOSTNET_CT101_UPGRADE_CONFIRMATION=${CONFIRMATION_PHRASE} for the reviewed dist-upgrade outage"
|
||||
|
||||
for command_name in awk find grep pct pvesm pveversion seq sha256sum sort vzdump; do
|
||||
require_command "$command_name"
|
||||
done
|
||||
|
||||
pveversion >/dev/null 2>&1 || die 'this does not appear to be a Proxmox VE host'
|
||||
pct config "$CTID" >/dev/null 2>&1 || die "CT ${CTID} does not exist"
|
||||
[[ "$(pct status "$CTID")" == 'status: running' ]] ||
|
||||
die "CT ${CTID} is not running"
|
||||
pct config "$CTID" | grep -qx 'unprivileged: 1' ||
|
||||
die "CT ${CTID} is not unprivileged (GNAI-015)"
|
||||
[[ -d "$BACKUP_DUMP_DIR" ]] ||
|
||||
die "backup dump directory does not exist: ${BACKUP_DUMP_DIR} (set GHOSTNET_BACKUP_DUMP_DIR for non-local storage)"
|
||||
|
||||
log 'preflight of the productive CT 101 guest'
|
||||
pct exec "$CTID" -- /bin/bash -s -- "$EXPECTED_IPV4" <<'GUEST_PREFLIGHT' ||
|
||||
die 'guest preflight failed; CT 101 is unchanged'
|
||||
set -Eeuo pipefail
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-preflight] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-preflight] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ipv4="$1"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
[[ "${ID:-}" == 'debian' ]] || die "unsupported guest OS: ${ID:-unknown}"
|
||||
[[ "${VERSION_ID:-}" == '12' ]] ||
|
||||
die "guest is Debian ${VERSION_ID:-unknown}, expected 12 (bookworm) before the upgrade"
|
||||
systemctl is-active --quiet ssh.service smbd.service nmbd.service ||
|
||||
die 'ssh, smbd or nmbd is not active on the productive file server'
|
||||
ip -o -4 address show scope global |
|
||||
grep -Eq "[[:space:]]inet[[:space:]]+${ipv4//./\\.}/" ||
|
||||
die "guest does not own the expected IPv4 ${ipv4}"
|
||||
[[ "$(head -n 1 /proc/self/uid_map)" != '0 0 4294967295' ]] ||
|
||||
die 'guest lost its unprivileged user-namespace mapping'
|
||||
|
||||
log "hostname: $(hostname)"
|
||||
log "samba: $(/usr/sbin/smbd --version)"
|
||||
log "smb.conf sha256: $(sha256sum /etc/samba/smb.conf | awk '{print $1}')"
|
||||
log 'ssh, smbd and nmbd are active; the productive state is healthy'
|
||||
GUEST_PREFLIGHT
|
||||
|
||||
mapfile -t mount_lines < <(pct config "$CTID" | grep -E '^mp[0-9]+: ' || true)
|
||||
[[ "${#mount_lines[@]}" -eq "${#MEDIA_MOUNT_POINTS[@]}" ]] ||
|
||||
die "expected ${#MEDIA_MOUNT_POINTS[@]} media mounts on CT ${CTID}, found ${#mount_lines[@]}"
|
||||
for mp_line in "${mount_lines[@]}"; do
|
||||
grep -q 'backup=no' <<<"$mp_line" ||
|
||||
die "media mount lacks backup=no (vzdump would archive the media library): ${mp_line}"
|
||||
done
|
||||
for mount_point in "${MEDIA_MOUNT_POINTS[@]}"; do
|
||||
pct config "$CTID" | grep -Eq "^mp[0-9]+: .*mp=${mount_point}(,|\$)" ||
|
||||
die "media mount point is not configured on the PVE side: ${mount_point}"
|
||||
done
|
||||
|
||||
log "creating the pre-upgrade vzdump (mode stop) in storage ${BACKUP_STORAGE}"
|
||||
vzdump "$CTID" --mode stop --storage "$BACKUP_STORAGE" --compress zstd ||
|
||||
die 'vzdump failed; CT 101 may be stopped - investigate and run pct start 101'
|
||||
dump_archive="$(
|
||||
find "$BACKUP_DUMP_DIR" -maxdepth 1 -name "vzdump-${CTID}-*.vma.*" -mmin -30 -printf '%T@ %p\n' 2>/dev/null |
|
||||
sort -rn | head -n 1 | awk '{print $2}' || true
|
||||
)"
|
||||
[[ -n "$dump_archive" && -s "$dump_archive" ]] ||
|
||||
die "no fresh vzdump archive found in ${BACKUP_DUMP_DIR}"
|
||||
log "pre-upgrade dump: ${dump_archive} ($(stat -c %s "$dump_archive") bytes)"
|
||||
|
||||
# vzdump --mode stop may leave the container stopped depending on the PVE
|
||||
# version; the dist-upgrade needs it running.
|
||||
if [[ "$(pct status "$CTID")" != 'status: running' ]]; then
|
||||
log 'starting CT 101 after the vzdump'
|
||||
pct start "$CTID" || die 'pct start failed after the vzdump'
|
||||
fi
|
||||
responsive='no'
|
||||
for _ in $(seq 1 60); do
|
||||
if pct exec "$CTID" -- /bin/true 2>/dev/null; then
|
||||
responsive='yes'
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
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.
|
||||
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"
|
||||
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' ||
|
||||
die 'the in-guest dist-upgrade failed; CT 101 needs manual recovery from the vzdump'
|
||||
set -Eeuo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-upgrade] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-upgrade] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
[[ "${ID:-}" == 'debian' && "${VERSION_ID:-}" == '12' ]] ||
|
||||
die "unexpected guest release before the upgrade: ${ID:-unknown} ${VERSION_ID:-unknown}"
|
||||
|
||||
# bookworm -> trixie in deb822 and one-line source styles alike.
|
||||
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
|
||||
sed -i 's/\bbookworm\b/trixie/g' /etc/apt/sources.list.d/debian.sources
|
||||
fi
|
||||
if [[ -f /etc/apt/sources.list ]]; then
|
||||
sed -i 's/\bbookworm\b/trixie/g' /etc/apt/sources.list
|
||||
fi
|
||||
grep -rq 'bookworm' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null &&
|
||||
die 'apt sources still reference bookworm after the rewrite'
|
||||
|
||||
apt-get update
|
||||
apt-get -y -o Dpkg::Options::='--force-confold' dist-upgrade
|
||||
apt-get -y autoremove
|
||||
|
||||
# Debian 13's wtmpdb package no longer ships wtmpdb-rotate.timer. Remove only
|
||||
# the known dangling enable link (same rule as ghostnet-lxc-baseline.sh).
|
||||
wtmpdb_timer_unit='/usr/lib/systemd/system/wtmpdb-rotate.timer'
|
||||
wtmpdb_timer_link='/etc/systemd/system/timers.target.wants/wtmpdb-rotate.timer'
|
||||
if [[ -L "$wtmpdb_timer_link" && ! -e "$wtmpdb_timer_unit" ]]; then
|
||||
unlink -- "$wtmpdb_timer_link"
|
||||
systemctl daemon-reload
|
||||
systemctl reset-failed wtmpdb-rotate.timer >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
[[ "${ID:-}" == 'debian' && "${VERSION_ID:-}" == '13' ]] ||
|
||||
die "the guest did not reach Debian 13 (got ${ID:-unknown} ${VERSION_ID:-unknown})"
|
||||
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'
|
||||
pct reboot "$CTID" 2>/dev/null || pct start "$CTID" ||
|
||||
die 'reboot failed; run pct start 101 manually'
|
||||
responsive='no'
|
||||
for _ in $(seq 1 90); do
|
||||
if pct exec "$CTID" -- /bin/true 2>/dev/null; then
|
||||
responsive='yes'
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[[ "$responsive" == 'yes' ]] ||
|
||||
die 'CT 101 did not become responsive after the reboot'
|
||||
|
||||
log 'post-upgrade verification of the guest'
|
||||
pct exec "$CTID" -- /bin/bash -s -- "$EXPECTED_IPV4" \
|
||||
"${MEDIA_MOUNT_POINTS[@]}" <<'GUEST_POSTVERIFY' ||
|
||||
die 'post-upgrade verification failed'
|
||||
set -Eeuo pipefail
|
||||
|
||||
log() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-postverify] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[ghostnet-ct101-upgrade/guest-postverify] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ipv4="$1"
|
||||
shift
|
||||
mounts=("$@")
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
[[ "${ID:-}" == 'debian' ]] || die "unexpected OS: ${ID:-unknown}"
|
||||
[[ "${VERSION_ID:-}" == '13' ]] ||
|
||||
die "unexpected Debian release: ${VERSION_ID:-unknown}"
|
||||
systemctl is-active --quiet ssh.service smbd.service nmbd.service ||
|
||||
die 'ssh, smbd or nmbd is not active after the upgrade'
|
||||
if systemctl --failed --no-legend --plain | grep -q .; then
|
||||
die 'failed systemd units are present after the upgrade'
|
||||
fi
|
||||
ip -o -4 address show scope global |
|
||||
grep -Eq "[[:space:]]inet[[:space:]]+${ipv4//./\\.}/" ||
|
||||
die "guest does not own the expected IPv4 ${ipv4}"
|
||||
listeners="$(ss -H -tuln)"
|
||||
grep -q ':22 ' <<<"$listeners" || die 'SSH listener is missing'
|
||||
grep -q ':139 ' <<<"$listeners" || die 'SMB listener 139 is missing'
|
||||
grep -q ':445 ' <<<"$listeners" || die 'SMB listener 445 is missing'
|
||||
/usr/bin/smbclient -L localhost -N >/dev/null ||
|
||||
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
|
||||
findmnt --noheadings --target "$mount_point" >/dev/null ||
|
||||
die "media mount is not mounted: ${mount_point}"
|
||||
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 "media mounts verified: ${mounts[*]}"
|
||||
GUEST_POSTVERIFY
|
||||
|
||||
log "CT ${CTID} is now Debian 13 and the productive Samba service survived"
|
||||
log "pre-upgrade dump: ${dump_archive}"
|
||||
log 'next steps (ansible/README.md, adoption runbook):'
|
||||
log ' 1. workstation: ssh aliases ghostnet-ct101-agent / ghostnet-ct101-ghost and'
|
||||
log ' seed ~/.ssh/known_hosts_ghostnet_ct101 with the unchanged host key'
|
||||
log ' 2. preflight.yml (agent inventory), then fill every REPLACE-WITH placeholder'
|
||||
log ' 3. audit-production.yml, bootstrap.yml, verify-deploy-access.yml, backup.yml'
|
||||
log ' 4. site.yml --check --diff, then site.yml, then verify-production.yml'
|
||||
Reference in New Issue
Block a user