Compare commits

10 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
Fabio Scotto di Santolo
64aebe8c34 Add Atlas media and storage services 2026-09-10 22:27:33 +02:00
50 changed files with 2071 additions and 460 deletions

View File

@@ -53,6 +53,14 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff` - AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff`
- Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh` - Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh`
- 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:
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
- Atlas network/share hardening:
`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
@@ -104,12 +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
same server Compose stack with a `podman-compose-server` systemd unit. It does not start, enable, transfer data, existing Nginx Proxy Manager/Gitea Compose stack with a `podman-compose-server` systemd unit. PostgreSQL and
update DNS, or cut over traffic; activating the stack is a manual step. Navidrome are no longer part of the desired Prometheus configuration. The role does not stop or remove legacy
containers, delete `/opt/postgres/data`, start the Compose stack, update DNS, or cut over traffic.
- 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
@@ -119,19 +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` and `atlas_manage_firewall` remain false until their placeholders are Never partition, force, destroy, roll back, or modify the vdev layout of an existing pool.
replaced; only then may the profile manage datasets, shares and LAN-restricted firewall rules. - `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as
- Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false
and, when storage is enabled, `vault_atlas_samba_password`. Never print these values. until the future rootful Immich stack has its required Vault inputs and target validation.
- Atlas uses NFSv4 for Linux and SMB for Windows/WSL, restricted to the configured LAN. Snapshot, - Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled,
Borg/Hetzner offsite backup, Prometheus pull and USB backup automation are intentionally deferred. `vault_atlas_samba_password`. The future rootful media stack also requires
`vault_atlas_immich_db_password`. Never print these values.
- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `work`, `archive`,
`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 `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive
`/dev/dri`, while the Photobook external library is read-only at `/external/photobook`.
- 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
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
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. `atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated.
- 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
@@ -140,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.
- Add Tailscale or WireGuard and corresponding LAN/VPN-only firewalld rules before enabling remote - Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services;
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,14 +180,25 @@ 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 dei template server, incluso il `docker-compose.yml` dello stack servizi e dell'unit `podman-compose-server` (attivazione manuale) - copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager e Gitea,
piu l'unita `podman-compose-server` (attivazione manuale)
- 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`.
La fase 1 su Atlas non modifica questo deployment NPM ne i suoi dati persistenti. Dopo aver attivato
WireGuard e i servizi Atlas, configurare i proxy host NPM correnti con upstream Navidrome
`http://10.0.0.2:4533` e upstream per la GUI Syncthing `http://10.0.0.2:8384`. Solo la GUI web di
Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native pubblicate esplicitamente solo
sull'indirizzo WireGuard di Atlas. Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI.
### DuckDNS ### DuckDNS
`profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello `profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello
@@ -211,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:
@@ -234,14 +246,16 @@ 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 Atlas e fornire Per il primo avvio fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`,
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` e `vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite
`vault_atlas_samba_password`. Eseguire il bootstrap tramite l'amministratore esistente: l'amministratore esistente:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit atlas \
@@ -249,13 +263,73 @@ 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. mountpoint esistenti, subnet LAN e zona firewalld attiva. `atlas_manage_media_stack` resta disabilitato
finche non saranno validati `/dev/dri`, i percorsi dei container e il segreto del database Immich.
Restano da implementare retention delle snapshot, topologia Syncthing, VPN, pull da Prometheus, Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpool` esistente o creato esplicitamente:
backup cifrati con Borg su una Hetzner Storage Box, backup USB, monitoraggio e test di disaster `work`, `archive`, `archive/app_data`, i dataset applicativi separati
recovery. Il backlog operativo dettagliato e in `AGENTS.md`. `archive/app_data/navidrome` e `archive/app_data/syncthing`, `media`, `media/music`,
`media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e
di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`;
`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
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
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.
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,
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`.
--- ---
@@ -317,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 |
@@ -332,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
@@ -348,7 +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 container server include `navidrome`, `postgres`, `gitea` e `nginx-proxy-manager` - lo stack Compose server include soltanto `gitea` e `nginx-proxy-manager`; Navidrome e Syncthing
della fase 1 sono Quadlet rootless su Atlas
# Dotfiles # Dotfiles
@@ -455,6 +534,8 @@ ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" -
ansible-lint ansible/roles/<role> 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 backend_phase1 --check --diff
``` ```
## Tag supportati dal playbook ## Tag supportati dal playbook
@@ -471,6 +552,9 @@ 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 |
| `backend_phase1` | Quadlet rootless Navidrome e Syncthing | 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 |
| `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME | | `dotfiles:desktop` | dotfiles desktop | desktop Void, Fedora/GNOME |
@@ -484,16 +568,21 @@ Allo stato attuale `ansible/site.yml` espone questi tag:
| `fzf` | configurazione FZF | dotfiles comuni | | `fzf` | configurazione FZF | dotfiles comuni |
| `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 |
| `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 |
| `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 |
| `storage` | dataset ZFS figli | NAS Atlas |
| `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:

149
README.md
View File

@@ -104,16 +104,25 @@ That gives it Fedora packages through DNF, Docker from the official repository,
dotfiles and templates. The profile provisions configuration only: it does not transfer data, start dotfiles and templates. The profile provisions configuration only: it does not transfer data, start
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, The server profile installs platform-specific packages, Podman and podman-compose, declared systemd
declared systemd services, the server Compose stack behind the `podman-compose-server` systemd unit, and firewalld. The Rocky server excludes services, and firewalld. The manually activated `podman-compose-server` unit contains the existing
Syncthing. Rocky bind mounts use private SELinux relabeling for application data while host system Nginx Proxy Manager and Gitea services. The desired Compose file no longer includes Navidrome,
files remain unchanged. Syncthing, or the obsolete Navidrome PostgreSQL database. Navidrome and Syncthing belong to Atlas;
official Navidrome uses SQLite instead. Applying the profile does not stop or remove legacy
containers and does not delete `/opt/postgres/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
@@ -124,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
@@ -146,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 \
@@ -159,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
@@ -187,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
@@ -196,28 +211,97 @@ 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 placeholders and provide For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, and `vault_atlas_immich_db_password`. Bootstrap the host through its
`vault_atlas_samba_password`. Bootstrap the host through its existing administrator: existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then
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 WireGuard waits for a real peer handshake before the play continues.
`atlas_manage_storage` only after checking the existing pool and mountpoints; enable
`atlas_manage_firewall` only after checking the LAN subnet and active firewalld zone.
Snapshot retention, Syncthing topology, VPN access, Prometheus pulls, encrypted Borg backups to a `vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests remain follow-up work. The Cockpit password. Subsequent runs use `atlas_admin_username`. Atlas declares storage, sharing, and its
detailed operational backlog is kept in `AGENTS.md`. LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet,
and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container
paths, and the Immich database secret are validated. Atlas reads its declared SSH public keys from
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
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 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.
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,
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`.
## How layering works ## How layering works
@@ -303,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. |
@@ -314,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
@@ -384,6 +472,7 @@ ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" -
ansible-lint ansible/roles/<role> 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
``` ```
## Tags ## Tags
@@ -398,6 +487,9 @@ 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. |
| `backend_phase1` | Rootless Atlas Navidrome and Syncthing 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. |
| `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. | | `dotfiles:desktop` | Void and Fedora/GNOME desktop dotfiles. |
@@ -406,10 +498,15 @@ ansible-playbook ansible/site.yml --list-tags
| `dotfiles:workstation` | Personal workstation and WSL dotfiles. | | `dotfiles:workstation` | Personal workstation and WSL dotfiles. |
| `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. |
| `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. |
| `services` | runit and systemd services. | | `services` | runit and systemd services. |
| `sharing` | Atlas NFSv4 and SMB3 configuration. |
| `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

@@ -62,14 +62,6 @@ server_directories:
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- path: /opt/navidrome/data
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: /opt/music
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
@@ -78,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
@@ -107,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,10 +1,10 @@
--- ---
# Replace every CHANGEME value before enabling Atlas storage management. # Atlas declares its intended steady state; set a feature flag false only for a deliberate suspension.
hostname: atlas hostname: atlas
ansible_host: CHANGEME_ATLAS_HOST ansible_host: 192.168.178.55
ansible_user: "{{ atlas_connection_username }}" ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22 ansible_port: 22
ansible_ssh_private_key_file: CHANGEME_ATLAS_SSH_PRIVATE_KEY_PATH ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
atlas_admin_username: admin atlas_admin_username: admin
atlas_connection_username: "{{ atlas_admin_username }}" atlas_connection_username: "{{ atlas_admin_username }}"
@@ -12,49 +12,105 @@ 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: CHANGEME_LAN_SUBNET atlas_lan_subnet: 192.168.178.0/24
atlas_manage_firewall: false atlas_aegis_ip: 192.168.178.54
atlas_manage_firewall: true
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL 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_syncthing: syncthing 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_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos atlas_zfs_dataset_photobook: media/photobook
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_zfs_dataset_backups: backups
atlas_manage_storage: false atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool
atlas_manage_storage: true
atlas_manage_sharing: true
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_podman_packages:
- podman
host_packages: host_packages:
- cockpit - cockpit
- nfs-utils - nfs-utils
- policycoreutils
- policycoreutils-python-utils - policycoreutils-python-utils
- python3-libselinux
- samba - samba
- samba-client
- samba-common-tools
- zfs - zfs
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_work_mountpoint }}" - path: "{{ atlas_photobook_mountpoint }}"
clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)" client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: work atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users: atlas_samba_encryption: required
- "{{ atlas_admin_username }}" # Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
atlas_samba_password: "{{ vault_atlas_samba_password | default('') }}" atlas_samba_accounts:
- username: "{{ atlas_admin_username }}"
password: "{{ vault_atlas_samba_password | default('') }}"
atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') | list }}"
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_lan_subnet }}" port port="2049" protocol="tcp" 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'
host_enabled_services: host_enabled_services:
- sshd - sshd
- cockpit.socket - cockpit.socket
- nfs-server
- smb
- zfs.target - zfs.target

View File

@@ -7,6 +7,24 @@ 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_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"
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

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,29 +1,77 @@
--- ---
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_group: immich
atlas_immich_uid: 1100
atlas_immich_gid: 1100
atlas_immich_supplementary_groups:
- video
- render
atlas_lan_subnet: CHANGEME_LAN_SUBNET atlas_lan_subnet: CHANGEME_LAN_SUBNET
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_syncthing: syncthing 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_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" 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_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_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
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_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing" atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}"
atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync" atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
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
@@ -34,18 +82,61 @@ atlas_45drives_packages:
- cockpit-zfs - cockpit-zfs
- cockpit-scheduler - cockpit-scheduler
atlas_nfs_exports: [] atlas_nfs_exports:
atlas_samba_share_name: work - path: "{{ atlas_photobook_mountpoint }}"
client: "{{ atlas_aegis_ip }}"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
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_password: "" atlas_samba_accounts: []
atlas_samba_password_marker: /var/lib/samba/private/.atlas-password.sha256 atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords
atlas_firewalld_rich_rules: [] atlas_firewalld_rich_rules: []
atlas_firewalld_restricted_services: atlas_firewalld_restricted_services:
- ssh - ssh
- cockpit - cockpit
- nfs - nfs
- samba - samba
- http
- https
atlas_selinux_booleans: atlas_selinux_booleans:
- samba_export_all_rw - samba_export_all_rw
- nfs_export_all_rw - nfs_export_all_rw
atlas_manage_media_stack: false
atlas_container_quadlet_dir: /etc/containers/systemd
atlas_container_data_root: /var/lib/atlas-containers
atlas_npm_data_dir: "{{ atlas_container_data_root }}/npm/data"
atlas_npm_letsencrypt_dir: "{{ atlas_container_data_root }}/npm/letsencrypt"
atlas_npm_admin_bind_address: 127.0.0.1
atlas_timezone: Europe/Rome
atlas_immich_upload_dir: "{{ atlas_container_data_root }}/immich/upload"
atlas_immich_model_cache_dir: "{{ atlas_container_data_root }}/immich/model-cache"
atlas_immich_machine_learning_config_dir: "{{ atlas_container_data_root }}/immich/machine-learning-config"
atlas_immich_machine_learning_cache_dir: "{{ atlas_container_data_root }}/immich/machine-learning-cache"
atlas_immich_redis_data_dir: "{{ atlas_container_data_root }}/immich/redis"
atlas_immich_postgres_data_dir: "{{ atlas_container_data_root }}/immich/postgres"
atlas_npm_image: docker.io/jc21/nginx-proxy-manager:latest
atlas_immich_version: release
atlas_immich_server_image: "ghcr.io/immich-app/immich-server:{{ atlas_immich_version }}"
atlas_immich_machine_learning_image: "ghcr.io/immich-app/immich-machine-learning:{{ atlas_immich_version }}-openvino"
atlas_immich_redis_image: docker.io/valkey/valkey:9
atlas_immich_postgres_image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
atlas_immich_db_username: postgres
atlas_immich_db_name: immich
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_media_quadlet_services:
- atlas-immich-redis.service
- atlas-immich-postgres.service
- atlas-immich-machine-learning.service
- atlas-immich-server.service
- atlas-npm.service

View File

@@ -28,7 +28,14 @@
name: smb name: smb
state: restarted state: restarted
- name: Restart Atlas Syncthing service - name: Restart Atlas media Quadlets
ansible.builtin.systemd: ansible.builtin.systemd:
name: atlas-syncthing name: "{{ item }}"
state: restarted state: restarted
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- not ansible_check_mode

View File

@@ -6,9 +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"
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
@@ -31,6 +33,41 @@
state: present state: present
no_log: true no_log: true
- name: Create Immich primary group
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ atlas_immich_group }}"
gid: "{{ atlas_immich_gid }}"
state: present
- name: Ensure Immich hardware-access groups exist
tags: [atlas, accounts, containers, immich]
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ atlas_immich_supplementary_groups }}"
loop_control:
label: "{{ item }}"
- name: Create unprivileged Immich account
tags: [atlas, accounts, containers, immich]
ansible.builtin.user:
name: "{{ atlas_immich_username }}"
uid: "{{ atlas_immich_uid }}"
group: "{{ atlas_immich_group }}"
groups: "{{ atlas_immich_supplementary_groups }}"
append: false
home: /nonexistent
create_home: false
shell: /sbin/nologin
system: true
state: present
- name: Read Immich hardware-access group IDs
tags: [atlas, accounts, containers, immich]
ansible.builtin.getent:
database: group
- name: Grant Atlas administrator passwordless sudo - name: Grant Atlas administrator passwordless sudo
tags: [atlas, services] tags: [atlas, services]
ansible.builtin.copy: ansible.builtin.copy:
@@ -41,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]
@@ -113,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,140 @@
---
- name: Require completed Atlas media-stack configuration
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_manage_storage | bool
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
- atlas_mount_root != '/CHANGEME_ATLAS_MOUNT_ROOT'
- atlas_immich_db_password | length > 0
- atlas_npm_admin_bind_address == '127.0.0.1'
fail_msg: >-
Enable and configure Atlas storage, provide vault_atlas_immich_db_password,
and keep the NPM administration port bound to loopback before enabling the media stack.
no_log: true
when: atlas_manage_media_stack | bool
- name: Check Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.stat:
path: /dev/dri
register: atlas_dri_device
when: atlas_manage_media_stack | bool
- name: Require Atlas Intel graphics device
tags: [atlas, containers, immich]
ansible.builtin.assert:
that:
- atlas_dri_device.stat.isdir | default(false)
fail_msg: /dev/dri is required for Immich QuickSync and OpenVINO acceleration.
when: atlas_manage_media_stack | bool
- name: Create Atlas rootful Quadlet directory
tags: [atlas, containers]
ansible.builtin.file:
path: "{{ atlas_container_quadlet_dir }}"
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich configuration directory
tags: [atlas, containers, immich]
ansible.builtin.file:
path: /etc/immich
state: directory
owner: root
group: "{{ atlas_immich_group }}"
mode: "0750"
when: atlas_manage_media_stack | bool
- name: Create Atlas Immich persistent directories
tags: [atlas, containers, immich]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
mode: "0750"
loop:
- "{{ atlas_immich_upload_dir }}"
- "{{ atlas_immich_model_cache_dir }}"
- "{{ atlas_immich_machine_learning_config_dir }}"
- "{{ atlas_immich_machine_learning_cache_dir }}"
- "{{ atlas_immich_redis_data_dir }}"
- "{{ atlas_immich_postgres_data_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Create Atlas NPM persistent directories
tags: [atlas, containers, npm]
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: "0750"
loop:
- "{{ atlas_npm_data_dir }}"
- "{{ atlas_npm_letsencrypt_dir }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_media_stack | bool
- name: Allow confined containers to use Atlas graphics devices
tags: [atlas, containers, immich]
ansible.posix.seboolean:
name: container_use_devices
state: true
persistent: true
when:
- atlas_manage_media_stack | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Vault-backed Immich environment
tags: [atlas, containers, immich]
ansible.builtin.template:
src: immich.env.j2
dest: /etc/immich/immich.env
owner: root
group: "{{ atlas_immich_group }}"
mode: "0640"
no_log: true
diff: false
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Render Atlas media Quadlets
tags: [atlas, containers]
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "{{ atlas_container_quadlet_dir }}/{{ item }}"
owner: root
group: root
mode: "0644"
loop:
- atlas-media.network
- atlas-immich-redis.container
- atlas-immich-postgres.container
- atlas-immich-machine-learning.container
- atlas-immich-server.container
- atlas-npm.container
loop_control:
label: "{{ item }}"
notify: Restart Atlas media Quadlets
when: atlas_manage_media_stack | bool
- name: Start Atlas media Quadlet services
tags: [atlas, containers, services]
ansible.builtin.systemd:
name: "{{ item }}"
state: started
daemon_reload: true
loop: "{{ atlas_media_quadlet_services }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_media_stack | bool
- 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,11 +5,17 @@
- 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
- name: Import Atlas file sharing tasks - name: Import Atlas file sharing tasks
ansible.builtin.import_tasks: sharing.yml ansible.builtin.import_tasks: sharing.yml
- name: Import Atlas Syncthing tasks - name: Import Atlas media-container tasks
ansible.builtin.import_tasks: syncthing.yml ansible.builtin.import_tasks: containers.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

@@ -0,0 +1,64 @@
---
- name: Require an existing Unix account for Atlas Samba
ansible.builtin.getent:
database: passwd
key: "{{ atlas_samba_account.username }}"
- name: Read Atlas Samba account
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_samba_account.username }}"
register: atlas_samba_account_check
changed_when: false
failed_when: false
- name: Check Atlas Samba password marker
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_stat
- name: Read Atlas Samba password marker
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
register: atlas_samba_password_marker_content
no_log: true
when: atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_account.password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account_check.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_account.password | hash('sha256'))
}}
no_log: true
- name: Set Atlas Samba account password
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_samba_account.username }}"
stdin: "{{ atlas_samba_account.password }}\n{{ atlas_samba_account.password }}"
changed_when: true
no_log: true
when: atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker_dir }}/{{ atlas_samba_account.username }}.sha256"
owner: root
group: root
mode: "0600"
no_log: true
when: atlas_samba_password_update_required | bool

View File

@@ -1,187 +1,12 @@
---
- 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: 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 Atlas Samba password
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_password | length > 0
fail_msg: Define vault_atlas_samba_password before enabling Atlas storage.
no_log: true
when: atlas_manage_storage | bool
- name: Read Atlas Samba account
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_admin_username }}"
register: atlas_samba_account
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker | dirname }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_storage | bool
- name: Check Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_stat
when: atlas_manage_storage | bool
- name: Read Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_content
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
tags: [atlas, sharing]
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_password | hash('sha256'))
}}
no_log: true
when: atlas_manage_storage | bool
- name: Set Atlas Samba account password
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_admin_username }}"
stdin: "{{ atlas_samba_password }}\n{{ atlas_samba_password }}"
changed_when: true
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
tags: [atlas, sharing]
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | 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:
that: that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' - atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0 - atlas_firewalld_zone | length > 0
fail_msg: Replace the Atlas LAN subnet and firewall zone placeholders. - ansible_facts.default_ipv4.interface | default('') | length > 0
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules - name: Apply Atlas firewalld rich rules
@@ -197,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:
@@ -209,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

@@ -5,12 +5,80 @@
atlas_zfs_datasets: atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
mountpoint: "{{ atlas_work_mountpoint }}" mountpoint: "{{ atlas_work_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_work_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
mountpoint: "{{ atlas_archive_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
mountpoint: "{{ atlas_app_data_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
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 }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
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 }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}" owner: "{{ atlas_admin_username }}"
mountpoint: "{{ atlas_icloud_photos_mountpoint }}" group: "{{ atlas_admin_group }}"
properties:
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}"
mountpoint: "{{ atlas_photobook_mountpoint }}"
owner: "{{ atlas_immich_username }}"
group: "{{ atlas_immich_group }}"
properties:
compression: lz4
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 }}"
mountpoint: "{{ atlas_service_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_service_backups_mountpoint }}"
refreservation: "{{ atlas_zfs_service_backups_refreservation }}"
- name: Require completed Atlas storage placeholders - name: Require completed Atlas storage placeholders
tags: [atlas, storage] tags: [atlas, storage]
@@ -56,8 +124,7 @@
community.general.zfs: community.general.zfs:
name: "{{ item.name }}" name: "{{ item.name }}"
state: present state: present
extra_zfs_properties: extra_zfs_properties: "{{ item.properties }}"
mountpoint: "{{ item.mountpoint }}"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:
label: "{{ item.name }}" label: "{{ item.name }}"
@@ -68,8 +135,8 @@
ansible.builtin.file: ansible.builtin.file:
path: "{{ item.mountpoint }}" path: "{{ item.mountpoint }}"
state: directory state: directory
owner: "{{ atlas_admin_username }}" owner: "{{ item.owner }}"
group: "{{ atlas_admin_group }}" group: "{{ item.group }}"
mode: "0770" mode: "0770"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:

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

@@ -0,0 +1,27 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich machine learning
[Container]
ContainerName=atlas-immich-machine-learning
Image={{ atlas_immich_machine_learning_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-machine-learning
Environment=TZ={{ atlas_timezone }}
Volume={{ atlas_immich_model_cache_dir }}:/cache:Z
Volume={{ atlas_immich_machine_learning_config_dir }}:/.config:Z
Volume={{ atlas_immich_machine_learning_cache_dir }}:/.cache:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,22 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich PostgreSQL database
[Container]
ContainerName=atlas-immich-postgres
Image={{ atlas_immich_postgres_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-postgres
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_postgres_data_dir }}:/var/lib/postgresql/data:Z
ShmSize=128m
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,20 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich Redis-compatible cache
[Container]
ContainerName=atlas-immich-redis
Image={{ atlas_immich_redis_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
Network=atlas-media.network
NetworkAlias=atlas-immich-redis
Volume={{ atlas_immich_redis_data_dir }}:/data:Z
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,29 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Immich server
Requires=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
After=atlas-immich-redis.service atlas-immich-postgres.service atlas-immich-machine-learning.service
[Container]
ContainerName=atlas-immich-server
Image={{ atlas_immich_server_image }}
User={{ atlas_immich_uid }}:{{ atlas_immich_gid }}
{% for group_name in atlas_immich_supplementary_groups %}
GroupAdd={{ ansible_facts['getent_group'][group_name][1] }}
{% endfor %}
AddDevice=/dev/dri
Network=atlas-media.network
NetworkAlias=atlas-immich-server
EnvironmentFile=/etc/immich/immich.env
Volume={{ atlas_immich_upload_dir }}:/data:Z
Volume={{ atlas_photobook_mountpoint }}:/external/photobook:ro,Z
Volume=/etc/localtime:/etc/localtime:ro
NoNewPrivileges=true
DropCapability=NET_RAW
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,3 @@
# Managed by Ansible. Do not edit manually.
[Network]
NetworkName=atlas-media

View File

@@ -0,0 +1,21 @@
# Managed by Ansible. Do not edit manually.
[Unit]
Description=Atlas Nginx Proxy Manager
[Container]
ContainerName=atlas-npm
Image={{ atlas_npm_image }}
Network=atlas-media.network
NetworkAlias=atlas-npm
PublishPort=80:80
PublishPort=443:443
PublishPort={{ atlas_npm_admin_bind_address }}:81:81
Volume={{ atlas_npm_data_dir }}:/data:Z
Volume={{ atlas_npm_letsencrypt_dir }}:/etc/letsencrypt:Z
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target

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

@@ -1,4 +1,4 @@
# Managed by Ansible. Do not edit manually. # Managed by Ansible. Do not edit manually.
{% for export in atlas_nfs_exports %} {% for export in atlas_nfs_exports %}
{{ export.path }} {{ export.clients }} {{ export.path }} {{ export.client }}({{ export.options | join(',') }})
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,13 @@
# Managed by Ansible. Contains a Vault secret; do not edit or copy.
TZ={{ atlas_timezone | to_json }}
DB_HOSTNAME="atlas-immich-postgres"
DB_USERNAME={{ atlas_immich_db_username | to_json }}
DB_PASSWORD={{ atlas_immich_db_password | to_json }}
DB_DATABASE_NAME={{ atlas_immich_db_name | to_json }}
DB_VECTOR_EXTENSION="vectorchord"
POSTGRES_USER={{ atlas_immich_db_username | to_json }}
POSTGRES_PASSWORD={{ atlas_immich_db_password | to_json }}
POSTGRES_DB={{ atlas_immich_db_name | to_json }}
POSTGRES_INITDB_ARGS="--data-checksums"
REDIS_HOSTNAME="atlas-immich-redis"
IMMICH_MACHINE_LEARNING_URL="http://atlas-immich-machine-learning:3003"

View File

@@ -3,11 +3,16 @@
workgroup = {{ atlas_samba_workgroup }} workgroup = {{ atlas_samba_workgroup }}
security = user security = user
map to guest = Never map to guest = Never
server min protocol = SMB2 server min protocol = SMB3
server signing = mandatory
smb encrypt = {{ atlas_samba_encryption }}
disable netbios = yes
smb ports = 445
hosts allow = {{ atlas_lan_subnet }}
include = registry include = registry
[{{ atlas_samba_share_name }}] [{{ atlas_samba_share_name }}]
path = {{ atlas_work_mountpoint }} path = {{ atlas_archive_mountpoint }}
browseable = yes browseable = yes
read only = no read only = no
valid users = {{ atlas_samba_valid_users | join(' ') }} valid users = {{ atlas_samba_valid_users | join(' ') }}

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

@@ -8,16 +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 container secret variables
tags: [dotfiles, dotfiles:server, services]
ansible.builtin.assert:
that:
- (vault_navidrome_db_password | default('')) | length > 0
- (vault_postgres_root_password | default('')) | length > 0
fail_msg: >-
Server container secrets are missing. Define vault_navidrome_db_password and
vault_postgres_root_password in secrets/vault.yml or another vars source.
- 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
@@ -71,6 +61,42 @@
tags: [services, podman] tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml ansible.builtin.include_tasks: podman-compose.yml
- name: Ensure server SSH authorized key fragments directory exists
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: Manage server SSH authorized key fragments
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]
ansible.builtin.lineinfile: ansible.builtin.lineinfile:

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

@@ -3,25 +3,6 @@
name: server name: server
services: services:
navidrome:
image: docker.io/deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
expose:
- "4533"
environment:
ND_DATABASE_URL: "postgres://navidrome:{{ vault_navidrome_db_password }}@navidromedb:5432/navidrome_db?sslmode=disable"
ND_SESSIONTIMEOUT: 24h
ND_ENABLETRANSCODING: "true"
volumes:
- "/opt/navidrome/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/music:/music:ro{{ ',' ~ selinux_volume_option if selinux_volume_option else '' }}"
user: "1000:1000"
networks:
- web
depends_on:
- navidromedb
nginx-proxy-manager: nginx-proxy-manager:
image: docker.io/jc21/nginx-proxy-manager:latest image: docker.io/jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager container_name: nginx-proxy-manager
@@ -30,6 +11,8 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "127.0.0.1:81:81" - "127.0.0.1:81:81"
extra_hosts:
- "host.containers.internal:host-gateway"
volumes: volumes:
- "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/npm/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
@@ -55,20 +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 '' }}"
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,45 +1,78 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
62633966363435396136666635303362663932366464323231363563373462383134326361346533 31386434333363613930316363393564373332303236306233643365386639346631336232373361
3465353461326565356435633866393230383838356235340a333036383038653134323066663031 3732633931376438313835373537666438383664306266380a633531316432343166323361303465
37373135643331356366373831343163363835656462343962636538623937356231376337616330 39306136386664356163346266303963373839373763343136346135633236333333326331313063
3831626261343761310a306133633264613430613165343265303664383439333230663665333231 6264306265373865310a373735393632373861333433326632303931633732636535326264346537
30393934323438333066383737663665316634613932353232343334626261636661393434646163 39633766326361666432396432623132616666363462373237623664363664373362346366383230
63316537383866353338353261373161623562343037383032633932383630666339633234326639 65326665303762356233356531306331333231376163353638363936626562623937323166623065
64303735663532376433393464616230616236353134383734623132333061356664613039323265 62633165383033303362336238343037353930396430336537363565333132666532323662653262
35373434336261313438633866643765323330623336343732346465613264613666616631303633 65373531313264353938666161373064393239653165666462336665323962336134343432316134
31333437623437626135623032306434306538323461623161336431353630336539646531313365 32373361613539356262313333663964623034313230356439626139383539326261663765373034
30356330356438353037663262643463363636386162393764323133373261376263663338636333 36633933366662656464306561393130373238313730336638663932633935313037363133636266
33336537383061643234356365653137333639623863303530666665373663376166316533623737 36663935303735623132663464376633306431396662306166393831313566323238363865303162
31616262363132326133666230396636663035343432633830323430336332326261613932633837 66303732376530653435613966373832333161333137366230613166303061333433336461386234
62303232306331663936366435666164383335313335326662663363643563643938623739386663 63393030616662373666643164663862653037383336323766646330653131623930353265336231
65356431353362623634333437346566363166366161313132653131613861386437306562633934 39646335353635616465663763306262353931316562616437313362616136623735646535616431
38306135636161366530303461636665386639396639356265323361623639616265323462333534 33363439623361343231363663363535663265366166346439623935323632366336363135373462
61626439616631356532353437653539333631636265346238653439313130326239633035613130 62646662316538323734333334393566333463653833326162663065646634363336373865623932
65366161356231356638313633393735356465393339343034356138316366656431336435386437 33646533636638386534366561663930613536663935663638313137646537626431393035653466
36366339366463396638303062366263356436666232393437613766623631313264646565316534 36623435613966323166363630383531656436373366343364346439343034333934623836336636
32373534366363373737616563616161346265323936336665653934656365323731633637376331 38653534343563393434646430306662343435653465363439326261373537666233353731306436
31336262356632313533393366346565373434373134653630303431643037363561613830663339 35323739336361343665633239366633393530346335396635316238323435663466316235376536
39313837356362643164386432653030306132643234303162363463376130666239653461623736 38303839313433643038353236616632626363653339346334303136306138336461383831626536
65333734303532356462636538326430353831336562666563336333323764346234643037373064 39303966393034333739363061363865326165303236656438316537396463383935393764346135
63343533386564356232633661346232666135393365323133323365663637373261396264333432 66323632323833353833643434316566326366633562666262386232653730303038613336663264
64616463643931653162373666323731653635616633386231653863623830666637623638353062 62636332383834393861373665393364653362636339346632303463386565633830336363393065
61363339613364343239363336626266386561343032333262396436663736353835303863643865 31383530613161366436323163393366386635316562633436383134623061353937326363396439
35616136363830326661383066316537633537616565343361633036373163316566653638343638 38636438396637616362373937666361303536376164636533653536306338356263313965623336
33323162376537353961383965666165663631396663356533643432356630396435356636366636 32633461623861643138613734313164633562613932386163643062353636376266633166373838
32363264303633656335333335356166323164386338363437663435336635643334646532616539 39613438653531316333663736366161313832613830353566356461393435656234383037353335
66646336393834613337336661336638613463626230336132386265396165656461623533333738 32343661376266353538643531313239306432646335383734623233373063316430383362643531
33353063306336623463396130386335383338383861376330616566643638333230303862356462 35393263343534363936373361316265653934383735663933663365363564663966646335353337
63356464313263666538306230636363643665643933323236363864303634343539326239363134 63366365383466393765336533363130643236313331633537346237353631623334633330376164
65376231623836363730376631303032393330326362383438373434383633656134373539653461 32616530623032663761643437336536323332343130393339313232623364656334613233323833
38306632636463346365396131316234623736626335633361663861316437326635633237346262 64346637343738386335343631333035636337633732333662663763326362636432646136353064
38643730666130656335663232366362333164353433613731363131366138633766623433363031 30393061326435323837633632333765623931663265326137373135303035623464333366653566
37613337383961386661393733623238393062663065663931313231366366626261666336656134 66613133396339376264313964353932396136613538383264363865633536373839663465333437
63646237643864643462653337306462646631663433663461343764306163646466326334323335 37613765626131343330653063663764346364316264363635653438396331363263623562333735
61396162353032633937333066313438333461376161666166336337356430373737313266363238 30646563303439663639376430336336383761363365353838613036333032306434623661636138
63633365613331636234333765633536653639343234396262653438353132623239303431316134 63616439636437663732386164346365643834376631303263376563323662373734653631613730
35663930323535373830313766353630636237343330346232316637336531336466326564306630 31373039656238303462643930303531613931373065316435303661343862623034653364653736
62356639356330396465353432333135313130653366303931616165663638633461613961643766 37613835616630316133333130633631653639336266313438633166316539373433333661613839
62633564666566616665366336396436386330323536356633323965343566396232303434666534 32356139366232313336343062323265366563633735383661393335386163653133313664613264
33333636313136326535303136643265393834613733343362346333336339636437353236326161 63303739323863333439316461366137656434666366306466356633306663623730653939663430
65633230656531633238623830343432663365313532343435623133636239333763633930316666 39313764663534316562326435316264633236373834323665356266346532323565323532333438
34363535313731666432 33306237623430613463393164383332356533303433343465343930636563363862373330613632
38643937613263313435626166313464316133373338373261666331653436373063393162636339
66356434663239646334383433626566336433653265336332323866633666646132663836313537
66663766306438396131623533613834626331653731323137303539303835306632663132616363
64303438353836613136623562626664326364656133383865383730373762666539383036396337
66393636343561333435313032353939316138306336316337353861383238326136373265663433
62366566343866626336646466386561353061343735306565383437333931613635393034363430
37633765663239643435623066313331353862393966306563393838386334393162306562363062
61633363386564373834383432323861323364623365306439353631323463326461383039356466
34396464653233626435656463326439303665633532656639376633353932666630626564616564
34326638383634353033326232646339393638663637313136653763336265616635326666336530
33663261376262626136346265656130653831636662306132393837306135643831353534626636
34383762386665666363313932336632326230646439663366663037323562633630373137333232
37393164656137303131313738396131613561306332356436303436636338623233343637363332
36613137316337666461333237373266326238303531396432383461616239316630346230303735
63636535383766353338643932366339333130663632386337643932636630316432313465393766
39613065623631316165313134386231616165366266323634643632626230303861376461323433
35346137326563646332303334313530383464373838373633363635373766333362386466323836
32613236646561343365656239346239353866663336393930356238653463336361333033643331
61323430663031383239353363616666336637666230663633616464616638303966343631386632
32333735643936353638666336303133356435396338653465323234316234626536666635333238
38313661303466333464666538363938656631396666643566343763396638336663376430646532
38666234653330646262633062356238343536336637356432313137313561363937363936383364
30643937383833663339643862363234643765386164316138636565643434373734383339363138
36323163633837326132393365333236633264386664373234313061373835346634663137383837
62666561353532303663346365343131316233633163323938623066656332383030393864363536
38383939383935613432613837333863313239653831333438383133343763633838353964353161
61323462343835613937653465633563306462613631323762656437626133336638396663646362
30323661383134653336366234663333336261353162373030626266656336356233316265636661
34303865313433633138363936373561636537353831373033303163646436303932626138356633
63656364353163313037613262396338636230646330666331616534313466306361363433656132
66633231626665303165346339373764666264313838313063323732653837383736633235363064
37353632336238623366313432376163653535656134633634313065356533343933666135396633
30613134646132613637656461303431613064393438363231383464663765316638

View File

@@ -9,7 +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_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH"
vault_atlas_samba_password: "REPLACE_ME"
vault_atlas_immich_db_password: "REPLACE_ME"