Compare commits

..

6 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
4166044ceb fix(aegis): restore supported Fedora IoT bootstrap 2026-09-05 16:57:27 +02:00
Fabio Scotto di Santolo
6b3143ccbf Merge branch 'main' into feature/aegis-coreos 2026-09-05 16:49:55 +02:00
Fabio Scotto di Santolo
e47e435421 chore(aegis): remove local TLS configuration 2026-09-05 16:28:22 +02:00
Fabio Scotto di Santolo
f07391c404 feat(aegis): harden Fedora IoT services 2026-09-05 14:44:14 +02:00
Fabio Scotto di Santolo
1933ddbe8f feat(aegis): manage firewall and SSH hardening 2026-09-05 13:30:25 +02:00
Fabio Scotto di Santolo
46ae536570 feat(aegis): add ARM image bootstrap workflow 2026-09-05 00:07:40 +02:00
70 changed files with 738 additions and 2675 deletions

2
.gitignore vendored
View File

@@ -1,2 +0,0 @@
# DuckDNS runtime files contain a rendered Vault token and must stay on the host.
/dotfiles/server/duckdns/

101
AGENTS.md
View File

@@ -1,6 +1,6 @@
# AGENTS.md # AGENTS.md
Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora IoT, WSL, a Rocky Linux 9 server, and an Atlas NAS. Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora IoT, WSL, and servers.
## Source Of Truth ## Source Of Truth
- Main orchestration: `ansible/site.yml` - Main orchestration: `ansible/site.yml`
@@ -15,7 +15,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Current laptop: `nymph = platform_fedora + graphical_desktop + desktop_gnome` - Current laptop: `nymph = platform_fedora + graphical_desktop + desktop_gnome`
- Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop` - Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop`
- Workstation: `deadalus` is Windows + Fedora WSL. - Workstation: `deadalus` is Windows + Fedora WSL.
- Rocky server: `prometheus` belongs to `rocky_server`. - Ubuntu server: `prometheus`
- NAS: `atlas` (Rocky Linux 9, reached through SSH) - NAS: `atlas` (Rocky Linux 9, reached through SSH)
- Always-on LAN node: `aegis` (Fedora IoT on Raspberry Pi 4, reached through SSH) - Always-on LAN node: `aegis` (Fedora IoT on Raspberry Pi 4, reached through SSH)
- Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions. - Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions.
@@ -45,23 +45,13 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff` - Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff`
- WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff` - WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff`
- Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff` - Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff`
- Rocky server after activation: `ansible-playbook ansible/site.yml --limit <host> --check --diff`
- Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff` - Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff`
- Aegis IoT: `ansible-playbook ansible/site.yml --limit aegis --check --diff` - Aegis IoT: `ansible-playbook ansible/site.yml --limit aegis --check --diff`
- Focused checks: - Focused checks:
- Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit <host> --tags emacs --check --diff -e emacs_enabled=true` - Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit <host> --tags emacs --check --diff -e emacs_enabled=true`
- 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: `docker compose -f /opt/docker/server/docker-compose.yml config`
- 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`
## Conventions ## Conventions
- Use FQCN Ansible modules. - Use FQCN Ansible modules.
@@ -93,9 +83,7 @@ The Void desktop package lists in `ansible/inventory/group_vars/void.yml` are ke
- `desktop_common_packages` — GUI infrastructure shared by the minimal desktop mode. - `desktop_common_packages` — GUI infrastructure shared by the minimal desktop mode.
- `desktop_minimal_packages` — applications, integration components, and the `emptty` display manager. - `desktop_minimal_packages` — applications, integration components, and the `emptty` display manager.
- `desktop_sway_packages` — binaries specific to the Sway session. - `desktop_sway_packages` — binaries specific to the Sway session.
`profile_packages` remains the shared package bucket for Void and Fedora profiles. Rocky uses `profile_packages` in the same file is cross-distro and is overridden by `group_vars/server.yml` and the workstation group vars; do not move desktop-specific Void entries through it.
`rocky_profile_packages` so RPM-specific names do not leak back into the other platforms; do not move
desktop-specific Void entries through either bucket.
The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content and `desktop_minimal_dotfiles` carries Thunar, Udiskie, and MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime. The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-independent content and `desktop_minimal_dotfiles` carries Thunar, Udiskie, and MIME defaults. `desktop_void_dotfiles` remains reserved for files that need the Void runtime.
## Workstation Notes ## Workstation Notes
@@ -104,73 +92,23 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- Fedora WSL installs Mise from the official `jdxcode/mise` COPR and uses its pinned Temurin Java 11 JDK; update the declared Mise version deliberately. - Fedora WSL installs Mise from the official `jdxcode/mise` COPR and uses its pinned Temurin Java 11 JDK; update the declared Mise version deliberately.
- Windows applications are installed manually and are not managed from the WSL profile. - Windows applications are installed manually and are not managed from the WSL profile.
## Rocky Server Notes
- DuckDNS is rendered by `profile_server` from host-local `server_duckdns_domain` and
`vault_duckdns_token`. Keep the rotated token in encrypted Vault or untracked local vars, never in
dotfiles. The private `~/duckdns/duck.sh` keeps the existing entrypoint; rendering uses `no_log`
and disables diffs. Provisioning does not execute the updater or change its external schedule.
- `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 Rocky profile installs Podman and podman-compose, uses firewalld, preserves SELinux enforcement, and renders the
existing Nginx Proxy Manager/Gitea Compose stack with a `podman-compose-server` systemd unit. PostgreSQL and
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
`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.
- `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.
- 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
Prometheus through its host variables.
## Atlas NAS Notes ## Atlas NAS Notes
- `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 normally pre-existing. A one-time bootstrap may create it only when `atlas_create_pool=true` - The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback,
is explicitly supplied and `atlas_zpool_disks` contains exactly four real `/dev/disk/by-id/...` paths. or destruction to the Atlas profile.
Never partition, force, destroy, roll back, or modify the vdev layout of an existing pool. - `atlas_manage_storage` and `atlas_manage_firewall` remain false until their placeholders are
- `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as replaced; only then may the profile manage datasets, shares and LAN-restricted firewall rules.
the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false - Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit
until the future rootful Immich stack has its required Vault inputs and target validation. and, when storage is enabled, `vault_atlas_samba_password`. Never print these values.
- Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled, - Atlas uses NFSv4 for Linux and SMB for Windows/WSL, restricted to the configured LAN. Snapshot,
`vault_atlas_samba_password`. The future rootful media stack also requires Borg/Hetzner offsite backup, Prometheus pull and USB backup automation are intentionally deferred.
`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
- Provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host. - Replace every Atlas `CHANGEME` value, provide the required Vault variables and validate the first
Before the first apply, confirm the pool, mountpoints, LAN subnet and firewalld zone. Keep remote bootstrap on the real Rocky Linux 9 host. Enable `atlas_manage_storage` first and
`atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated. `atlas_manage_firewall` only after confirming the pool, mountpoints, LAN subnet and firewalld zone.
- 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
@@ -179,11 +117,10 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
or manual operations, not as the only source of configuration, and never automate snapshot rollback. or manual operations, not as the only source of configuration, and never automate snapshot rollback.
- Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI - Manage the Syncthing star topology, device IDs, folders, folder modes, ignore rules and protected GUI
or API access for the selected clients. or API access for the selected clients.
- Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services; - Add Tailscale or WireGuard and corresponding LAN/VPN-only firewalld rules before enabling remote
never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding. services; never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared - Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity,
read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key, pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer.
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

@@ -33,6 +33,7 @@ infra/
│ ├── common/ │ ├── common/
│ ├── desktop/ │ ├── desktop/
│ ├── fedora/ │ ├── fedora/
│ ├── ubuntu/
│ ├── server/ │ ├── server/
│ ├── workstation/ │ ├── workstation/
│ ├── workstation_dev_wsl/ │ ├── workstation_dev_wsl/
@@ -55,8 +56,8 @@ Il repository è diviso in due componenti principali:
# Macchine gestite # Macchine gestite
Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Rocky Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Ubuntu e
Linux 9 e un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti: un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti:
```text ```text
common user environment common user environment
@@ -73,8 +74,8 @@ Matrice target:
| ikaros | Fedora | Personal workstation | GNOME | | ikaros | Fedora | Personal workstation | GNOME |
| nymph | Fedora | Desktop laptop | GNOME | | nymph | Fedora | Desktop laptop | GNOME |
| deadalus | Fedora WSL | Workstation dev | — | | deadalus | Fedora WSL | Workstation dev | — |
| prometheus | Rocky Linux | Server | — | | prometheus | Ubuntu | Server | — |
| atlas | Rocky Linux | NAS | — | | atlas | Rocky 9 | NAS | — |
Regola operativa: Regola operativa:
@@ -92,7 +93,7 @@ Nota sullo stato attuale del playbook principale:
- `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph` - `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph`
- `ansible/site.yml` applica il profilo Fedora WSL alla workstation `deadalus` - `ansible/site.yml` applica il profilo Fedora WSL alla workstation `deadalus`
- `ansible/site.yml` applica il profilo server Rocky a `prometheus` con DNF, systemd, dotfiles server e firewalld - `ansible/site.yml` applica anche il profilo `ubuntu_server` con baseline apt, systemd, dotfiles server e firewall UFW
- `ansible/site.yml` applica il profilo NAS Rocky su `atlas` tramite SSH remoto - `ansible/site.yml` applica il profilo NAS Rocky su `atlas` tramite SSH remoto
## Desktop ## Desktop
@@ -163,7 +164,7 @@ Le applicazioni Windows sono installate e gestite manualmente; il profilo WSL no
Sistema operativo: Sistema operativo:
- Rocky Linux 9 - Ubuntu LTS
Configurazione: Configurazione:
@@ -177,54 +178,12 @@ Profilo orientato a servizi server e gestione di dotfiles dedicati.
Lo stato attuale del profilo server include: Lo stato attuale del profilo server include:
- installazione pacchetti Rocky via DNF, EPEL e CRB - installazione pacchetti base Ubuntu via apt
- installazione di Podman e podman-compose - installazione e configurazione di Docker dal repository ufficiale
- abilitazione dei servizi systemd dichiarati in inventory/group vars - abilitazione dei servizi systemd dichiarati in inventory/group vars
- copia dei dotfiles server e rendering del `docker-compose.yml` per Nginx Proxy Manager e Gitea, - copia dei dotfiles server e rendering dei template server, incluso il `docker-compose.yml` dello stack servizi
piu l'unita `podman-compose-server` (attivazione manuale) - attivazione del firewall UFW con regola SSH esplicita
- attivazione di firewalld con SSH, Cockpit (`9090/tcp`), HTTP e HTTPS abilitati - apertura delle porte Syncthing `22000/tcp`, `22000/udp` e `21027/udp`, lasciando la GUI non esposta direttamente su UFW
- 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
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`.
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
`profile_server` genera `~/duckdns/duck.sh` con permessi `0700`, mantenendo il percorso dello
script e `duck.log`. Definire `server_duckdns_domain` negli host vars del server e salvare il
**nuovo token rigenerato** in `vault_duckdns_token`, nel Vault cifrato `secrets/vault.yml`
(`ansible-vault edit secrets/vault.yml`) oppure negli override non versionati `secrets/vault.local.yml`.
Non committare lo script generato e non passare il token sulla riga di comando. Il rendering
nasconde output e diff sensibili; lo script verifica TLS e passa il token a curl tramite stdin.
Il playbook non esegue lo script e non modifica la sua schedulazione esterna.
```bash
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns
```
La cancellazione dalla cronologia non revoca il token: rigenerarlo sul pannello DuckDNS.
Dopo la bonifica, riclonare gli altri checkout senza unire nuovamente la vecchia storia;
salvare separatamente eventuali modifiche non committate senza copiare segreti.
### Migrazione dati
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
fermare lo stack sorgente e copiare in modo consistente soltanto i dati di Nginx Proxy Manager e
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:
@@ -246,16 +205,14 @@ ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser -
## NAS ## NAS
`atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Normalmente il pool ZFS esiste gia e il profilo `atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Il pool ZFS esiste gia: il profilo gestisce
gestisce solo i dataset figli. Un bootstrap RAIDZ2 una tantum e disponibile solo con conferma esplicita solo i dataset figli e non deve mai creare, partizionare, distruggere, fare rollback o modificare il
(`atlas_create_pool=true`) e quattro percorsi reali e verificati `/dev/disk/by-id/...` in pool. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN
`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 fornire `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, Per il primo avvio sostituire i placeholder Atlas e fornire
`vault_atlas_samba_password` e `vault_atlas_immich_db_password`. Eseguire il bootstrap tramite `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` e
l'amministratore esistente: `vault_atlas_samba_password`. Eseguire il bootstrap tramite l'amministratore esistente:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit atlas \
@@ -263,73 +220,13 @@ 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`. Atlas dichiara password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Abilitare
abilitati storage, condivisioni e regole firewall LAN. Prima della prima applicazione verificare pool e `atlas_manage_storage` solo dopo aver verificato pool e mountpoint esistenti; abilitare
mountpoint esistenti, subnet LAN e zona firewalld attiva. `atlas_manage_media_stack` resta disabilitato `atlas_manage_firewall` solo dopo aver verificato subnet LAN e zona firewalld attiva.
finche non saranno validati `/dev/dri`, i percorsi dei container e il segreto del database Immich.
Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpool` esistente o creato esplicitamente: Restano da implementare retention delle snapshot, topologia Syncthing, VPN, pull da Prometheus,
`work`, `archive`, `archive/app_data`, i dataset applicativi separati backup cifrati con Borg su una Hetzner Storage Box, backup USB, monitoraggio e test di disaster
`archive/app_data/navidrome` e `archive/app_data/syncthing`, `media`, `media/music`, recovery. Il backlog operativo dettagliato e in `AGENTS.md`.
`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`.
--- ---
@@ -377,6 +274,7 @@ I principali ruoli attualmente presenti sono:
| ------------------------- | ----------------------------------- | | ------------------------- | ----------------------------------- |
| base | configurazione base comune | | base | configurazione base comune |
| packages_void | installazione pacchetti su Void | | packages_void | installazione pacchetti su Void |
| packages_ubuntu | installazione pacchetti su Ubuntu |
| packages_fedora | installazione pacchetti su Fedora | | packages_fedora | installazione pacchetti su Fedora |
| packages_rocky | installazione pacchetti su Rocky Linux 9 | | packages_rocky | installazione pacchetti su Rocky Linux 9 |
| services_runit | gestione servizi runit | | services_runit | gestione servizi runit |
@@ -391,8 +289,6 @@ 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 |
@@ -408,14 +304,12 @@ 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
workstation_dev_wsl -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora) workstation_dev_wsl -> profile_workstation_dev_wsl (dopo platform_fedora + workstation_dev_fedora)
rocky_server -> dotfiles_common + profile_server (dopo platform_rocky) ubuntu_server -> packages_ubuntu + services_systemd + profile_server
``` ```
Questo significa che, allo stato attuale: Questo significa che, allo stato attuale:
@@ -424,10 +318,9 @@ Questo significa che, allo stato attuale:
- `nymph` riceve Fedora Workstation/GNOME come target laptop - `nymph` riceve Fedora Workstation/GNOME come target laptop
- il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri - il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri
- `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 Ubuntu (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewall
- il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives - il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives
- lo stack Compose server include soltanto `gitea` e `nginx-proxy-manager`; Navidrome e Syncthing - lo stack container server include `navidrome`, `postgres`, `gitea`, `nginx-proxy-manager` e `syncthing`, con GUI Syncthing raggiungibile tramite la rete Docker `web`
della fase 1 sono Quadlet rootless su Atlas
# Dotfiles # Dotfiles
@@ -439,6 +332,7 @@ dotfiles/
├── desktop ├── desktop
├── server ├── server
├── fedora ├── fedora
├── ubuntu
├── workstation ├── workstation
├── workstation_dev_wsl ├── workstation_dev_wsl
└── nymph └── nymph
@@ -500,7 +394,8 @@ Allo stato attuale questo comando:
- per `platform_fedora & role_personal_workstation` applica il layer personale a `ikaros` - per `platform_fedora & role_personal_workstation` applica il layer personale a `ikaros`
- per `platform_fedora & desktop_gnome` applica il profilo GNOME a `ikaros` e `nymph` - per `platform_fedora & desktop_gnome` applica il profilo GNOME a `ikaros` e `nymph`
- per `workstation_dev_wsl` applica i tweak WSL dopo il layer Fedora a `deadalus`, escludendo Flatpak e Snap - per `workstation_dev_wsl` applica i tweak WSL dopo il layer Fedora a `deadalus`, escludendo Flatpak e Snap
- per `platform_rocky` applica pacchetti Rocky e servizi systemd ad `atlas` e `prometheus`; quindi applica il profilo NAS ad `atlas` e il profilo server a `prometheus` - per gli host `ubuntu_server` applica pacchetti Ubuntu, servizi systemd, profilo server, UFW, dotfiles e template dedicati
- per `platform_rocky` applica pacchetti Rocky e servizi systemd ad `atlas`, quindi il profilo NAS dedicato
- non riavvia automaticamente il display manager - non riavvia automaticamente il display manager
- carica `secrets/vault.yml` solo se presente - carica `secrets/vault.yml` solo se presente
- carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza - carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza
@@ -533,9 +428,7 @@ ansible-playbook ansible/site.yml --limit <host> --tags <tag1>,<tag2> --check --
ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff
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 docker 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
@@ -552,9 +445,6 @@ 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 |
@@ -568,21 +458,16 @@ 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:

203
README.md
View File

@@ -20,6 +20,7 @@ infra/
│ ├── common/ │ ├── common/
│ ├── desktop/ │ ├── desktop/
│ ├── fedora/ │ ├── fedora/
│ ├── ubuntu/
│ ├── server/ │ ├── server/
│ ├── workstation/ │ ├── workstation/
│ ├── workstation_dev_wsl/ │ ├── workstation_dev_wsl/
@@ -36,8 +37,8 @@ infra/
## Managed machines ## Managed machines
The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora IoT LAN The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora IoT LAN
node, a Rocky Linux 9 server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied node, an Ubuntu server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied to
to host names: host names:
```text ```text
common user environment common user environment
@@ -53,8 +54,8 @@ common user environment
| `nymph` | Fedora | Desktop laptop | GNOME | | `nymph` | Fedora | Desktop laptop | GNOME |
| `deadalus` | Fedora WSL | Development workstation | — | | `deadalus` | Fedora WSL | Development workstation | — |
| `aegis` | Fedora IoT | Always-on LAN node | — | | `aegis` | Fedora IoT | Always-on LAN node | — |
| `prometheus` | Rocky Linux | Server | — | | `prometheus` | Ubuntu | Server | — |
| `atlas` | Rocky Linux | NAS | — | | `atlas` | Rocky 9 | NAS | — |
```text ```text
ikaros must be boring ikaros must be boring
@@ -100,28 +101,9 @@ That gives it Fedora packages through DNF, Docker from the official repository,
## Server ## Server
`prometheus` is the Rocky Linux 9 server. It has no graphical environment and gets server-specific `prometheus` is the Ubuntu LTS server. It has no graphical environment and gets server-specific dotfiles and templates.
dotfiles and templates. The profile provisions configuration only: it does not transfer data, start
the Compose stack, update DNS, or perform a cutover.
The server profile installs platform-specific packages, Podman and podman-compose, declared systemd The server profile installs Ubuntu packages, Docker from the official repository, declared systemd services, UFW rules, and the server Compose stack. Syncthing ports `22000/tcp`, `22000/udp`, and `21027/udp` are opened; the Syncthing GUI is not directly opened in UFW.
services, and firewalld. The manually activated `podman-compose-server` unit contains the existing
Nginx Proxy Manager and Gitea services. The desired Compose file no longer includes Navidrome,
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
`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
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:
@@ -132,51 +114,7 @@ ansible-playbook ansible/site.yml --limit prometheus \
-e server_user_home=/srv/myuser -e server_user_home=/srv/myuser
``` ```
The target must already provide `server_username` with local sudo access. ## Aegis
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
`profile_server` renders `~/duckdns/duck.sh` with mode `0700`, keeping the existing updater path
and `duck.log`. Set `server_duckdns_domain` in the server's host vars and store the **rotated**
`vault_duckdns_token` in encrypted `secrets/vault.yml` (using `ansible-vault edit secrets/vault.yml`)
or untracked `secrets/vault.local.yml`. Never commit the rendered script or put the token on a
command line. Rendering hides secret output/diffs; the updater verifies TLS and passes the token
to curl through stdin. The playbook neither runs the updater nor changes its external schedule.
```bash
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff
ansible-playbook ansible/site.yml --limit prometheus --tags duckdns
```
An exposed token must be revoked/regenerated on DuckDNS: deleting it from Git history does not
revoke it. After a history cleanup, re-clone other checkouts rather than merging the old history
back in; preserve any uncommitted work separately without copying secrets.
### Data migration
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 application data:
```bash
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519
sudo ./scripts/migrate_prometheus_data.sh \
--destination rocky@179.237.102.172 \
--identity /root/.ssh/id_ed25519 \
--quiesce-source --execute
```
The script copies only Nginx Proxy Manager and Gitea data. It does not delete data, move
Navidrome/Syncthing, copy `/home/git/.ssh`, start containers, update DNS, or perform a cutover. The
destination SSH host key must already be trusted and the destination account needs passwordless sudo
for `rsync`. It preserves ACLs but not extended attributes, so source SELinux labels are not
transferred; the Rocky Compose bind mounts apply their own `:Z` labels when containers start.
## DNS Filter
`aegis` is a Raspberry Pi 4 running Fedora IoT. Generate Ignition from `aegis` is a Raspberry Pi 4 running Fedora IoT. Generate Ignition from
`ansible/bootstrap/aegis.bu` with the included Podman/Butane helper, then write the SD card with `ansible/bootstrap/aegis.bu` with the included Podman/Butane helper, then write the SD card with
@@ -198,10 +136,6 @@ 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
@@ -211,97 +145,28 @@ ansible-playbook ansible/site.yml --limit aegis --check --diff --ask-become-pass
## NAS ## NAS
`atlas` is a Rocky Linux 9 NAS reached through SSH. Normally its pool already exists and the profile `atlas` is a Rocky Linux 9 NAS reached through SSH. Its pool already exists: the profile only
only manages child datasets. A one-time RAIDZ2 bootstrap is available only with explicit confirmation manages child datasets and must never create, partition, destroy, roll back, or otherwise alter the
(`atlas_create_pool=true`) and exactly four verified `/dev/disk/by-id/...` paths in `atlas_zpool_disks`. pool itself. Linux clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the
It never partitions, forces, destroys, rolls back, or changes the vdev layout of an existing pool. Linux configured LAN.
clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the configured LAN.
For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and For the first run, replace the Atlas placeholders and provide
`vault_atlas_immich_db_password`. Bootstrap the host through its `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, and
existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then `vault_atlas_samba_password`. Bootstrap the host through its existing administrator:
include both WireGuard peers in the same idempotent playbook run:
```bash ```bash
ansible-playbook ansible/site.yml --limit prometheus,atlas \ ansible-playbook ansible/site.yml --limit atlas \
-e atlas_connection_username=<existing-admin> \ -e atlas_connection_username=<existing-admin>
-e atlas_create_pool=true
``` ```
The explicit pool gate is safe to repeat: the role creates the RAIDZ2 pool only when it is absent.
WireGuard waits for a real peer handshake before the play continues.
`vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text `vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
Cockpit password. Subsequent runs use `atlas_admin_username`. Atlas declares storage, sharing, and its Cockpit password. Subsequent runs use `atlas_admin_username`. Enable
LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet, `atlas_manage_storage` only after checking the existing pool and mountpoints; enable
and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container `atlas_manage_firewall` only after checking the LAN subnet and active firewalld zone.
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 Snapshot retention, Syncthing topology, VPN access, Prometheus pulls, encrypted Borg backups to a
explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests remain follow-up work. The
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`, detailed operational backlog is kept in `AGENTS.md`.
`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
@@ -373,6 +238,7 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
| Role | What it does | | Role | What it does |
| --- | --- | | --- | --- |
| `packages_void` | Installs packages on Void. | | `packages_void` | Installs packages on Void. |
| `packages_ubuntu` | Installs packages on Ubuntu. |
| `packages_fedora` | Installs packages on Fedora. | | `packages_fedora` | Installs packages on Fedora. |
| `packages_rocky` | Installs packages on Rocky Linux 9. | | `packages_rocky` | Installs packages on Rocky Linux 9. |
| `services_runit` | Manages runit services. | | `services_runit` | Manages runit services. |
@@ -387,35 +253,31 @@ 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. |
## What `site.yml` runs ## What `site.yml` runs
```text ```text
all except platform_rocky -> dotfiles_common all -> dotfiles_common
platform_void -> packages_void + services_runit 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)
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
workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora) workstation_dev_wsl -> profile_workstation_dev_wsl (after platform_fedora + workstation_dev_fedora)
ubuntu_server -> packages_ubuntu + services_systemd + profile_server
``` ```
So, in practice: So, in practice:
- `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`. - `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`.
- `deadalus` gets the Fedora development layer followed by the WSL layer. - `deadalus` gets the Fedora development layer followed by the WSL layer.
- `rocky_server` configures the Rocky 9 server, `prometheus`. - `ubuntu_server` configures `prometheus`.
- `atlas` receives the Rocky platform layer and the NAS profile through SSH. - `atlas` receives the Rocky platform layer and the NAS profile through SSH.
- `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive - `aegis` receives only the immutable Fedora IoT profile through SSH; it does not receive
mutable Fedora package or common dotfile roles. mutable Fedora package or common dotfile roles.
@@ -471,8 +333,7 @@ ansible-playbook ansible/site.yml --limit <host> --tags <tag1>,<tag2> --check --
ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff ansible-playbook ansible/site.yml --limit <host> --start-at-task "<task name>" --check --diff
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 docker compose -f /opt/docker/server/docker-compose.yml config
ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff
``` ```
## Tags ## Tags
@@ -487,9 +348,6 @@ 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. |
@@ -498,15 +356,10 @@ 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,14 +9,12 @@ variant: fiot
version: 1.0.0 version: 1.0.0
passwd: passwd:
users: users:
- name: admin - name: pi
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

@@ -116,9 +116,9 @@ ai_agents:
uninstall_enabled: false uninstall_enabled: false
github_copilot: github_copilot:
npm_package: "@github/copilot" npm_package: "@github/copilot"
install_enabled: false install_enabled: true
deploy_enabled: false deploy_enabled: true
uninstall_enabled: true uninstall_enabled: false
ibm_bob: ibm_bob:
install_enabled: true install_enabled: true
deploy_enabled: false deploy_enabled: false

View File

@@ -5,9 +5,6 @@ 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: >-
@@ -19,37 +16,17 @@ rocky_syncthing_archive_url: >-
~ '.tar.gz' ~ '.tar.gz'
}} }}
rocky_manage_openzfs_repo: false
rocky_manage_syncthing_binary: false
rocky_manage_podman: false
rocky_manage_github_cli_repo: false
rocky_github_cli_repo_url: https://cli.github.com/packages/rpm/gh-cli.repo
rocky_github_cli_repo_file: /etc/yum.repos.d/gh-cli.repo
enabled_services: enabled_services:
- firewalld - firewalld
rocky_common_packages:
- bash-completion
- bat
- fzf
- git
- jq
- tree
- unzip
- vim-enhanced
- wget
- zip
- zoxide
rocky_packages_base: rocky_packages_base:
- cockpit
- dnf-plugins-core - dnf-plugins-core
- firewalld - firewalld
- nfs-utils
- openssh-server - openssh-server
- policycoreutils-python-utils
- rsync - rsync
- selinux-policy-targeted - samba
- sudo - sudo
- zfs
rocky_profile_packages: []
rocky_podman_packages: []

View File

@@ -1,43 +0,0 @@
---
rocky_manage_podman: true
rocky_manage_github_cli_repo: true
rocky_profile_packages:
- avahi
- curl
- dmidecode
- dosfstools
- gcc
- gcc-c++
- git-delta
- gh
- gnupg2
- htop
- make
- nmap-ncat
- nodejs
- parted
- pciutils
- pinentry
- ranger
- ripgrep
rocky_podman_packages:
- podman
- podman-compose
enabled_services:
- firewalld
- podman.socket
server_firewall_backend: firewalld
server_firewalld_zone: public
server_firewalld_services:
- ssh
- cockpit
- http
- https
server_firewalld_ports: []
server_sshd_service_name: sshd
server_compose_selinux_mount_option: Z
server_syncthing_enabled: false

View File

@@ -13,4 +13,12 @@ personal_workstation_directories:
- path: "{{ user_home }}/Remotes" - path: "{{ user_home }}/Remotes"
mode: "0755" mode: "0755"
personal_workstation_dotfiles:
- src: .gitignore_global
dest: .gitignore_global
mode: "0644"
- src: .themes.gitignore
dest: .themes.gitignore
mode: "0644"
personal_workstation_flatpak_packages: [] personal_workstation_flatpak_packages: []

View File

@@ -25,9 +25,6 @@ profile_packages:
- rsync - rsync
server_dotfiles: server_dotfiles:
- src: .bashrc.d/20-editor-server.sh
dest: .bashrc.d/20-editor-server.sh
mode: "0644"
- src: .gnupg/gpg-agent.conf - src: .gnupg/gpg-agent.conf
dest: .gnupg/gpg-agent.conf dest: .gnupg/gpg-agent.conf
mode: "0600" mode: "0600"
@@ -37,6 +34,9 @@ server_dotfiles:
- src: .themes.gitignore - src: .themes.gitignore
dest: .themes.gitignore dest: .themes.gitignore
mode: "0644" mode: "0644"
- src: duckdns/
dest: duckdns/
mode: preserve
server_templates: server_templates:
- src: server/.gitconfig.j2 - src: server/.gitconfig.j2
@@ -50,10 +50,6 @@ server_templates:
no_log: true no_log: true
server_directories: server_directories:
- path: "{{ server_user_home }}/.bashrc.d"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0755"
- path: "{{ server_user_home }}/.gnupg" - path: "{{ server_user_home }}/.gnupg"
owner: "{{ server_username }}" owner: "{{ server_username }}"
group: "{{ server_user_group }}" group: "{{ server_user_group }}"
@@ -62,6 +58,14 @@ 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
@@ -70,13 +74,14 @@ 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
mode: "0755" mode: "0755"
server_syncthing_enabled: true
server_syncthing_directories:
- path: /opt/syncthing/config - path: /opt/syncthing/config
owner: root owner: root
group: root group: root
@@ -89,11 +94,33 @@ server_syncthing_directories:
owner: "1000" owner: "1000"
group: "1000" group: "1000"
mode: "0755" mode: "0755"
- path: /srv/nextcloud
owner: root
group: root
mode: "0755"
- path: /srv/nextcloud/data
owner: root
group: root
mode: "0755"
server_ufw_rules:
- rule: allow
name: OpenSSH
- rule: allow
port: "22000"
proto: tcp
comment: Syncthing sync traffic
- rule: allow
port: "22000"
proto: udp
comment: Syncthing QUIC sync traffic
- rule: allow
port: "21027"
proto: udp
comment: Syncthing local discovery
server_sshd_settings: server_sshd_settings:
PermitRootLogin: "no" PermitRootLogin: "no"
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

@@ -0,0 +1,37 @@
---
ubuntu_packages_base:
- curl
- ufw
- htop
- build-essential
- nodejs
- git-delta
- jq
- ripgrep
ubuntu_manage_docker_repo: true
ubuntu_emacs_packages:
- emacs
- pandoc
- latexmk
- texlive-luatex
- texlive-latex-base
- texlive-latex-recommended
- texlive-fonts-recommended
ubuntu_docker_packages:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
enabled_services:
- ufw
- docker
ubuntu_dotfiles:
- src: .bashrc.d/
dest: .bashrc.d/
mode: preserve

View File

@@ -9,12 +9,5 @@ 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 @@
--- ---
# Atlas declares its intended steady state; set a feature flag false only for a deliberate suspension. # Replace every CHANGEME value before enabling Atlas storage management.
hostname: atlas hostname: atlas
ansible_host: 192.168.178.55 ansible_host: CHANGEME_ATLAS_HOST
ansible_user: "{{ atlas_connection_username }}" ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22 ansible_port: 22
ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519 ansible_ssh_private_key_file: CHANGEME_ATLAS_SSH_PRIVATE_KEY_PATH
atlas_admin_username: admin atlas_admin_username: admin
atlas_connection_username: "{{ atlas_admin_username }}" atlas_connection_username: "{{ atlas_admin_username }}"
@@ -12,105 +12,39 @@ 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: "{{ vault_atlas_authorized_ssh_keys | default([]) }}"
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}" atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_ssh_allow_tcp_forwarding: local
atlas_lan_subnet: 192.168.178.0/24 atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_aegis_ip: 192.168.178.54 atlas_manage_firewall: false
atlas_manage_firewall: true
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: zpool atlas_zfs_pool: CHANGEME_ZFS_POOL
# Populate only for the first pool bootstrap with four real persistent disk paths.
# Confirmed empty 4 TB IronWolf data disks; the NVMe system disk is intentionally excluded.
atlas_zpool_disks:
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1XM
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A73T2
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM
atlas_create_pool: false
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_syncthing: syncthing
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_photobook: media/photobook atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_zfs_dataset_backups: backups atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_zfs_dataset_service_backups: backups/services atlas_manage_storage: false
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_syncthing_binary: false
rocky_manage_podman: true
rocky_podman_packages:
- podman
host_packages:
- cockpit
- nfs-utils
- policycoreutils
- policycoreutils-python-utils
- python3-libselinux
- samba
- samba-client
- samba-common-tools
- zfs
atlas_nfs_exports: atlas_nfs_exports:
- path: "{{ atlas_photobook_mountpoint }}" - path: "{{ atlas_work_mountpoint }}"
client: "{{ atlas_aegis_ip }}" clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)"
options:
- rw
- sync
- no_subtree_check
- all_squash
- "anonuid={{ atlas_immich_uid }}"
- "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: Archive atlas_samba_share_name: work
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_encryption: required atlas_samba_valid_users:
# Append any pre-existing laptop Unix account here and keep its Samba password in Vault. - "{{ atlas_admin_username }}"
atlas_samba_accounts: atlas_samba_password: "{{ vault_atlas_samba_password | default('') }}"
- 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 }}" port port="22" protocol="tcp" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="9090" protocol="tcp" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept'
- 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" port port="2049" protocol="tcp" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="445" protocol="tcp" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept'
host_enabled_services: host_enabled_services:
- sshd - sshd
- cockpit.socket - cockpit.socket
- nfs-server
- smb
- zfs.target - zfs.target

View File

@@ -1,35 +1,5 @@
--- ---
hostname: prometheus hostname: prometheus
ansible_host: 179.237.102.172
ansible_user: rocky
ansible_port: 22
ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
server_username: rocky host_packages: []
server_duckdns_domain: fscotto host_enabled_services: []
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:
- cockpit
- cockpit-navigator
- cockpit-podman
host_enabled_services:
- cockpit.socket

View File

@@ -20,8 +20,6 @@ all:
platform_rocky: platform_rocky:
hosts: hosts:
atlas: atlas:
children:
rocky_server:
void: void:
children: children:
@@ -69,6 +67,10 @@ all:
nymph: nymph:
ansible_connection: local ansible_connection: local
ubuntu:
children:
ubuntu_server:
workstation: workstation:
children: children:
workstation_dev: workstation_dev:
@@ -80,7 +82,7 @@ all:
server: server:
children: children:
rocky_server: ubuntu_server:
workstation_dev_fedora: workstation_dev_fedora:
hosts: hosts:
@@ -92,15 +94,7 @@ all:
deadalus: deadalus:
ansible_connection: local ansible_connection: local
rocky_server: ubuntu_server:
hosts: hosts:
prometheus: prometheus:
ansible_connection: local
wireguard_overlay:
hosts:
prometheus:
atlas:
role_backend_phase1:
hosts:
atlas:

View File

@@ -23,7 +23,6 @@
dest: "{{ effective_user_home }}/Templates" dest: "{{ effective_user_home }}/Templates"
owner: "{{ effective_username }}" owner: "{{ effective_username }}"
group: "{{ effective_user_group }}" group: "{{ effective_user_group }}"
creates: "{{ effective_user_home }}/Templates/code/main.c"
when: "'desktop' in group_names" when: "'desktop' in group_names"
- name: Ensure Emacs authoring directories exist - name: Ensure Emacs authoring directories exist

View File

@@ -27,20 +27,11 @@
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:
name: "{{ rocky_openzfs_release_rpm }}" name: "{{ rocky_openzfs_release_rpm }}"
state: present state: present
when: rocky_manage_openzfs_repo | bool
- name: Disable OpenZFS DKMS repository - name: Disable OpenZFS DKMS repository
tags: [packages, storage] tags: [packages, storage]
@@ -48,7 +39,6 @@
name: name:
- zfs - zfs
state: disabled state: disabled
when: rocky_manage_openzfs_repo | bool
- name: Enable OpenZFS kmod repository - name: Enable OpenZFS kmod repository
tags: [packages, storage] tags: [packages, storage]
@@ -56,17 +46,6 @@
name: name:
- zfs-kmod - zfs-kmod
state: enabled state: enabled
when: rocky_manage_openzfs_repo | bool
- name: Configure official GitHub CLI RPM repository
tags: [packages]
ansible.builtin.get_url:
url: "{{ rocky_github_cli_repo_url }}"
dest: "{{ rocky_github_cli_repo_file }}"
owner: root
group: root
mode: "0644"
when: rocky_manage_github_cli_repo | bool
- name: Refresh Rocky package metadata - name: Refresh Rocky package metadata
tags: [packages] tags: [packages]
@@ -79,32 +58,20 @@
name: >- name: >-
{{ {{
( (
(rocky_common_packages | default([])) (common_packages | default([]))
+ (rocky_packages_base | default([])) + (rocky_packages_base | default([]))
+ (rocky_profile_packages | default([])) + (profile_packages | default([]))
+ (rocky_podman_packages | default([]))
+ (host_packages | default([])) + (host_packages | default([]))
) | unique ) | unique
}} }}
state: present state: present
- name: Ensure Podman runtime socket directory exists
tags: [packages, podman]
ansible.builtin.file:
path: /run/podman
state: directory
owner: root
group: root
mode: "0700"
when: rocky_manage_podman | bool
- name: Require supported architecture for Syncthing - name: Require supported architecture for Syncthing
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- ansible_facts['architecture'] == 'x86_64' - ansible_facts['architecture'] == 'x86_64'
fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only. fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only.
when: rocky_manage_syncthing_binary | bool
- name: Read installed Syncthing version - name: Read installed Syncthing version
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -116,7 +83,6 @@
changed_when: false changed_when: false
failed_when: false failed_when: false
check_mode: false check_mode: false
when: rocky_manage_syncthing_binary | bool
- name: Determine whether Syncthing must be installed - name: Determine whether Syncthing must be installed
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -126,7 +92,6 @@
('syncthing v' ~ rocky_syncthing_version ~ ' ') ('syncthing v' ~ rocky_syncthing_version ~ ' ')
not in (rocky_syncthing_version_check.stdout | default('')) not in (rocky_syncthing_version_check.stdout | default(''))
}} }}
when: rocky_manage_syncthing_binary | bool
- name: Create temporary Syncthing extraction directory - name: Create temporary Syncthing extraction directory
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -136,9 +101,7 @@
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
when: when: rocky_syncthing_install_required | bool
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Download pinned Syncthing release - name: Download pinned Syncthing release
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -149,9 +112,7 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: when: rocky_syncthing_install_required | bool
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Extract pinned Syncthing release - name: Extract pinned Syncthing release
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -159,9 +120,7 @@
src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz" src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}" dest: "/tmp/syncthing-{{ rocky_syncthing_version }}"
remote_src: true remote_src: true
when: when: rocky_syncthing_install_required | bool
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Install pinned Syncthing binary - name: Install pinned Syncthing binary
tags: [packages, syncthing] tags: [packages, syncthing]
@@ -177,20 +136,16 @@
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
when: when: rocky_syncthing_install_required | bool
- rocky_manage_syncthing_binary | bool
- rocky_syncthing_install_required | bool
- name: Remove Syncthing release archive - name: Remove Syncthing release archive
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.file: ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz" path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
state: absent state: absent
when: rocky_manage_syncthing_binary | bool
- name: Remove Syncthing extraction directory - name: Remove Syncthing extraction directory
tags: [packages, syncthing] tags: [packages, syncthing]
ansible.builtin.file: ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}" path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: absent state: absent
when: rocky_manage_syncthing_binary | bool

View File

@@ -0,0 +1,82 @@
---
- name: Copy Ubuntu dotfiles
tags: [dotfiles, dotfiles:common]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/ubuntu/{{ item.src }}"
dest: "{{ effective_user_home }}/{{ item.dest }}"
owner: "{{ effective_username }}"
group: "{{ effective_user_group }}"
mode: "{{ item.mode }}"
loop: "{{ ubuntu_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"
- name: Ensure Docker apt keyrings directory exists
tags: [packages]
ansible.builtin.file:
path: /etc/apt/keyrings
state: directory
owner: root
group: root
mode: "0755"
when: ubuntu_manage_docker_repo | default(false)
- name: Download Docker apt repository signing key
tags: [packages]
ansible.builtin.get_url:
url: https://download.docker.com/linux/ubuntu/gpg
dest: /etc/apt/keyrings/docker.asc
owner: root
group: root
mode: "0644"
when: ubuntu_manage_docker_repo | default(false)
- name: Configure Docker apt repository
tags: [packages]
ansible.builtin.apt_repository:
repo: >-
deb [arch={{ 'amd64' if ansible_facts['architecture'] == 'x86_64'
else 'arm64' if ansible_facts['architecture'] in ['aarch64', 'arm64']
else ansible_facts['architecture'] }}
signed-by=/etc/apt/keyrings/docker.asc]
https://download.docker.com/linux/ubuntu
{{ ansible_facts['distribution_release'] }} stable
filename: docker
state: present
update_cache: true
when: ubuntu_manage_docker_repo | default(false)
- name: Refresh apt package cache
tags: [packages]
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install packages on Ubuntu
tags: [packages]
ansible.builtin.apt:
name: >-
{{
(
(common_packages | default([]))
+ (ubuntu_packages_base | default([]))
+ (ubuntu_docker_packages | default([]))
+ (profile_packages | default([]))
+ (
(ubuntu_emacs_packages | default([]))
if (emacs_enabled | default(false) | bool)
else []
)
+ (host_packages | default([]))
)
| unique
}}
state: present
- name: Add user to docker group
tags: [packages]
ansible.builtin.user:
name: "{{ effective_username }}"
groups: docker
append: true
when: (ubuntu_docker_packages | default([])) | length > 0

View File

@@ -12,6 +12,5 @@ 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:
- "{{ ansible_user }}" - pi
aegis_ssh_user_home: "/var/home/{{ ansible_user }}" aegis_ssh_user_home: "/var/home/{{ ansible_user }}"
aegis_ssh_authorized_keys: []

View File

@@ -165,29 +165,6 @@
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:
@@ -201,8 +178,7 @@
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- >- - >-
(aegis_ssh_authorized_keys | length > 0) (aegis_authorized_keys.stat.exists and aegis_authorized_keys.stat.size | int > 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,77 +1,29 @@
--- ---
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_archive: archive atlas_zfs_dataset_syncthing: syncthing
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_photobook: media/photobook atlas_zfs_dataset_icloud_photos: icloud_photos
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_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}" atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}" atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}"
atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}" atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}" atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync"
atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo
atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo
@@ -82,61 +34,18 @@ atlas_45drives_packages:
- cockpit-zfs - cockpit-zfs
- cockpit-scheduler - cockpit-scheduler
atlas_nfs_exports: atlas_nfs_exports: []
- path: "{{ atlas_photobook_mountpoint }}" atlas_samba_share_name: work
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_accounts: [] atlas_samba_password: ""
atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords atlas_samba_password_marker: /var/lib/samba/private/.atlas-password.sha256
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,14 +28,7 @@
name: smb name: smb
state: restarted state: restarted
- name: Restart Atlas media Quadlets - name: Restart Atlas Syncthing service
ansible.builtin.systemd: ansible.builtin.systemd:
name: "{{ item }}" name: atlas-syncthing
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,11 +6,9 @@
- 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, atlas_admin_ssh_keys and vault_atlas_admin_password_hash Define atlas_admin_username, vault_atlas_authorized_ssh_keys and
before applying the Atlas profile. vault_atlas_admin_password_hash before applying the Atlas profile.
no_log: true no_log: true
- name: Create Atlas administrator group - name: Create Atlas administrator group
@@ -33,41 +31,6 @@
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:
@@ -78,26 +41,13 @@
mode: "0440" mode: "0440"
validate: "visudo -cf %s" validate: "visudo -cf %s"
- name: Ensure Atlas administrator SSH authorized key fragments directory exists - name: Manage Atlas administrator authorized SSH keys exclusively
tags: [atlas, services] tags: [atlas, services]
ansible.builtin.file: ansible.posix.authorized_key:
path: "{{ atlas_admin_ssh_key_directory }}" user: "{{ atlas_admin_username }}"
state: directory key: "{{ atlas_admin_ssh_keys | join('\n') }}"
owner: "{{ atlas_admin_username }}" state: present
group: "{{ atlas_admin_group }}" exclusive: true
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]
@@ -163,18 +113,6 @@
- "'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

@@ -1,140 +0,0 @@
---
- 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

@@ -1,45 +0,0 @@
---
- 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,17 +5,11 @@
- 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 media-container tasks - name: Import Atlas Syncthing tasks
ansible.builtin.import_tasks: containers.yml ansible.builtin.import_tasks: syncthing.yml

View File

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

View File

@@ -1,64 +0,0 @@
---
- 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,12 +1,187 @@
---
- 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
- ansible_facts.default_ipv4.interface | default('') | length > 0 fail_msg: Replace the Atlas LAN subnet and firewall zone placeholders.
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules - name: Apply Atlas firewalld rich rules
@@ -22,16 +197,6 @@
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:
@@ -44,228 +209,3 @@
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,80 +5,12 @@
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 }}"
owner: "{{ atlas_admin_username }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}"
group: "{{ atlas_admin_group }}" mountpoint: "{{ atlas_icloud_photos_mountpoint }}"
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]
@@ -124,7 +56,8 @@
community.general.zfs: community.general.zfs:
name: "{{ item.name }}" name: "{{ item.name }}"
state: present state: present
extra_zfs_properties: "{{ item.properties }}" extra_zfs_properties:
mountpoint: "{{ item.mountpoint }}"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:
label: "{{ item.name }}" label: "{{ item.name }}"
@@ -135,8 +68,8 @@
ansible.builtin.file: ansible.builtin.file:
path: "{{ item.mountpoint }}" path: "{{ item.mountpoint }}"
state: directory state: directory
owner: "{{ item.owner }}" owner: "{{ atlas_admin_username }}"
group: "{{ item.group }}" group: "{{ atlas_admin_group }}"
mode: "0770" mode: "0770"
loop: "{{ atlas_zfs_datasets }}" loop: "{{ atlas_zfs_datasets }}"
loop_control: loop_control:

View File

@@ -0,0 +1,40 @@
---
- 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,22 +3,4 @@ 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

@@ -1,27 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,20 +0,0 @@
# 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

@@ -1,29 +0,0 @@
# 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

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

View File

@@ -1,21 +0,0 @@
# 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

@@ -0,0 +1,16 @@
[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.client }}({{ export.options | join(',') }}) {{ export.path }} {{ export.clients }}
{% endfor %} {% endfor %}

View File

@@ -1,13 +0,0 @@
# 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,16 +3,11 @@
workgroup = {{ atlas_samba_workgroup }} workgroup = {{ atlas_samba_workgroup }}
security = user security = user
map to guest = Never map to guest = Never
server min protocol = SMB3 server min protocol = SMB2
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_archive_mountpoint }} path = {{ atlas_work_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

@@ -1,28 +0,0 @@
---
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

@@ -1,32 +0,0 @@
---
- 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

@@ -1,163 +0,0 @@
---
- 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

@@ -1,28 +0,0 @@
# 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

@@ -1,30 +0,0 @@
# 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

@@ -10,3 +10,15 @@
loop: "{{ personal_workstation_directories | default([]) }}" loop: "{{ personal_workstation_directories | default([]) }}"
loop_control: loop_control:
label: "{{ item.path }}" label: "{{ item.path }}"
- name: Copy personal workstation dotfiles
tags: [dotfiles, dotfiles:workstation]
ansible.builtin.copy:
src: "{{ playbook_dir }}/../dotfiles/workstation/{{ item.src }}"
dest: "{{ user_home }}/{{ item.dest }}"
owner: "{{ username }}"
group: "{{ user_group }}"
mode: "{{ item.mode }}"
loop: "{{ personal_workstation_dotfiles | default([]) }}"
loop_control:
label: "{{ item.dest }}"

View File

@@ -2,10 +2,5 @@
- name: Reload SSH service - name: Reload SSH service
tags: [services] tags: [services]
ansible.builtin.service: ansible.builtin.service:
name: "{{ server_sshd_service_name }}" name: ssh
state: reloaded state: reloaded
- name: Reload systemd for Podman Compose
tags: [services]
ansible.builtin.systemd:
daemon_reload: true

View File

@@ -1,33 +0,0 @@
---
- name: Require DuckDNS domain and Vault token before deployment
ansible.builtin.assert:
that:
- >-
server_duckdns_domain | default('') is
regex('[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?', match_type='fullmatch')
- >-
vault_duckdns_token | default('') is
regex('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', match_type='fullmatch')
fail_msg: >-
Define server_duckdns_domain in host_vars and the rotated vault_duckdns_token
in encrypted Vault or an untracked local vars file before deploying DuckDNS.
no_log: true
- name: Ensure private DuckDNS directory exists
ansible.builtin.file:
path: "{{ server_user_home }}/duckdns"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
- name: Render DuckDNS updater with the Vault token
ansible.builtin.template:
src: duck.sh.j2
dest: "{{ server_user_home }}/duckdns/duck.sh"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
validate: /bin/sh -n %s
no_log: true
diff: false

View File

@@ -1,37 +1,5 @@
--- ---
- name: Require supported server firewall backend
tags: [services, packages]
ansible.builtin.assert:
that:
- server_firewall_backend == 'firewalld'
fail_msg: >-
server_firewall_backend must be firewalld for the Rocky server profile.
- name: Configure DuckDNS updater
tags: [dotfiles, dotfiles:server, duckdns]
ansible.builtin.import_tasks: duckdns.yml
- name: Ensure server directories exist
tags: [dotfiles, services]
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: >-
{{
(server_directories | default([]))
+ (
server_syncthing_directories | default([])
if server_syncthing_enabled | default(true) | bool
else []
)
}}
loop_control:
label: "{{ item.path }}"
- name: Copy server dotfiles - name: Copy server dotfiles
tags: [dotfiles, dotfiles:server] tags: [dotfiles, dotfiles:server]
ansible.builtin.copy: ansible.builtin.copy:
@@ -44,6 +12,28 @@
loop_control: loop_control:
label: "{{ item.dest }}" label: "{{ item.dest }}"
- 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: Ensure server directories exist
tags: [dotfiles, services]
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop: "{{ server_directories | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Render server templates - name: Render server templates
tags: [dotfiles, dotfiles:server] tags: [dotfiles, dotfiles:server]
ansible.builtin.template: ansible.builtin.template:
@@ -57,46 +47,6 @@
label: "{{ item.dest }}" label: "{{ item.dest }}"
no_log: "{{ item.no_log | default(false) }}" no_log: "{{ item.no_log | default(false) }}"
- name: Manage Podman Compose stack
tags: [services, podman]
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:
@@ -118,28 +68,33 @@
notify: Reload SSH service notify: Reload SSH service
when: (server_sshd_allow_users | default([])) | length > 0 when: (server_sshd_allow_users | default([])) | length > 0
- name: Allow server services through firewalld - name: Define effective server UFW rules
tags: [services, packages] tags: [services, packages]
ansible.posix.firewalld: ansible.builtin.set_fact:
service: "{{ item }}" server_ufw_rules_effective: "{{ server_ufw_rules | default([]) }}"
zone: "{{ server_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop: "{{ server_firewalld_services | default([]) }}"
loop_control:
label: "{{ item }}"
when: server_firewall_backend == 'firewalld'
- name: Allow server ports through firewalld - name: Apply server UFW rules
tags: [services, packages] tags: [services, packages]
ansible.posix.firewalld: community.general.ufw:
port: "{{ item }}" rule: "{{ item.rule }}"
zone: "{{ server_firewalld_zone }}" name: "{{ item.name | default(omit) }}"
state: enabled port: "{{ item.port | default(omit) }}"
permanent: true proto: "{{ item.proto | default(omit) }}"
immediate: true from_ip: "{{ item.src | default(omit) }}"
loop: "{{ server_firewalld_ports | default([]) }}" to_ip: "{{ item.dest | default(omit) }}"
from_port: "{{ item.from_port | default(omit) }}"
direction: "{{ item.direction | default(omit) }}"
interface: "{{ item.interface | default(omit) }}"
interface_in: "{{ item.interface_in | default(omit) }}"
interface_out: "{{ item.interface_out | default(omit) }}"
route: "{{ item.route | default(omit) }}"
comment: "{{ item.comment | default(omit) }}"
loop: "{{ server_ufw_rules_effective }}"
loop_control: loop_control:
label: "{{ item }}" label: "{{ item.name | default(item.port) }}"
when: server_firewall_backend == 'firewalld'
- name: Enable UFW firewall on server
tags: [services, packages]
community.general.ufw:
state: enabled
when: (server_ufw_rules_effective | default([])) | length > 0

View File

@@ -1,10 +0,0 @@
---
- name: Render Podman Compose systemd unit
tags: [services, podman]
ansible.builtin.template:
src: podman-compose-server.service.j2
dest: /etc/systemd/system/podman-compose-server.service
owner: root
group: root
mode: "0644"
notify: Reload systemd for Podman Compose

View File

@@ -1,24 +0,0 @@
#!/bin/sh
# Managed by Ansible. Contains a Vault token; never copy this file into Git.
set -eu
umask 077
log_file={{ (server_user_home ~ '/duckdns/duck.log') | quote }}
# Keep the token out of process arguments and verify the HTTPS certificate.
if ! response=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 --config - <<'DUCKDNS_CONFIG'
url = "https://www.duckdns.org/update?domains={{ server_duckdns_domain }}&token={{ vault_duckdns_token }}&ip="
DUCKDNS_CONFIG
); then
printf 'ERROR\n' > "$log_file"
exit 1
fi
case "$response" in
OK) printf 'OK\n' > "$log_file" ;;
*)
printf 'KO\n' > "$log_file"
printf 'DuckDNS update failed.\n' >&2
exit 1
;;
esac

View File

@@ -1,15 +0,0 @@
[Unit]
Description=Podman Compose stack for {{ server_username }}
Requires=network-online.target
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory={{ server_container_stack_dir }}
ExecStart=/usr/bin/podman-compose up -d
ExecStop=/usr/bin/podman-compose down
ExecReload=/usr/bin/podman-compose up -d
[Install]
WantedBy=multi-user.target

View File

@@ -1,18 +0,0 @@
---
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

@@ -1,10 +0,0 @@
---
- 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

@@ -1,192 +0,0 @@
---
- 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

@@ -1,25 +0,0 @@
# 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

@@ -7,7 +7,6 @@
tags: [always] tags: [always]
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../secrets/vault.yml" file: "{{ playbook_dir }}/../secrets/vault.yml"
no_log: true
when: >- when: >-
lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.yml', lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.yml',
errors='ignore', wantlist=True) | length > 0 errors='ignore', wantlist=True) | length > 0
@@ -16,7 +15,6 @@
tags: [always] tags: [always]
ansible.builtin.include_vars: ansible.builtin.include_vars:
file: "{{ playbook_dir }}/../secrets/vault.local.yml" file: "{{ playbook_dir }}/../secrets/vault.local.yml"
no_log: true
when: >- when: >-
lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.local.yml', lookup('ansible.builtin.fileglob', playbook_dir + '/../secrets/vault.local.yml',
errors='ignore', wantlist=True) | length > 0 errors='ignore', wantlist=True) | length > 0
@@ -73,13 +71,6 @@
- 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
@@ -94,21 +85,6 @@
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
hosts: rocky_server
become: true
roles:
- dotfiles_common
- profile_server
- name: Configure personal workstation role on Fedora - name: Configure personal workstation role on Fedora
hosts: platform_fedora:&role_personal_workstation hosts: platform_fedora:&role_personal_workstation
become: true become: true
@@ -136,3 +112,12 @@
roles: roles:
- profile_workstation_dev_wsl - profile_workstation_dev_wsl
- name: Configure Ubuntu server
hosts: ubuntu_server
become: true
roles:
- packages_ubuntu
- services_systemd
- profile_server

View File

@@ -1,23 +1,40 @@
--- ---
{% set selinux_volume_option = server_compose_selinux_mount_option | default('') %} version: "3.8"
name: server
services: services:
navidrome:
image: 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"
- "/opt/music:/music:ro"
user: "1000:1000"
networks:
- web
depends_on:
- navidromedb
nginx-proxy-manager: nginx-proxy-manager:
image: docker.io/jc21/nginx-proxy-manager:latest image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager container_name: nginx-proxy-manager
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "127.0.0.1:81:81" - "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"
- "/opt/npm/letsencrypt:/etc/letsencrypt{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/npm/letsencrypt:/etc/letsencrypt"
networks: networks:
- web - web
- gitea
# Disabled: prometheus does not have enough resources to run Nextcloud AIO. # Disabled: prometheus does not have enough resources to run Nextcloud AIO.
# nextcloud-aio-mastercontainer: # nextcloud-aio-mastercontainer:
@@ -38,6 +55,20 @@ services:
# networks: # networks:
# - web # - web
navidromedb:
image: 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"
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
@@ -46,39 +77,39 @@ services:
- USER_GID=1100 - USER_GID=1100
restart: always restart: always
networks: networks:
- web - gitea
volumes: volumes:
- /opt/gitea/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }} - /opt/gitea/data:/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /home/git/.ssh:/data/git/.ssh{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }} - /home/git/.ssh:/data/git/.ssh
ports: ports:
- "3000:3000" - "3000:3000"
- "127.0.0.1:222:22" - "127.0.0.1:222:22"
{% if server_syncthing_enabled | default(true) | bool %}
syncthing: syncthing:
image: docker.io/syncthing/syncthing:2 image: syncthing/syncthing:2
container_name: syncthing container_name: syncthing
hostname: syncthing hostname: syncthing
restart: unless-stopped restart: unless-stopped
expose: expose:
- "8384" - "8384"
volumes: volumes:
- "/opt/syncthing/config:/var/syncthing{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/opt/syncthing/config:/var/syncthing"
- "/srv/syncthing/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}" - "/srv/syncthing/data:/data"
ports: ports:
- "22000:22000/tcp" - "22000:22000/tcp"
- "22000:22000/udp" - "22000:22000/udp"
- "21027:21027/udp" - "21027:21027/udp"
networks: networks:
- web - web
{% endif %}
networks: networks:
web: web:
name: server_web name: server_web
external: false external: false
gitea:
external: false
# volumes: # volumes:
# nextcloud_aio_mastercontainer: # nextcloud_aio_mastercontainer:

View File

@@ -1,5 +1,2 @@
# Additional Bash aliases. # Additional Bash aliases.
# Keep this file as the extension point for shared aliases. # Keep this file as the extension point for shared aliases.
# Nginx Proxy Manager administration tunnel on Prometheus.
alias npm-tunnel='ssh -N -o ExitOnForwardFailure=yes -L 8181:127.0.0.1:81 rocky@179.237.102.172'

View File

@@ -1,5 +1,5 @@
# Server # Server
- Rocky Linux 9 server - Ubuntu minimal server
- Self-hosting services - Self-hosting services
- Low attack surface - Low attack surface

View File

@@ -1,4 +1,4 @@
Host prometheus-ubuntu Host vps
IdentityFile ~/.ssh/id_rsa_vps IdentityFile ~/.ssh/id_rsa_vps
Host * Host *

View File

@@ -0,0 +1 @@
OK

View File

@@ -0,0 +1 @@
echo url="https://www.duckdns.org/update?domains=fscotto&token=d173a037-8a10-4e40-8d9d-bebedbdde086&ip=" | curl -k -o ~/duckdns/duck.log -K -

View File

@@ -0,0 +1,17 @@
case "$(uname -r 2>/dev/null)" in
*[Mm]icrosoft*) ;;
*) return ;;
esac
command -v gpgconf >/dev/null 2>&1 || return
if tty -s; then
export GPG_TTY="$(tty)"
fi
gpgconf --launch gpg-agent >/dev/null 2>&1
export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
if [ -n "${GPG_TTY-}" ]; then
gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1
fi

View File

@@ -1,161 +0,0 @@
#!/usr/bin/env sh
# 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
# unless --execute and --quiesce-source are both supplied. Extended attributes
# are deliberately not copied: Rocky must assign its own SELinux labels.
set -eu
SOURCE_COMPOSE_FILE=/opt/docker/server/docker-compose.yml
DESTINATION=
IDENTITY_FILE=
EXECUTE=false
QUIESCE_SOURCE=false
DATA_PATHS='
/opt/npm/data
/opt/npm/letsencrypt
/opt/gitea/data
'
usage() {
cat <<'EOF'
Usage: sudo ./scripts/migrate_prometheus_data.sh --destination USER@HOST [options]
Copies persistent Nginx Proxy Manager and Gitea data to the Rocky server with
rsync. The destination Docker containers must be stopped.
Options:
--destination USER@HOST Rocky SSH destination (required).
--identity PATH SSH private key readable by root on the source host.
--source-compose PATH Source Compose file (default: /opt/docker/server/docker-compose.yml).
--quiesce-source Stop the source Compose stack before copying.
--execute Perform the transfer; otherwise only show changes.
-h, --help Show this help.
The script never deletes source data, destination-only files, containers, or
volumes. It intentionally excludes Syncthing and /home/git/.ssh.
EOF
}
fail() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1"
}
while [ "$#" -gt 0 ]; do
case "$1" in
--destination)
[ "$#" -ge 2 ] || fail '--destination requires USER@HOST'
DESTINATION=$2
shift 2
;;
--identity)
[ "$#" -ge 2 ] || fail '--identity requires a path'
IDENTITY_FILE=$2
shift 2
;;
--source-compose)
[ "$#" -ge 2 ] || fail '--source-compose requires a path'
SOURCE_COMPOSE_FILE=$2
shift 2
;;
--quiesce-source)
QUIESCE_SOURCE=true
shift
;;
--execute)
EXECUTE=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "unknown option: $1"
;;
esac
done
[ "$(id -u)" -eq 0 ] || fail 'run this script with sudo on the Ubuntu source host'
[ -n "$DESTINATION" ] || fail '--destination is required'
if [ -n "$IDENTITY_FILE" ]; then
[ -r "$IDENTITY_FILE" ] || fail "SSH identity is not readable: $IDENTITY_FILE"
case "$IDENTITY_FILE" in
*' '*|*"$(printf '\t')"*) fail 'SSH identity paths must not contain whitespace' ;;
esac
fi
if [ "$EXECUTE" = true ] && [ "$QUIESCE_SOURCE" != true ]; then
fail '--execute requires --quiesce-source to keep application data consistent'
fi
require_command rsync
require_command ssh
SSH_COMMAND='ssh -o BatchMode=yes'
if [ -n "$IDENTITY_FILE" ]; then
SSH_COMMAND="$SSH_COMMAND -i $IDENTITY_FILE"
fi
run_ssh() {
# shellcheck disable=SC2086
$SSH_COMMAND "$DESTINATION" "$@"
}
printf 'Destination: %s\n' "$DESTINATION"
printf 'Mode: %s\n' "$( [ "$EXECUTE" = true ] && printf execute || printf dry-run )"
printf 'Data paths:\n%s\n' "$DATA_PATHS"
run_ssh 'sudo -n true' || fail 'destination sudo must be passwordless for this transfer'
run_ssh 'sudo -n docker info >/dev/null' \
|| fail 'destination Docker daemon is unavailable'
if run_ssh 'sudo -n docker ps -q | grep -q .'; then
fail 'destination Docker containers must be stopped before migration'
fi
for path in $DATA_PATHS; do
[ -d "$path" ] || fail "source directory is missing: $path"
run_ssh "sudo -n test -d $path" || fail "destination directory is missing: $path"
done
if [ "$QUIESCE_SOURCE" = true ]; then
require_command docker
[ -f "$SOURCE_COMPOSE_FILE" ] || fail "source Compose file is missing: $SOURCE_COMPOSE_FILE"
if [ "$EXECUTE" = true ]; then
printf 'Stopping source Compose stack...\n'
docker compose -f "$SOURCE_COMPOSE_FILE" stop
else
printf 'Dry-run: source Compose stack would be stopped.\n'
fi
fi
for path in $DATA_PATHS; do
printf '\nSyncing %s\n' "$path"
if [ "$EXECUTE" = true ]; then
rsync -aHA --numeric-ids --itemize-changes --human-readable --partial \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
else
rsync -aHA --numeric-ids --itemize-changes --human-readable --partial --dry-run \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
fi
done
if [ "$EXECUTE" = true ]; then
printf '\nVerifying source-to-destination parity...\n'
for path in $DATA_PATHS; do
rsync -aHA --numeric-ids --itemize-changes --dry-run \
--rsync-path='sudo -n rsync' -e "$SSH_COMMAND" "$path/" "$DESTINATION:$path/"
done
printf '\nTransfer completed. Keep the source stack stopped until application validation on Rocky succeeds.\n'
else
printf '\nDry-run completed. Re-run with --quiesce-source --execute after reviewing the changes.\n'
fi

View File

@@ -1,78 +1,40 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
31386434333363613930316363393564373332303236306233643365386639346631336232373361 65623833316230316230376465656261383230643661393032396462643232383334383236653134
3732633931376438313835373537666438383664306266380a633531316432343166323361303465 3964353234623165633736303035623132643565393461660a323930623462323635613361373836
39306136386664356163346266303963373839373763343136346135633236333333326331313063 37613164656538353734333932326136303532346161663238613466616236636432346534626331
6264306265373865310a373735393632373861333433326632303931633732636535326264346537 3434663266366637650a336235363033333562626135333962626265666332386261643666306638
39633766326361666432396432623132616666363462373237623664363664373362346366383230 64343262353661346435326662633833656533643464653665356233316466333432303536356539
65326665303762356233356531306331333231376163353638363936626562623937323166623065 32623430303663616261386636613534326231613366636536303761646335396132623231336361
62633165383033303362336238343037353930396430336537363565333132666532323662653262 61656337313766353533643266396230626439353033373630666336616230316362616135323462
65373531313264353938666161373064393239653165666462336665323962336134343432316134 30306633373234386639633932393437323931613739383135353835393630623937636639343338
32373361613539356262313333663964623034313230356439626139383539326261663765373034 35636530643330376538393262383738333735656230363633383463386366343065633961663562
36633933366662656464306561393130373238313730336638663932633935313037363133636266 37653264326565376166666262643136616333656233386564333030663532353836306131316530
36663935303735623132663464376633306431396662306166393831313566323238363865303162 62633738306434303236623239646638613230323434393761656265393064373235333435393264
66303732376530653435613966373832333161333137366230613166303061333433336461386234 36363936336330636433643133346164643733356239616530653831626165316238643834356236
63393030616662373666643164663862653037383336323766646330653131623930353265336231 36333262313764653137633930663131306132306434376433306230306564366633363738313634
39646335353635616465663763306262353931316562616437313362616136623735646535616431 35316230623134623932396132316530366662643366643439643334366233646334633764346266
33363439623361343231363663363535663265366166346439623935323632366336363135373462 35373764633138343235323931346134613265356238616138323134326338633335336339313862
62646662316538323734333334393566333463653833326162663065646634363336373865623932 61653939366263386431636334333661306538643166353661336561396365366434353830643465
33646533636638386534366561663930613536663935663638313137646537626431393035653466 31626136626336633363653531363064643534396336623331363130313966653966373636663232
36623435613966323166363630383531656436373366343364346439343034333934623836336636 65653132363139653239383636333861323435613362666637366162323062376139633033373563
38653534343563393434646430306662343435653465363439326261373537666233353731306436 66656134313931313965303134613836383534636438363936353836326639346565653031363839
35323739336361343665633239366633393530346335396635316238323435663466316235376536 63623738366261363734376536646532396231616233303061363364383238616434333366613933
38303839313433643038353236616632626363653339346334303136306138336461383831626536 63616539613336393863633361303466353738313666393164343638373530376666663666666235
39303966393034333739363061363865326165303236656438316537396463383935393764346135 33666234353736633335343639623664326566333162333539636235346663663737366432366665
66323632323833353833643434316566326366633562666262386232653730303038613336663264 66373831303666666533343234373466333132343135326261663532663062313939623963383437
62636332383834393861373665393364653362636339346632303463386565633830336363393065 34653732653830313638343631313437623931623531323566306232373165636662316633333438
31383530613161366436323163393366386635316562633436383134623061353937326363396439 66666537623966623932323463323330316337613637383339613637653637333832623965636434
38636438396637616362373937666361303536376164636533653536306338356263313965623336 34653330336663323263353636353139323337646166623862343565646637616136646232373633
32633461623861643138613734313164633562613932386163643062353636376266633166373838 33356134373464646664303865353763323531383661323930353038646333386366353665396431
39613438653531316333663736366161313832613830353566356461393435656234383037353335 64303739643735376235626134663566343165646433393966373961393738323036353437346436
32343661376266353538643531313239306432646335383734623233373063316430383362643531 66656166306561353637623462303039393465363261343164393062643130383736623261383232
35393263343534363936373361316265653934383735663933663365363564663966646335353337 38623036626464613138363635646233323737653137633830316138663835656633336663626338
63366365383466393765336533363130643236313331633537346237353631623334633330376164 33386232633862616432396662343738343462623263303337393533313264396637356134373739
32616530623032663761643437336536323332343130393339313232623364656334613233323833 61323266623665353631316461633462646536386266343934356235363065653162363566623661
64346637343738386335343631333035636337633732333662663763326362636432646136353064 35643332303333306362303538333034316461326363653632313765396664643533373862333533
30393061326435323837633632333765623931663265326137373135303035623464333366653566 37306536356465393933366238663031653630386239383839316365323761316236326330306637
66613133396339376264313964353932396136613538383264363865633536373839663465333437 36373434643864343064646437323736393330326361663231373933613864303339653661616139
37613765626131343330653063663764346364316264363635653438396331363263623562333735 36356134636136333830663031656132656237383331663362313238653133366538323462376632
30646563303439663639376430336336383761363365353838613036333032306434623661636138 63373766383939656265313934636462626633376162623931333363393837303336363036303439
63616439636437663732386164346365643834376631303263376563323662373734653631613730 32643730383735383535633439326564376261343937343934366366373039653761383934316162
31373039656238303462643930303531613931373065316435303661343862623034653364653736 383035326562353431616536646238323164
37613835616630316133333130633631653639336266313438633166316539373433333661613839
32356139366232313336343062323265366563633735383661393335386163653133313664613264
63303739323863333439316461366137656434666366306466356633306663623730653939663430
39313764663534316562326435316264633236373834323665356266346532323565323532333438
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

@@ -1,5 +1,4 @@
--- ---
vault_duckdns_token: "CHANGEME"
vault_personal_full_name: "REPLACE_ME" vault_personal_full_name: "REPLACE_ME"
vault_git_email: "REPLACE_ME" vault_git_email: "REPLACE_ME"
vault_git_signing_key: "REPLACE_ME" vault_git_signing_key: "REPLACE_ME"
@@ -9,8 +8,7 @@ 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"