Compare commits

..

7 Commits

Author SHA1 Message Date
Fabio Scotto di Santolo
76f3e10ee4 Verify WireGuard handshakes and restore Podman networking 2026-09-15 22:38:56 +02:00
Fabio Scotto di Santolo
c899bb7192 Harden Rocky storage and sharing configuration 2026-09-15 22:05:48 +02:00
Fabio Scotto di Santolo
2ab5ba6818 Manage SSH authorized key fragments for infrastructure hosts 2026-09-13 15:16:40 +02:00
Fabio Scotto di Santolo
a347880d4c Manage Aegis SSH authorized key fragments 2026-09-13 14:46:08 +02:00
Fabio Scotto di Santolo
b05a2b4e74 Allow configurable Aegis SSH users and authorized keys 2026-09-13 14:35:43 +02:00
Fabio Scotto di Santolo
75d9081a11 Rotate Ansible Vault secrets 2026-09-13 13:53:03 +02:00
Fabio Scotto di Santolo
b4b3bd10b8 Harden Atlas network, SSH, firewall, and sharing 2026-09-13 00:01:48 +02:00
26 changed files with 557 additions and 150 deletions

View File

@@ -55,6 +55,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
- Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server` - Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server`
- Atlas media stack: - Atlas media stack:
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
- Atlas network/share hardening:
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
- Atlas phase-one rootless services: - Atlas phase-one rootless services:
`ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff` `ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff`
- Prometheus/Atlas WireGuard overlay: - Prometheus/Atlas WireGuard overlay:
@@ -132,8 +134,8 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as - `atlas_manage_storage`, `atlas_manage_sharing`, and `atlas_manage_firewall` are enabled in Atlas host vars as
the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false the declared steady state; set one false only for a deliberate suspension. `atlas_manage_media_stack` remains false
until the future rootful Immich stack has its required Vault inputs and target validation. until the future rootful Immich stack has its required Vault inputs and target validation.
- Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit - Atlas requires `vault_atlas_admin_password_hash` for Cockpit and, while sharing is enabled,
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: `work`, `archive`,
`archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`, `archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`,
@@ -142,8 +144,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- 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`.
- Atlas exports Photobook only to the configured Aegis IP with all access squashed to UID/GID `1100`. SMB3 exposes - Atlas applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, agent and remote forwarding
`Archive` to Vault-backed authorized accounts and admits the configured LAN without host-specific exclusions. are disabled, while local forwarding remains available for private administrative tunnels. Photobook is exported only to the configured Aegis IP with all access squashed to UID/GID
`1100`. Targeted SELinux is enforced persistently; a required reboot is reported but never initiated automatically. The primary LAN interface is assigned explicitly to the managed firewalld zone, and firewall rules are applied before NFS or SMB are started; their service state and TCP listeners are then verified. SMB3 exposes `Archive` to Vault-backed authorized accounts on mandatory encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific exclusions.
- Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains - Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet. bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet.
- `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome - `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome
@@ -155,11 +158,14 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application 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. 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`; - 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. Native Syncthing transfer/discovery does use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking:
not use the HTTP proxy. 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 - `wireguard_overlay` manages the required `wg0` path between Prometheus and Atlas, persists private keys only on their
respective hosts, and exchanges only derived public keys. The initial run must include both hosts. Prometheus respective hosts, exchanges only derived public keys, and verifies a real peer handshake. The initial run must
opens `51820/udp`; the Atlas backend role admits service ports only in the WireGuard firewalld zone. 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. - Provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host.
@@ -175,8 +181,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
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; - Validate the managed WireGuard path and its LAN/VPN-only firewalld rules before enabling remote services;
never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding. never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity, - Add the Atlas-initiated least-privilege Prometheus backup pull: Prometheus exposes only prepared
pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer. read-only dumps through a dedicated account and Atlas retains the private SSH key, pinned host key,
atomic pull, verification, retention and systemd service/timer.
- Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity, - Add the encrypted offsite backup with Borg to a Hetzner Storage Box: use a dedicated SSH identity,
pin the host key, keep Borg repository credentials and encryption material in Vault, use pin the host key, keep Borg repository credentials and encryption material in Vault, use
snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores. snapshot-consistent sources, and manage retries, logging, pruning, repository checks and restores.

View File

@@ -196,8 +196,8 @@ 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 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 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 `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 limitate a WireGuard. Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native pubblicate esplicitamente solo
Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI. sull'indirizzo WireGuard di Atlas. Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI.
### DuckDNS ### DuckDNS
@@ -274,9 +274,10 @@ Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpo
`media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e `media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e
di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`; di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`;
`backups/services` mantiene inoltre una `refreservation` di `500G`. `backups/services` mantiene inoltre una `refreservation` di `500G`.
SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e ammette la LAN 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
configurata senza esclusioni specifiche per host. NFSv4 esporta soltanto 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
`media/photobook` all'IP configurato di Aegis con `all_squash` verso UID/GID anonimi `1100`. ammette la LAN configurata su SMB3 cifrato e firmato, esclusivamente su TCP/445. NFSv4 esporta soltanto
`media/photobook` all'IP configurato di Aegis su TCP/2049, con `all_squash` verso UID/GID anonimi `1100`.
L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e
i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile

View File

@@ -119,8 +119,9 @@ does not provision any `/srv/nextcloud` directories.
The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard 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 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 `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 its native WireGuard-restricted ports. Configure both uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas
Syncthing authentication and an appropriate NPM access policy before publishing its GUI. 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,6 +133,8 @@ ansible-playbook ansible/site.yml --limit prometheus \
``` ```
The target must already provide `server_username` with local sudo access. The target must already provide `server_username` with local sudo access.
Prometheus authorizes its declared SSH public keys through separate files below
`~/.ssh/authorized_keys.d/`, while `sshd` is configured to read those files directly.
### DuckDNS ### DuckDNS
@@ -195,6 +198,10 @@ for AdGuard while retaining DNS learned from the router. Define
`vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still requires interactive MFA `vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still requires interactive MFA
initialization after its first deployment. initialization after its first deployment.
New Aegis images create the `admin` account in Butane. Before configuring a newly imaged node, run its
first playbook execution with `-e ansible_user=admin`; the SSH hardening role then permits that same
account. Keep the inventory on `pi` until the existing node has been replaced.
Validate the profile before deployment: Validate the profile before deployment:
```bash ```bash
@@ -210,29 +217,37 @@ only manages child datasets. A one-time RAIDZ2 bootstrap is available only with
It never partitions, forces, destroys, rolls back, or changes the vdev layout of an existing pool. Linux It never partitions, forces, destroys, rolls back, or changes the vdev layout of an existing pool. Linux
clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the configured LAN. clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the configured LAN.
For the first run, provide `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, For the first run, provide `vault_atlas_admin_password_hash`, `vault_atlas_samba_password`, and
`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: existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then
include both WireGuard peers in the same idempotent playbook run:
```bash ```bash
ansible-playbook ansible/site.yml --limit atlas \ ansible-playbook ansible/site.yml --limit prometheus,atlas \
-e atlas_connection_username=<existing-admin> -e atlas_connection_username=<existing-admin> \
-e atlas_create_pool=true
``` ```
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
LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet, LAN firewall rules enabled. Before the first apply, check the existing pool and mountpoints, LAN subnet,
and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container and active firewalld zone. `atlas_manage_media_stack` remains disabled until `/dev/dri`, the container
paths, and the Immich database secret are validated. 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 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`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`, `archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`,
`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`, `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` while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G`
refreservation. SMB3 exposes `Archive` only to the configured Vault-backed Samba accounts and admits 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,
the configured LAN without host-specific exclusions. NFSv4 exports only agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed
`media/photobook` to the configured Aegis IP, using `all_squash` with anonymous UID/GID `1100`. Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific
exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using
`all_squash` with anonymous UID/GID `1100`.
The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and
receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL, receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL,
@@ -252,7 +267,9 @@ storage paths from the `zpool` mounted at `/zpool`: music is read-only at
containers. The backend role never creates the pool. The separate `wireguard_overlay` role manages `wg0` 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 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 on their respective hosts and exchanging only public keys through Ansible. Prometheus alone opens
`51820/udp` publicly. Backend ports are admitted only in the WireGuard firewalld zone. `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_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 backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome

View File

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

View File

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

View File

@@ -95,3 +95,5 @@ server_sshd_settings:
server_sshd_allow_users: server_sshd_allow_users:
- "{{ server_username }}" - "{{ server_username }}"
server_ssh_authorized_keys: []
server_ssh_authorized_key_directory: "{{ server_user_home }}/.ssh/authorized_keys.d"

View File

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

View File

@@ -12,8 +12,15 @@ atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}" atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000 atlas_admin_uid: 1000
atlas_admin_gid: 1000 atlas_admin_gid: 1000
atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}" atlas_admin_ssh_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}" atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_ssh_allow_tcp_forwarding: local
atlas_lan_subnet: 192.168.178.0/24 atlas_lan_subnet: 192.168.178.0/24
atlas_aegis_ip: 192.168.178.54 atlas_aegis_ip: 192.168.178.54
@@ -21,8 +28,12 @@ atlas_manage_firewall: true
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_zfs_pool: zpool atlas_zfs_pool: zpool
# Populate only for the first pool bootstrap with four real persistent disk paths. # Populate only for the first pool bootstrap with four real persistent disk paths.
# Example: /dev/disk/by-id/ata-Seagate_IronWolf_... # Confirmed empty 4 TB IronWolf data disks; the NVMe system disk is intentionally excluded.
atlas_zpool_disks: [] 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_create_pool: false
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_archive: archive
@@ -63,7 +74,9 @@ rocky_podman_packages:
host_packages: host_packages:
- cockpit - cockpit
- nfs-utils - nfs-utils
- policycoreutils
- policycoreutils-python-utils - policycoreutils-python-utils
- python3-libselinux
- samba - samba
- samba-client - samba-client
- samba-common-tools - samba-common-tools
@@ -82,6 +95,7 @@ atlas_nfs_exports:
atlas_samba_share_name: Archive atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
atlas_samba_encryption: required
# Append any pre-existing laptop Unix account here and keep its Samba password in Vault. # Append any pre-existing laptop Unix account here and keep its Samba password in Vault.
atlas_samba_accounts: atlas_samba_accounts:
- username: "{{ atlas_admin_username }}" - username: "{{ atlas_admin_username }}"
@@ -91,12 +105,10 @@ atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') |
atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}" atlas_immich_db_password: "{{ vault_atlas_immich_db_password | default('') }}"
atlas_firewalld_rich_rules: atlas_firewalld_rich_rules:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="22" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="9090" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" service name="nfs" accept' - 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept' - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="445" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="http" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="https" accept'
host_enabled_services: host_enabled_services:
- sshd - sshd

View File

@@ -7,11 +7,19 @@ ansible_ssh_private_key_file: /home/fscotto/.ssh/id_ed25519
server_username: rocky server_username: rocky
server_duckdns_domain: fscotto server_duckdns_domain: fscotto
server_ssh_authorized_keys:
- name: ikaros
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
- name: nymph
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEH/7GJfGt0ZVmKeEzceoFkFkeCXFryKK9vAbaip+HCx nymph"
- name: siren
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA95wYlzpfN3rjUhpMeP4KHn8I6ZrjQXoDTgwgRIa++b siren"
wireguard_overlay_enabled: true wireguard_overlay_enabled: true
wireguard_address: 10.0.0.1/24 wireguard_address: 10.0.0.1/24
wireguard_listen_port: 51820 wireguard_listen_port: 51820
wireguard_enable_ipv4_forwarding: true wireguard_enable_ipv4_forwarding: true
wireguard_reload_rootful_podman_networks: true
wireguard_peers: wireguard_peers:
- name: atlas - name: atlas
host: atlas host: atlas

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,10 @@ atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000 atlas_admin_uid: 1000
atlas_admin_gid: 1000 atlas_admin_gid: 1000
atlas_admin_ssh_keys: [] atlas_admin_ssh_keys: []
atlas_admin_ssh_key_directory: "{{ atlas_admin_home }}/.ssh/authorized_keys.d"
atlas_admin_password_hash: "!" atlas_admin_password_hash: "!"
# Local forwarding permits an administrator to tunnel a private service without allowing remote forwards.
atlas_ssh_allow_tcp_forwarding: local
atlas_immich_username: immich atlas_immich_username: immich
atlas_immich_group: immich atlas_immich_group: immich
@@ -24,6 +27,25 @@ atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_aegis_ip: CHANGEME_AEGIS_IP atlas_aegis_ip: CHANGEME_AEGIS_IP
atlas_manage_firewall: false atlas_manage_firewall: false
atlas_firewalld_zone: public atlas_firewalld_zone: public
atlas_hardening_sysctls:
net.ipv4.conf.all.accept_redirects: "0"
net.ipv4.conf.default.accept_redirects: "0"
net.ipv4.conf.all.send_redirects: "0"
net.ipv4.conf.default.send_redirects: "0"
net.ipv4.conf.all.accept_source_route: "0"
net.ipv4.conf.default.accept_source_route: "0"
net.ipv4.conf.all.log_martians: "1"
net.ipv4.conf.default.log_martians: "1"
net.ipv4.conf.all.rp_filter: "2"
net.ipv4.conf.default.rp_filter: "2"
net.ipv4.tcp_syncookies: "1"
net.ipv4.icmp_echo_ignore_broadcasts: "1"
net.ipv4.icmp_ignore_bogus_error_responses: "1"
net.ipv4.ip_forward: "0"
net.ipv6.conf.all.accept_redirects: "0"
net.ipv6.conf.default.accept_redirects: "0"
net.ipv6.conf.all.accept_source_route: "0"
net.ipv6.conf.default.accept_source_route: "0"
atlas_zfs_pool: CHANGEME_ZFS_POOL atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work atlas_zfs_dataset_work: work
atlas_zfs_dataset_archive: archive atlas_zfs_dataset_archive: archive
@@ -72,6 +94,8 @@ atlas_nfs_exports:
- "anongid={{ atlas_immich_gid }}" - "anongid={{ atlas_immich_gid }}"
atlas_samba_share_name: Archive atlas_samba_share_name: Archive
atlas_samba_workgroup: WORKGROUP atlas_samba_workgroup: WORKGROUP
# Keep encryption mandatory unless a verified client-compatibility exception is explicitly required.
atlas_samba_encryption: required
atlas_samba_valid_users: [] atlas_samba_valid_users: []
atlas_samba_accounts: [] atlas_samba_accounts: []
atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords

View File

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

View File

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

View File

@@ -5,6 +5,9 @@
- 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 - name: Import Atlas pool bootstrap tasks
ansible.builtin.import_tasks: pool.yml ansible.builtin.import_tasks: pool.yml

View File

@@ -16,6 +16,7 @@
- name: Inspect declared Atlas pool disks - name: Inspect declared Atlas pool disks
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ item }}" path: "{{ item }}"
follow: true
loop: "{{ atlas_zpool_disks }}" loop: "{{ atlas_zpool_disks }}"
loop_control: loop_control:
label: "{{ item }}" label: "{{ item }}"

View File

@@ -1,4 +1,50 @@
--- - name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services]
ansible.builtin.assert:
that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP'
- atlas_firewalld_zone | length > 0
- ansible_facts.default_ipv4.interface | default('') | length > 0
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders.
when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules
tags: [atlas, sharing, services]
ansible.posix.firewalld:
rich_rule: "{{ item }}"
permanent: true
immediate: true
state: enabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_rich_rules }}"
loop_control:
label: "{{ item }}"
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
tags: [atlas, sharing, services]
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: disabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_restricted_services }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_firewall | bool
- name: Render Atlas NFS exports - name: Render Atlas NFS exports
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.template: ansible.builtin.template:
@@ -10,17 +56,14 @@
notify: Reload NFS exports notify: Reload NFS exports
when: atlas_manage_sharing | bool when: atlas_manage_sharing | bool
- name: Enable Atlas file-sharing services - name: Ensure Atlas NFS configuration drop-in directory exists
tags: [atlas, sharing, services] tags: [atlas, sharing]
ansible.builtin.systemd: ansible.builtin.file:
name: "{{ item }}" path: /etc/nfs.conf.d
enabled: true state: directory
state: started owner: root
loop: group: root
- nfs-server.service mode: "0755"
- smb.service
loop_control:
label: "{{ item }}"
when: atlas_manage_sharing | bool when: atlas_manage_sharing | bool
- name: Configure Atlas NFSv4-only service - name: Configure Atlas NFSv4-only service
@@ -99,6 +142,7 @@
tags: [atlas, sharing] tags: [atlas, sharing]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- atlas_samba_encryption in ['required', 'desired']
- atlas_samba_accounts | length > 0 - atlas_samba_accounts | length > 0
- >- - >-
atlas_samba_accounts | map(attribute='username') | list atlas_samba_accounts | map(attribute='username') | list
@@ -131,38 +175,97 @@
no_log: true no_log: true
when: atlas_manage_sharing | bool when: atlas_manage_sharing | bool
- name: Require completed Atlas firewall placeholders - name: Enable Atlas file-sharing services
tags: [atlas, 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: ansible.builtin.assert:
that: that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' - atlas_firewalld_zone in atlas_firewalld_info.firewalld_info.zones
- atlas_aegis_ip != 'CHANGEME_AEGIS_IP' - >-
- atlas_firewalld_zone | length > 0 ansible_facts.default_ipv4.interface
fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders. in atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].interfaces
when: atlas_manage_firewall | bool - >-
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 Atlas firewalld rich rules - name: Apply pending Atlas sharing handlers before verification
tags: [atlas, sharing, services] tags: [atlas, sharing, services, security]
ansible.posix.firewalld: ansible.builtin.meta: flush_handlers
rich_rule: "{{ item }}" when:
permanent: true - atlas_manage_sharing | bool
immediate: true - not ansible_check_mode
state: enabled
zone: "{{ atlas_firewalld_zone }}" - name: Check active Atlas file-sharing services
loop: "{{ atlas_firewalld_rich_rules }}" tags: [atlas, sharing, services, security]
ansible.builtin.command:
argv:
- systemctl
- is-active
- --quiet
- "{{ item }}"
loop:
- nfs-server.service
- smb.service
loop_control: loop_control:
label: "{{ item }}" label: "{{ item }}"
when: atlas_manage_firewall | bool register: atlas_file_sharing_service_activity
changed_when: false
failed_when: false
when:
- atlas_manage_sharing | bool
- not ansible_check_mode
- name: Remove unrestricted Atlas services from firewalld zone - name: Require active Atlas file-sharing services
tags: [atlas, sharing, services] tags: [atlas, sharing, services, security]
ansible.posix.firewalld: ansible.builtin.assert:
service: "{{ item }}" that:
permanent: true - atlas_file_sharing_service_activity.results | map(attribute='rc') | list == [0, 0]
immediate: true fail_msg: Atlas NFSv4 or SMB3 did not start after its managed configuration was applied.
state: disabled when:
zone: "{{ atlas_firewalld_zone }}" - atlas_manage_sharing | bool
loop: "{{ atlas_firewalld_restricted_services }}" - 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: loop_control:
label: "{{ item }}" label: "{{ item }}/tcp"
when: atlas_manage_firewall | bool when:
- atlas_manage_sharing | bool
- not ansible_check_mode

View File

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

View File

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

View File

@@ -6,13 +6,16 @@ Description=Atlas rootless Syncthing backend
ContainerName=atlas-syncthing ContainerName=atlas-syncthing
Image={{ backend_phase1_syncthing_image }} Image={{ backend_phase1_syncthing_image }}
HostName=atlas-syncthing HostName=atlas-syncthing
Network=host
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_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=HOME=/var/syncthing
Environment=STHOMEDIR=/var/syncthing/config Environment=STHOMEDIR=/var/syncthing/config
Environment=STGUIADDRESS={{ backend_phase1_wireguard_address }}:{{ 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
NoNewPrivileges=true NoNewPrivileges=true

View File

@@ -61,6 +61,42 @@
tags: [services, podman] tags: [services, podman]
ansible.builtin.include_tasks: podman-compose.yml ansible.builtin.include_tasks: podman-compose.yml
- name: Ensure server SSH authorized key fragments directory exists
tags: [services, ssh]
ansible.builtin.file:
path: "{{ server_ssh_authorized_key_directory }}"
state: directory
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0700"
when: server_ssh_authorized_keys | length > 0
- name: Manage server SSH authorized key fragments
tags: [services, ssh]
ansible.builtin.copy:
content: "{{ item.key }}\n"
dest: "{{ server_ssh_authorized_key_directory }}/{{ item.name }}"
owner: "{{ server_username }}"
group: "{{ server_user_group }}"
mode: "0600"
loop: "{{ server_ssh_authorized_keys }}"
loop_control:
label: "{{ item.name }}"
when: server_ssh_authorized_keys | length > 0
- name: Configure server SSH authorized key fragments
tags: [services, ssh]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*AuthorizedKeysFile\s+'
line: >-
AuthorizedKeysFile {{ server_ssh_authorized_keys | map(attribute='name')
| map('regex_replace', '^', '%h/.ssh/authorized_keys.d/') | join(' ') }}
state: present
validate: "sshd -t -f %s"
notify: Reload SSH service
when: server_ssh_authorized_keys | length > 0
- name: Disable SSH root login on server - name: Disable SSH root login on server
tags: [services] tags: [services]
ansible.builtin.lineinfile: ansible.builtin.lineinfile:

View File

@@ -10,6 +10,9 @@ 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
wireguard_reload_rootful_podman_networks: false
wireguard_handshake_retries: 12
wireguard_handshake_delay: 5
wireguard_peers: [] wireguard_peers: []
wireguard_packages: wireguard_packages:
- wireguard-tools - wireguard-tools

View File

@@ -132,6 +132,20 @@
- wireguard_firewalld_zone_result is changed - wireguard_firewalld_zone_result is changed
- not ansible_check_mode - 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 - name: Assign the WireGuard interface to its firewalld zone
ansible.posix.firewalld: ansible.posix.firewalld:
interface: "{{ wireguard_interface }}" interface: "{{ wireguard_interface }}"
@@ -156,3 +170,23 @@
state: started state: started
daemon_reload: true daemon_reload: true
when: not ansible_check_mode 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,61 +1,78 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
38633239386338323332633564613266393665666562353139646234343261303131336663316634 31386434333363613930316363393564373332303236306233643365386639346631336232373361
6535313265356434373464326664303937353132643636630a373966646565663564613463643732 3732633931376438313835373537666438383664306266380a633531316432343166323361303465
35646136346564636565616134303064313636643263343765396138653830646664633366656365 39306136386664356163346266303963373839373763343136346135633236333333326331313063
3137643164396265650a663338346161333832643732653539366462323039313563383735626661 6264306265373865310a373735393632373861333433326632303931633732636535326264346537
62626461656636366634353731613932653235323463336263363866336466356534636435313831 39633766326361666432396432623132616666363462373237623664363664373362346366383230
64373235383930663232613932623935346465643633666233386461346635353663323638633964 65326665303762356233356531306331333231376163353638363936626562623937323166623065
61366664323631313631663662643437386365616162333161636166666635636633333061323739 62633165383033303362336238343037353930396430336537363565333132666532323662653262
38386330326534303966613165656363633838313366666164656133386263646561306139383032 65373531313264353938666161373064393239653165666462336665323962336134343432316134
66646133643239396436653564323837623031393439303938323138326330636633613864623565 32373361613539356262313333663964623034313230356439626139383539326261663765373034
62616335633563333962653561633665643336303230623934356565363531323237643433306436 36633933366662656464306561393130373238313730336638663932633935313037363133636266
33363238306438333765323034636261333830653836656538636662303363363733356462353764 36663935303735623132663464376633306431396662306166393831313566323238363865303162
34636136666563333138383663643931363039303737313763373765356336656564633665666530 66303732376530653435613966373832333161333137366230613166303061333433336461386234
32346262636530646365636565656139623639346262373963346462626638316162616666373339 63393030616662373666643164663862653037383336323766646330653131623930353265336231
33353031313365356537646237356139613834343737343361636133343837613638643432346134 39646335353635616465663763306262353931316562616437313362616136623735646535616431
64313232646638633035383234303934666139616162356431306161353866316136663830343737 33363439623361343231363663363535663265366166346439623935323632366336363135373462
38646564393936383939373435313363653338653737346333643165626437386366346339303764 62646662316538323734333334393566333463653833326162663065646634363336373865623932
34623735613130643636623733353161393034303964656635366466396362393435376565313832 33646533636638386534366561663930613536663935663638313137646537626431393035653466
32623936646534656166383930666133663132663363623435393434663734363533653036663230 36623435613966323166363630383531656436373366343364346439343034333934623836336636
30663130626237303938613236306465613534346466663333663432316539653836323030643636 38653534343563393434646430306662343435653465363439326261373537666233353731306436
64646564643435373938363430303531333461613438633435356231656666373261636135396534 35323739336361343665633239366633393530346335396635316238323435663466316235376536
66343435663835646161376636633662353337386238316131623737353364383130653334643864 38303839313433643038353236616632626363653339346334303136306138336461383831626536
63353836303230303263343939373436646262346565313039323037383966636636656638366231 39303966393034333739363061363865326165303236656438316537396463383935393764346135
31343431373632363031346635663834393036623166346262393736346361323837303462346130 66323632323833353833643434316566326366633562666262386232653730303038613336663264
66643563306633323433616461343364326162336435363534326237366665316638353365376138 62636332383834393861373665393364653362636339346632303463386565633830336363393065
61633263643637323031613462303138303133623136323637633862623032623465616538643331 31383530613161366436323163393366386635316562633436383134623061353937326363396439
31343134353166383964666431353231636339636366626164346461386339356437386261336365 38636438396637616362373937666361303536376164636533653536306338356263313965623336
36373936356534623035663531353132633837333366663732393661613162316234373137333861 32633461623861643138613734313164633562613932386163643062353636376266633166373838
34656465343231623532343231646565316664373662626134346530653164393966333738656130 39613438653531316333663736366161313832613830353566356461393435656234383037353335
62663238373135643033396334386236313531646131313765323039306431323938386163316434 32343661376266353538643531313239306432646335383734623233373063316430383362643531
64616530343431303865336431666662343766303931653336376131313633306431346165623335 35393263343534363936373361316265653934383735663933663365363564663966646335353337
64383262626332336430373337313462613766613931613337393335393237616463646631623562 63366365383466393765336533363130643236313331633537346237353631623334633330376164
35343938323933663835313334306536343231663866616566653434366564313134393831343362 32616530623032663761643437336536323332343130393339313232623364656334613233323833
63396630366338626133383232666265383266396166306536633233306139666531613031643262 64346637343738386335343631333035636337633732333662663763326362636432646136353064
37356262363466303032336264333864646632373533396239393565653538383161653762313432 30393061326435323837633632333765623931663265326137373135303035623464333366653566
37313230666237626530316133393134303133613463363362323132653932333663313236623639 66613133396339376264313964353932396136613538383264363865633536373839663465333437
65326665626432386132323131353838346439393061343963616565306464636264626630396230 37613765626131343330653063663764346364316264363635653438396331363263623562333735
31373963326134383762373737626238353964383565336132623836333535313635316637653966 30646563303439663639376430336336383761363365353838613036333032306434623661636138
39386563396661663439343463353262656237616633646466323638346362376335306330373161 63616439636437663732386164346365643834376631303263376563323662373734653631613730
37343431316163626365386130383862643461363132636138646634663832383933303937643934 31373039656238303462643930303531613931373065316435303661343862623034653364653736
34326131353262643165336138643835333066663432353263333939633534383137393136633034 37613835616630316133333130633631653639336266313438633166316539373433333661613839
35386538616633336230353332306631353566343831376662306264386330623631653437393666 32356139366232313336343062323265366563633735383661393335386163653133313664613264
30646438666663383334333431336230373635663162626661373539336434646131346238623435 63303739323863333439316461366137656434666366306466356633306663623730653939663430
32303361366565353161646136386237366639343664616637303232376232316334366437636630 39313764663534316562326435316264633236373834323665356266346532323565323532333438
37633866376433633537373166613436303335663766303539336332366231353435646163386538 33306237623430613463393164383332356533303433343465343930636563363862373330613632
66323963303034393237323035366634303736653735623937323763336234376563663563333132 38643937613263313435626166313464316133373338373261666331653436373063393162636339
39646237306334326635353563326536633764306534313535383836633033373832316336383530 66356434663239646334383433626566336433653265336332323866633666646132663836313537
33316432356366363934666264306661373037663563333165633132643834353262376661636463 66663766306438396131623533613834626331653731323137303539303835306632663132616363
63326662663739616139393438393734353833376239363531376261303639376363646532666465 64303438353836613136623562626664326364656133383865383730373762666539383036396337
66356533393331393535393161653131343536353535373432323463346331646663353439383237 66393636343561333435313032353939316138306336316337353861383238326136373265663433
37386262633765326331383335613263653331303561343636626337333265313633373533363331 62366566343866626336646466386561353061343735306565383437333931613635393034363430
32373564646464626633323833663432663562363639373931623535306534646664346165356161 37633765663239643435623066313331353862393966306563393838386334393162306562363062
33306330363239386439646563353865646339323633646535316336363239303564373937636233 61633363386564373834383432323861323364623365306439353631323463326461383039356466
37396434353830386130316237386532333939363330306165633630336662333537663265363736 34396464653233626435656463326439303665633532656639376633353932666630626564616564
61636437623832383032643539626338313932303335333461363566363532663633353631353933 34326638383634353033326232646339393638663637313136653763336265616635326666336530
62376531303638303838663737373462323339633632626439353439396139656639373331656636 33663261376262626136346265656130653831636662306132393837306135643831353534626636
61303531383433343961666336363231353638656137666364323133326233623964363536303065 34383762386665666363313932336632326230646439663366663037323562633630373137333232
33623130653564363261653333343732653539386565326133386165313536613732366463303537 37393164656137303131313738396131613561306332356436303436636338623233343637363332
63616538383536636232636562653863323334633839656366373761626561656462316161323630 36613137316337666461333237373266326238303531396432383461616239316630346230303735
33396566333463326136626231653536336134653232306536396434636438366335633137623433 63636535383766353338643932366339333130663632386337643932636630316432313465393766
31636362366431396364 39613065623631316165313134386231616165366266323634643632626230303861376461323433
35346137326563646332303334313530383464373838373633363635373766333362386466323836
32613236646561343365656239346239353866663336393930356238653463336361333033643331
61323430663031383239353363616666336637666230663633616464616638303966343631386632
32333735643936353638666336303133356435396338653465323234316234626536666635333238
38313661303466333464666538363938656631396666643566343763396638336663376430646532
38666234653330646262633062356238343536336637356432313137313561363937363936383364
30643937383833663339643862363234643765386164316138636565643434373734383339363138
36323163633837326132393365333236633264386664373234313061373835346634663137383837
62666561353532303663346365343131316233633163323938623066656332383030393864363536
38383939383935613432613837333863313239653831333438383133343763633838353964353161
61323462343835613937653465633563306462613631323762656437626133336638396663646362
30323661383134653336366234663333336261353162373030626266656336356233316265636661
34303865313433633138363936373561636537353831373033303163646436303932626138356633
63656364353163313037613262396338636230646330666331616534313466306361363433656132
66633231626665303165346339373764666264313838313063323732653837383736633235363064
37353632336238623366313432376163653535656134633634313065356533343933666135396633
30613134646132613637656461303431613064393438363231383464663765316638

View File

@@ -11,8 +11,6 @@ vault_git_work_gpg: "REPLACE_ME"
vault_openai_api_key: "REPLACE_ME" vault_openai_api_key: "REPLACE_ME"
vault_ikaros_authorized_ssh_keys: vault_ikaros_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME" - "ssh-ed25519 REPLACE_ME"
vault_atlas_authorized_ssh_keys:
- "ssh-ed25519 REPLACE_ME atlas-admin"
vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH" vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH"
vault_atlas_samba_password: "REPLACE_ME" vault_atlas_samba_password: "REPLACE_ME"
vault_atlas_immich_db_password: "REPLACE_ME" vault_atlas_immich_db_password: "REPLACE_ME"