Compare commits

..

9 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
de2c24d15c Mount the shared Archive in Syncthing 2026-09-17 19:41:23 +02:00
Fabio Scotto di Santolo
bd13cb65cc Move temporary Navidrome and Syncthing services to Atlas 2026-09-17 10:18:23 +02:00
Fabio Scotto di Santolo
77afdda0a3 Move WireGuard gateway to Aegis 2026-09-17 09:08:25 +02:00
Fabio Scotto di Santolo
add75d74e9 refactor(atlas): separate service and host backup datasets 2026-09-17 00:06:46 +02:00
Fabio Scotto di Santolo
e46a2b22d7 Update Atlas NAS validation status and remaining TODOs 2026-09-16 10:25:38 +02:00
Fabio Scotto di Santolo
a97c64b08f Configure Aegis host DNS and NFS support 2026-09-16 10:19:06 +02:00
Fabio Scotto di Santolo
160d63c02d Add Atlas media and storage services [Phase 1] (#9)
* Add Atlas media and storage services

* Document Atlas backend phase one and WireGuard deployment

* Enable Atlas NAS management and document bootstrap workflow

* Harden Atlas network, SSH, firewall, and sharing

* Rotate Ansible Vault secrets

* Allow configurable Aegis SSH users and authorized keys

* Manage Aegis SSH authorized key fragments

* Manage SSH authorized key fragments for infrastructure hosts

* Harden Rocky storage and sharing configuration

* Verify WireGuard handshakes and restore Podman networking
2026-09-15 22:39:28 +02:00
Fabio Scotto di Santolo
73bf2cd62a feature: add WSL container tooling 2026-09-15 16:38:33 +02:00
Fabio Scotto di Santolo
e8d059c8ed Install ChatGPT desktop on Fedora desktops 2026-09-11 21:27:48 +02:00
30 changed files with 549 additions and 248 deletions

View File

@@ -48,6 +48,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Rocky server after activation: `ansible-playbook ansible/site.yml --limit <host> --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`
- Aegis NFS client layer: `ansible-playbook ansible/site.yml --limit aegis --tags nfs --list-tasks`
- Aegis host DNS: `ansible-playbook ansible/site.yml --limit aegis --tags dns --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`
@@ -57,10 +59,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
- Atlas network/share hardening: - Atlas network/share hardening:
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
- Atlas phase-one rootless services: - Prometheus/Aegis WireGuard gateway:
`ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff` `ansible-playbook ansible/site.yml --limit prometheus,aegis --tags wireguard --check --diff`
- Prometheus/Atlas WireGuard overlay:
`ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard --check --diff`
- DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff` - DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff`
## Conventions ## Conventions
@@ -137,10 +137,10 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled, - Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled,
`vault_atlas_samba_password`. The future rootful media stack also requires `vault_atlas_samba_password`. The future rootful media stack also requires
`vault_atlas_immich_db_password`. Never print these values. `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`, - Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `archive`,
`archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`, `services`, `services/data`, `services/data/navidrome`, `services/data/syncthing`, `media`, `media/music`,
`media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. `backups/services` has a `500G` `media/photobook`, `backup`, `backup/hosts`, and `backup/hosts/prometheus`. `backup` has a `500G`
refreservation. There is no separate legacy `zpool/syncthing` dataset. reservation covering its descendants. `archive` is the SMB-shared raw-data namespace; container state is never beneath it.
- The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only - The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only
the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive
`/dev/dri`, while the Photobook external library is read-only at `/external/photobook`. `/dev/dri`, while the Photobook external library is read-only at `/external/photobook`.
@@ -149,38 +149,36 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
`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. `1100`. Targeted SELinux is enforced persistently; a required reboot is reported but never initiated automatically. The primary LAN interface is assigned explicitly to the managed firewalld zone, and firewall rules are applied before NFS or SMB are started; their service state and TCP listeners are then verified. SMB3 exposes `Archive` to Vault-backed authorized accounts on mandatory encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific exclusions.
- Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains - Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet. bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet.
- `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome - `profile_backend_phase1` temporarily runs rootless Navidrome and Syncthing on Atlas until Uranus replaces
`0.63.2` uses SQLite below `/data` and does not support `ND_DATABASE_URL` or an external PostgreSQL backend; do not them. It binds only to Atlas' LAN IP, never `wg0`; Navidrome and the Syncthing GUI admit only Aegis as
recreate the obsolete Prometheus `navidromedb` service. The role requires the storage role's `zpool/media/music`, the source-NAT gateway, while native Syncthing ports admit the configured LAN. It initializes fresh
`zpool/archive/app_data`, `zpool/archive/app_data/navidrome`, and `zpool/archive/app_data/syncthing` datasets at state only and never migrates or deletes source application data.
their exact paths. It never creates the pool. - `wireguard_overlay` manages `wg0` between Prometheus (`10.0.0.1`) and Aegis (`10.0.0.2`). It persists private
- Keep `backend_phase1_start_services` false until the stopped Prometheus Navidrome data directory has been copied to keys only on their respective hosts, exchanges only derived public keys through Ansible, and verifies a real peer
Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application handshake. Prometheus opens `51820/udp`; Aegis is the LAN gateway. Its persistent IPv4 forwarding, narrowly scoped
data; after cutover, set the flag true to enable and start Navidrome and Syncthing. WireGuard-to-LAN firewalld policy, and source masquerading permit Prometheus to reach LAN services without a static
- Phase 1 must not change Prometheus' existing NPM deployment. NPM continues to be managed exactly by `profile_server`; route on the router. Prometheus includes `192.168.178.0/24` in Aegis' peer `AllowedIPs`; add the Uranus VIP there
use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking: when it is assigned. After a firewalld reload, restore Prometheus' rootful Podman networking with
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. `podman network reload --all` so the existing proxy stack retains container DNS.
- `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. Completed validation: the existing RAIDZ2 pool and datasets, SELinux, LAN firewall, SSH, Cockpit with
Before the first apply, confirm the pool, mountpoints, LAN subnet and firewalld zone. Keep the selected 45Drives plugins, encrypted SMB3 `Archive`, the Aegis-only NFSv4 `photobook` export, and
`atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated. the former Prometheus--Atlas WireGuard path were operational. Aegis has validated NFSv4.2 read, write, delete,
- Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset and `all_squash` mapping to UID/GID `1100` end-to-end.
mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing. - Validate the Prometheus--Aegis WireGuard gateway after migration: peer handshake and counters, Aegis IPv4
- Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files forwarding and masquerading, and an NPM request from Prometheus to an Atlas LAN address. Add the Uranus VIP to
through both NFS and SMB before considering multiprotocol access complete. Prometheus' Aegis peer when the cluster control plane is assigned.
- Validate temporary Atlas Navidrome and Syncthing through Aegis before creating their NPM Proxy Hosts.
Keep NPM host configuration manual; plan their eventual Uranus migration with storage and routing declared
separately from the NAS baseline.
- Decide whether a common SMB/NFS namespace is required. `Archive` (SMB) and `photobook` (NFS) are
intentionally distinct today; only if a shared namespace is selected, finalize its UID/GID, group,
and POSIX ACL model and test the same files through both protocols.
- Keep `atlas_manage_media_stack` disabled until the future Immich deployment has validated `/dev/dri`,
container paths, and the required Vault database secret.
- Add Ansible-managed ZFS snapshot retention and scrub timers. Use Cockpit Scheduler for visibility - Add Ansible-managed ZFS snapshot retention and scrub timers. Use Cockpit Scheduler for visibility
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
or API access for the selected clients.
- Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services;
never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared - Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared
read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key, read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key,
atomic pull, verification, retention and systemd service/timer. atomic pull, verification, retention and systemd service/timer.
@@ -193,8 +191,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
failed backup timers, plus a controlled Rocky kernel/OpenZFS update and reboot procedure. failed backup timers, plus a controlled Rocky kernel/OpenZFS update and reboot procedure.
- Document and test disaster recovery: rebuild Atlas with Ansible, import the existing pool, restore - Document and test disaster recovery: rebuild Atlas with Ansible, import the existing pool, restore
from snapshot/USB/Hetzner, preserve Vault and Borg recovery material offline, and define RPO/RTO. from snapshot/USB/Hetzner, preserve Vault and Borg recovery material offline, and define RPO/RTO.
- Optionally design iCloud photo ingestion as a separate workflow after the storage and backup layers - Optionally design iCloud photo ingestion and an Aegis persistent NFS mount as a separate workflow
are validated; do not make it a dependency of the Atlas baseline. after the storage and backup layers are validated; do not make either a dependency of the Atlas
baseline.
## Coding Agent Notes ## Coding Agent Notes
- Shared agent definitions and lifecycle flags live in `ai_agents` in `ansible/inventory/group_vars/all.yml`. - Shared agent definitions and lifecycle flags live in `ai_agents` in `ansible/inventory/group_vars/all.yml`.
@@ -221,14 +220,19 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `aegis` is a remote Fedora IoT Raspberry Pi 4 node. Bootstrap it once with - `aegis` is a remote Fedora IoT Raspberry Pi 4 node. Bootstrap it once with
`ansible/bootstrap/aegis.bu`; the remaining configuration is applied by `profile_aegis` over SSH. `ansible/bootstrap/aegis.bu`; the remaining configuration is applied by `profile_aegis` over SSH.
- Fedora IoT is immutable. Do not add it to mutable Fedora package or shared dotfile roles. - Fedora IoT is immutable. Do not add it to mutable Fedora package or shared dotfile roles.
- `profile_aegis` owns rootful Podman Quadlets, persistent container state under `/var/lib`, the - `profile_aegis` owns the `nfs-utils` and `wireguard-tools` rpm-ostree layers and reports the required reboot
Podman auto-update timer, LAN-restricted firewalld rules, and SSH hardening. Keep without initiating it. `wireguard_overlay` then configures Aegis as the WireGuard LAN gateway with persistent IPv4
`aegis_lan_subnet` and `aegis_adguard_web_port` host-specific; SSH permits only the declared forwarding, a scoped inter-zone policy, and source masquerading. It also owns rootful Podman Quadlets, persistent container
state under `/var/lib`, the Podman auto-update timer, LAN-restricted firewalld rules, and SSH hardening. Keep
`aegis_lan_subnet`, `aegis_adguard_web_port`, and `aegis_network_connection_uuid` host-specific;
SSH permits only the declared
key-authenticated users, never root or password authentication. Keep Apple IDs and other key-authenticated users, never root or password authentication. Keep Apple IDs and other
credentials in Vault and use `no_log` for their rendering. credentials in Vault and use `no_log` for their rendering.
- `aegis_adguard_web_port` defaults to `80`. The initial AdGuard Home wizard port `3000` is intentionally unmanaged: open and close it manually only while - `aegis_adguard_web_port` defaults to `80`. The initial AdGuard Home wizard port `3000` is intentionally unmanaged: open and close it manually only while
completing initial setup. Disable the local systemd-resolved stub through `profile_aegis` before completing initial setup. Disable the local systemd-resolved stub through `profile_aegis` before
AdGuard binds port 53; keep AdGuard binds port 53; keep
`/etc/resolv.conf` linked to `/run/systemd/resolve/resolv.conf` so Aegis retains router-provided DNS. `/etc/resolv.conf` linked to `/run/systemd/resolve/resolv.conf`. LAN clients may use AdGuard, but
Aegis must use the independent upstream DNS declared by `aegis_host_dns_servers` so Greenboot does
not depend on the AdGuard container during startup.
- iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is - iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is
persisted in `/var/lib/icloudpd/config`. persisted in `/var/lib/icloudpd/config`.

145
README.md
View File

@@ -63,6 +63,10 @@ nymph is allowed to break
`ikaros` is the stable personal Fedora/GNOME desktop. `nymph` is the laptop and gets the same shared desktop dotfiles while GNOME itself stays close to the Fedora defaults. The legacy `void` and `desktop` groups are compatibility parents; the main axes are `platform_*`, `role_*`, and `desktop_*`. `ikaros` is the stable personal Fedora/GNOME desktop. `nymph` is the laptop and gets the same shared desktop dotfiles while GNOME itself stays close to the Fedora defaults. The legacy `void` and `desktop` groups are compatibility parents; the main axes are `platform_*`, `role_*`, and `desktop_*`.
The official ChatGPT desktop RPM is enabled only on `ikaros` and `nymph`. The
playbook configures OpenAI's signed RPM repository and imports its pinned RPM
signing key before installation; subsequent updates are handled by DNF.
## Desktop profiles ## Desktop profiles
- `ikaros`: stable Fedora Workstation + GNOME desktop. - `ikaros`: stable Fedora Workstation + GNOME desktop.
@@ -107,21 +111,19 @@ the Compose stack, update DNS, or perform a cutover.
The server profile installs platform-specific packages, Podman and podman-compose, declared systemd The server profile installs platform-specific packages, Podman and podman-compose, declared systemd
services, and firewalld. The manually activated `podman-compose-server` unit contains the existing 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, 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; Syncthing, or the obsolete Navidrome PostgreSQL database; their temporary Atlas deployment is managed
official Navidrome uses SQLite instead. Applying the profile does not stop or remove legacy by `profile_backend_phase1`. Applying the profile does not stop or remove legacy containers and does
containers and does not delete `/opt/postgres/data`. not delete `/opt/postgres/data`.
Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only
`80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached `80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached
from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile
does not provision any `/srv/nextcloud` directories. does not provision any `/srv/nextcloud` directories.
The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard NPM remains managed only by `profile_server`. Its WireGuard peer is Aegis (`10.0.0.2`), which forwards
and the Atlas services are active, configure the current NPM proxy hosts with Navidrome upstream selected requests to LAN addresses and source-NATs them so no static route is required on the router.
`http://10.0.0.2:4533` and Syncthing GUI upstream `http://10.0.0.2:8384`. Only the Syncthing web GUI Use an Atlas LAN address for any current NAS-backed upstream; when Uranus receives its VIP, add that VIP
uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas to Prometheus' Aegis peer `AllowedIPs` and declare the corresponding proxy target separately.
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:
@@ -189,12 +191,19 @@ ansible/bootstrap/generate-aegis-ign.sh --write IMAGE DEVICE
The controller manages it remotely as `pi@aegis`; unlike local desktop profiles, Aegis is The controller manages it remotely as `pi@aegis`; unlike local desktop profiles, Aegis is
intentionally an SSH inventory target. `profile_aegis` manages rootful Podman Quadlets for AdGuard intentionally an SSH inventory target. `profile_aegis` manages rootful Podman Quadlets for AdGuard
Home and iCloudPD, persistent data under `/var/lib`, the Podman auto-update timer, LAN-restricted Home and iCloudPD, persistent data under `/var/lib`, the Podman auto-update timer, LAN-restricted
firewalld rules, SSH key-only access for `pi`, and `wake-ikaros`. Set the host-local firewalld rules, SSH key-only access for `pi`, the `nfs-utils` and `wireguard-tools` rpm-ostree layers,
`aegis_lan_subnet` and `aegis_adguard_web_port` values before applying it. The playbook permits and `wake-ikaros`. `wireguard_overlay` makes Aegis the internal endpoint and LAN gateway for Prometheus:
it enables persistent IPv4 forwarding, installs a scoped WireGuard-to-LAN firewalld policy, and source-NATs
forwarded tunnel traffic so the router needs no static route. A new layered package deployment requires a manual reboot; the
role reports this condition but never reboots Aegis automatically. Set the host-local
`aegis_lan_subnet`, `aegis_adguard_web_port`, and `aegis_network_connection_uuid` values before
applying it. The playbook permits
AdGuard Home HTTP on port `80`; the initial wizard port `3000` is intentionally unmanaged and must be AdGuard Home HTTP on port `80`; the initial wizard port `3000` is intentionally unmanaged and must be
opened and closed manually during initial setup. The profile disables the local systemd-resolved DNS opened and closed manually during initial setup. The profile disables the local systemd-resolved DNS
stub and points `/etc/resolv.conf` to its full resolver data, freeing port 53 stub and points `/etc/resolv.conf` to its full resolver data, freeing port 53 for AdGuard. LAN clients
for AdGuard while retaining DNS learned from the router. Define may use AdGuard on Aegis, while Aegis itself uses the independent upstream DNS declared by
`aegis_host_dns_servers`; this prevents Greenboot from depending on the AdGuard container during
startup. Reboot Aegis after changing its NetworkManager DNS profile. 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.
@@ -209,6 +218,21 @@ ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit aegis --check --diff --ask-become-pass ansible-playbook ansible/site.yml --limit aegis --check --diff --ask-become-pass
``` ```
Apply only the independent host DNS configuration, then reboot Aegis manually:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit aegis --tags dns --ask-become-pass
```
Layer the Aegis NFS and WireGuard client tools independently, then reboot Aegis manually when the role reports
that the new deployment is ready:
```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit aegis --tags nfs --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. Normally its pool already exists and the profile
@@ -219,17 +243,9 @@ clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the co
For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and
`vault_atlas_immich_db_password`. Bootstrap the host through its `vault_atlas_immich_db_password`. Bootstrap the host through its
existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then existing administrator. The explicit pool gate is safe to repeat: the role creates the RAIDZ2 pool only when
include both WireGuard peers in the same idempotent playbook run: it is absent. Atlas no longer participates in the WireGuard overlay; its old interface is retired manually only after
Prometheus and Aegis have completed the replacement handshake.
```bash
ansible-playbook ansible/site.yml --limit prometheus,atlas \
-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`. Atlas declares storage, sharing, and its
@@ -239,11 +255,11 @@ paths, and the Immich database secret are validated. Atlas reads its declared SS
separate files below `~/.ssh/authorized_keys.d/`. separate files below `~/.ssh/authorized_keys.d/`.
With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or
explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and explicitly bootstrapped `zpool`: SMB-shared `archive`, private `services/data` with separate
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`, `services/data/navidrome` and `services/data/syncthing` application datasets, `media`, `media/music`,
`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`, `media/photobook`, and `backup/hosts/prometheus`. Application/archive datasets use `zstd`, while media,
while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G` Syncthing, and host-backup datasets use `lz4`; `backup` has a `500G` reservation covering its descendants.
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, 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 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 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 exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using
@@ -255,49 +271,33 @@ and NPM Quadlets share one Podman network. Immich runs as `1100:1100`; Server an
and Photobook is mounted read-only at `/external/photobook`. NPM publishes ports `80` and `443`; its and Photobook is mounted read-only at `/external/photobook`. NPM publishes ports `80` and `443`; its
administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access. administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access.
Phase 1 is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. It is enabled in the Atlas temporarily hosts rootless Navidrome and Syncthing until Uranus replaces them. They bind only to
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 Atlas' LAN address (`192.168.178.55`); WireGuard remains exclusively between Prometheus (`10.0.0.1`)
not support `ND_DATABASE_URL` or an external PostgreSQL backend. The obsolete `navidromedb` service and Aegis (`10.0.0.2`). Their state is initialized ex novo in `/zpool/services/data/navidrome` and
was therefore removed from Prometheus instead of being reproduced on Atlas. The role derives all `/zpool/services/data/syncthing`; no source application state is migrated. The music library at
storage paths from the `zpool` mounted at `/zpool`: music is read-only at `/zpool/media/music` is populated separately.
`/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 The separate `wireguard_overlay` role manages `wg0` between Prometheus (`10.0.0.1`) and Aegis
backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome (`10.0.0.2`), generating private keys once on their respective hosts and exchanging only public keys
on Prometheus, copy the complete `/opt/navidrome/data/` directory into through Ansible. Prometheus alone opens `51820/udp`. Aegis forwards only the declared overlay-to-LAN
`/zpool/archive/app_data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set traffic and source-NATs it, so Atlas and future Uranus nodes require neither a VPN interface nor a router
this variable to true and rerun the role to enable and start Navidrome and Syncthing. The playbook static route. Atlas permits Navidrome (`4533/tcp`) and the Syncthing GUI (`8384/tcp`) only from Aegis;
never copies or deletes application data. Syncthing native ports are limited to the LAN. Configure NPM manually with
`http://192.168.178.55:4533` and `http://192.168.178.55:8384` after the services are healthy.
Prometheus' peer includes the LAN subnet in `AllowedIPs`; add the Uranus VIP there when it exists.
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.
Validate and render the Atlas services with: Validate the gateway with:
```bash ```bash
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \ ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
ansible-playbook ansible/site.yml --limit atlas --tags storage ansible-playbook ansible/site.yml --limit prometheus,aegis --tags wireguard --check --diff
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 first real WireGuard run must include both peers. If Fedora IoT has just layered `wireguard-tools`,
the Atlas `admin` account and confirm that the copied SQLite database is present before changing reboot Aegis manually and rerun the command without `--check`; the role then waits for a real peer
`backend_phase1_start_services: true` in `host_vars/atlas.yml`. Keep the source data and the stopped handshake.
legacy `navidromedb` container until Navidrome on Atlas and a restore test have been validated.
Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls, Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls,
encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests
@@ -334,9 +334,10 @@ ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=t
## AI coding agents ## AI coding agents
The shared npm-managed agents are OpenCode, Claude Code, Codex, Gemini CLI, and The shared npm-managed agents are OpenCode, Claude Code, Codex, Gemini CLI, and
GitHub Copilot; IBM Bob is also managed on `deadalus`. Each agent has its own GitHub Copilot; IBM Bob is also managed on `deadalus`. Codex Relay is installed
lifecycle flags in `ansible/inventory/group_vars/all.yml`, so one agent can be only on `ikaros`. Each agent has its own lifecycle flags in
installed, configured, or removed without affecting the others: `ansible/inventory/group_vars/all.yml`, so one agent can be installed,
configured, or removed without affecting the others:
```yaml ```yaml
ai_agents: ai_agents:
@@ -387,8 +388,8 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
| `profile_workstation_dev_wsl` | WSL development setup. | | `profile_workstation_dev_wsl` | WSL development setup. |
| `profile_server` | Server setup. | | `profile_server` | Server setup. |
| `profile_atlas` | Rocky Linux 9 NAS setup. | | `profile_atlas` | Rocky Linux 9 NAS setup. |
| `profile_backend_phase1` | Rootless Navidrome and Syncthing on Atlas. | | `profile_backend_phase1` | Temporary rootless Atlas Navidrome and Syncthing services. |
| `wireguard_overlay` | Prometheus/Atlas WireGuard overlay. | | `wireguard_overlay` | Prometheus/Aegis WireGuard LAN gateway. |
| `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. |
@@ -400,8 +401,8 @@ 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
wireguard_overlay -> wireguard_overlay (after Aegis profile and platform_rocky)
atlas -> profile_atlas atlas -> profile_atlas
role_backend_phase1 -> profile_backend_phase1 (after atlas) role_backend_phase1 -> profile_backend_phase1 (after atlas)
rocky_server -> dotfiles_common + profile_server (after platform_rocky) rocky_server -> dotfiles_common + profile_server (after platform_rocky)
@@ -506,7 +507,7 @@ ansible-playbook ansible/site.yml --list-tags
| `sharing` | Atlas NFSv4 and SMB3 configuration. | | `sharing` | Atlas NFSv4 and SMB3 configuration. |
| `storage` | Atlas child ZFS datasets. | | `storage` | Atlas child ZFS datasets. |
| `tmux` | tmux configuration and plugins. | | `tmux` | tmux configuration and plugins. |
| `wireguard` | Prometheus/Atlas WireGuard overlay. | | `wireguard` | Prometheus/Aegis WireGuard LAN gateway. |
| `wsl` | WSL bootstrap and configuration. | | `wsl` | WSL bootstrap and configuration. |
## Bootstrapping a new machine ## Bootstrapping a new machine

View File

@@ -93,6 +93,8 @@ common_dotfiles:
# AI coding-agent lifecycle controls are independent for every agent. # AI coding-agent lifecycle controls are independent for every agent.
# Installation and removal are mutually exclusive per agent; the common # Installation and removal are mutually exclusive per agent; the common
# pre-task enforces this before any changes are made. # pre-task enforces this before any changes are made.
codex_relay_install_enabled: false
ai_agents: ai_agents:
opencode: opencode:
npm_package: opencode-ai npm_package: opencode-ai
@@ -109,6 +111,11 @@ ai_agents:
install_enabled: true install_enabled: true
deploy_enabled: true deploy_enabled: true
uninstall_enabled: false uninstall_enabled: false
codex_relay:
npm_package: codex-relay
install_enabled: "{{ codex_relay_install_enabled }}"
deploy_enabled: false
uninstall_enabled: false
gemini_cli: gemini_cli:
npm_package: "@google/gemini-cli" npm_package: "@google/gemini-cli"
install_enabled: true install_enabled: true

View File

@@ -58,3 +58,13 @@ fedora_dev_packages:
fedora_flatpak_remote_name: flathub fedora_flatpak_remote_name: flathub
fedora_flatpak_remote_url: https://dl.flathub.org/repo/flathub.flatpakrepo fedora_flatpak_remote_url: https://dl.flathub.org/repo/flathub.flatpakrepo
chatgpt_desktop_enabled: false
chatgpt_desktop_supported_fedora_versions:
- "43"
- "44"
chatgpt_desktop_supported_architectures:
- x86_64
- aarch64
chatgpt_desktop_rpm_gpg_key: RPM-GPG-KEY-chatgpt-3BFA0E4AE8B8CC16A2D9BA684A3B4A566C4660E4.asc
chatgpt_desktop_rpm_repository: https://persistent.oaistatic.com/codex-app-prod/linux/rpm/$basearch

View File

@@ -40,4 +40,3 @@ server_firewalld_services:
server_firewalld_ports: [] server_firewalld_ports: []
server_sshd_service_name: sshd server_sshd_service_name: sshd
server_compose_selinux_mount_option: Z server_compose_selinux_mount_option: Z
server_syncthing_enabled: false

View File

@@ -75,21 +75,6 @@ server_directories:
group: root group: root
mode: "0755" mode: "0755"
server_syncthing_enabled: true
server_syncthing_directories:
- path: /opt/syncthing/config
owner: root
group: root
mode: "0755"
- path: /srv/syncthing
owner: root
group: root
mode: "0755"
- path: /srv/syncthing/data
owner: "1000"
group: "1000"
mode: "0755"
server_sshd_settings: server_sshd_settings:
PermitRootLogin: "no" PermitRootLogin: "no"

View File

@@ -3,6 +3,10 @@ enabled_services:
- docker - docker
workstation_dev_wsl_packages: workstation_dev_wsl_packages:
- buildah
- helm
# Fedora ships the kubectl client binary in the kubernetes-client package.
- kubernetes-client
- mise - mise
- pinentry-curses - pinentry-curses
- python3-pip - python3-pip

View File

@@ -9,6 +9,32 @@ 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_network_connection_name: FRITZ!Box 7530 NR
aegis_network_connection_uuid: a52fda3d-3eb6-351f-bf04-753edcb76944
aegis_host_dns_servers:
- 192.168.178.1
aegis_host_dns_search_domains:
- fritz.box
# Aegis is the internal WireGuard endpoint and NAT gateway for the LAN.
wireguard_overlay_enabled: true
wireguard_address: 10.0.0.2/24
wireguard_enable_ipv4_forwarding: true
wireguard_reload_rootful_podman_networks: true
wireguard_forwarding_policies:
- name: aegis-wireguard-to-lan
ingress_zone: wireguard
egress_zone: public
source: 10.0.0.0/24
destination: "{{ aegis_lan_subnet }}"
masquerade: true
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
aegis_ssh_authorized_keys: aegis_ssh_authorized_keys:
- name: ikaros - name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros" key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"

View File

@@ -35,35 +35,31 @@ atlas_zpool_disks:
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK - /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM - /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM
atlas_create_pool: false atlas_create_pool: false
atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data atlas_zfs_dataset_services: services
atlas_zfs_dataset_navidrome: archive/app_data/navidrome atlas_zfs_dataset_app_data: services/data
atlas_zfs_dataset_syncthing: archive/app_data/syncthing atlas_zfs_dataset_navidrome: services/data/navidrome
atlas_zfs_dataset_syncthing: services/data/syncthing
atlas_zfs_dataset_media: media atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup: backup
atlas_zfs_dataset_host_backups: backup/hosts
atlas_zfs_dataset_backup_prometheus: backup/hosts/prometheus
atlas_zfs_backup_reservation: 500G
atlas_zfs_dataset_photobook: media/photobook atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services
atlas_mount_root: /zpool atlas_mount_root: /zpool
atlas_manage_storage: true atlas_manage_storage: true
atlas_manage_sharing: true atlas_manage_sharing: true
atlas_manage_media_stack: false atlas_manage_media_stack: false
wireguard_overlay_enabled: true # WireGuard is retired on Atlas. These rootless services are a temporary home
wireguard_address: 10.0.0.2/24 # until Uranus replaces them.
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_enabled: true
backend_phase1_start_services: false backend_phase1_start_services: true
backend_phase1_wireguard_address: 10.0.0.2 backend_phase1_bind_address: "{{ ansible_host }}"
backend_phase1_firewalld_zone: "{{ atlas_firewalld_zone }}"
backend_phase1_npm_source_ip: "{{ atlas_aegis_ip }}"
backend_phase1_syncthing_native_subnet: "{{ atlas_lan_subnet }}"
rocky_manage_openzfs_repo: true rocky_manage_openzfs_repo: true
rocky_manage_syncthing_binary: false rocky_manage_syncthing_binary: false

View File

@@ -1,6 +1,9 @@
--- ---
hostname: ikaros hostname: ikaros
codex_relay_install_enabled: true
chatgpt_desktop_enabled: true
host_packages: host_packages:
- openssh-clients - openssh-clients
- openssh-server - openssh-server
@@ -229,7 +232,7 @@ gnome_gsettings_settings:
- schema: org.gnome.shell - schema: org.gnome.shell
key: app-picker-layout key: app-picker-layout
value: >- value: >-
[{'System': <{'position': <0>}>, 'Utilities': <{'position': <1>}>, 'org.fedoraproject.MediaWriter.desktop': <{'position': <2>}>, 'org.gnome.Contacts.desktop': <{'position': <3>}>, 'org.gnome.Calendar.desktop': <{'position': <4>}>, 'org.gnome.Calculator.desktop': <{'position': <5>}>, 'org.gnome.Weather.desktop': <{'position': <6>}>, 'org.gnome.clocks.desktop': <{'position': <7>}>, 'org.gnome.Maps.desktop': <{'position': <8>}>, 'org.gnome.SimpleScan.desktop': <{'position': <9>}>, 'org.gnome.TextEditor.desktop': <{'position': <10>}>, 'org.gnome.Settings.desktop': <{'position': <11>}>, 'org.gnome.Boxes.desktop': <{'position': <12>}>, 'org.gnome.Showtime.desktop': <{'position': <13>}>, 'org.gnome.Snapshot.desktop': <{'position': <14>}>, 'org.gnome.Characters.desktop': <{'position': <15>}>, 'org.gnome.Tour.desktop': <{'position': <16>}>, 'org.gnome.Yelp.desktop': <{'position': <17>}>, 'org.gnome.Software.desktop': <{'position': <18>}>, 'be.alexandervanhee.gradia.desktop': <{'position': <19>}>, 'com.github.jeromerobert.pdfarranger.desktop': <{'position': <20>}>, 'org.onlyoffice.desktopeditors.desktop': <{'position': <21>}>, 'org.gnome.Meld.desktop': <{'position': <22>}>, 'com.mattjakeman.ExtensionManager.desktop': <{'position': <23>}>}, {'org.remmina.Remmina.desktop': <{'position': <0>}>, 'ch.protonmail.protonmail-bridge.desktop': <{'position': <1>}>, 'syncthing-start.desktop': <{'position': <2>}>, 'syncthing-ui.desktop': <{'position': <3>}>, 'emacs.desktop': <{'position': <4>}>, 'emacs-client.desktop': <{'position': <5>}>, 'org.gnome.gitlab.somas.Apostrophe.desktop': <{'position': <6>}>, 'mpv.desktop': <{'position': <7>}>, 'htop.desktop': <{'position': <8>}>, 'org.mozilla.thunderbird.desktop': <{'position': <9>}>}] [{'System': <{'position': <0>}>, 'Utilities': <{'position': <1>}>, 'org.fedoraproject.MediaWriter.desktop': <{'position': <2>}>, 'org.gnome.Contacts.desktop': <{'position': <3>}>, 'org.gnome.Calendar.desktop': <{'position': <4>}>, 'org.gnome.Calculator.desktop': <{'position': <5>}>, 'org.gnome.Weather.desktop': <{'position': <6>}>, 'org.gnome.clocks.desktop': <{'position': <7>}>, 'org.gnome.Maps.desktop': <{'position': <8>}>, 'org.gnome.SimpleScan.desktop': <{'position': <9>}>, 'org.gnome.TextEditor.desktop': <{'position': <10>}>, 'org.gnome.Settings.desktop': <{'position': <11>}>, 'org.gnome.Boxes.desktop': <{'position': <12>}>, 'org.gnome.Showtime.desktop': <{'position': <13>}>, 'org.gnome.Snapshot.desktop': <{'position': <14>}>, 'org.gnome.Characters.desktop': <{'position': <15>}>, 'org.gnome.Tour.desktop': <{'position': <16>}>, 'org.gnome.Yelp.desktop': <{'position': <17>}>, 'org.gnome.Software.desktop': <{'position': <18>}>, 'be.alexandervanhee.gradia.desktop': <{'position': <19>}>, 'com.github.jeromerobert.pdfarranger.desktop': <{'position': <20>}>, 'org.onlyoffice.desktopeditors.desktop': <{'position': <21>}>, 'org.gnome.Meld.desktop': <{'position': <22>}>, 'com.mattjakeman.ExtensionManager.desktop': <{'position': <23>}>}, {'org.remmina.Remmina.desktop': <{'position': <0>}>, 'ch.protonmail.protonmail-bridge.desktop': <{'position': <1>}>, 'syncthing-start.desktop': <{'position': <2>}>, 'syncthing-ui.desktop': <{'position': <3>}>, 'emacs.desktop': <{'position': <4>}>, 'emacs-client.desktop': <{'position': <5>}>, 'org.gnome.gitlab.somas.Apostrophe.desktop': <{'position': <6>}>, 'mpv.desktop': <{'position': <7>}>, 'htop.desktop': <{'position': <8>}>, 'com.raspberrypi.rpi-imager.desktop': <{'position': <9>}>}]
- schema: org.gnome.shell - schema: org.gnome.shell
key: disabled-extensions key: disabled-extensions
value: >- value: >-
@@ -241,7 +244,7 @@ gnome_gsettings_settings:
- schema: org.gnome.shell - schema: org.gnome.shell
key: favorite-apps key: favorite-apps
value: >- value: >-
['org.gnome.Nautilus.desktop', 'org.mozilla.firefox.desktop', 'com.spotify.Client.desktop', 'org.gnome.Evolution.desktop', 'org.telegram.desktop.desktop', 'org.gnome.Ptyxis.desktop', 'virt-manager.desktop'] ['org.gnome.Nautilus.desktop', 'org.mozilla.firefox.desktop', 'com.spotify.Client.desktop', 'org.gnome.Evolution.desktop', 'org.telegram.desktop.desktop', 'chatgpt.desktop', 'org.gnome.Ptyxis.desktop', 'virt-manager.desktop']
- schema: org.gnome.shell - schema: org.gnome.shell
key: remember-mount-password key: remember-mount-password
value: "true" value: "true"

View File

@@ -1,6 +1,8 @@
--- ---
hostname: nymph hostname: nymph
chatgpt_desktop_enabled: true
# Fedora Workstation/GNOME laptop target. GNOME is provided by the base Fedora # Fedora Workstation/GNOME laptop target. GNOME is provided by the base Fedora
# Workstation install; Ansible deploys shared desktop dotfiles but leaves GNOME # Workstation install; Ansible deploys shared desktop dotfiles but leaves GNOME
# settings unmanaged for now. # settings unmanaged for now.

View File

@@ -21,10 +21,12 @@ wireguard_listen_port: 51820
wireguard_enable_ipv4_forwarding: true wireguard_enable_ipv4_forwarding: true
wireguard_reload_rootful_podman_networks: true wireguard_reload_rootful_podman_networks: true
wireguard_peers: wireguard_peers:
- name: atlas - name: aegis
host: atlas host: aegis
# TODO: Aggiungere VIP di Uranus in futuro
allowed_ips: allowed_ips:
- 10.0.0.2/32 - 10.0.0.2/32
- 192.168.178.0/24
host_packages: host_packages:
- cockpit - cockpit

View File

@@ -99,7 +99,7 @@ all:
wireguard_overlay: wireguard_overlay:
hosts: hosts:
prometheus: prometheus:
atlas: aegis:
role_backend_phase1: role_backend_phase1:
hosts: hosts:

View File

@@ -0,0 +1,28 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGpypFUBEACi1Vvzq9pIpA6lj7chbqELuxJtVuzUzxrasa6ZU0yF4yhq7jf8
3YkJRHwbezBKeQyzJ5lkX0EhXS8aXxUhMAm3PFpAlwcInfKzmV7atJwvaxIw6Rmd
GYe9fBWKjTN/SmPIjtyxrTznZY97+TfD1AeGZpLaJ8fsnhrC+HkiN2TACiTocgpe
hFiP0OWK7mWZeTWnY2scpIYXP1Ro7nQv4KacmY4JacTQ7m/HM0Qej/3olhuEv2Cw
lMVWw57/oHhmTllfLDQOogFQyIVqaaR98y/Eu6cAabSfcsqAAZ2A8vfHYD27z28J
vLO2PZEJd5ThlnX4Zqv0eIpZdBj//8Sl/MSqTshFZ1NDsRoqwdqw284X5MpnOJ4k
4Sc2Se8tJxt/nCeibH3dJ504Fb1X/mnOqhCAQ6pVJz4RB5HRlFPSkxVPyag1v1m/
7T4vie+OR4eqFQNz6mudrOoMmeVIfyL5fbe4cOr4fk/FyvEE2xMgkFatPqXn7vM9
og+zremPCfwRAFpBPyX74VowFY7llcdaj/w8K5T8PzM14Hb3E4ZKizMluKmTvTq9
WE1/eSQJLLQqXD5VmtmdUaC/VyE/1ZlIxcA1LWqvEQ327UXREvX/nHsrkKrl956W
jzkiHFUTsD1NJ0dMfs+csOt8Furb5jZj+HsMmCm9jLdfz5b/4WKLPbvxIwARAQAB
tBZDb2RleCBMaW51eCBSZXBvc2l0b3J5iQJRBBMBCgA7FiEEO/oOSui4zBai2bpo
SjtKVmxGYOQFAmpypFUCGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQ
SjtKVmxGYORlCQ/9FyikZo8HQcJBP9E/oXVPds/fQnIFB2qJR2z3DrfYEonNt/ev
SAySkPPq4/mEOjaI0pFlDDGSaps+FTcJFgoVRTasBIF7JJivvjW9ap8iWEbhhVLe
IrFLbMLpUcTRntUx7R4fVMJ/1/cGn+NWZmNwS9ORorzSyCH0IAgCw1Xc3ZrjuMbF
VjdToMC1TiXXCEmlYpQakmQ3Ay1cH0FHC2BBNn1MNVkJdPhpZIZCdhaMPHfYFpyo
pg8wFvZ5iIcvlbMgyuy8CPJVRWUcYy2dOhEOGnYJnXRPkE3E1hf8YOHNzRlduH89
6lT9qcEK2+fpLfrVGoc4zscLZ+Ey+Ko6iQRdVE1j67+wNR3hX8ukue574v1N/xxu
i575jumSE19lEj1sH4+P4gFHOtTbF0JhKKzLctbga0IAwTPKhnt3qzj1U5Yj/MZS
uEVjrLhdRauOuFBXUclgyVf2w/lE85UUOdlcollsYA6Huq7xDamqf8SslZQGre3E
I+lhpqJR1cOwDMUzzcl40uTyhrxXXd/bk4QSlhZbwHR25Pnt+ZMtWavlQWS0eDEV
8djuXAURCmx5WOqAFB/TJe1mn5EvyWg4VFzrY/NVNOpzgY5+Xp7J28z7f637r712
Eu9j4imVcdPigwS+jf/0f81i2o9b82Y26TN8+EtDLCY841MJ1lrjDrX/dno=
=Y+3h
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -130,6 +130,56 @@
loop_control: loop_control:
label: "{{ item.key }}" label: "{{ item.key }}"
- name: Validate ChatGPT desktop architecture on Fedora
tags: [packages, chatgpt]
ansible.builtin.assert:
that:
- ansible_facts['architecture'] in chatgpt_desktop_supported_architectures
- ansible_facts['distribution_major_version'] in chatgpt_desktop_supported_fedora_versions
fail_msg: >-
ChatGPT desktop supports Fedora {{ chatgpt_desktop_supported_fedora_versions | join(' and ') }}
on x86_64 or aarch64; detected Fedora {{ ansible_facts['distribution_major_version'] }}
on {{ ansible_facts['architecture'] }}.
when: chatgpt_desktop_enabled | bool
- name: Install ChatGPT desktop RPM signing key
tags: [packages, chatgpt]
ansible.builtin.copy:
src: "{{ chatgpt_desktop_rpm_gpg_key }}"
dest: "/etc/pki/rpm-gpg/{{ chatgpt_desktop_rpm_gpg_key }}"
owner: root
group: root
mode: "0644"
when: chatgpt_desktop_enabled | bool
- name: Import ChatGPT desktop RPM signing key
tags: [packages, chatgpt]
ansible.builtin.rpm_key:
state: present
key: "/etc/pki/rpm-gpg/{{ chatgpt_desktop_rpm_gpg_key }}"
when: chatgpt_desktop_enabled | bool
- name: Configure ChatGPT desktop RPM repository
tags: [packages, chatgpt]
ansible.builtin.yum_repository:
name: openai-chatgpt
description: ChatGPT
baseurl: "{{ chatgpt_desktop_rpm_repository }}"
enabled: true
gpgcheck: true
repo_gpgcheck: true
gpgkey: "file:///etc/pki/rpm-gpg/{{ chatgpt_desktop_rpm_gpg_key }}"
state: present
when: chatgpt_desktop_enabled | bool
- name: Install ChatGPT desktop on selected Fedora hosts
tags: [packages, chatgpt]
ansible.builtin.dnf:
name: chatgpt
state: latest
update_cache: true
when: chatgpt_desktop_enabled | bool
- name: Define Fedora Flatpak applications - name: Define Fedora Flatpak applications
tags: [packages] tags: [packages]
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@@ -1,5 +1,12 @@
--- ---
aegis_hostname: aegis aegis_hostname: aegis
aegis_layered_packages:
- nfs-utils
- wireguard-tools
aegis_network_connection_name: ""
aegis_network_connection_uuid: ""
aegis_host_dns_servers: []
aegis_host_dns_search_domains: []
aegis_adguard_image: docker.io/adguard/adguardhome:latest aegis_adguard_image: docker.io/adguard/adguardhome:latest
aegis_icloudpd_image: docker.io/boredazfcuk/icloudpd:latest aegis_icloudpd_image: docker.io/boredazfcuk/icloudpd:latest
aegis_icloudpd_folder_structure: '{:%Y/%m/%d}' aegis_icloudpd_folder_structure: '{:%Y/%m/%d}'

View File

@@ -1,4 +1,18 @@
--- ---
- name: Manage Aegis layered packages
tags: [aegis, packages, nfs, wireguard]
community.general.rpm_ostree_pkg:
name: "{{ aegis_layered_packages }}"
state: present
register: aegis_layered_packages_result
when: aegis_layered_packages | length > 0
- name: Report reboot required for Aegis layered packages
tags: [aegis, packages, nfs, wireguard]
ansible.builtin.debug:
msg: Reboot Aegis to activate the newly layered packages, then rerun the playbook.
when: aegis_layered_packages_result.needs_reboot | default(false)
- name: Require Aegis iCloudPD Apple ID - name: Require Aegis iCloudPD Apple ID
tags: [aegis, icloudpd] tags: [aegis, icloudpd]
ansible.builtin.assert: ansible.builtin.assert:
@@ -8,15 +22,86 @@
no_log: true no_log: true
- name: Require completed Aegis network placeholders - name: Require completed Aegis network placeholders
tags: [aegis, firewall, services] tags: [aegis, dns, firewall, network, services]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- aegis_lan_subnet != 'CHANGEME_LAN_SUBNET' - aegis_lan_subnet != 'CHANGEME_LAN_SUBNET'
- aegis_firewalld_zone | length > 0 - aegis_firewalld_zone | length > 0
- aegis_adguard_web_port | int > 0 - aegis_adguard_web_port | int > 0
- aegis_adguard_web_port | int < 65536 - aegis_adguard_web_port | int < 65536
- aegis_network_connection_name | length > 0
- aegis_network_connection_uuid | length > 0
- aegis_host_dns_servers | length > 0
- ansible_facts["default_ipv4"]["address"] not in aegis_host_dns_servers
- aegis_ssh_allowed_users | length > 0 - aegis_ssh_allowed_users | length > 0
fail_msg: Define the Aegis LAN subnet, firewalld zone, AdGuard web port, and SSH users. fail_msg: >-
Define the Aegis LAN subnet, firewalld zone, AdGuard web port, independent host DNS,
NetworkManager connection, and SSH users. Aegis must not use its own address as upstream DNS.
- name: Verify the declared Aegis NetworkManager connection exists
tags: [aegis, dns, network, services]
ansible.builtin.command:
argv:
- nmcli
- --get-values
- connection.id
- connection
- show
- uuid
- "{{ aegis_network_connection_uuid }}"
register: aegis_network_connection
changed_when: false
failed_when: >-
aegis_network_connection.rc != 0
or aegis_network_connection.stdout != aegis_network_connection_name
- name: Read the current Aegis host DNS configuration
tags: [aegis, dns, network, services]
ansible.builtin.command:
argv:
- nmcli
- --get-values
- ipv4.ignore-auto-dns,ipv4.dns,ipv4.dns-search,ipv6.ignore-auto-dns
- connection
- show
- uuid
- "{{ aegis_network_connection_uuid }}"
register: aegis_host_dns_current
changed_when: false
- name: Configure independent upstream DNS for the Aegis host
tags: [aegis, dns, network, services]
ansible.builtin.command:
argv:
- nmcli
- connection
- modify
- uuid
- "{{ aegis_network_connection_uuid }}"
- ipv4.ignore-auto-dns
- "yes"
- ipv4.dns
- "{{ aegis_host_dns_servers | join(',') }}"
- ipv4.dns-search
- "{{ aegis_host_dns_search_domains | join(',') }}"
- ipv6.ignore-auto-dns
- "yes"
register: aegis_host_dns_profile
when: >-
aegis_host_dns_current.stdout_lines !=
[
'yes',
aegis_host_dns_servers | join(','),
aegis_host_dns_search_domains | join(','),
'yes'
]
changed_when: true
- name: Report Aegis reboot required for host DNS changes
tags: [aegis, dns, network, services]
ansible.builtin.debug:
msg: Reboot Aegis to activate its independent upstream DNS before testing another OS update.
when: aegis_host_dns_profile.changed | default(false)
- name: Set Aegis hostname - name: Set Aegis hostname
tags: [aegis, services] tags: [aegis, services]

View File

@@ -47,31 +47,31 @@ atlas_hardening_sysctls:
net.ipv6.conf.all.accept_source_route: "0" net.ipv6.conf.all.accept_source_route: "0"
net.ipv6.conf.default.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_archive: archive atlas_zfs_dataset_archive: archive
atlas_zfs_dataset_app_data: archive/app_data atlas_zfs_dataset_services: services
atlas_zfs_dataset_navidrome: archive/app_data/navidrome atlas_zfs_dataset_app_data: services/data
atlas_zfs_dataset_syncthing: archive/app_data/syncthing atlas_zfs_dataset_navidrome: services/data/navidrome
atlas_zfs_dataset_syncthing: services/data/syncthing
atlas_zfs_dataset_media: media atlas_zfs_dataset_media: media
atlas_zfs_dataset_music: media/music atlas_zfs_dataset_music: media/music
atlas_zfs_dataset_backup_prometheus: backup_prometheus atlas_zfs_dataset_backup: backup
atlas_zfs_dataset_host_backups: backup/hosts
atlas_zfs_dataset_backup_prometheus: backup/hosts/prometheus
atlas_zfs_backup_reservation: 500G
atlas_zfs_dataset_photobook: media/photobook atlas_zfs_dataset_photobook: media/photobook
atlas_zfs_dataset_backups: backups
atlas_zfs_dataset_service_backups: backups/services
atlas_zfs_service_backups_refreservation: 500G
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}" atlas_archive_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
atlas_services_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_services }}"
atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}" atlas_app_data_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}"
atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}" atlas_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}" atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}" atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}"
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}" atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}" atlas_backup_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup }}"
atlas_host_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_host_backups }}"
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_photobook_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_photobook }}"
atlas_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backups }}"
atlas_service_backups_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_service_backups }}"
atlas_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

View File

@@ -3,12 +3,6 @@
tags: [atlas, storage] tags: [atlas, storage]
ansible.builtin.set_fact: ansible.builtin.set_fact:
atlas_zfs_datasets: atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
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 }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
mountpoint: "{{ atlas_archive_mountpoint }}" mountpoint: "{{ atlas_archive_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
@@ -16,6 +10,13 @@
properties: properties:
compression: zstd compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}" mountpoint: "{{ atlas_archive_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_services }}"
mountpoint: "{{ atlas_services_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: zstd
mountpoint: "{{ atlas_services_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
mountpoint: "{{ atlas_app_data_mountpoint }}" mountpoint: "{{ atlas_app_data_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
@@ -51,11 +52,27 @@
properties: properties:
compression: lz4 compression: lz4
mountpoint: "{{ atlas_music_mountpoint }}" mountpoint: "{{ atlas_music_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup }}"
mountpoint: "{{ atlas_backup_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_backup_mountpoint }}"
reservation: "{{ atlas_zfs_backup_reservation }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_host_backups }}"
mountpoint: "{{ atlas_host_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_host_backups_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}"
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
owner: "{{ atlas_admin_username }}" owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}" group: "{{ atlas_admin_group }}"
properties: properties:
compression: lz4
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}" mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}" - name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}"
mountpoint: "{{ atlas_photobook_mountpoint }}" mountpoint: "{{ atlas_photobook_mountpoint }}"
@@ -64,21 +81,6 @@
properties: properties:
compression: lz4 compression: lz4
mountpoint: "{{ atlas_photobook_mountpoint }}" mountpoint: "{{ atlas_photobook_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backups }}"
mountpoint: "{{ atlas_backups_mountpoint }}"
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
properties:
compression: lz4
mountpoint: "{{ atlas_backups_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}"
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]

View File

@@ -5,14 +5,18 @@ backend_phase1_username: "{{ atlas_admin_username }}"
backend_phase1_user_group: "{{ atlas_admin_group }}" backend_phase1_user_group: "{{ atlas_admin_group }}"
backend_phase1_user_home: "{{ atlas_admin_home }}" backend_phase1_user_home: "{{ atlas_admin_home }}"
backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd" backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd"
backend_phase1_wireguard_interface: wg0 backend_phase1_bind_address: CHANGEME_ATLAS_BIND_ADDRESS
backend_phase1_wireguard_address: CHANGEME_ATLAS_WIREGUARD_ADDRESS backend_phase1_firewalld_zone: public
backend_phase1_wireguard_firewalld_zone: wireguard backend_phase1_npm_source_ip: CHANGEME_AEGIS_IP
backend_phase1_syncthing_native_subnet: CHANGEME_LAN_SUBNET
backend_phase1_music_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}" backend_phase1_music_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_archive_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_archive }}"
backend_phase1_syncthing_folder_path: "{{ backend_phase1_archive_dir }}/Org"
backend_phase1_app_data_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}" 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_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}" backend_phase1_syncthing_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
backend_phase1_music_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}" backend_phase1_music_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_archive_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
backend_phase1_app_data_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}" 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_navidrome_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
backend_phase1_syncthing_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}" backend_phase1_syncthing_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"

View File

@@ -7,17 +7,22 @@
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- not (atlas_manage_media_stack | bool) - not (atlas_manage_media_stack | bool)
- backend_phase1_wireguard_interface in ansible_facts.interfaces - backend_phase1_bind_address != 'CHANGEME_ATLAS_BIND_ADDRESS'
- backend_phase1_wireguard_address != 'CHANGEME_ATLAS_WIREGUARD_ADDRESS' - backend_phase1_bind_address is match('^[0-9]{1,3}(\.[0-9]{1,3}){3}$')
- backend_phase1_firewalld_zone | length > 0
- backend_phase1_npm_source_ip != 'CHANGEME_AEGIS_IP'
- backend_phase1_syncthing_native_subnet != 'CHANGEME_LAN_SUBNET'
- backend_phase1_music_dir.startswith('/') - backend_phase1_music_dir.startswith('/')
- backend_phase1_archive_dir.startswith('/')
- backend_phase1_syncthing_folder_path.startswith(backend_phase1_archive_dir + '/')
- backend_phase1_app_data_root.startswith('/') - backend_phase1_app_data_root.startswith('/')
- backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/') - backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/')
- backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/') - backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/')
fail_msg: >- fail_msg: >-
Disable the rootful media-stack gate and provide the active Disable the rootful media-stack gate and provide the Atlas LAN bind
WireGuard interface/address and absolute ZFS-backed paths before address, firewall sources, and absolute ZFS-backed paths before
enabling phase one. This role does not manage Prometheus or migrate enabling phase one. This role does not manage Prometheus or migrate
Navidrome application data. application data.
- name: Read the rootless service account - name: Read the rootless service account
ansible.builtin.getent: ansible.builtin.getent:
@@ -49,6 +54,8 @@
loop: loop:
- dataset: "{{ backend_phase1_music_dataset }}" - dataset: "{{ backend_phase1_music_dataset }}"
mountpoint: "{{ backend_phase1_music_dir }}" mountpoint: "{{ backend_phase1_music_dir }}"
- dataset: "{{ backend_phase1_archive_dataset }}"
mountpoint: "{{ backend_phase1_archive_dir }}"
- dataset: "{{ backend_phase1_app_data_dataset }}" - dataset: "{{ backend_phase1_app_data_dataset }}"
mountpoint: "{{ backend_phase1_app_data_root }}" mountpoint: "{{ backend_phase1_app_data_root }}"
- dataset: "{{ backend_phase1_navidrome_dataset }}" - dataset: "{{ backend_phase1_navidrome_dataset }}"
@@ -101,6 +108,8 @@
mode: "0750" mode: "0750"
- path: "{{ backend_phase1_syncthing_root }}" - path: "{{ backend_phase1_syncthing_root }}"
mode: "0750" mode: "0750"
- path: "{{ backend_phase1_syncthing_folder_path }}"
mode: "0770"
loop_control: loop_control:
label: "{{ item.path }}" label: "{{ item.path }}"
@@ -132,19 +141,35 @@
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus" DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when: not ansible_check_mode when: not ansible_check_mode
- name: Permit phase-one services only through the WireGuard zone - name: Permit NPM access to phase-one web interfaces through Aegis
ansible.posix.firewalld: ansible.posix.firewalld:
port: "{{ item }}" rich_rule: >-
zone: "{{ backend_phase1_wireguard_firewalld_zone }}" rule family="ipv4" source address="{{ backend_phase1_npm_source_ip }}"
port port="{{ item }}" protocol="tcp" accept
zone: "{{ backend_phase1_firewalld_zone }}"
state: enabled state: enabled
permanent: true permanent: true
immediate: true immediate: true
loop: loop:
- "{{ backend_phase1_navidrome_port }}/tcp" - "{{ backend_phase1_navidrome_port }}"
- "{{ backend_phase1_syncthing_gui_port }}/tcp" - "{{ backend_phase1_syncthing_gui_port }}"
- "{{ backend_phase1_syncthing_transfer_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/udp" - name: Permit native Syncthing traffic from the LAN
- "{{ backend_phase1_syncthing_discovery_port }}/udp" ansible.posix.firewalld:
rich_rule: >-
rule family="ipv4" source address="{{ backend_phase1_syncthing_native_subnet }}"
port port="{{ item.port }}" protocol="{{ item.protocol }}" accept
zone: "{{ backend_phase1_firewalld_zone }}"
state: enabled
permanent: true
immediate: true
loop:
- port: "{{ backend_phase1_syncthing_transfer_port }}"
protocol: tcp
- port: "{{ backend_phase1_syncthing_transfer_port }}"
protocol: udp
- port: "{{ backend_phase1_syncthing_discovery_port }}"
protocol: udp
- name: Start rootless phase-one Quadlets - name: Start rootless phase-one Quadlets
become_user: "{{ backend_phase1_username }}" become_user: "{{ backend_phase1_username }}"

View File

@@ -8,7 +8,7 @@ Image={{ backend_phase1_navidrome_image }}
UserNS=keep-id UserNS=keep-id
User={{ backend_phase1_uid }} User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }} Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_navidrome_port }}:4533 PublishPort={{ backend_phase1_bind_address }}:{{ backend_phase1_navidrome_port }}:4533
Environment=ND_LOGLEVEL=info Environment=ND_LOGLEVEL=info
Environment=ND_SCANSCHEDULE=1h Environment=ND_SCANSCHEDULE=1h
Environment=ND_SESSIONTIMEOUT=24h Environment=ND_SESSIONTIMEOUT=24h

View File

@@ -9,15 +9,17 @@ HostName=atlas-syncthing
UserNS=keep-id UserNS=keep-id
User={{ backend_phase1_uid }} User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }} Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }} PublishPort={{ backend_phase1_bind_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_bind_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_bind_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 PublishPort={{ backend_phase1_bind_address }}:{{ backend_phase1_syncthing_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
Environment=HOME=/var/syncthing Environment=HOME=/var/syncthing
Environment=STHOMEDIR=/var/syncthing/config Environment=STHOMEDIR=/var/syncthing/config
Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }} Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }}
Environment=TZ={{ backend_phase1_timezone }} Environment=TZ={{ backend_phase1_timezone }}
Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z
# Archive is intentionally shared with Samba; use a shared SELinux container label.
Volume={{ backend_phase1_archive_dir }}:/data:z
NoNewPrivileges=true NoNewPrivileges=true
DropCapability=all DropCapability=all

View File

@@ -20,15 +20,7 @@
owner: "{{ item.owner }}" owner: "{{ item.owner }}"
group: "{{ item.group }}" group: "{{ item.group }}"
mode: "{{ item.mode }}" mode: "{{ item.mode }}"
loop: >- loop: "{{ server_directories | default([]) }}"
{{
(server_directories | default([]))
+ (
server_syncthing_directories | default([])
if server_syncthing_enabled | default(true) | bool
else []
)
}}
loop_control: loop_control:
label: "{{ item.path }}" label: "{{ item.path }}"

View File

@@ -10,6 +10,12 @@ wireguard_mtu: 1420
wireguard_firewalld_zone: wireguard wireguard_firewalld_zone: wireguard
wireguard_public_firewalld_zone: public wireguard_public_firewalld_zone: public
wireguard_enable_ipv4_forwarding: false wireguard_enable_ipv4_forwarding: false
# Legacy zone-level masquerading; policy-level masquerading is required for inter-zone forwarding.
wireguard_enable_masquerade: false
# A list of narrowly scoped inter-zone forwarding rules, rendered as persistent
# firewalld policies. Each item requires name, ingress_zone, egress_zone,
# source, and destination.
wireguard_forwarding_policies: []
wireguard_reload_rootful_podman_networks: false wireguard_reload_rootful_podman_networks: false
wireguard_handshake_retries: 12 wireguard_handshake_retries: 12
wireguard_handshake_delay: 5 wireguard_handshake_delay: 5

View File

@@ -14,10 +14,44 @@
Configure this host's WireGuard address and peers, and run the first Configure this host's WireGuard address and peers, and run the first
key bootstrap against every peer in the same play. key bootstrap against every peer in the same play.
- name: Install WireGuard userspace tools - name: Validate WireGuard forwarding policies
ansible.builtin.assert:
that:
- item.name is defined
- item.ingress_zone is defined
- item.egress_zone is defined
- item.source is defined
- item.destination is defined
fail_msg: >-
Every WireGuard forwarding policy requires name, ingress_zone,
egress_zone, source, and destination.
loop: "{{ wireguard_forwarding_policies }}"
loop_control:
label: "{{ item.name | default('unnamed policy') }}"
- name: Install WireGuard userspace tools on mutable hosts
ansible.builtin.dnf: ansible.builtin.dnf:
name: "{{ wireguard_packages }}" name: "{{ wireguard_packages }}"
state: present state: present
when: "'platform_fedora_iot' not in group_names"
- name: Require WireGuard userspace tools in the booted deployment
ansible.builtin.command:
argv:
- wg
- --version
changed_when: false
failed_when: false
register: wireguard_userspace_tools
- name: Require active WireGuard userspace tools
ansible.builtin.assert:
that:
- wireguard_userspace_tools.rc == 0
fail_msg: >-
WireGuard userspace tools are not present in the booted deployment.
On Fedora IoT, reboot after rpm-ostree layers wireguard-tools, then
rerun the WireGuard play.
- name: Create private WireGuard configuration directory - name: Create private WireGuard configuration directory
ansible.builtin.file: ansible.builtin.file:
@@ -124,12 +158,33 @@
permanent: true permanent: true
register: wireguard_firewalld_zone_result register: wireguard_firewalld_zone_result
- name: Reload firewalld after creating the WireGuard zone - name: Create the firewalld policy directory
ansible.builtin.file:
path: /etc/firewalld/policies
state: directory
owner: root
group: root
mode: "0755"
when: wireguard_forwarding_policies | length > 0
- name: Render WireGuard forwarding policies
ansible.builtin.template:
src: wireguard-forwarding-policy.xml.j2
dest: "/etc/firewalld/policies/{{ item.name }}.xml"
owner: root
group: root
mode: "0644"
loop: "{{ wireguard_forwarding_policies }}"
loop_control:
label: "{{ item.name }}"
register: wireguard_forwarding_policy_result
- name: Reload firewalld after WireGuard firewall changes
ansible.builtin.systemd: ansible.builtin.systemd:
name: firewalld.service name: firewalld.service
state: reloaded state: reloaded
when: when:
- wireguard_firewalld_zone_result is changed - wireguard_firewalld_zone_result is changed or (wireguard_forwarding_policy_result | default({})) is changed
- not ansible_check_mode - not ansible_check_mode
- name: Restore rootful Podman networking after firewalld reload - name: Restore rootful Podman networking after firewalld reload
@@ -142,7 +197,7 @@
register: wireguard_podman_network_reload register: wireguard_podman_network_reload
changed_when: wireguard_podman_network_reload.stdout_lines | length > 0 changed_when: wireguard_podman_network_reload.stdout_lines | length > 0
when: when:
- wireguard_firewalld_zone_result is changed - wireguard_firewalld_zone_result is changed or (wireguard_forwarding_policy_result | default({})) is changed
- wireguard_reload_rootful_podman_networks | bool - wireguard_reload_rootful_podman_networks | bool
- not ansible_check_mode - not ansible_check_mode
@@ -154,6 +209,14 @@
permanent: true permanent: true
immediate: true immediate: true
- name: Manage legacy WireGuard zone masquerading
ansible.posix.firewalld:
zone: "{{ wireguard_firewalld_zone }}"
masquerade: true
state: "{{ 'enabled' if wireguard_enable_masquerade | bool else 'disabled' }}"
permanent: true
immediate: true
- name: Permit this host's public WireGuard listener - name: Permit this host's public WireGuard listener
ansible.posix.firewalld: ansible.posix.firewalld:
port: "{{ wireguard_listen_port }}/udp" port: "{{ wireguard_listen_port }}/udp"

View File

@@ -11,7 +11,8 @@ ListenPort = {{ wireguard_listen_port }}
{% for peer in wireguard_peers %} {% for peer in wireguard_peers %}
[Peer] [Peer]
# {{ peer.name }} # {{ peer.name }}
PublicKey = {{ hostvars[peer.host].wireguard_public_key }} {% if peer.comment is defined %}# {{ peer.comment }}
{% endif %}PublicKey = {{ hostvars[peer.host].wireguard_public_key }}
AllowedIPs = {{ peer.allowed_ips | join(', ') }} AllowedIPs = {{ peer.allowed_ips | join(', ') }}
{% if peer.endpoint is defined %} {% if peer.endpoint is defined %}
Endpoint = {{ peer.endpoint }} Endpoint = {{ peer.endpoint }}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<policy target="CONTINUE">
<short>WireGuard forwarding: {{ item.name }}</short>
<description>Managed WireGuard-to-LAN forwarding policy.</description>
{% if item.masquerade | default(false) %}
<masquerade/>
{% endif %}
<ingress-zone name="{{ item.ingress_zone }}"/>
<egress-zone name="{{ item.egress_zone }}"/>
<rule family="ipv4">
<source address="{{ item.source }}"/>
<destination address="{{ item.destination }}"/>
<accept/>
</rule>
</policy>

View File

@@ -73,13 +73,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
@@ -87,6 +80,13 @@
roles: roles:
- profile_aegis - profile_aegis
- name: Configure WireGuard overlay
hosts: wireguard_overlay
become: true
roles:
- wireguard_overlay
- name: Configure Atlas NAS profile - name: Configure Atlas NAS profile
hosts: atlas hosts: atlas
become: true become: true

View File

@@ -56,24 +56,6 @@ services:
- "3000:3000" - "3000:3000"
- "127.0.0.1:222:22" - "127.0.0.1:222:22"
{% if server_syncthing_enabled | default(true) | bool %}
syncthing:
image: docker.io/syncthing/syncthing:2
container_name: syncthing
hostname: syncthing
restart: unless-stopped
expose:
- "8384"
volumes:
- "/opt/syncthing/config:/var/syncthing{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
- "/srv/syncthing/data:/data{{ ':' ~ selinux_volume_option if selinux_volume_option else '' }}"
ports:
- "22000:22000/tcp"
- "22000:22000/udp"
- "21027:21027/udp"
networks:
- web
{% endif %}
networks: networks:
web: web: