Compare commits

...

9 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
76f3e10ee4 Verify WireGuard handshakes and restore Podman networking 2026-09-15 22:38:56 +02:00
Fabio Scotto di Santolo
c899bb7192 Harden Rocky storage and sharing configuration 2026-09-15 22:05:48 +02:00
Fabio Scotto di Santolo
2ab5ba6818 Manage SSH authorized key fragments for infrastructure hosts 2026-09-13 15:16:40 +02:00
Fabio Scotto di Santolo
a347880d4c Manage Aegis SSH authorized key fragments 2026-09-13 14:46:08 +02:00
Fabio Scotto di Santolo
b05a2b4e74 Allow configurable Aegis SSH users and authorized keys 2026-09-13 14:35:43 +02:00
Fabio Scotto di Santolo
75d9081a11 Rotate Ansible Vault secrets 2026-09-13 13:53:03 +02:00
Fabio Scotto di Santolo
b4b3bd10b8 Harden Atlas network, SSH, firewall, and sharing 2026-09-13 00:01:48 +02:00
Fabio Scotto di Santolo
db10d1296e Enable Atlas NAS management and document bootstrap workflow 2026-09-12 18:53:21 +02:00
Fabio Scotto di Santolo
8c35ef63c9 Document Atlas backend phase one and WireGuard deployment 2026-09-12 17:19:20 +02:00
48 changed files with 1501 additions and 808 deletions

View File

@@ -55,8 +55,12 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server` - Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server`
- Atlas media stack: - Atlas media stack:
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
- Prometheus media mount: - Atlas network/share hardening:
`ansible-playbook ansible/site.yml --limit prometheus --tags rclone,navidrome --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
- Atlas phase-one rootless services:
`ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff`
- Prometheus/Atlas WireGuard overlay:
`ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard --check --diff`
- DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff` - DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff`
## Conventions ## Conventions
@@ -108,16 +112,13 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `rocky_server` is a child of both `platform_rocky` and `server`; `prometheus` is its active target. - `rocky_server` is a child of both `platform_rocky` and `server`; `prometheus` is its active target.
- The target must already provide `server_username` with local sudo access before the profile runs. - The target must already provide `server_username` with local sudo access before the profile runs.
- The Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the - The Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the
Nginx Proxy Manager/Gitea/Navidrome-PostgreSQL Compose stack with a `podman-compose-server` systemd unit. It does not existing Nginx Proxy Manager/Gitea Compose stack with a `podman-compose-server` systemd unit. PostgreSQL and
start or enable that Compose stack, transfer data, update DNS, or cut over traffic; activating it remains manual. Navidrome are no longer part of the desired Prometheus configuration. The role does not stop or remove legacy
- Prometheus has a gated system `rclone-music.service` and rootless Navidrome Quadlet. They remain disabled until the containers, delete `/opt/postgres/data`, start the Compose stack, update DNS, or cut over traffic.
Atlas WireGuard address, pinned SSH host key and Vault-backed SFTP private key are configured. The rclone mount is
read-only at `/mnt/music_atlas`; Navidrome must not start against the underlying empty mountpoint or while the legacy
rootful Navidrome container is still running. The role never removes that legacy container or its data.
- Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes `80/tcp` and - Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes `80/tcp` and
`443/tcp`; bind its administration interface only to `127.0.0.1:81` and use `npm-tunnel` from Ikaros or Nymph. `443/tcp`; bind its administration interface only to `127.0.0.1:81` and use `npm-tunnel` from Ikaros or Nymph.
Nextcloud remains disabled; do not provision `/srv/nextcloud` directories. Nextcloud remains disabled; do not provision `/srv/nextcloud` directories.
- `scripts/migrate_prometheus_data.sh` is the separate, source-host-run migration path. It dry-runs by - `scripts/migrate_prometheus_data.sh` is the separate, source-host-run NPM/Gitea migration path. It dry-runs by
default and requires explicit source-stack quiescing before copying persistent Docker data with rsync. default and requires explicit source-stack quiescing before copying persistent Docker data with rsync.
- Atlas-only OpenZFS, NFS, Samba, and Syncthing stay selected through Atlas host variables and must not - Atlas-only OpenZFS, NFS, Samba, and Syncthing stay selected through Atlas host variables and must not
leak into `rocky_server`. Cockpit plus its Navigator and Podman extensions are selected explicitly for leak into `rocky_server`. Cockpit plus its Navigator and Podman extensions are selected explicitly for
@@ -127,30 +128,49 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `atlas` is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in - `atlas` is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in
`host_vars/atlas.yml`. Bootstrap it once with `-e atlas_connection_username=<existing-admin>`; `host_vars/atlas.yml`. Bootstrap it once with `-e atlas_connection_username=<existing-admin>`;
subsequent runs use the dedicated Atlas account. subsequent runs use the dedicated Atlas account.
- The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback, - The pool is normally pre-existing. A one-time bootstrap may create it only when `atlas_create_pool=true`
or destruction to the Atlas profile. is explicitly supplied and `atlas_zpool_disks` contains exactly four real `/dev/disk/by-id/...` paths.
- `atlas_manage_storage`, `atlas_manage_firewall`, and `atlas_manage_media_stack` remain false until their placeholders Never partition, force, destroy, roll back, or modify the vdev layout of an existing pool.
and Vault inputs are replaced; only then may the profile manage datasets, shares, LAN-restricted firewall rules, and - `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as
rootful media Quadlets. the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false
- Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit until the future rootful Immich stack has its required Vault inputs and target validation.
and, when the relevant gates are enabled, `vault_atlas_samba_password` and `vault_atlas_immich_db_password`. Never - Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled,
print these values. `vault_atlas_samba_password`. The future rootful media stack also requires
- Atlas creates `archive`, `media/music`, `media/icloud_photos`, and `backups/services` only under the verified `vault_atlas_immich_db_password`. Never print these values.
pre-existing pool; `backups/services` has a `500G` refreservation. Existing Work, Syncthing, and - Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `work`, `archive`,
Prometheus-backup datasets remain managed and separate. `archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`,
`media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. `backups/services` has a `500G`
refreservation. There is no separate legacy `zpool/syncthing` dataset.
- The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only - The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only
the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive
`/dev/dri`, while the iCloud Photos external library is read-only. `/dev/dri`, while the Photobook external library is read-only at `/external/photobook`.
- Atlas exports iCloud Photos only to the configured Aegis IP with all access squashed to UID/GID `1100`. SMB3 exposes - Atlas applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, agent and remote forwarding
`Archive` to Vault-backed authorized accounts and admits the configured LAN without host-specific exclusions. are disabled, while local forwarding remains available for private administrative tunnels. Photobook is exported only to the configured Aegis IP with all access squashed to UID/GID
`1100`. Targeted SELinux is enforced persistently; a required reboot is reported but never initiated automatically. The primary LAN interface is assigned explicitly to the managed firewalld zone, and firewall rules are applied before NFS or SMB are started; their service state and TCP listeners are then verified. SMB3 exposes `Archive` to Vault-backed authorized accounts on mandatory encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific exclusions.
- Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains - Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet. bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet.
- `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome
`0.63.2` uses SQLite below `/data` and does not support `ND_DATABASE_URL` or an external PostgreSQL backend; do not
recreate the obsolete Prometheus `navidromedb` service. The role requires the storage role's `zpool/media/music`,
`zpool/archive/app_data`, `zpool/archive/app_data/navidrome`, and `zpool/archive/app_data/syncthing` datasets at
their exact paths. It never creates the pool.
- Keep `backend_phase1_start_services` false until the stopped Prometheus Navidrome data directory has been copied to
Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application
data; after cutover, set the flag true to enable and start Navidrome and Syncthing.
- Phase 1 must not change Prometheus' existing NPM deployment. NPM continues to be managed exactly by `profile_server`;
use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking:
its GUI, transfer, QUIC and discovery ports are explicitly published only on `10.0.0.2`; native transfer/discovery does not use the HTTP proxy.
- `wireguard_overlay` manages the required `wg0` path between Prometheus and Atlas, persists private keys only on their
respective hosts, exchanges only derived public keys, and verifies a real peer handshake. The initial run must
include both hosts. Prometheus
opens `51820/udp`; after creating the WireGuard firewalld zone, restore Prometheus' rootful Podman networking with
`podman network reload --all` so the existing proxy stack retains container DNS. The Atlas backend role admits
service ports only in the WireGuard firewalld zone.
## Atlas NAS TODO ## Atlas NAS TODO
- Replace every Atlas `CHANGEME` value, provide the required Vault variables and validate the first - Provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host.
remote bootstrap on the real Rocky Linux 9 host. Enable `atlas_manage_storage` first and Before the first apply, confirm the pool, mountpoints, LAN subnet and firewalld zone. Keep
`atlas_manage_firewall` only after confirming the pool, mountpoints, LAN subnet and firewalld zone. Enable `atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated.
`atlas_manage_media_stack` last, after validating `/dev/dri`, the container paths and the Immich database secret.
- Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset - Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset
mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing. mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing.
- Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files - Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files
@@ -159,10 +179,11 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
or manual operations, not as the only source of configuration, and never automate snapshot rollback. or manual operations, not as the only source of configuration, and never automate snapshot rollback.
- Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI - Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI
or API access for the selected clients. or API access for the selected clients.
- Validate the existing WireGuard path and add its LAN/VPN-only firewalld rules before enabling remote services; - Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services;
never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding. never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity, - Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared
pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer. read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key,
atomic pull, verification, retention and systemd service/timer.
- Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity, - Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity,
pin the host key, keep Borg repository credentials and encryption material in Vault, use pin the host key, keep Borg repository credentials and encryption material in Vault, use
snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores. snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores.

View File

@@ -180,28 +180,24 @@ Lo stato attuale del profilo server include:
- installazione pacchetti Rocky via DNF, EPEL e CRB - installazione pacchetti Rocky via DNF, EPEL e CRB
- installazione di Podman e podman-compose - installazione di Podman e podman-compose
- abilitazione dei servizi systemd dichiarati in inventory/group vars - abilitazione dei servizi systemd dichiarati in inventory/group vars
- copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager, Gitea e il - copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager e Gitea,
database PostgreSQL esistente di Navidrome, piu l'unita `podman-compose-server` (attivazione manuale) piu l'unita `podman-compose-server` (attivazione manuale)
- mount di `/pool/media/music` da Atlas su `/mnt/music_atlas` tramite il servizio di sistema
`rclone-music.service`, e Navidrome tramite Quadlet utente rootless
- attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati - attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati
- Syncthing escluso dal profilo server Rocky - Syncthing escluso dal profilo server Rocky
Il Compose desiderato su Prometheus non include piu Navidrome ne il database PostgreSQL obsoleto.
Navidrome e Syncthing appartengono ad Atlas; Navidrome ufficiale usa invece SQLite. Il profilo non
arresta o rimuove automaticamente eventuali container legacy e non elimina `/opt/postgres/data`.
Nginx Proxy Manager pubblica solo `80/tcp` e `443/tcp`; la sua interfaccia di amministrazione e Nginx Proxy Manager pubblica solo `80/tcp` e `443/tcp`; la sua interfaccia di amministrazione e
associata a `127.0.0.1:81` ed e raggiungibile da Ikaros o Nymph con l'alias Bash `npm-tunnel`. associata a `127.0.0.1:81` ed e raggiungibile da Ikaros o Nymph con l'alias Bash `npm-tunnel`.
Nextcloud resta disabilitato e il profilo non crea directory `/srv/nextcloud`. Nextcloud resta disabilitato e il profilo non crea directory `/srv/nextcloud`.
Il mount musicale e protetto da `server_atlas_music_enabled`. Prima di abilitarlo, sostituire La fase 1 su Atlas non modifica questo deployment NPM ne i suoi dati persistenti. Dopo aver attivato
l'indirizzo WireGuard e la chiave host SSH fissata in `host_vars/prometheus.yml`, quindi fornire WireGuard e i servizi Atlas, configurare i proxy host NPM correnti con upstream Navidrome
`vault_prometheus_atlas_sftp_private_key` tramite Vault cifrato o variabili locali non tracciate. La `http://10.0.0.2:4533` e upstream per la GUI Syncthing `http://10.0.0.2:8384`. Solo la GUI web di
chiave pubblica corrispondente deve essere gia presente nelle chiavi autorizzate gestite su Atlas. Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native pubblicate esplicitamente solo
Rclone usa il percorso remoto esatto `/pool/media/music` in sola lettura e una cache VFS completa da sull'indirizzo WireGuard di Atlas. Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI.
`15G`; systemd lingering mantiene disponibile il manager utente per il Quadlet rootless.
Configurare il proxy host NPM di Prometheus per Navidrome come `host.containers.internal:4533`; la
porta Navidrome non viene aperta in firewalld.
Prima della prima attivazione, arrestare il vecchio container rootful `navidrome`. Il ruolo rifiuta
di avviare il sostituto rootless mentre il container precedente e in esecuzione e non rimuove mai
automaticamente il container o i dati esistenti.
### DuckDNS ### DuckDNS
@@ -226,8 +222,9 @@ salvare separatamente eventuali modifiche non committate senza copiare segreti.
Dopo il provisioning Rocky, eseguire `scripts/migrate_prometheus_data.sh` **sul server Ubuntu Dopo il provisioning Rocky, eseguire `scripts/migrate_prometheus_data.sh` **sul server Ubuntu
sorgente**. Lo script usa rsync, e in dry-run di default; richiede `--quiesce-source --execute` per sorgente**. Lo script usa rsync, e in dry-run di default; richiede `--quiesce-source --execute` per
fermare lo stack sorgente e copiare in modo consistente i dati PostgreSQL. Non avvia container, non fermare lo stack sorgente e copiare in modo consistente soltanto i dati di Nginx Proxy Manager e
cancella dati e non esegue il cutover. Gitea. Non sposta Navidrome o Syncthing, non avvia container, non cancella dati e non esegue il
cutover.
Utente del profilo server: Utente del profilo server:
@@ -249,13 +246,14 @@ ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser -
## NAS ## NAS
`atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Il pool ZFS esiste gia: il profilo gestisce `atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Normalmente il pool ZFS esiste gia e il profilo
solo i dataset figli e non deve mai creare, partizionare, distruggere, fare rollback o modificare il gestisce solo i dataset figli. Un bootstrap RAIDZ2 una tantum e disponibile solo con conferma esplicita
pool. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN (`atlas_create_pool=true`) e quattro percorsi reali e verificati `/dev/disk/by-id/...` in
`atlas_zpool_disks`. Non partiziona, forza, distrugge, esegue rollback o modifica il layout vdev di un
pool esistente. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN
configurata. configurata.
Per il primo avvio sostituire i placeholder di host, pool, mount root, LAN e IP di Aegis e Per il primo avvio fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
`vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite `vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite
l'amministratore esistente: l'amministratore esistente:
@@ -265,25 +263,70 @@ ansible-playbook ansible/site.yml --limit atlas \
``` ```
`vault_atlas_admin_password_hash` deve essere un hash compatibile con `/etc/shadow`, non una `vault_atlas_admin_password_hash` deve essere un hash compatibile con `/etc/shadow`, non una
password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Abilitare password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Atlas dichiara
`atlas_manage_storage` solo dopo aver verificato pool e mountpoint esistenti; abilitare abilitati storage, condivisioni e regole firewall LAN. Prima della prima applicazione verificare pool e
`atlas_manage_firewall` solo dopo aver verificato subnet LAN e zona firewalld attiva. Abilitare mountpoint esistenti, subnet LAN e zona firewalld attiva. `atlas_manage_media_stack` resta disabilitato
`atlas_manage_media_stack` per ultimo, dopo aver verificato `/dev/dri`, i percorsi dei container e il finche non saranno validati `/dev/dri`, i percorsi dei container e il segreto del database Immich.
segreto del database Immich.
Con la gestione storage attiva, Atlas crea `archive` (`zstd`), `media/music` (`lz4`), Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpool` esistente o creato esplicitamente:
`media/icloud_photos` (`lz4`) e `backups/services` (`lz4`, `refreservation=500G`) sotto il pool `work`, `archive`, `archive/app_data`, i dataset applicativi separati
preesistente. I dataset esistenti Work, Syncthing e backup Prometheus restano gestiti e separati. `archive/app_data/navidrome` e `archive/app_data/syncthing`, `media`, `media/music`,
SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e ammette la LAN `media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e
configurata senza esclusioni specifiche per host. NFSv4 esporta soltanto di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`;
`media/icloud_photos` all'IP configurato di Aegis con `all_squash` verso UID/GID anonimi `1100`. `backups/services` mantiene inoltre una `refreservation` di `500G`.
Atlas impone SELinux targeted in modo persistente e segnala, senza avviarlo, l’eventuale reboot necessario per attivarlo. Assegna esplicitamente l’interfaccia LAN primaria alla zona firewalld gestita e applica hardening persistente del kernel di rete: rifiuta redirect e source-route, registra i martian, usa reverse-path filtering loose per WireGuard e disabilita il forwarding IPv4. SSH consente solo l’amministratore dichiarato tramite chiave pubblica; root, password, agent e forwarding
remoto sono disabilitati, mentre il forwarding locale resta disponibile per tunnel amministrativi privati. SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e
ammette la LAN configurata su SMB3 cifrato e firmato, esclusivamente su TCP/445. NFSv4 esporta soltanto
`media/photobook` all'IP configurato di Aegis su TCP/2049, con `all_squash` verso UID/GID anonimi `1100`.
L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e
i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile
Redis, PostgreSQL e NPM condividono una rete Podman. Immich viene eseguito come `1100:1100`; Server e Redis, PostgreSQL e NPM condividono una rete Podman. Immich viene eseguito come `1100:1100`; Server e
ML ricevono `/dev/dri` e la libreria iCloud Photos e montata in sola lettura. NPM pubblica `80` e ML ricevono `/dev/dri` e Photobook e montato in sola lettura su `/external/photobook`. NPM pubblica `80` e
`443`, mentre l'amministrazione resta vincolata a `127.0.0.1:81` per l'accesso tramite tunnel SSH. `443`, mentre l'amministrazione resta vincolata a `127.0.0.1:81` per l'accesso tramite tunnel SSH.
La fase 1 e limitata ai Quadlet utente rootless di Navidrome e Syncthing su Atlas. E abilitata nella
configurazione host di Atlas e puo essere impostata a `false` solo per una sospensione intenzionale. Navidrome ufficiale `0.63.2` usa il database SQLite sotto `/data` e
non supporta `ND_DATABASE_URL` ne un backend PostgreSQL esterno. Il servizio obsoleto `navidromedb`
e quindi rimosso da Prometheus invece di essere replicato su Atlas. Il ruolo deriva i percorsi dal
pool `zpool`, montato in `/zpool`: musica in sola lettura da `/zpool/media/music`, stato
applicativo Navidrome e `navidrome.db` in `/zpool/archive/app_data/navidrome` e dati Syncthing in
`/zpool/archive/app_data/syncthing`. `profile_atlas` crea questi dataset quando
`atlas_manage_storage` e attivo; il ruolo backend verifica i mountpoint esatti prima di avviare i
container. Il ruolo backend non crea mai il pool. Il ruolo separato `wireguard_overlay`
gestisce `wg0` tra Prometheus (`10.0.0.1`) e Atlas (`10.0.0.2`), genera una sola volta le chiavi
private sui rispettivi host e scambia tramite Ansible soltanto quelle pubbliche. Solo Prometheus apre
pubblicamente `51820/udp`. Le porte backend sono ammesse esclusivamente nella zona firewalld WireGuard.
`backend_phase1_start_services` resta falso durante il trasferimento dello stato applicativo, quindi
la prima esecuzione reale del backend genera i Quadlet senza creare un database Atlas vuoto. Dopo aver
arrestato Navidrome su Prometheus, copiare l'intera directory `/opt/navidrome/data/` in
`/zpool/archive/app_data/navidrome/`, preservando `navidrome.db` e gli eventuali file SQLite laterali.
Impostare quindi questa variabile a vero e rieseguire il ruolo per abilitare e avviare Navidrome e
Syncthing. Il playbook non copia e non elimina mai i dati applicativi.
Validare e generare i servizi Atlas con:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags storage
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1
```
Per il cutover, arrestare il vecchio Navidrome prima di copiare la sua directory dati, verificare
l'ownership dell'account `admin` su Atlas e confermare la presenza del database SQLite copiato prima
di impostare `backend_phase1_start_services: true` in `host_vars/atlas.yml`. Conservare i dati sorgente
e il container legacy `navidromedb` fermo finche Navidrome su Atlas e una prova di restore non sono
stati validati.
Restano da completare retention delle snapshot, topologia Syncthing, validazione WireGuard/firewall, Restano da completare retention delle snapshot, topologia Syncthing, validazione WireGuard/firewall,
pull di backup da Prometheus, backup cifrati con Borg su una Hetzner Storage Box, backup USB, pull di backup da Prometheus, backup cifrati con Borg su una Hetzner Storage Box, backup USB,
monitoraggio e test di disaster recovery. Il backlog operativo dettagliato e in `AGENTS.md`. monitoraggio e test di disaster recovery. Il backlog operativo dettagliato e in `AGENTS.md`.
@@ -348,6 +391,8 @@ I principali ruoli attualmente presenti sono:
| profile_workstation_dev_wsl | configurazione WSL condivisa per sviluppo | | profile_workstation_dev_wsl | configurazione WSL condivisa per sviluppo |
| profile_server | configurazione server | | profile_server | configurazione server |
| profile_atlas | configurazione NAS Rocky Linux 9 | | profile_atlas | configurazione NAS Rocky Linux 9 |
| profile_backend_phase1 | Navidrome e Syncthing rootless su Atlas |
| wireguard_overlay | overlay WireGuard Prometheus/Atlas |
| dotfiles_common | distribuzione dotfiles comuni | | dotfiles_common | distribuzione dotfiles comuni |
| dotfiles | distribuzione configurazioni utente | | dotfiles | distribuzione configurazioni utente |
@@ -363,7 +408,9 @@ platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_fedora -> packages_fedora + services_systemd platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd platform_rocky -> packages_rocky + services_systemd
wireguard_overlay -> wireguard_overlay (dopo platform_rocky)
atlas -> profile_atlas atlas -> profile_atlas
role_backend_phase1 -> profile_backend_phase1 (dopo atlas)
platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common workstation_dev_fedora -> profile_workstation_dev_common
@@ -379,8 +426,8 @@ Questo significa che, allo stato attuale:
- `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati - `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati
- il server Rocky (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewalld - il server Rocky (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewalld
- il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives - il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives
- lo stack Compose server include `gitea`, `nginx-proxy-manager` e il database PostgreSQL di - lo stack Compose server include soltanto `gitea` e `nginx-proxy-manager`; Navidrome e Syncthing
Navidrome; Navidrome usa un Quadlet rootless separato e legge il mount rclone di Atlas della fase 1 sono Quadlet rootless su Atlas
# Dotfiles # Dotfiles
@@ -488,7 +535,7 @@ ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config podman-compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags rclone,navidrome --check --diff ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
``` ```
## Tag supportati dal playbook ## Tag supportati dal playbook
@@ -506,6 +553,7 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common | | `always` | pre-task sempre eseguiti, inclusi caricamento vault e validazioni preliminari | common |
| `ai_agents` | installazione agenti AI condivisi | Fedora, WSL | | `ai_agents` | installazione agenti AI condivisi | Fedora, WSL |
| `atlas` | account, storage, condivisioni e container Atlas | NAS Atlas | | `atlas` | account, storage, condivisioni e container Atlas | NAS Atlas |
| `backend_phase1` | Quadlet rootless Navidrome e Syncthing | NAS Atlas |
| `containers` | Quadlet rootful Atlas | NAS Atlas | | `containers` | Quadlet rootful Atlas | NAS Atlas |
| `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili | | `dotfiles` | distribuzione/configurazione dotfiles | tutti i profili |
| `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server | | `dotfiles:common` | dotfiles comuni condivisi | common, workstation, server |
@@ -521,14 +569,12 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void | | `git` | configurazione Git e GPG desktop | Fedora/GNOME, desktop Void |
| `gnome` | configurazione host GNOME | Fedora/GNOME desktop | | `gnome` | configurazione host GNOME | Fedora/GNOME desktop |
| `immich` | account e Quadlet Immich | NAS Atlas | | `immich` | account e Quadlet Immich | NAS Atlas |
| `navidrome` | mount rclone e Quadlet Navidrome rootless | Prometheus |
| `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void | | `sway` | sessione/configurazione sway / SwayFX (Wayland) | desktop Void |
| `niri` | sessione/configurazione Niri (Wayland) | desktop Void | | `niri` | sessione/configurazione Niri (Wayland) | desktop Void |
| `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, WSL | | `npm` | installazione pacchetti npm globali | Fedora/GNOME, desktop Void, WSL |
| `nvidia` | componenti NVIDIA desktop | desktop Void | | `nvidia` | componenti NVIDIA desktop | desktop Void |
| `packages` | installazione e aggiornamento pacchetti | tutti i profili | | `packages` | installazione e aggiornamento pacchetti | tutti i profili |
| `podman` | integrazione Podman Compose e Quadlet rootless | server | | `podman` | integrazione Podman Compose e Quadlet rootless | server |
| `rclone` | mount musica Atlas | Prometheus |
| `portal` | configurazione xdg-desktop-portal | desktop Void | | `portal` | configurazione xdg-desktop-portal | desktop Void |
| `services` | gestione servizi runit/systemd | tutti i profili | | `services` | gestione servizi runit/systemd | tutti i profili |
| `sharing` | condivisioni NFSv4 e SMB3 | NAS Atlas | | `sharing` | condivisioni NFSv4 e SMB3 | NAS Atlas |
@@ -536,6 +582,7 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| `theme` | configurazione del tema GTK/Qt | desktop Void | | `theme` | configurazione del tema GTK/Qt | desktop Void |
| `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL | | `tmux` | configurazione e plugin tmux | desktop Fedora/Void, WSL |
| `vim` | configurazione Vim | dotfiles comuni | | `vim` | configurazione Vim | dotfiles comuni |
| `wireguard` | overlay WireGuard Prometheus/Atlas | Prometheus, NAS Atlas |
| `wsl` | bootstrap e configurazione WSL | WSL | | `wsl` | bootstrap e configurazione WSL | WSL |
Esempi pratici: Esempi pratici:

154
README.md
View File

@@ -105,29 +105,24 @@ dotfiles and templates. The profile provisions configuration only: it does not t
the Compose stack, update DNS, or perform a cutover. the Compose stack, update DNS, or perform a cutover.
The server profile installs platform-specific packages, Podman and podman-compose, declared systemd The server profile installs platform-specific packages, Podman and podman-compose, declared systemd
services, and firewalld. The manually activated `podman-compose-server` unit now contains Nginx Proxy services, and firewalld. The manually activated `podman-compose-server` unit contains the existing
Manager, Gitea, and the existing Navidrome PostgreSQL database. Navidrome itself runs as a rootless Nginx Proxy Manager and Gitea services. The desired Compose file no longer includes Navidrome,
user Quadlet and reads the Atlas music dataset from the system `rclone-music.service` mount at Syncthing, or the obsolete Navidrome PostgreSQL database. Navidrome and Syncthing belong to Atlas;
`/mnt/music_atlas`. The Rocky server excludes Syncthing. official Navidrome uses SQLite instead. Applying the profile does not stop or remove legacy
Rocky bind mounts use private SELinux relabeling where supported; the read-only FUSE music mount is containers and does not delete `/opt/postgres/data`.
passed to Navidrome without relabeling.
The Atlas music path is gated by `server_atlas_music_enabled`. Before enabling it, replace the
WireGuard address and pinned SSH host-key placeholders in `host_vars/prometheus.yml`, and provide
`vault_prometheus_atlas_sftp_private_key` through encrypted Vault or untracked local vars. The SFTP
key's public half must already be present in Atlas' managed authorized keys. Rclone mounts the exact
remote path `/pool/media/music` read-only and uses a `15G` full VFS cache; the rootless user manager is
kept alive through systemd lingering.
Configure the Prometheus NPM proxy host for Navidrome as `host.containers.internal:4533`; the
Navidrome port is not opened through firewalld.
Before the first enablement, stop the legacy rootful `navidrome` container. The role refuses to start
the rootless replacement while that container is running and never removes the old container or data.
Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only
`80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached `80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached
from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile
does not provision any `/srv/nextcloud` directories. does not provision any `/srv/nextcloud` directories.
The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard
and the Atlas services are active, configure the current NPM proxy hosts with Navidrome upstream
`http://10.0.0.2:4533` and Syncthing GUI upstream `http://10.0.0.2:8384`. Only the Syncthing web GUI
uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas
WireGuard address. Configure both Syncthing authentication and an appropriate NPM access policy before
publishing its GUI.
Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example: Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example:
```bash ```bash
@@ -138,6 +133,8 @@ ansible-playbook ansible/site.yml --limit prometheus \
``` ```
The target must already provide `server_username` with local sudo access. The target must already provide `server_username` with local sudo access.
Prometheus authorizes its declared SSH public keys through separate files below
`~/.ssh/authorized_keys.d/`, while `sshd` is configured to read those files directly.
### DuckDNS ### DuckDNS
@@ -160,7 +157,7 @@ back in; preserve any uncommitted work separately without copying secrets.
### Data migration ### Data migration
Provision Rocky first, then run the migration script **on the retired Ubuntu source host**. It is Provision Rocky first, then run the migration script **on the retired Ubuntu source host**. It is
dry-run by default and requires an explicit source-stack stop before it can copy PostgreSQL data: dry-run by default and requires an explicit source-stack stop before it can copy application data:
```bash ```bash
sudo ./scripts/migrate_prometheus_data.sh \ sudo ./scripts/migrate_prometheus_data.sh \
@@ -173,11 +170,11 @@ sudo ./scripts/migrate_prometheus_data.sh \
--quiesce-source --execute --quiesce-source --execute
``` ```
The script copies Navidrome, music, Nginx Proxy Manager, PostgreSQL and Gitea data. It does not The script copies only Nginx Proxy Manager and Gitea data. It does not delete data, move
delete data, move Syncthing, copy `/home/git/.ssh`, start containers, update DNS, or perform a Navidrome/Syncthing, copy `/home/git/.ssh`, start containers, update DNS, or perform a cutover. The
cutover. The destination SSH host key must already be trusted and the destination account needs destination SSH host key must already be trusted and the destination account needs passwordless sudo
passwordless sudo for `rsync`. It preserves ACLs but not extended attributes, so source SELinux labels for `rsync`. It preserves ACLs but not extended attributes, so source SELinux labels are not
are not transferred; the Rocky Compose bind mounts apply their own `:Z` labels when containers start. transferred; the Rocky Compose bind mounts apply their own `:Z` labels when containers start.
## DNS Filter ## DNS Filter
@@ -201,6 +198,10 @@ for AdGuard while retaining DNS learned from the router. Define
`vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still requires interactive MFA `vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still requires interactive MFA
initialization after its first deployment. initialization after its first deployment.
New Aegis images create the `admin` account in Butane. Before configuring a newly imaged node, run its
first playbook execution with `-e ansible_user=admin`; the SSH hardening role then permits that same
account. Keep the inventory on `pi` until the existing node has been replaced.
Validate the profile before deployment: Validate the profile before deployment:
```bash ```bash
@@ -210,40 +211,94 @@ ansible-playbook ansible/site.yml --limit aegis --check --diff --ask-become-pass
## NAS ## NAS
`atlas` is a Rocky Linux 9 NAS reached through SSH. Its pool already exists: the profile only `atlas` is a Rocky Linux 9 NAS reached through SSH. Normally its pool already exists and the profile
manages child datasets and must never create, partition, destroy, roll back, or otherwise alter the only manages child datasets. A one-time RAIDZ2 bootstrap is available only with explicit confirmation
pool itself. Linux clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the (`atlas_create_pool=true`) and exactly four verified `/dev/disk/by-id/...` paths in `atlas_zpool_disks`.
configured LAN. It never partitions, forces, destroys, rolls back, or changes the vdev layout of an existing pool. Linux
clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the configured LAN.
For the first run, replace the Atlas host, pool, mount-root, LAN, and Aegis-IP For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and
placeholders and provide `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, `vault_atlas_immich_db_password`. Bootstrap the host through its
`vault_atlas_samba_password`, and `vault_atlas_immich_db_password`. Bootstrap the host through its existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then
existing administrator: include both WireGuard peers in the same idempotent playbook run:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit prometheus,atlas \
-e atlas_connection_username=<existing-admin> -e atlas_connection_username=<existing-admin> \
-e atlas_create_pool=true
``` ```
`vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text The explicit pool gate is safe to repeat: the role creates the RAIDZ2 pool only when it is absent.
Cockpit password. Subsequent runs use `atlas_admin_username`. Enable `atlas_manage_storage` only after WireGuard waits for a real peer handshake before the play continues.
checking the existing pool and mountpoints; enable `atlas_manage_firewall` only after checking the LAN
subnet and active firewalld zone. Enable `atlas_manage_media_stack` last, after validating `/dev/dri`,
the container paths and the Immich database secret.
With storage management enabled, Atlas creates `archive` (`zstd`), `media/music` (`lz4`), `vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
`media/icloud_photos` (`lz4`), and `backups/services` (`lz4`, `refreservation=500G`) beneath the Cockpit password. Subsequent runs use `atlas_admin_username`. Atlas declares storage, sharing, and its
pre-existing pool. The existing Work, Syncthing, and Prometheus-backup datasets remain managed and LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet,
separate. SMB3 exposes `Archive` only to the configured Vault-backed Samba accounts and admits the and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container
configured LAN without host-specific exclusions. NFSv4 exports only paths, and the Immich database secret are validated. Atlas reads its declared SSH public keys from
`media/icloud_photos` to the configured Aegis IP, using `all_squash` with anonymous UID/GID `1100`. separate files below `~/.ssh/authorized_keys.d/`.
With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or
explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`,
`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`,
while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G`
refreservation. Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords,
agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed
Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific
exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using
`all_squash` with anonymous UID/GID `1100`.
The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and
receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL, receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL,
and NPM Quadlets share one Podman network. Immich runs as `1100:1100`; Server and ML receive `/dev/dri`, and NPM Quadlets share one Podman network. Immich runs as `1100:1100`; Server and ML receive `/dev/dri`,
and iCloud Photos is mounted read-only as an external library. NPM publishes ports `80` and `443`; its and Photobook is mounted read-only at `/external/photobook`. NPM publishes ports `80` and `443`; its
administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access. administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access.
Phase 1 is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. It is enabled in the
Atlas host configuration and can be set to `false` only for a deliberate suspension. Official Navidrome `0.63.2` uses its SQLite database below `/data`; it does
not support `ND_DATABASE_URL` or an external PostgreSQL backend. The obsolete `navidromedb` service
was therefore removed from Prometheus instead of being reproduced on Atlas. The role derives all
storage paths from the `zpool` mounted at `/zpool`: music is read-only at
`/zpool/media/music`, Navidrome application state and `navidrome.db` are stored at
`/zpool/archive/app_data/navidrome`, and Syncthing persists at
`/zpool/archive/app_data/syncthing`. `profile_atlas` creates these datasets when
`atlas_manage_storage` is enabled; the backend role verifies their exact mountpoints before starting
containers. The backend role never creates the pool. The separate `wireguard_overlay` role manages `wg0`
between Prometheus (`10.0.0.1`) and Atlas (`10.0.0.2`), generating private keys once
on their respective hosts and exchanging only public keys through Ansible. Prometheus alone opens
`51820/udp` publicly. When the WireGuard zone is created, Ansible reloads firewalld and immediately
reloads Prometheus' rootful Podman networks so the existing proxy stack retains container DNS and
connectivity. Backend ports are admitted only in the WireGuard firewalld zone.
`backend_phase1_start_services` stays false during the application-state transfer, so the first real
backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome
on Prometheus, copy the complete `/opt/navidrome/data/` directory into
`/zpool/archive/app_data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set
this variable to true and rerun the role to enable and start Navidrome and Syncthing. The playbook
never copies or deletes application data.
Validate and render the Atlas services with:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags storage
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1
```
For the cutover, stop the old Navidrome writer before copying its data directory, verify ownership by
the Atlas `admin` account and confirm that the copied SQLite database is present before changing
`backend_phase1_start_services: true` in `host_vars/atlas.yml`. Keep the source data and the stopped
legacy `navidromedb` container until Navidrome on Atlas and a restore test have been validated.
Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls, Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls,
encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests
remain follow-up work. The detailed operational backlog is kept in `AGENTS.md`. remain follow-up work. The detailed operational backlog is kept in `AGENTS.md`.
@@ -332,6 +387,8 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
| `profile_workstation_dev_wsl` | WSL development setup. | | `profile_workstation_dev_wsl` | WSL development setup. |
| `profile_server` | Server setup. | | `profile_server` | Server setup. |
| `profile_atlas` | Rocky Linux 9 NAS setup. | | `profile_atlas` | Rocky Linux 9 NAS setup. |
| `profile_backend_phase1` | Rootless Navidrome and Syncthing on Atlas. |
| `wireguard_overlay` | Prometheus/Atlas WireGuard overlay. |
| `profile_aegis` | Fedora IoT always-on LAN node. | | `profile_aegis` | Fedora IoT always-on LAN node. |
| `dotfiles_common` | Shared user dotfiles. | | `dotfiles_common` | Shared user dotfiles. |
@@ -343,8 +400,10 @@ platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_fedora -> packages_fedora + services_systemd platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd platform_rocky -> packages_rocky + services_systemd
wireguard_overlay -> wireguard_overlay (after platform_rocky)
role_aegis -> profile_aegis role_aegis -> profile_aegis
atlas -> profile_atlas atlas -> profile_atlas
role_backend_phase1 -> profile_backend_phase1 (after atlas)
rocky_server -> dotfiles_common + profile_server (after platform_rocky) rocky_server -> dotfiles_common + profile_server (after platform_rocky)
platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome platform_fedora & desktop_gnome -> profile_desktop_gnome
@@ -414,7 +473,6 @@ ansible-lint ansible/roles/<role>
yamllint ansible/path/to/file.yml yamllint ansible/path/to/file.yml
podman-compose -f /opt/docker/server/docker-compose.yml config podman-compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags rclone,navidrome --check --diff
``` ```
## Tags ## Tags
@@ -430,6 +488,7 @@ ansible-playbook ansible/site.yml --list-tags
| `always` | Common pre-tasks, including optional vault loading. | | `always` | Common pre-tasks, including optional vault loading. |
| `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. | | `ai_agents` | AI coding-agent install, configuration deployment, and managed-binary removal. |
| `atlas` | Atlas NAS account, storage, sharing, and container configuration. | | `atlas` | Atlas NAS account, storage, sharing, and container configuration. |
| `backend_phase1` | Rootless Atlas Navidrome and Syncthing Quadlets. |
| `containers` | Rootful Atlas Quadlets. | | `containers` | Rootful Atlas Quadlets. |
| `dotfiles` | User configuration across all profiles. | | `dotfiles` | User configuration across all profiles. |
| `dotfiles:common` | Shared dotfiles. | | `dotfiles:common` | Shared dotfiles. |
@@ -440,15 +499,14 @@ ansible-playbook ansible/site.yml --list-tags
| `emacs` | Shared Emacs setup and authoring dependencies. | | `emacs` | Shared Emacs setup and authoring dependencies. |
| `gnome` | Fedora/GNOME desktop configuration. | | `gnome` | Fedora/GNOME desktop configuration. |
| `immich` | Atlas Immich account and Quadlets. | | `immich` | Atlas Immich account and Quadlets. |
| `navidrome` | Prometheus rclone mount and rootless Navidrome Quadlet. |
| `npm` | Global npm packages. | | `npm` | Global npm packages. |
| `packages` | Package installation and updates. | | `packages` | Package installation and updates. |
| `podman` | Podman Compose and rootless Quadlet integration. | | `podman` | Podman Compose and rootless Quadlet integration. |
| `rclone` | Prometheus Atlas music mount. |
| `services` | runit and systemd services. | | `services` | runit and systemd services. |
| `sharing` | Atlas NFSv4 and SMB3 configuration. | | `sharing` | Atlas NFSv4 and SMB3 configuration. |
| `storage` | Atlas child ZFS datasets. | | `storage` | Atlas child ZFS datasets. |
| `tmux` | tmux configuration and plugins. | | `tmux` | tmux configuration and plugins. |
| `wireguard` | Prometheus/Atlas WireGuard overlay. |
| `wsl` | WSL bootstrap and configuration. | | `wsl` | WSL bootstrap and configuration. |
## Bootstrapping a new machine ## Bootstrapping a new machine

View File

@@ -9,12 +9,14 @@ variant: fiot
version: 1.0.0 version: 1.0.0
passwd: passwd:
users: users:
- name: pi - name: admin
password_hash: "$6$bNDsU0XC5BxNJS5U$ENDGpdOUPJM3wcXBgNESCQkOqqQ9gN72/xEQoJsAI6QdsaY6mD4G5DBVIv7nHwHQOP35IH1oGRl.Uy3R2iUYQ1" password_hash: "$6$bNDsU0XC5BxNJS5U$ENDGpdOUPJM3wcXBgNESCQkOqqQ9gN72/xEQoJsAI6QdsaY6mD4G5DBVIv7nHwHQOP35IH1oGRl.Uy3R2iUYQ1"
groups: groups:
- wheel - wheel
ssh_authorized_keys: ssh_authorized_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros" - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
systemd: systemd:
units: units:
- name: sshd.service - name: sshd.service

View File

@@ -5,6 +5,9 @@ platform_package_manager: dnf
platform_service_manager: systemd platform_service_manager: systemd
rocky_openzfs_release_rpm: https://zfsonlinux.org/epel/zfs-release-3-0.el9.noarch.rpm rocky_openzfs_release_rpm: https://zfsonlinux.org/epel/zfs-release-3-0.el9.noarch.rpm
rocky_openzfs_gpg_key_url: >-
https://raw.githubusercontent.com/zfsonlinux/zfsonlinux.github.com/master/zfs-release/RPM-GPG-KEY-openzfs-key2
rocky_openzfs_gpg_key_fingerprint: 7DC7 299D CF7C 7FD9 CD87 701B A599 FD5E 9DB8 4141
rocky_syncthing_version: 2.1.3 rocky_syncthing_version: 2.1.3
rocky_syncthing_archive_checksum: sha256:f929eb8e5b72a85543eeeefb2c38f34a68e0c530e70758a2905b78840c76602c rocky_syncthing_archive_checksum: sha256:f929eb8e5b72a85543eeeefb2c38f34a68e0c530e70758a2905b78840c76602c
rocky_syncthing_archive_url: >- rocky_syncthing_archive_url: >-

View File

@@ -6,29 +6,6 @@ effective_username: "{{ server_username }}"
effective_user_group: "{{ server_user_group }}" effective_user_group: "{{ server_user_group }}"
effective_user_home: "{{ server_user_home }}" effective_user_home: "{{ server_user_home }}"
server_container_stack_dir: /opt/docker/server server_container_stack_dir: /opt/docker/server
server_atlas_music_enabled: false
server_atlas_sftp_remote_name: atlas
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP
server_atlas_sftp_port: 22
server_atlas_sftp_username: admin
server_atlas_sftp_remote_path: /pool/media/music
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}"
server_atlas_sftp_private_key_file: /etc/rclone/atlas_ed25519
server_atlas_sftp_known_host: ""
server_atlas_sftp_known_hosts_file: /etc/rclone/known_hosts
server_atlas_wireguard_unit: ""
server_rclone_config_dir: /etc/rclone
server_rclone_config_file: /etc/rclone/rclone.conf
server_rclone_music_service: rclone-music.service
server_rclone_music_mountpoint: /mnt/music_atlas
server_rclone_music_cache_dir: /var/cache/rclone-music
server_navidrome_data_dir: /opt/navidrome/data
server_navidrome_quadlet_dir: "{{ server_user_home }}/.config/containers/systemd"
server_navidrome_env_dir: "{{ server_user_home }}/.config/navidrome"
server_navidrome_env_file: "{{ server_navidrome_env_dir }}/navidrome.env"
server_navidrome_image: docker.io/deluan/navidrome:latest
server_navidrome_port: 4533
server_navidrome_db_password: "{{ vault_navidrome_db_password | default('') }}"
ai_agents: {} ai_agents: {}
vim_plugins_enabled: false vim_plugins_enabled: false
@@ -85,10 +62,6 @@ server_directories:
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- path: "{{ server_navidrome_data_dir }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/npm/data - path: /opt/npm/data
owner: root owner: root
group: root group: root
@@ -97,10 +70,6 @@ server_directories:
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- path: /opt/postgres/data
owner: root
group: root
mode: "0755"
- path: /opt/gitea/data - path: /opt/gitea/data
owner: root owner: root
group: root group: root
@@ -126,3 +95,5 @@ server_sshd_settings:
server_sshd_allow_users: server_sshd_allow_users:
- "{{ server_username }}" - "{{ server_username }}"
server_ssh_authorized_keys: []
server_ssh_authorized_key_directory: "{{ server_user_home }}/.ssh/authorized_keys.d"

View File

@@ -9,5 +9,12 @@ ansible_ssh_use_tty: false
aegis_lan_subnet: 192.168.178.0/24 aegis_lan_subnet: 192.168.178.0/24
aegis_adguard_web_port: 80 aegis_adguard_web_port: 80
aegis_ssh_authorized_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
aegis_icloudpd_apple_id: "{{ vault_aegis_icloudpd_apple_id | default('') }}" aegis_icloudpd_apple_id: "{{ vault_aegis_icloudpd_apple_id | default('') }}"

View File

@@ -1,5 +1,5 @@
--- ---
# Keep Atlas management gates disabled until the NAS and required Vault inputs are ready. # Atlas declares its intended steady state; set a feature flag false only for a deliberate suspension.
hostname: atlas hostname: atlas
ansible_host: 192.168.178.55 ansible_host: 192.168.178.55
ansible_user: "{{ atlas_connection_username }}" ansible_user: "{{ atlas_connection_username }}"
@@ -12,27 +12,61 @@ atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}" atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000 atlas_admin_uid: 1000
atlas_admin_gid: 1000 atlas_admin_gid: 1000
atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}" atlas_admin_ssh_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}" atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_ssh_allow_tcp_forwarding: local
atlas_lan_subnet: 192.168.178.0/24 atlas_lan_subnet: 192.168.178.0/24
atlas_aegis_ip: 192.168.178.54 atlas_aegis_ip: 192.168.178.54
atlas_manage_firewall: false atlas_manage_firewall: true
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: zpool atlas_zfs_pool: zpool
# Populate only for the first pool bootstrap with four real persistent disk paths.
# Confirmed empty 4 TB IronWolf data disks; the NVMe system disk is intentionally excluded.
atlas_zpool_disks:
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1XM
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A73T2
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM
atlas_create_pool: false
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: media/icloud_photos atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool atlas_mount_root: /zpool
atlas_manage_storage: false atlas_manage_storage: true
atlas_manage_sharing: true
atlas_manage_media_stack: false atlas_manage_media_stack: false
wireguard_overlay_enabled: true
wireguard_address: 10.0.0.2/24
wireguard_peers:
- name: prometheus
host: prometheus
endpoint: "{{ hostvars['prometheus']['ansible_host'] }}:{{ hostvars['prometheus']['wireguard_listen_port'] }}"
allowed_ips:
- 10.0.0.0/24
persistent_keepalive: 25
backend_phase1_enabled: true
backend_phase1_start_services: false
backend_phase1_wireguard_address: 10.0.0.2
rocky_manage_openzfs_repo: true rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: true rocky_manage_syncthing_binary: false
rocky_manage_podman: true rocky_manage_podman: true
rocky_podman_packages: rocky_podman_packages:
- podman - podman
@@ -40,14 +74,16 @@ rocky_podman_packages:
host_packages: host_packages:
- cockpit - cockpit
- nfs-utils - nfs-utils
- policycoreutils
- policycoreutils-python-utils - policycoreutils-python-utils
- python3-libselinux
- samba - samba
- samba-client - samba-client
- samba-common-tools - samba-common-tools
- zfs - zfs
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_icloud_photos_mountpoint }}" - path: "{{ atlas_photobook_mountpoint }}"
client: "{{ atlas_aegis_ip }}" client: "{{ atlas_aegis_ip }}"
options: options:
- rw - rw
@@ -59,6 +95,7 @@ atlas_nfs_exports:
atlas_samba_share_name: Archive atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_encryption: required
# Append any pre-existing laptop Unix account here and keep its Samba password in Vault. # Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
atlas_samba_accounts: atlas_samba_accounts:
- username: "{{ atlas_admin_username }}" - username: "{{ atlas_admin_username }}"
@@ -68,12 +105,10 @@ atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') |
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}" atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_firewalld_rich_rules: atlas_firewalld_rich_rules:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="22" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="9090" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" service name="nfs" accept' - 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="445" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="http" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="https" accept'
host_enabled_services: host_enabled_services:
- sshd - sshd

View File

@@ -7,19 +7,29 @@ ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
server_username: rocky server_username: rocky
server_duckdns_domain: fscotto server_duckdns_domain: fscotto
server_atlas_music_enabled: false server_ssh_authorized_keys:
server_atlas_sftp_host: CHANGEME_ATLAS_WIREGUARD_IP - name: ikaros
server_atlas_sftp_username: admin key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
server_atlas_sftp_known_host: CHANGEME_ATLAS_SSH_HOST_KEY - name: nymph
server_atlas_wireguard_unit: wg-quick@wg0.service key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
server_atlas_sftp_private_key: "{{ vault_prometheus_atlas_sftp_private_key | default('') }}" - name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
wireguard_overlay_enabled: true
wireguard_address: 10.0.0.1/24
wireguard_listen_port: 51820
wireguard_enable_ipv4_forwarding: true
wireguard_reload_rootful_podman_networks: true
wireguard_peers:
- name: atlas
host: atlas
allowed_ips:
- 10.0.0.2/32
host_packages: host_packages:
- cockpit - cockpit
- cockpit-navigator - cockpit-navigator
- cockpit-podman - cockpit-podman
- fuse3
- rclone
host_enabled_services: host_enabled_services:
- cockpit.socket - cockpit.socket

View File

@@ -95,3 +95,12 @@ all:
rocky_server: rocky_server:
hosts: hosts:
prometheus: prometheus:
wireguard_overlay:
hosts:
prometheus:
atlas:
role_backend_phase1:
hosts:
atlas:

View File

@@ -27,6 +27,14 @@
name: epel-release name: epel-release
state: present state: present
- name: Import official OpenZFS EL9+ signing key
tags: [packages, storage]
ansible.builtin.rpm_key:
state: present
key: "{{ rocky_openzfs_gpg_key_url }}"
fingerprint: "{{ rocky_openzfs_gpg_key_fingerprint }}"
when: rocky_manage_openzfs_repo | bool
- name: Install official OpenZFS repository package - name: Install official OpenZFS repository package
tags: [packages, storage] tags: [packages, storage]
ansible.builtin.dnf: ansible.builtin.dnf:

View File

@@ -12,5 +12,6 @@ aegis_lan_subnet: CHANGEME_LAN_SUBNET
aegis_firewalld_zone: public aegis_firewalld_zone: public
aegis_adguard_web_port: 80 aegis_adguard_web_port: 80
aegis_ssh_allowed_users: aegis_ssh_allowed_users:
- pi - "{{ ansible_user }}"
aegis_ssh_user_home: "/var/home/{{ ansible_user }}" aegis_ssh_user_home: "/var/home/{{ ansible_user }}"
aegis_ssh_authorized_keys: []

View File

@@ -165,6 +165,29 @@
path: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys" path: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys"
register: aegis_authorized_keys register: aegis_authorized_keys
- name: Ensure Aegis SSH authorized key fragments directory exists
tags: [aegis, ssh, services]
ansible.builtin.file:
path: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys.d"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0700"
when: aegis_ssh_authorized_keys | length > 0
- name: Manage Aegis SSH authorized key fragments
tags: [aegis, ssh, services]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ aegis_ssh_user_home }}/.ssh/authorized_keys.d/{{ item.name }}"
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0600"
loop: "{{ aegis_ssh_authorized_keys }}"
loop_control:
label: "{{ item.name }}"
when: aegis_ssh_authorized_keys | length > 0
- name: Find Aegis SSH authorized key fragments - name: Find Aegis SSH authorized key fragments
tags: [aegis, ssh, services] tags: [aegis, ssh, services]
ansible.builtin.find: ansible.builtin.find:
@@ -178,7 +201,8 @@
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- >- - >-
(aegis_authorized_keys.stat.exists and aegis_authorized_keys.stat.size | int > 0) (aegis_ssh_authorized_keys | length > 0)
or (aegis_authorized_keys.stat.exists and aegis_authorized_keys.stat.size | int > 0)
or aegis_authorized_key_fragments.matched | int > 0 or aegis_authorized_key_fragments.matched | int > 0
fail_msg: Add a public key for the Ansible SSH user before disabling password authentication. fail_msg: Add a public key for the Ansible SSH user before disabling password authentication.

View File

@@ -1,12 +1,19 @@
--- ---
atlas_manage_storage: false atlas_manage_storage: false
atlas_manage_sharing: false
# Destructive first-boot action; normally false once the pool exists.
atlas_create_pool: false
atlas_zpool_disks: []
atlas_admin_username: CHANGEME_ATLAS_ADMIN atlas_admin_username: CHANGEME_ATLAS_ADMIN
atlas_admin_group: "{{ atlas_admin_username }}" atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}" atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000 atlas_admin_uid: 1000
atlas_admin_gid: 1000 atlas_admin_gid: 1000
atlas_admin_ssh_keys: [] atlas_admin_ssh_keys: []
atlas_admin_ssh_key_directory: "{{ atlas_admin_home }}/.ssh/authorized_keys.d"
atlas_admin_password_hash: "!" atlas_admin_password_hash: "!"
# Local forwarding permits an administrator to tunnel a private service without allowing remote forwards.
atlas_ssh_allow_tcp_forwarding: local
atlas_immich_username: immich atlas_immich_username: immich
atlas_immich_group: immich atlas_immich_group: immich
@@ -20,26 +27,51 @@ atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_aegis_ip: CHANGEME_AEGIS_IP atlas_aegis_ip: CHANGEME_AEGIS_IP
atlas_manage_firewall: false atlas_manage_firewall: false
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_hardening_sysctls:
net.ipv4.conf.all.accept_redirects: "0"
net.ipv4.conf.default.accept_redirects: "0"
net.ipv4.conf.all.send_redirects: "0"
net.ipv4.conf.default.send_redirects: "0"
net.ipv4.conf.all.accept_source_route: "0"
net.ipv4.conf.default.accept_source_route: "0"
net.ipv4.conf.all.log_martians: "1"
net.ipv4.conf.default.log_martians: "1"
net.ipv4.conf.all.rp_filter: "2"
net.ipv4.conf.default.rp_filter: "2"
net.ipv4.tcp_syncookies: "1"
net.ipv4.icmp_echo_ignore_broadcasts: "1"
net.ipv4.icmp_ignore_bogus_error_responses: "1"
net.ipv4.ip_forward: "0"
net.ipv6.conf.all.accept_redirects: "0"
net.ipv6.conf.default.accept_redirects: "0"
net.ipv6.conf.all.accept_source_route: "0"
net.ipv6.conf.default.accept_source_route: "0"
atlas_zfs_pool: CHANGEME_ZFS_POOL atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: media/icloud_photos atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}" atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}" atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}" atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}" atlas_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}"
atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}" atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing"
atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync"
atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo
atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo
@@ -51,7 +83,7 @@ atlas_45drives_packages:
- cockpit-scheduler - cockpit-scheduler
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_icloud_photos_mountpoint }}" - path: "{{ atlas_photobook_mountpoint }}"
client: "{{ atlas_aegis_ip }}" client: "{{ atlas_aegis_ip }}"
options: options:
- rw - rw
@@ -62,6 +94,8 @@ atlas_nfs_exports:
- "anongid={{ atlas_immich_gid }}" - "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: Archive atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
# Keep encryption mandatory unless a verified client-compatibility exception is explicitly required.
atlas_samba_encryption: required
atlas_samba_valid_users: [] atlas_samba_valid_users: []
atlas_samba_accounts: [] atlas_samba_accounts: []
atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords

View File

@@ -28,11 +28,6 @@
name: smb name: smb
state: restarted state: restarted
- name: Restart Atlas Syncthing service
ansible.builtin.systemd:
name: atlas-syncthing
state: restarted
- name: Restart Atlas media Quadlets - name: Restart Atlas media Quadlets
ansible.builtin.systemd: ansible.builtin.systemd:
name: "{{ item }}" name: "{{ item }}"

View File

@@ -6,10 +6,11 @@
- atlas_admin_username != 'CHANGEME_ATLAS_ADMIN' - atlas_admin_username != 'CHANGEME_ATLAS_ADMIN'
- (atlas_admin_ssh_keys | default([])) | length > 0 - (atlas_admin_ssh_keys | default([])) | length > 0
- atlas_admin_password_hash not in ['', '!', '*'] - atlas_admin_password_hash not in ['', '!', '*']
- atlas_ssh_allow_tcp_forwarding in ['no', 'yes', 'local', 'remote', 'all']
- "'wheel' not in atlas_immich_supplementary_groups" - "'wheel' not in atlas_immich_supplementary_groups"
fail_msg: >- fail_msg: >-
Define atlas_admin_username, vault_atlas_authorized_ssh_keys and Define atlas_admin_username, atlas_admin_ssh_keys and vault_atlas_admin_password_hash
vault_atlas_admin_password_hash before applying the Atlas profile. before applying the Atlas profile.
no_log: true no_log: true
- name: Create Atlas administrator group - name: Create Atlas administrator group
@@ -77,13 +78,26 @@
mode: "0440" mode: "0440"
validate: "visudo -cf %s" validate: "visudo -cf %s"
- name: Manage Atlas administrator authorized SSH keys exclusively - name: Ensure Atlas administrator SSH authorized key fragments directory exists
tags: [atlas, services] tags: [atlas, services]
ansible.posix.authorized_key: ansible.builtin.file:
user: "{{ atlas_admin_username }}" path: "{{ atlas_admin_ssh_key_directory }}"
key: "{{ atlas_admin_ssh_keys | join('\n') }}" state: directory
state: present owner: "{{ atlas_admin_username }}"
exclusive: true group: "{{ atlas_admin_group }}"
mode: "0700"
- name: Manage Atlas administrator SSH authorized key fragments
tags: [atlas, services]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ atlas_admin_ssh_key_directory }}/{{ item.name }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0600"
loop: "{{ atlas_admin_ssh_keys }}"
loop_control:
label: "{{ item.name }}"
- name: Check whether the Atlas SSH host key exists - name: Check whether the Atlas SSH host key exists
tags: [atlas, services] tags: [atlas, services]
@@ -149,6 +163,18 @@
- "'pubkeyauthentication yes' in atlas_sshd_effective_configuration.stdout_lines" - "'pubkeyauthentication yes' in atlas_sshd_effective_configuration.stdout_lines"
- "'passwordauthentication no' in atlas_sshd_effective_configuration.stdout_lines" - "'passwordauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "'kbdinteractiveauthentication no' in atlas_sshd_effective_configuration.stdout_lines" - "'kbdinteractiveauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "'authenticationmethods publickey' in atlas_sshd_effective_configuration.stdout_lines"
- "'x11forwarding no' in atlas_sshd_effective_configuration.stdout_lines"
- "('allowtcpforwarding ' ~ atlas_ssh_allow_tcp_forwarding) in atlas_sshd_effective_configuration.stdout_lines"
- "'allowagentforwarding no' in atlas_sshd_effective_configuration.stdout_lines"
- "'gatewayports no' in atlas_sshd_effective_configuration.stdout_lines"
- "'permittunnel no' in atlas_sshd_effective_configuration.stdout_lines"
- "'permituserenvironment no' in atlas_sshd_effective_configuration.stdout_lines"
- "'maxauthtries 3' in atlas_sshd_effective_configuration.stdout_lines"
- "'logingracetime 30' in atlas_sshd_effective_configuration.stdout_lines"
- "'clientaliveinterval 300' in atlas_sshd_effective_configuration.stdout_lines"
- "'clientalivecountmax 2' in atlas_sshd_effective_configuration.stdout_lines"
- "'loglevel VERBOSE' in atlas_sshd_effective_configuration.stdout_lines"
- "('allowusers ' ~ atlas_admin_username) in atlas_sshd_effective_configuration.stdout_lines" - "('allowusers ' ~ atlas_admin_username) in atlas_sshd_effective_configuration.stdout_lines"
fail_msg: The effective Atlas SSH configuration does not match the required hardening. fail_msg: The effective Atlas SSH configuration does not match the required hardening.
when: not ansible_check_mode when: not ansible_check_mode

View File

@@ -0,0 +1,45 @@
---
- name: Enforce targeted SELinux on Atlas
tags: [atlas, hardening, security]
ansible.posix.selinux:
policy: targeted
state: enforcing
update_kernel_param: true
register: atlas_selinux_enforcement
- name: Report when Atlas requires a reboot for SELinux enforcement
tags: [atlas, hardening, security]
ansible.builtin.debug:
msg: Reboot Atlas before relying on SELinux enforcement.
when: atlas_selinux_enforcement.reboot_required | default(false)
- name: Apply Atlas network-kernel hardening
tags: [atlas, hardening, security]
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
state: present
sysctl_set: true
reload: true
loop: "{{ atlas_hardening_sysctls | dict2items }}"
loop_control:
label: "{{ item.key }}={{ item.value }}"
- name: Read active Atlas SELinux mode
tags: [atlas, hardening, security]
ansible.builtin.command:
argv:
- getenforce
register: atlas_selinux_mode
changed_when: false
when: not ansible_check_mode
- name: Require active SELinux enforcement on Atlas
tags: [atlas, hardening, security]
ansible.builtin.assert:
that:
- atlas_selinux_mode.stdout == 'Enforcing'
fail_msg: >-
Atlas SELinux is not enforcing. Reboot if requested by the preceding
SELinux task, then rerun the hardening role before enabling services.
when: not ansible_check_mode

View File

@@ -5,6 +5,12 @@
- name: Import Atlas 45Drives tasks - name: Import Atlas 45Drives tasks
ansible.builtin.import_tasks: cockpit.yml ansible.builtin.import_tasks: cockpit.yml
- name: Import Atlas network hardening tasks
ansible.builtin.import_tasks: hardening.yml
- name: Import Atlas pool bootstrap tasks
ansible.builtin.import_tasks: pool.yml
- name: Import Atlas storage tasks - name: Import Atlas storage tasks
ansible.builtin.import_tasks: storage.yml ansible.builtin.import_tasks: storage.yml
@@ -13,6 +19,3 @@
- name: Import Atlas media-container tasks - name: Import Atlas media-container tasks
ansible.builtin.import_tasks: containers.yml ansible.builtin.import_tasks: containers.yml
- name: Import Atlas Syncthing tasks
ansible.builtin.import_tasks: syncthing.yml

View File

@@ -0,0 +1,57 @@
---
- name: Bootstrap Atlas ZFS pool
tags: [atlas, storage, pool]
when: atlas_create_pool | bool
block:
- name: Validate Atlas pool bootstrap inputs
ansible.builtin.assert:
that:
- atlas_zpool_disks | length == 4
- atlas_zpool_disks | unique | length == 4
- atlas_zpool_disks | select('match', '^/dev/disk/by-id/') | list | length == 4
fail_msg: >-
Set exactly four distinct persistent /dev/disk/by-id paths in
atlas_zpool_disks before creating the RAIDZ2 pool.
- name: Inspect declared Atlas pool disks
ansible.builtin.stat:
path: "{{ item }}"
follow: true
loop: "{{ atlas_zpool_disks }}"
loop_control:
label: "{{ item }}"
register: atlas_zpool_disk_stats
- name: Require every declared Atlas pool disk
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isblk | default(false)
fail_msg: "Declared Atlas pool disk is unavailable or is not a block device: {{ item.item }}"
loop: "{{ atlas_zpool_disk_stats.results }}"
loop_control:
label: "{{ item.item }}"
- name: Check whether the Atlas ZFS pool already exists
ansible.builtin.command:
argv:
- zpool
- list
- -H
- -o
- name
- "{{ atlas_zfs_pool }}"
register: atlas_zpool_bootstrap_check
changed_when: false
failed_when: atlas_zpool_bootstrap_check.rc not in [0, 1]
- name: Create the Atlas RAIDZ2 pool when absent
community.general.zpool:
name: "{{ atlas_zfs_pool }}"
state: present
mountpoint: "{{ atlas_mount_root }}"
force: false
vdevs:
- type: raidz2
disks: "{{ atlas_zpool_disks }}"
when: atlas_zpool_bootstrap_check.rc == 1

View File

@@ -1,136 +1,3 @@
---
- name: Render Atlas NFS exports
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas.exports.j2
dest: /etc/exports.d/atlas.exports
owner: root
group: root
mode: "0644"
notify: Reload NFS exports
when: atlas_manage_storage | bool
- name: Enable Atlas file-sharing services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
when: atlas_manage_storage | bool
- name: Configure Atlas NFSv4-only service
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas-nfs.conf.j2
dest: /etc/nfs.conf.d/atlas.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS server
when: atlas_manage_storage | bool
- name: Mask Atlas NFSv3 RPC services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
masked: true
loop:
- rpc-statd.service
- rpcbind.service
- rpcbind.socket
loop_control:
label: "{{ item }}"
when: atlas_manage_storage | bool
- name: Ensure Atlas NFS mount daemon drop-in directory exists
tags: [atlas, sharing, services]
ansible.builtin.file:
path: /etc/systemd/system/nfs-mountd.service.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_storage | bool
- name: Disable Atlas NFSv3 mount daemon listeners
tags: [atlas, sharing, services]
ansible.builtin.template:
src: nfs-mountd-v4only.conf.j2
dest: /etc/systemd/system/nfs-mountd.service.d/v4only.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS mount daemon
when: atlas_manage_storage | bool
- name: Enable SELinux access for Atlas file sharing
tags: [atlas, sharing, services]
ansible.posix.seboolean:
name: "{{ item }}"
state: true
persistent: true
loop: "{{ atlas_selinux_booleans }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_storage | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Atlas Samba configuration
tags: [atlas, sharing]
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: "0644"
validate: "testparm --suppress-prompt %s"
notify: Restart Samba service
when: atlas_manage_storage | bool
- name: Require Vault-backed Atlas Samba accounts
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_accounts | length > 0
- >-
atlas_samba_accounts | map(attribute='username') | list
| difference(atlas_samba_valid_users) | length == 0
- >-
atlas_samba_valid_users
| difference(atlas_samba_accounts | map(attribute='username') | list) | length == 0
- atlas_samba_accounts | selectattr('password', 'equalto', '') | list | length == 0
fail_msg: Define every authorized Samba account and its Vault-backed password.
no_log: true
when: atlas_manage_storage | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker_dir }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_storage | bool
- name: Manage Vault-backed Atlas Samba credentials
tags: [atlas, sharing]
ansible.builtin.include_tasks: samba_account.yml
loop: "{{ atlas_samba_accounts }}"
loop_control:
loop_var: atlas_samba_account
label: "{{ atlas_samba_account.username }}"
no_log: true
when: atlas_manage_storage | bool
- name: Require completed Atlas firewall placeholders - name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services] tags: [atlas, sharing, services]
ansible.builtin.assert: ansible.builtin.assert:
@@ -138,6 +5,7 @@
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' - atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP' - atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0 - atlas_firewalld_zone | length > 0
- ansible_facts.default_ipv4.interface | default('') | length > 0
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders. fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
@@ -154,6 +22,16 @@
label: "{{ item }}" label: "{{ item }}"
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
- name: Assign primary Atlas LAN interface to managed firewalld zone
tags: [atlas, sharing, services]
ansible.posix.firewalld:
interface: "{{ ansible_facts.default_ipv4.interface }}"
zone: "{{ atlas_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
when: atlas_manage_firewall | bool
- name: Remove unrestricted Atlas services from firewalld zone - name: Remove unrestricted Atlas services from firewalld zone
tags: [atlas, sharing, services] tags: [atlas, sharing, services]
ansible.posix.firewalld: ansible.posix.firewalld:
@@ -166,3 +44,228 @@
loop_control: loop_control:
label: "{{ item }}" label: "{{ item }}"
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
- name: Render Atlas NFS exports
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas.exports.j2
dest: /etc/exports.d/atlas.exports
owner: root
group: root
mode: "0644"
notify: Reload NFS exports
when: atlas_manage_sharing | bool
- name: Ensure Atlas NFS configuration drop-in directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: /etc/nfs.conf.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_sharing | bool
- name: Configure Atlas NFSv4-only service
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas-nfs.conf.j2
dest: /etc/nfs.conf.d/atlas.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS server
when: atlas_manage_sharing | bool
- name: Mask Atlas NFSv3 RPC services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
masked: true
loop:
- rpc-statd.service
- rpcbind.service
- rpcbind.socket
loop_control:
label: "{{ item }}"
when: atlas_manage_sharing | bool
- name: Ensure Atlas NFS mount daemon drop-in directory exists
tags: [atlas, sharing, services]
ansible.builtin.file:
path: /etc/systemd/system/nfs-mountd.service.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_sharing | bool
- name: Disable Atlas NFSv3 mount daemon listeners
tags: [atlas, sharing, services]
ansible.builtin.template:
src: nfs-mountd-v4only.conf.j2
dest: /etc/systemd/system/nfs-mountd.service.d/v4only.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS mount daemon
when: atlas_manage_sharing | bool
- name: Enable SELinux access for Atlas file sharing
tags: [atlas, sharing, services]
ansible.posix.seboolean:
name: "{{ item }}"
state: true
persistent: true
loop: "{{ atlas_selinux_booleans }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_sharing | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Atlas Samba configuration
tags: [atlas, sharing]
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: "0644"
validate: "testparm --suppress-prompt %s"
notify: Restart Samba service
when: atlas_manage_sharing | bool
- name: Require Vault-backed Atlas Samba accounts
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_encryption in ['required', 'desired']
- atlas_samba_accounts | length > 0
- >-
atlas_samba_accounts | map(attribute='username') | list
| difference(atlas_samba_valid_users) | length == 0
- >-
atlas_samba_valid_users
| difference(atlas_samba_accounts | map(attribute='username') | list) | length == 0
- atlas_samba_accounts | selectattr('password', 'equalto', '') | list | length == 0
fail_msg: Define every authorized Samba account and its Vault-backed password.
no_log: true
when: atlas_manage_sharing | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker_dir }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_sharing | bool
- name: Manage Vault-backed Atlas Samba credentials
tags: [atlas, sharing]
ansible.builtin.include_tasks: samba_account.yml
loop: "{{ atlas_samba_accounts }}"
loop_control:
loop_var: atlas_samba_account
label: "{{ atlas_samba_account.username }}"
no_log: true
when: atlas_manage_sharing | bool
- name: Enable Atlas file-sharing services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
when: atlas_manage_sharing | bool
- name: Gather effective Atlas firewalld zone configuration
tags: [atlas, sharing, services, security]
ansible.posix.firewalld_info:
zones:
- "{{ atlas_firewalld_zone }}"
register: atlas_firewalld_info
when:
- atlas_manage_firewall | bool
- not ansible_check_mode
- name: Verify effective Atlas firewalld restrictions
tags: [atlas, sharing, services, security]
ansible.builtin.assert:
that:
- atlas_firewalld_zone in atlas_firewalld_info.firewalld_info.zones
- >-
ansible_facts.default_ipv4.interface
in atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].interfaces
- >-
atlas_firewalld_restricted_services
| intersect(atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].services)
| length == 0
fail_msg: >-
The effective Atlas firewalld zone does not restrict the primary LAN
interface and unmanaged service exposure as declared.
when:
- atlas_manage_firewall | bool
- not ansible_check_mode
- name: Apply pending Atlas sharing handlers before verification
tags: [atlas, sharing, services, security]
ansible.builtin.meta: flush_handlers
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Check active Atlas file-sharing services
tags: [atlas, sharing, services, security]
ansible.builtin.command:
argv:
- systemctl
- is-active
- --quiet
- "{{ item }}"
loop:
- nfs-server.service
- smb.service
loop_control:
label: "{{ item }}"
register: atlas_file_sharing_service_activity
changed_when: false
failed_when: false
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Require active Atlas file-sharing services
tags: [atlas, sharing, services, security]
ansible.builtin.assert:
that:
- atlas_file_sharing_service_activity.results | map(attribute='rc') | list == [0, 0]
fail_msg: Atlas NFSv4 or SMB3 did not start after its managed configuration was applied.
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Require Atlas file-sharing TCP listeners
tags: [atlas, sharing, services, security]
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ item }}"
state: started
timeout: 15
loop:
- 2049
- 445
loop_control:
label: "{{ item }}/tcp"
when:
- atlas_manage_sharing | bool
- not ansible_check_mode

View File

@@ -16,13 +16,20 @@
properties: properties:
compression: zstd compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}" mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
mountpoint: "{{ atlas_music_mountpoint }}" mountpoint: "{{ atlas_app_data_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}" group: "{{ atlas_admin_group }}"
properties: properties:
compression: lz4 compression: zstd
mountpoint: "{{ atlas_music_mountpoint }}" mountpoint: "{{ atlas_app_data_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
mountpoint: "{{ atlas_navidrome_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_navidrome_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
mountpoint: "{{ atlas_syncthing_mountpoint }}" mountpoint: "{{ atlas_syncthing_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
@@ -30,19 +37,40 @@
properties: properties:
compression: lz4 compression: lz4
mountpoint: "{{ atlas_syncthing_mountpoint }}" mountpoint: "{{ atlas_syncthing_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_media }}"
mountpoint: "{{ atlas_media_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_media_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
mountpoint: "{{ atlas_music_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_music_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}"
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}" group: "{{ atlas_admin_group }}"
properties: properties:
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}"
mountpoint: "{{ atlas_icloud_photos_mountpoint }}" mountpoint: "{{ atlas_photobook_mountpoint }}"
owner: "{{ atlas_immich_username }}" owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}" group: "{{ atlas_immich_group }}"
properties: properties:
compression: lz4 compression: lz4
mountpoint: "{{ atlas_icloud_photos_mountpoint }}" mountpoint: "{{ atlas_photobook_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backups }}"
mountpoint: "{{ atlas_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_backups_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}"
mountpoint: "{{ atlas_service_backups_mountpoint }}" mountpoint: "{{ atlas_service_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"

View File

@@ -1,40 +0,0 @@
---
- name: Create Atlas Syncthing configuration directory
tags: [atlas, syncthing]
ansible.builtin.file:
path: "{{ atlas_syncthing_config_dir }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0700"
when: atlas_manage_storage | bool
- name: Create Atlas Syncthing default data directory
tags: [atlas, syncthing]
ansible.builtin.file:
path: "{{ atlas_syncthing_default_dir }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0770"
when: atlas_manage_storage | bool
- name: Render Atlas Syncthing systemd service
tags: [atlas, syncthing]
ansible.builtin.template:
src: atlas-syncthing.service.j2
dest: /etc/systemd/system/atlas-syncthing.service
owner: root
group: root
mode: "0644"
notify: Restart Atlas Syncthing service
when: atlas_manage_storage | bool
- name: Enable Atlas Syncthing service
tags: [atlas, syncthing]
ansible.builtin.systemd:
name: atlas-syncthing
enabled: true
state: started
daemon_reload: true
when: atlas_manage_storage | bool

View File

@@ -3,4 +3,22 @@ PermitRootLogin no
PubkeyAuthentication yes PubkeyAuthentication yes
PasswordAuthentication no PasswordAuthentication no
KbdInteractiveAuthentication no KbdInteractiveAuthentication no
AuthenticationMethods publickey
AuthorizedKeysFile {{
atlas_admin_ssh_keys
| map(attribute='name')
| map('regex_replace', '^', '%h/.ssh/authorized_keys.d/')
| join(' ')
}}
X11Forwarding no
AllowTcpForwarding {{ atlas_ssh_allow_tcp_forwarding }}
AllowAgentForwarding no
GatewayPorts no
PermitTunnel no
PermitUserEnvironment no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
AllowUsers {{ atlas_admin_username }} AllowUsers {{ atlas_admin_username }}

View File

@@ -16,7 +16,7 @@ Network=atlas-media.network
NetworkAlias=atlas-immich-server NetworkAlias=atlas-immich-server
EnvironmentFile=/etc/immich/immich.env EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_upload_dir }}:/data:Z Volume={{ atlas_immich_upload_dir }}:/data:Z
Volume={{ atlas_icloud_photos_mountpoint }}:/external/icloud_photos:ro,Z Volume={{ atlas_photobook_mountpoint }}:/external/photobook:ro,Z
Volume=/etc/localtime:/etc/localtime:ro Volume=/etc/localtime:/etc/localtime:ro
NoNewPrivileges=true NoNewPrivileges=true
DropCapability=NET_RAW DropCapability=NET_RAW

View File

@@ -1,16 +0,0 @@
[Unit]
Description=Atlas Syncthing service
After=network-online.target
Wants=network-online.target
[Service]
User={{ atlas_admin_username }}
Group={{ atlas_admin_group }}
WorkingDirectory={{ atlas_syncthing_mountpoint }}
Environment=HOME={{ atlas_syncthing_mountpoint }}
ExecStart=/usr/local/bin/syncthing serve --no-browser --no-restart --no-upgrade --home={{ atlas_syncthing_config_dir }}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -4,6 +4,10 @@
security = user security = user
map to guest = Never map to guest = Never
server min protocol = SMB3 server min protocol = SMB3
server signing = mandatory
smb encrypt = {{ atlas_samba_encryption }}
disable netbios = yes
smb ports = 445
hosts allow = {{ atlas_lan_subnet }} hosts allow = {{ atlas_lan_subnet }}
include = registry include = registry

View File

@@ -0,0 +1,28 @@
---
backend_phase1_enabled: false
backend_phase1_start_services: false
backend_phase1_username: "{{ atlas_admin_username }}"
backend_phase1_user_group: "{{ atlas_admin_group }}"
backend_phase1_user_home: "{{ atlas_admin_home }}"
backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd"
backend_phase1_wireguard_interface: wg0
backend_phase1_wireguard_address: CHANGEME_ATLAS_WIREGUARD_ADDRESS
backend_phase1_wireguard_firewalld_zone: wireguard
backend_phase1_music_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_app_data_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
backend_phase1_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
backend_phase1_music_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_app_data_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
backend_phase1_navidrome_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
backend_phase1_navidrome_image: docker.io/deluan/navidrome:0.63.2
backend_phase1_syncthing_image: docker.io/syncthing/syncthing:2.1.3
backend_phase1_navidrome_port: 4533
backend_phase1_syncthing_gui_port: 8384
backend_phase1_syncthing_transfer_port: 22000
backend_phase1_syncthing_discovery_port: 21027
backend_phase1_timezone: Europe/Rome
backend_phase1_services:
- atlas-navidrome.service
- atlas-syncthing.service

View File

@@ -0,0 +1,32 @@
---
- name: Restart Atlas Navidrome Quadlet
tags: [backend_phase1, navidrome, podman, services]
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: atlas-navidrome.service
scope: user
state: restarted
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when:
- backend_phase1_enabled | bool
- backend_phase1_start_services | bool
- not ansible_check_mode
- name: Restart Atlas Syncthing Quadlet
tags: [backend_phase1, syncthing, podman, services]
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: atlas-syncthing.service
scope: user
state: restarted
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when:
- backend_phase1_enabled | bool
- backend_phase1_start_services | bool
- not ansible_check_mode

View File

@@ -0,0 +1,163 @@
---
- name: Configure Atlas phase-one backend services
tags: [backend_phase1, podman]
when: backend_phase1_enabled | bool
block:
- name: Validate phase-one backend inputs
ansible.builtin.assert:
that:
- not (atlas_manage_media_stack | bool)
- backend_phase1_wireguard_interface in ansible_facts.interfaces
- backend_phase1_wireguard_address != 'CHANGEME_ATLAS_WIREGUARD_ADDRESS'
- backend_phase1_music_dir.startswith('/')
- backend_phase1_app_data_root.startswith('/')
- backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/')
- backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/')
fail_msg: >-
Disable the rootful media-stack gate and provide the active
WireGuard interface/address and absolute ZFS-backed paths before
enabling phase one. This role does not manage Prometheus or migrate
Navidrome application data.
- name: Read the rootless service account
ansible.builtin.getent:
database: passwd
key: "{{ backend_phase1_username }}"
- name: Record rootless service account IDs
ansible.builtin.set_fact:
backend_phase1_uid: "{{ ansible_facts['getent_passwd'][backend_phase1_username][1] }}"
backend_phase1_gid: "{{ ansible_facts['getent_passwd'][backend_phase1_username][2] }}"
- name: Read system service state before starting rootless Syncthing
ansible.builtin.service_facts:
- name: Refuse to overlap a system-level Atlas Syncthing service
ansible.builtin.assert:
that:
- >-
ansible_facts.services['atlas-syncthing.service'] is not defined
or ansible_facts.services['atlas-syncthing.service'].state != 'running'
fail_msg: >-
Remove or stop the system-level atlas-syncthing.service before
enabling the rootless Syncthing Quadlet.
- name: Inspect required ZFS datasets
community.general.zfs_facts:
name: "{{ item.dataset }}"
properties: name,mounted,mountpoint
loop:
- dataset: "{{ backend_phase1_music_dataset }}"
mountpoint: "{{ backend_phase1_music_dir }}"
- dataset: "{{ backend_phase1_app_data_dataset }}"
mountpoint: "{{ backend_phase1_app_data_root }}"
- dataset: "{{ backend_phase1_navidrome_dataset }}"
mountpoint: "{{ backend_phase1_navidrome_data_dir }}"
- dataset: "{{ backend_phase1_syncthing_dataset }}"
mountpoint: "{{ backend_phase1_syncthing_root }}"
loop_control:
label: "{{ item.dataset }}"
register: backend_phase1_zfs_facts
- name: Require mounted datasets at the declared paths
ansible.builtin.assert:
that:
- item.ansible_facts.ansible_zfs_datasets | length == 1
- item.ansible_facts.ansible_zfs_datasets[0].mounted == 'yes'
- item.ansible_facts.ansible_zfs_datasets[0].mountpoint == item.item.mountpoint
fail_msg: >-
{{ item.item.dataset }} must already be mounted at
{{ item.item.mountpoint }}. The phase-one role never creates or
imports the pool.
loop: "{{ backend_phase1_zfs_facts.results }}"
loop_control:
label: "{{ item.item.dataset }}"
- name: Enable lingering for the rootless service account
ansible.builtin.command:
argv:
- loginctl
- enable-linger
- "{{ backend_phase1_username }}"
creates: "/var/lib/systemd/linger/{{ backend_phase1_username }}"
- name: Start the rootless user systemd manager
ansible.builtin.systemd:
name: "user@{{ backend_phase1_uid }}.service"
state: started
when: not ansible_check_mode
- name: Create rootless Quadlet and application directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "{{ item.mode }}"
loop:
- path: "{{ backend_phase1_quadlet_dir }}"
mode: "0700"
- path: "{{ backend_phase1_navidrome_data_dir }}"
mode: "0750"
- path: "{{ backend_phase1_syncthing_root }}"
mode: "0750"
loop_control:
label: "{{ item.path }}"
- name: Render the rootless Navidrome Quadlet
ansible.builtin.template:
src: atlas-navidrome.container.j2
dest: "{{ backend_phase1_quadlet_dir }}/atlas-navidrome.container"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
notify: Restart Atlas Navidrome Quadlet
- name: Render the rootless Syncthing Quadlet
ansible.builtin.template:
src: atlas-syncthing.container.j2
dest: "{{ backend_phase1_quadlet_dir }}/atlas-syncthing.container"
owner: "{{ backend_phase1_username }}"
group: "{{ backend_phase1_user_group }}"
mode: "0644"
notify: Restart Atlas Syncthing Quadlet
- name: Reload the rootless user systemd manager
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
scope: user
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when: not ansible_check_mode
- name: Permit phase-one services only through the WireGuard zone
ansible.posix.firewalld:
port: "{{ item }}"
zone: "{{ backend_phase1_wireguard_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop:
- "{{ backend_phase1_navidrome_port }}/tcp"
- "{{ backend_phase1_syncthing_gui_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/udp"
- "{{ backend_phase1_syncthing_discovery_port }}/udp"
- name: Start rootless phase-one Quadlets
become_user: "{{ backend_phase1_username }}"
ansible.builtin.systemd:
name: "{{ item }}"
scope: user
state: started
enabled: true
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ backend_phase1_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
loop: "{{ backend_phase1_services }}"
when:
- backend_phase1_start_services | bool
- not ansible_check_mode

View File

@@ -0,0 +1,28 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas rootless Navidrome backend
[Container]
ContainerName=atlas-navidrome
Image={{ backend_phase1_navidrome_image }}
UserNS=keep-id
User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_navidrome_port }}:4533
Environment=ND_LOGLEVEL=info
Environment=ND_SCANSCHEDULE=1h
Environment=ND_SESSIONTIMEOUT=24h
Environment=ND_ENABLETRANSCODING=true
Environment=TZ={{ backend_phase1_timezone }}
Volume={{ backend_phase1_navidrome_data_dir }}:/data:Z
Volume={{ backend_phase1_music_dir }}:/music:ro,z
NoNewPrivileges=true
DropCapability=all
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,30 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas rootless Syncthing backend
[Container]
ContainerName=atlas-syncthing
Image={{ backend_phase1_syncthing_image }}
HostName=atlas-syncthing
UserNS=keep-id
User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}/udp
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
Environment=HOME=/var/syncthing
Environment=STHOMEDIR=/var/syncthing/config
Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }}
Environment=TZ={{ backend_phase1_timezone }}
Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z
NoNewPrivileges=true
DropCapability=all
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -9,29 +9,3 @@
tags: [services] tags: [services]
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true
- name: Restart rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
state: restarted
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Restart rootless Navidrome
tags: [services, podman, rclone, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: restarted
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -8,17 +8,6 @@
fail_msg: >- fail_msg: >-
server_firewall_backend must be firewalld for the Rocky server profile. server_firewall_backend must be firewalld for the Rocky server profile.
- name: Require server database secret variables
tags: [dotfiles, dotfiles:server, services, navidrome]
ansible.builtin.assert:
that:
- server_navidrome_db_password | length > 0
- (vault_postgres_root_password | default('')) | length > 0
fail_msg: >-
Define vault_navidrome_db_password and vault_postgres_root_password in Vault
before rendering the Navidrome database configuration.
no_log: true
- name: Configure DuckDNS updater - name: Configure DuckDNS updater
tags: [dotfiles, dotfiles:server, duckdns] tags: [dotfiles, dotfiles:server, duckdns]
ansible.builtin.import_tasks: duckdns.yml ansible.builtin.import_tasks: duckdns.yml
@@ -72,11 +61,41 @@
tags: [services, podman] tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml ansible.builtin.include_tasks: podman-compose.yml
- name: Configure Atlas music mount - name: Ensure server SSH authorized key fragments directory exists
ansible.builtin.import_tasks: rclone-music.yml tags: [services, ssh]
ansible.builtin.file:
path: "{{ server_ssh_authorized_key_directory }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_ssh_authorized_keys | length > 0
- name: Configure rootless Navidrome - name: Manage server SSH authorized key fragments
ansible.builtin.import_tasks: navidrome.yml tags: [services, ssh]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ server_ssh_authorized_key_directory }}/{{ item.name }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
loop: "{{ server_ssh_authorized_keys }}"
loop_control:
label: "{{ item.name }}"
when: server_ssh_authorized_keys | length > 0
- name: Configure server SSH authorized key fragments
tags: [services, ssh]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*AuthorizedKeysFile\s+'
line: >-
AuthorizedKeysFile {{ server_ssh_authorized_keys | map(attribute='name')
| map('regex_replace', '^', '%h/.ssh/authorized_keys.d/') | join(' ') }}
state: present
validate: "sshd -t -f %s"
notify: Reload SSH service
when: server_ssh_authorized_keys | length > 0
- name: Disable SSH root login on server - name: Disable SSH root login on server
tags: [services] tags: [services]

View File

@@ -1,114 +0,0 @@
---
- name: Read server account details for rootless Navidrome
tags: [services, podman, rclone, navidrome]
ansible.builtin.getent:
database: passwd
key: "{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Check for a running legacy rootful Navidrome container
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- podman
- container
- inspect
- --format
- "{{ '{{.State.Running}}' }}"
- navidrome
register: server_legacy_navidrome_container
changed_when: false
failed_when: false
check_mode: false
when: server_atlas_music_enabled | bool
- name: Refuse concurrent legacy and rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.assert:
that:
- >-
server_legacy_navidrome_container.rc != 0
or server_legacy_navidrome_container.stdout | trim != 'true'
fail_msg: >-
Stop the legacy rootful Navidrome container before enabling the rootless Quadlet.
The playbook does not remove the old container or its data automatically.
when: server_atlas_music_enabled | bool
- name: Enable lingering for rootless Navidrome
tags: [services, podman, navidrome]
ansible.builtin.command:
argv:
- loginctl
- enable-linger
- "{{ server_username }}"
creates: "/var/lib/systemd/linger/{{ server_username }}"
when: server_atlas_music_enabled | bool
- name: Start server user systemd manager
tags: [services, podman, navidrome]
ansible.builtin.systemd:
name: "user@{{ ansible_facts['getent_passwd'][server_username][1] }}.service"
state: started
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode
- name: Create rootless Navidrome Quadlet directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_quadlet_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Create private rootless Navidrome configuration directory
tags: [services, podman, navidrome]
ansible.builtin.file:
path: "{{ server_navidrome_env_dir }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_atlas_music_enabled | bool
- name: Render Vault-backed rootless Navidrome environment
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.env.j2
dest: "{{ server_navidrome_env_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render rootless Navidrome Quadlet
tags: [services, podman, navidrome]
ansible.builtin.template:
src: navidrome.container.j2
dest: "{{ server_navidrome_quadlet_dir }}/navidrome.container"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0644"
notify: Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Start rootless Navidrome Quadlet
tags: [services, podman, navidrome]
become_user: "{{ server_username }}"
ansible.builtin.systemd:
name: navidrome.service
scope: user
state: started
daemon_reload: true
environment:
XDG_RUNTIME_DIR: "/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}"
DBUS_SESSION_BUS_ADDRESS: >-
unix:path=/run/user/{{ ansible_facts['getent_passwd'][server_username][1] }}/bus
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -1,105 +0,0 @@
---
- name: Require completed Atlas SFTP mount configuration
tags: [services, rclone, navidrome]
ansible.builtin.assert:
that:
- server_atlas_sftp_host != 'CHANGEME_ATLAS_WIREGUARD_IP'
- server_atlas_sftp_known_host != 'CHANGEME_ATLAS_SSH_HOST_KEY'
- server_atlas_sftp_known_host | length > 0
- server_atlas_sftp_private_key | length > 0
- server_atlas_sftp_remote_path == '/pool/media/music'
fail_msg: >-
Define the Atlas WireGuard address, pinned SSH host key and Vault-backed
SFTP private key before enabling the music mount.
no_log: true
when: server_atlas_music_enabled | bool
- name: Create private rclone configuration directory
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ server_rclone_config_dir }}"
state: directory
owner: root
group: "{{ server_user_group }}"
mode: "0750"
when: server_atlas_music_enabled | bool
- name: Install Vault-backed Atlas SFTP private key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_private_key | trim }}\n"
dest: "{{ server_atlas_sftp_private_key_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
no_log: true
diff: false
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Install pinned Atlas SSH host key
tags: [services, rclone, navidrome]
ansible.builtin.copy:
content: "{{ server_atlas_sftp_known_host | trim }}\n"
dest: "{{ server_atlas_sftp_known_hosts_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0640"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Render Atlas SFTP rclone configuration
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone.conf.j2
dest: "{{ server_rclone_config_file }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Create rclone music directories
tags: [services, rclone, navidrome]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0750"
loop:
- "{{ server_rclone_music_mountpoint }}"
- "{{ server_rclone_music_cache_dir }}"
loop_control:
label: "{{ item }}"
when: server_atlas_music_enabled | bool
- name: Render rclone music system service
tags: [services, rclone, navidrome]
ansible.builtin.template:
src: rclone-music.service.j2
dest: "/etc/systemd/system/{{ server_rclone_music_service }}"
owner: root
group: root
mode: "0644"
notify:
- Restart rclone music mount
- Restart rootless Navidrome
when: server_atlas_music_enabled | bool
- name: Enable and start rclone music mount
tags: [services, rclone, navidrome]
ansible.builtin.systemd:
name: "{{ server_rclone_music_service }}"
enabled: true
state: started
daemon_reload: true
when:
- server_atlas_music_enabled | bool
- not ansible_check_mode

View File

@@ -1,21 +0,0 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Rootless Navidrome music server
[Container]
ContainerName=navidrome
Image={{ server_navidrome_image }}
Network=host
EnvironmentFile={{ server_navidrome_env_file }}
Volume={{ server_navidrome_data_dir }}:/data
Volume={{ server_rclone_music_mountpoint }}:/music:ro
SecurityLabelDisable=true
[Service]
ExecStartPre=/usr/bin/mountpoint -q {{ server_rclone_music_mountpoint }}
Restart=always
RestartSec=10s
TimeoutStartSec=900
[Install]
WantedBy=default.target

View File

@@ -1,7 +0,0 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
ND_DATABASE_URL={{ ('postgres://navidrome:' ~ server_navidrome_db_password ~ '@127.0.0.1:5432/navidrome_db?sslmode=disable') | to_json }}
ND_PORT={{ server_navidrome_port }}
ND_SCANSCHEDULE="1h"
ND_SESSIONTIMEOUT="24h"
ND_ENABLETRANSCODING="true"
ND_LOGLEVEL="info"

View File

@@ -1,29 +0,0 @@
[Unit]
Description=Read-only Atlas music mount via rclone SFTP
Wants=network-online.target
After=network-online.target
{% if server_atlas_wireguard_unit | length > 0 %}
Wants={{ server_atlas_wireguard_unit }}
After={{ server_atlas_wireguard_unit }}
{% endif %}
[Service]
Type=notify
User={{ server_username }}
Group={{ server_user_group }}
ExecStart=/usr/bin/rclone mount \
{{ server_atlas_sftp_remote_name }}:{{ server_atlas_sftp_remote_path }} \
{{ server_rclone_music_mountpoint }} \
--config {{ server_rclone_config_file }} \
--cache-dir {{ server_rclone_music_cache_dir }} \
--read-only \
--vfs-cache-mode full \
--vfs-cache-max-size 15G \
--vfs-read-chunk-size 5M \
--dir-cache-time 24h
ExecStop=-/usr/bin/fusermount3 -uz {{ server_rclone_music_mountpoint }}
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=multi-user.target

View File

@@ -1,9 +0,0 @@
# Managed by Ansible. Authentication uses the separately deployed Vault-backed key.
[{{ server_atlas_sftp_remote_name }}]
type = sftp
host = {{ server_atlas_sftp_host }}
user = {{ server_atlas_sftp_username }}
port = {{ server_atlas_sftp_port }}
key_file = {{ server_atlas_sftp_private_key_file }}
known_hosts_file = {{ server_atlas_sftp_known_hosts_file }}
shell_type = unix

View File

@@ -0,0 +1,18 @@
---
wireguard_overlay_enabled: false
wireguard_interface: wg0
wireguard_config_dir: /etc/wireguard
wireguard_private_key_path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.key"
wireguard_config_path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.conf"
wireguard_address: CHANGEME_WIREGUARD_ADDRESS
wireguard_listen_port: 0
wireguard_mtu: 1420
wireguard_firewalld_zone: wireguard
wireguard_public_firewalld_zone: public
wireguard_enable_ipv4_forwarding: false
wireguard_reload_rootful_podman_networks: false
wireguard_handshake_retries: 12
wireguard_handshake_delay: 5
wireguard_peers: []
wireguard_packages:
- wireguard-tools

View File

@@ -0,0 +1,10 @@
---
- name: Restart WireGuard interface
tags: [wireguard, services]
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}.service"
state: restarted
daemon_reload: true
when:
- wireguard_overlay_enabled | bool
- not ansible_check_mode

View File

@@ -0,0 +1,192 @@
---
- name: Configure WireGuard overlay
tags: [wireguard, services]
when: wireguard_overlay_enabled | bool
block:
- name: Validate WireGuard host configuration
ansible.builtin.assert:
that:
- wireguard_address != 'CHANGEME_WIREGUARD_ADDRESS'
- wireguard_address is match('^[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}$')
- wireguard_peers | length > 0
- wireguard_peers | map(attribute='host') | difference(ansible_play_hosts_all) | length == 0
fail_msg: >-
Configure this host's WireGuard address and peers, and run the first
key bootstrap against every peer in the same play.
- name: Install WireGuard userspace tools
ansible.builtin.dnf:
name: "{{ wireguard_packages }}"
state: present
- name: Create private WireGuard configuration directory
ansible.builtin.file:
path: "{{ wireguard_config_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Check for an existing WireGuard private key
ansible.builtin.stat:
path: "{{ wireguard_private_key_path }}"
register: wireguard_private_key_stat
- name: Generate a missing WireGuard private key
ansible.builtin.command:
argv:
- wg
- genkey
register: wireguard_generated_private_key
changed_when: true
no_log: true
when:
- not wireguard_private_key_stat.stat.exists
- not ansible_check_mode
- name: Persist the generated WireGuard private key
ansible.builtin.copy:
content: "{{ wireguard_generated_private_key.stdout }}\n"
dest: "{{ wireguard_private_key_path }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- not wireguard_private_key_stat.stat.exists
- not ansible_check_mode
- name: Require a private key during check mode
ansible.builtin.assert:
that:
- wireguard_private_key_stat.stat.exists
fail_msg: >-
The initial WireGuard key generation cannot be simulated safely in
check mode. Run the gated WireGuard play once without --check.
when: ansible_check_mode
- name: Read the persisted WireGuard private key
ansible.builtin.slurp:
src: "{{ wireguard_private_key_path }}"
register: wireguard_private_key_material
no_log: true
- name: Derive this host's WireGuard public key
ansible.builtin.command:
argv:
- wg
- pubkey
stdin: "{{ wireguard_private_key_material.content | b64decode | trim }}"
register: wireguard_derived_public_key
changed_when: false
no_log: true
- name: Publish this host's WireGuard public key
ansible.builtin.set_fact:
wireguard_public_key: "{{ wireguard_derived_public_key.stdout }}"
- name: Require every peer's generated public key
ansible.builtin.assert:
that:
- hostvars[item.host].wireguard_public_key is defined
- hostvars[item.host].wireguard_public_key | length > 0
fail_msg: >-
The public key for {{ item.host }} is unavailable. The first
WireGuard run must include every overlay host.
loop: "{{ wireguard_peers }}"
loop_control:
label: "{{ item.name }}"
- name: Render the private WireGuard interface configuration
ansible.builtin.template:
src: wg.conf.j2
dest: "{{ wireguard_config_path }}"
owner: root
group: root
mode: "0600"
diff: false
no_log: true
notify: Restart WireGuard interface
- name: Enable IPv4 forwarding for the ingress host
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: "1"
state: present
sysctl_set: true
reload: true
when: wireguard_enable_ipv4_forwarding | bool
- name: Create the WireGuard firewalld zone
ansible.posix.firewalld:
zone: "{{ wireguard_firewalld_zone }}"
state: present
permanent: true
register: wireguard_firewalld_zone_result
- name: Reload firewalld after creating the WireGuard zone
ansible.builtin.systemd:
name: firewalld.service
state: reloaded
when:
- wireguard_firewalld_zone_result is changed
- not ansible_check_mode
- name: Restore rootful Podman networking after firewalld reload
ansible.builtin.command:
argv:
- podman
- network
- reload
- --all
register: wireguard_podman_network_reload
changed_when: wireguard_podman_network_reload.stdout_lines | length > 0
when:
- wireguard_firewalld_zone_result is changed
- wireguard_reload_rootful_podman_networks | bool
- not ansible_check_mode
- name: Assign the WireGuard interface to its firewalld zone
ansible.posix.firewalld:
interface: "{{ wireguard_interface }}"
zone: "{{ wireguard_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
- name: Permit this host's public WireGuard listener
ansible.posix.firewalld:
port: "{{ wireguard_listen_port }}/udp"
zone: "{{ wireguard_public_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
when: wireguard_listen_port | int > 0
- name: Enable the WireGuard interface
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}.service"
enabled: true
state: started
daemon_reload: true
when: not ansible_check_mode
- name: Apply pending WireGuard handlers before verification
ansible.builtin.meta: flush_handlers
when: not ansible_check_mode
- name: Wait for every WireGuard peer handshake
ansible.builtin.command:
argv:
- wg
- show
- "{{ wireguard_interface }}"
- latest-handshakes
register: wireguard_latest_handshakes
changed_when: false
retries: "{{ wireguard_handshake_retries }}"
delay: "{{ wireguard_handshake_delay }}"
until:
- wireguard_latest_handshakes.stdout_lines | length == wireguard_peers | length
- wireguard_latest_handshakes.stdout_lines | select('search', '\t0$') | list | length == 0
when: not ansible_check_mode

View File

@@ -0,0 +1,25 @@
# Managed by Ansible. Do not edit manually.
[Interface]
Address = {{ wireguard_address }}
PrivateKey = {{ wireguard_private_key_material.content | b64decode | trim }}
MTU = {{ wireguard_mtu }}
SaveConfig = false
{% if wireguard_listen_port | int > 0 %}
ListenPort = {{ wireguard_listen_port }}
{% endif %}
{% for peer in wireguard_peers %}
[Peer]
# {{ peer.name }}
PublicKey = {{ hostvars[peer.host].wireguard_public_key }}
AllowedIPs = {{ peer.allowed_ips | join(', ') }}
{% if peer.endpoint is defined %}
Endpoint = {{ peer.endpoint }}
{% endif %}
{% if peer.persistent_keepalive | default(0) | int > 0 %}
PersistentKeepalive = {{ peer.persistent_keepalive }}
{% endif %}
{% if not loop.last %}
{% endif %}
{% endfor %}

View File

@@ -73,6 +73,13 @@
- packages_rocky - packages_rocky
- services_systemd - services_systemd
- name: Configure WireGuard overlay
hosts: wireguard_overlay
become: true
roles:
- wireguard_overlay
- name: Configure Aegis Fedora IoT profile - name: Configure Aegis Fedora IoT profile
hosts: role_aegis hosts: role_aegis
become: true become: true
@@ -87,6 +94,13 @@
roles: roles:
- profile_atlas - profile_atlas
- name: Configure Atlas phase-one backend services
hosts: role_backend_phase1
become: true
roles:
- profile_backend_phase1
- name: Configure Rocky Linux server - name: Configure Rocky Linux server
hosts: rocky_server hosts: rocky_server
become: true become: true

View File

@@ -38,22 +38,6 @@ services:
# networks: # networks:
# - web # - web
navidromedb:
image: docker.io/library/postgres:13
container_name: navidromedb
restart: unless-stopped
mem_limit: 2048m
environment:
POSTGRES_DB: "navidrome_db"
POSTGRES_USER: "navidrome"
POSTGRES_PASSWORD: "{{ vault_postgres_root_password }}"
volumes:
- "/opt/postgres/data:/var/lib/postgresql/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports:
- "127.0.0.1:5432:5432"
networks:
- web
gitea: gitea:
image: docker.gitea.com/gitea:1.25.2 image: docker.gitea.com/gitea:1.25.2
container_name: gitea container_name: gitea

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Copy the persistent Docker data from the retired Ubuntu server to the Rocky # Copy the persistent NPM and Gitea data from the retired Ubuntu server to the Rocky
# replacement. Run this script on the Ubuntu source as root. It is a dry run # replacement. Run this script on the Ubuntu source as root. It is a dry run
# unless --execute and --quiesce-source are both supplied. Extended attributes # unless --execute and --quiesce-source are both supplied. Extended attributes
# are deliberately not copied: Rocky must assign its own SELinux labels. # are deliberately not copied: Rocky must assign its own SELinux labels.
@@ -14,11 +14,8 @@ EXECUTE=false
QUIESCE_SOURCE=false QUIESCE_SOURCE=false
DATA_PATHS=' DATA_PATHS='
/opt/navidrome/data
/opt/music
/opt/npm/data /opt/npm/data
/opt/npm/letsencrypt /opt/npm/letsencrypt
/opt/postgres/data
/opt/gitea/data /opt/gitea/data
' '
@@ -26,8 +23,8 @@ usage() {
cat <<'EOF' cat <<'EOF'
Usage: sudo ./scripts/migrate_prometheus_data.sh --destination USER@HOST [options] Usage: sudo ./scripts/migrate_prometheus_data.sh --destination USER@HOST [options]
Copies persistent Navidrome, Nginx Proxy Manager, PostgreSQL and Gitea data to Copies persistent Nginx Proxy Manager and Gitea data to the Rocky server with
the Rocky server with rsync. The destination Docker containers must be stopped. rsync. The destination Docker containers must be stopped.
Options: Options:
--destination USER@HOST Rocky SSH destination (required). --destination USER@HOST Rocky SSH destination (required).
@@ -97,7 +94,7 @@ if [ -n "$IDENTITY_FILE" ]; then
fi fi
if [ "$EXECUTE" = true ] && [ "$QUIESCE_SOURCE" != true ]; then if [ "$EXECUTE" = true ] && [ "$QUIESCE_SOURCE" != true ]; then
fail '--execute requires --quiesce-source to keep PostgreSQL data consistent' fail '--execute requires --quiesce-source to keep application data consistent'
fi fi
require_command rsync require_command rsync

View File

@@ -1,61 +1,78 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
38633239386338323332633564613266393665666562353139646234343261303131336663316634 31386434333363613930316363393564373332303236306233643365386639346631336232373361
6535313265356434373464326664303937353132643636630a373966646565663564613463643732 3732633931376438313835373537666438383664306266380a633531316432343166323361303465
35646136346564636565616134303064313636643263343765396138653830646664633366656365 39306136386664356163346266303963373839373763343136346135633236333333326331313063
3137643164396265650a663338346161333832643732653539366462323039313563383735626661 6264306265373865310a373735393632373861333433326632303931633732636535326264346537
62626461656636366634353731613932653235323463336263363866336466356534636435313831 39633766326361666432396432623132616666363462373237623664363664373362346366383230
64373235383930663232613932623935346465643633666233386461346635353663323638633964 65326665303762356233356531306331333231376163353638363936626562623937323166623065
61366664323631313631663662643437386365616162333161636166666635636633333061323739 62633165383033303362336238343037353930396430336537363565333132666532323662653262
38386330326534303966613165656363633838313366666164656133386263646561306139383032 65373531313264353938666161373064393239653165666462336665323962336134343432316134
66646133643239396436653564323837623031393439303938323138326330636633613864623565 32373361613539356262313333663964623034313230356439626139383539326261663765373034
62616335633563333962653561633665643336303230623934356565363531323237643433306436 36633933366662656464306561393130373238313730336638663932633935313037363133636266
33363238306438333765323034636261333830653836656538636662303363363733356462353764 36663935303735623132663464376633306431396662306166393831313566323238363865303162
34636136666563333138383663643931363039303737313763373765356336656564633665666530 66303732376530653435613966373832333161333137366230613166303061333433336461386234
32346262636530646365636565656139623639346262373963346462626638316162616666373339 63393030616662373666643164663862653037383336323766646330653131623930353265336231
33353031313365356537646237356139613834343737343361636133343837613638643432346134 39646335353635616465663763306262353931316562616437313362616136623735646535616431
64313232646638633035383234303934666139616162356431306161353866316136663830343737 33363439623361343231363663363535663265366166346439623935323632366336363135373462
38646564393936383939373435313363653338653737346333643165626437386366346339303764 62646662316538323734333334393566333463653833326162663065646634363336373865623932
34623735613130643636623733353161393034303964656635366466396362393435376565313832 33646533636638386534366561663930613536663935663638313137646537626431393035653466
32623936646534656166383930666133663132663363623435393434663734363533653036663230 36623435613966323166363630383531656436373366343364346439343034333934623836336636
30663130626237303938613236306465613534346466663333663432316539653836323030643636 38653534343563393434646430306662343435653465363439326261373537666233353731306436
64646564643435373938363430303531333461613438633435356231656666373261636135396534 35323739336361343665633239366633393530346335396635316238323435663466316235376536
66343435663835646161376636633662353337386238316131623737353364383130653334643864 38303839313433643038353236616632626363653339346334303136306138336461383831626536
63353836303230303263343939373436646262346565313039323037383966636636656638366231 39303966393034333739363061363865326165303236656438316537396463383935393764346135
31343431373632363031346635663834393036623166346262393736346361323837303462346130 66323632323833353833643434316566326366633562666262386232653730303038613336663264
66643563306633323433616461343364326162336435363534326237366665316638353365376138 62636332383834393861373665393364653362636339346632303463386565633830336363393065
61633263643637323031613462303138303133623136323637633862623032623465616538643331 31383530613161366436323163393366386635316562633436383134623061353937326363396439
31343134353166383964666431353231636339636366626164346461386339356437386261336365 38636438396637616362373937666361303536376164636533653536306338356263313965623336
36373936356534623035663531353132633837333366663732393661613162316234373137333861 32633461623861643138613734313164633562613932386163643062353636376266633166373838
34656465343231623532343231646565316664373662626134346530653164393966333738656130 39613438653531316333663736366161313832613830353566356461393435656234383037353335
62663238373135643033396334386236313531646131313765323039306431323938386163316434 32343661376266353538643531313239306432646335383734623233373063316430383362643531
64616530343431303865336431666662343766303931653336376131313633306431346165623335 35393263343534363936373361316265653934383735663933663365363564663966646335353337
64383262626332336430373337313462613766613931613337393335393237616463646631623562 63366365383466393765336533363130643236313331633537346237353631623334633330376164
35343938323933663835313334306536343231663866616566653434366564313134393831343362 32616530623032663761643437336536323332343130393339313232623364656334613233323833
63396630366338626133383232666265383266396166306536633233306139666531613031643262 64346637343738386335343631333035636337633732333662663763326362636432646136353064
37356262363466303032336264333864646632373533396239393565653538383161653762313432 30393061326435323837633632333765623931663265326137373135303035623464333366653566
37313230666237626530316133393134303133613463363362323132653932333663313236623639 66613133396339376264313964353932396136613538383264363865633536373839663465333437
65326665626432386132323131353838346439393061343963616565306464636264626630396230 37613765626131343330653063663764346364316264363635653438396331363263623562333735
31373963326134383762373737626238353964383565336132623836333535313635316637653966 30646563303439663639376430336336383761363365353838613036333032306434623661636138
39386563396661663439343463353262656237616633646466323638346362376335306330373161 63616439636437663732386164346365643834376631303263376563323662373734653631613730
37343431316163626365386130383862643461363132636138646634663832383933303937643934 31373039656238303462643930303531613931373065316435303661343862623034653364653736
34326131353262643165336138643835333066663432353263333939633534383137393136633034 37613835616630316133333130633631653639336266313438633166316539373433333661613839
35386538616633336230353332306631353566343831376662306264386330623631653437393666 32356139366232313336343062323265366563633735383661393335386163653133313664613264
30646438666663383334333431336230373635663162626661373539336434646131346238623435 63303739323863333439316461366137656434666366306466356633306663623730653939663430
32303361366565353161646136386237366639343664616637303232376232316334366437636630 39313764663534316562326435316264633236373834323665356266346532323565323532333438
37633866376433633537373166613436303335663766303539336332366231353435646163386538 33306237623430613463393164383332356533303433343465343930636563363862373330613632
66323963303034393237323035366634303736653735623937323763336234376563663563333132 38643937613263313435626166313464316133373338373261666331653436373063393162636339
39646237306334326635353563326536633764306534313535383836633033373832316336383530 66356434663239646334383433626566336433653265336332323866633666646132663836313537
33316432356366363934666264306661373037663563333165633132643834353262376661636463 66663766306438396131623533613834626331653731323137303539303835306632663132616363
63326662663739616139393438393734353833376239363531376261303639376363646532666465 64303438353836613136623562626664326364656133383865383730373762666539383036396337
66356533393331393535393161653131343536353535373432323463346331646663353439383237 66393636343561333435313032353939316138306336316337353861383238326136373265663433
37386262633765326331383335613263653331303561343636626337333265313633373533363331 62366566343866626336646466386561353061343735306565383437333931613635393034363430
32373564646464626633323833663432663562363639373931623535306534646664346165356161 37633765663239643435623066313331353862393966306563393838386334393162306562363062
33306330363239386439646563353865646339323633646535316336363239303564373937636233 61633363386564373834383432323861323364623365306439353631323463326461383039356466
37396434353830386130316237386532333939363330306165633630336662333537663265363736 34396464653233626435656463326439303665633532656639376633353932666630626564616564
61636437623832383032643539626338313932303335333461363566363532663633353631353933 34326638383634353033326232646339393638663637313136653763336265616635326666336530
62376531303638303838663737373462323339633632626439353439396139656639373331656636 33663261376262626136346265656130653831636662306132393837306135643831353534626636
61303531383433343961666336363231353638656137666364323133326233623964363536303065 34383762386665666363313932336632326230646439663366663037323562633630373137333232
33623130653564363261653333343732653539386565326133386165313536613732366463303537 37393164656137303131313738396131613561306332356436303436636338623233343637363332
63616538383536636232636562653863323334633839656366373761626561656462316161323630 36613137316337666461333237373266326238303531396432383461616239316630346230303735
33396566333463326136626231653536336134653232306536396434636438366335633137623433 63636535383766353338643932366339333130663632386337643932636630316432313465393766
31636362366431396364 39613065623631316165313134386231616165366266323634643632626230303861376461323433
35346137326563646332303334313530383464373838373633363635373766333362386466323836
32613236646561343365656239346239353866663336393930356238653463336361333033643331
61323430663031383239353363616666336637666230663633616464616638303966343631386632
32333735643936353638666336303133356435396338653465323234316234626536666635333238
38313661303466333464666538363938656631396666643566343763396638336663376430646532
38666234653330646262633062356238343536336637356432313137313561363937363936383364
30643937383833663339643862363234643765386164316138636565643434373734383339363138
36323163633837326132393365333236633264386664373234313061373835346634663137383837
62666561353532303663346365343131316233633163323938623066656332383030393864363536
38383939383935613432613837333863313239653831333438383133343763633838353964353161
61323462343835613937653465633563306462613631323762656437626133336638396663646362
30323661383134653336366234663333336261353162373030626266656336356233316265636661
34303865313433633138363936373561636537353831373033303163646436303932626138356633
63656364353163313037613262396338636230646330666331616534313466306361363433656132
66633231626665303165346339373764666264313838313063323732653837383736633235363064
37353632336238623366313432376163653535656134633634313065356533343933666135396633
30613134646132613637656461303431613064393438363231383464663765316638

View File

@@ -9,14 +9,8 @@ vault_icloud_mail_password: "REPLACE_ME"
vault_git_work_email: "REPLACE_ME" vault_git_work_email: "REPLACE_ME"
vault_git_work_gpg: "REPLACE_ME" vault_git_work_gpg: "REPLACE_ME"
vault_openai_api_key: "REPLACE_ME" vault_openai_api_key: "REPLACE_ME"
vault_navidrome_db_password: "REPLACE_ME"
vault_postgres_root_password: "REPLACE_ME"
vault_ikaros_authorized_ssh_keys: vault_ikaros_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME" - "ssh-ed25519 REPLACE_ME"
vault_atlas_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME atlas-admin"
vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH" vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH"
vault_atlas_samba_password: "REPLACE_ME" vault_atlas_samba_password: "REPLACE_ME"
vault_atlas_immich_db_password: "REPLACE_ME" vault_atlas_immich_db_password: "REPLACE_ME"
vault_prometheus_atlas_sftp_private_key: |
REPLACE_WITH_A_DEDICATED_ATLAS_SFTP_PRIVATE_KEY