mirror of
https://github.com/fscotto/infra.git
synced 2026-09-27 19:03:47 +00:00
Compare commits
14 Commits
feature/at
...
4c10af3187
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c10af3187 | ||
|
|
3ac732751c | ||
|
|
e837b0059b | ||
|
|
7e498514dd | ||
|
|
e7836ea25f | ||
|
|
de2c24d15c | ||
|
|
bd13cb65cc | ||
|
|
77afdda0a3 | ||
|
|
add75d74e9 | ||
|
|
e46a2b22d7 | ||
|
|
a97c64b08f | ||
|
|
160d63c02d | ||
|
|
73bf2cd62a | ||
|
|
e8d059c8ed |
136
AGENTS.md
136
AGENTS.md
@@ -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`
|
||||
- Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --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:
|
||||
- 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`
|
||||
@@ -57,10 +59,12 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff`
|
||||
- Atlas network/share hardening:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff`
|
||||
- Atlas phase-one rootless services:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff`
|
||||
- Prometheus/Atlas WireGuard overlay:
|
||||
`ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard --check --diff`
|
||||
- Atlas ZFS snapshot retention and scrub timers:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags snapshots,scrub --check --diff`
|
||||
- Atlas encrypted Borg backup:
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags packages,borg --check --diff`
|
||||
- Prometheus/Aegis WireGuard gateway:
|
||||
`ansible-playbook ansible/site.yml --limit prometheus,aegis --tags wireguard --check --diff`
|
||||
- DuckDNS config only: `ansible-playbook ansible/site.yml --limit prometheus --tags duckdns --check --diff`
|
||||
|
||||
## Conventions
|
||||
@@ -137,10 +141,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,
|
||||
`vault_atlas_samba_password`. The future rootful media stack also requires
|
||||
`vault_atlas_immich_db_password`. Never print these values.
|
||||
- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `work`, `archive`,
|
||||
`archive/app_data`, `archive/app_data/navidrome`, `archive/app_data/syncthing`, `media`, `media/music`,
|
||||
`media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. `backups/services` has a `500G`
|
||||
refreservation. There is no separate legacy `zpool/syncthing` dataset.
|
||||
- Atlas creates the complete declared hierarchy only under the verified existing or explicitly bootstrapped pool: `archive`,
|
||||
`services`, `services/data`, `services/data/navidrome`, `services/data/syncthing`, `media`, `media/music`,
|
||||
`media/photobook`, `backup`, `backup/hosts`, and `backup/hosts/prometheus`. `backup` has a `500G`
|
||||
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 `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`.
|
||||
@@ -149,52 +153,71 @@ 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.
|
||||
- Atlas NPM and Immich share a rootful Podman network. NPM publishes HTTP/HTTPS, but its administration port remains
|
||||
bound to `127.0.0.1:81`; do not expose it directly to the LAN or Internet.
|
||||
- `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome
|
||||
`0.63.2` uses SQLite below `/data` and does not support `ND_DATABASE_URL` or an external PostgreSQL backend; do not
|
||||
recreate the obsolete Prometheus `navidromedb` service. The role requires the storage role's `zpool/media/music`,
|
||||
`zpool/archive/app_data`, `zpool/archive/app_data/navidrome`, and `zpool/archive/app_data/syncthing` datasets at
|
||||
their exact paths. It never creates the pool.
|
||||
- Keep `backend_phase1_start_services` false until the stopped Prometheus Navidrome data directory has been copied to
|
||||
Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application
|
||||
data; after cutover, set the flag true to enable and start Navidrome and Syncthing.
|
||||
- Phase 1 must not change Prometheus' existing NPM deployment. NPM continues to be managed exactly by `profile_server`;
|
||||
use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking:
|
||||
its GUI, transfer, QUIC and discovery ports are explicitly published only on `10.0.0.2`; native transfer/discovery does not use the HTTP proxy.
|
||||
- `wireguard_overlay` manages the required `wg0` path between Prometheus and Atlas, persists private keys only on their
|
||||
respective hosts, exchanges only derived public keys, and verifies a real peer handshake. The initial run must
|
||||
include both hosts. Prometheus
|
||||
opens `51820/udp`; after creating the WireGuard firewalld zone, restore Prometheus' rootful Podman networking with
|
||||
`podman network reload --all` so the existing proxy stack retains container DNS. The Atlas backend role admits
|
||||
service ports only in the WireGuard firewalld zone.
|
||||
- `profile_backend_phase1` temporarily runs rootless Navidrome and Syncthing on Atlas until Uranus replaces
|
||||
them. It binds only to Atlas' LAN IP, never `wg0`; Navidrome and the Syncthing GUI admit only Aegis as
|
||||
the source-NAT gateway, while native Syncthing ports admit the configured LAN. It initializes fresh
|
||||
state only and never migrates or deletes source application data.
|
||||
- `wireguard_overlay` manages `wg0` between Prometheus (`10.0.0.1`) and Aegis (`10.0.0.2`). It persists private
|
||||
keys only on their respective hosts, exchanges only derived public keys through Ansible, and verifies a real peer
|
||||
handshake. Prometheus opens `51820/udp`; Aegis is the LAN gateway. Its persistent IPv4 forwarding, narrowly scoped
|
||||
WireGuard-to-LAN firewalld policy, and source masquerading permit Prometheus to reach LAN services without a static
|
||||
route on the router. Prometheus includes `192.168.178.0/24` in Aegis' peer `AllowedIPs`; add the Uranus VIP there
|
||||
when it is assigned. After a firewalld reload, restore Prometheus' rootful Podman networking with
|
||||
`podman network reload --all` so the existing proxy stack retains container DNS.
|
||||
|
||||
## Atlas NAS TODO
|
||||
- Provide the required Vault variables and validate the first remote bootstrap on the real Rocky Linux 9 host.
|
||||
Before the first apply, confirm the pool, mountpoints, LAN subnet and firewalld zone. Keep
|
||||
`atlas_manage_media_stack` disabled until `/dev/dri`, the container paths and the Immich database secret are validated.
|
||||
- Validate the complete baseline on the target: OpenZFS kmod loading, existing pool import, dataset
|
||||
mounts, SSH reconnect, Cockpit and all selected 45Drives plugins, NFSv4, SMB and Syncthing.
|
||||
- Finalize dataset properties and the shared UID/GID, group and POSIX ACL model; test the same files
|
||||
through both NFS and SMB before considering multiprotocol access complete.
|
||||
- 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.
|
||||
- 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
|
||||
Completed validation: the existing RAIDZ2 pool and datasets, SELinux, LAN firewall, SSH, Cockpit with
|
||||
the selected 45Drives plugins, encrypted SMB3 `Archive`, the Aegis-only NFSv4 `photobook` export, and the
|
||||
Prometheus--Aegis WireGuard gateway are operational. The gateway handshake, forwarding, source masquerading,
|
||||
and TCP reachability to Atlas were verified. Temporary Navidrome and Syncthing are available through their
|
||||
manual NPM Proxy Hosts; Syncthing uses `/data/Org` backed by the SMB-shared Archive dataset. Aegis has also
|
||||
validated NFSv4.2 read, write, delete, and `all_squash` mapping to UID/GID `1100` end-to-end. The ZFS
|
||||
snapshot timers are active and the first recursive hourly snapshot completed successfully; the first
|
||||
scheduled retention prune and monthly scrub remain runtime checks.
|
||||
|
||||
### Priority 1 - Data protection
|
||||
- [x] Deploy Ansible-managed recursive ZFS snapshots with 24 hourly, 30 daily, 8 weekly, and 12 monthly
|
||||
generations, plus a monthly scrub on the first Sunday at 03:00. The timers and first hourly snapshot were
|
||||
verified on Atlas. Still observe the first scheduled retention prune and scrub; Cockpit Scheduler is for
|
||||
visibility or manual operations only, and snapshot rollback is never automated.
|
||||
- [x] Activate and validate the encrypted offsite Borg backup to the Hetzner Storage Box. Atlas uses the
|
||||
dedicated SSH identity, pinned ED25519 host key, Vault-backed `repokey` encryption, and a locked
|
||||
non-login `borg` account with no sudo or supplementary groups. The initial snapshot-consistent backup,
|
||||
Borg repository check, and temporary-directory restore completed successfully; the restored `Archive`
|
||||
tree matched the live data, and temporary snapshots and mounts were removed. The exported recovery key
|
||||
was copied offline. Daily backup retries and logging, 30 daily, 8 weekly and 12 monthly archives,
|
||||
compaction, and monthly repository checks are enabled.
|
||||
- [ ] Add the UUID-bound offline USB backup with versioned rsync, locking, capacity checks, verification,
|
||||
safe unmounting and a tested restore procedure; never trigger it for an arbitrary USB disk.
|
||||
- [ ] Test restores independently from a ZFS snapshot, Borg, and the offline USB backup before relying on
|
||||
any backup path.
|
||||
- [ ] Add monitoring and alerting for pool health, scrub/resilver, SMART data, temperatures, free space,
|
||||
snapshot/backup capacity growth, and failed maintenance or backup timers.
|
||||
|
||||
### Priority 2 - NAS operability and recovery
|
||||
- [ ] 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.
|
||||
- [ ] Define a controlled Rocky kernel/OpenZFS update and reboot procedure.
|
||||
- [ ] 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,
|
||||
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,
|
||||
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.
|
||||
- Add the UUID-bound offline USB backup with versioned rsync, locking, capacity checks, verification,
|
||||
safe unmounting and a tested restore procedure; never trigger it for an arbitrary USB disk.
|
||||
- Add monitoring and alerting for pool health, scrub/resilver, SMART data, temperatures, free space and
|
||||
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
|
||||
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
|
||||
are validated; do not make it a dependency of the Atlas 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.
|
||||
|
||||
### Priority 3 - Service expansion
|
||||
- [ ] After data protection and recovery are validated, populate `/zpool/media/music` and validate Navidrome.
|
||||
- [ ] Design and deploy Nextcloud as another explicitly temporary Atlas service before Uranus. Give it
|
||||
separate persistent application, database, and cache storage; keep credentials in Vault; publish it only
|
||||
through NPM over the Prometheus--Aegis gateway; and define backup, upgrade, and eventual Uranus-migration
|
||||
procedures before exposing user data. Do not deploy Nextcloud before the data-protection checklist is complete.
|
||||
- [ ] Keep `atlas_manage_media_stack` disabled until the future Immich deployment has validated `/dev/dri`,
|
||||
container paths, and the required Vault database secret.
|
||||
|
||||
### Priority 4 - Optional workflows
|
||||
- [ ] Optionally design iCloud photo ingestion and an Aegis persistent NFS mount as a separate workflow
|
||||
after the storage and backup layers are validated; do not make either a dependency of the Atlas
|
||||
baseline.
|
||||
|
||||
## Coding Agent Notes
|
||||
- Shared agent definitions and lifecycle flags live in `ai_agents` in `ansible/inventory/group_vars/all.yml`.
|
||||
@@ -221,14 +244,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
|
||||
`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.
|
||||
- `profile_aegis` 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` and `aegis_adguard_web_port` host-specific; SSH permits only the declared
|
||||
- `profile_aegis` owns the `nfs-utils` and `wireguard-tools` rpm-ostree layers and reports the required reboot
|
||||
without initiating it. `wireguard_overlay` then configures Aegis as the WireGuard LAN gateway with persistent IPv4
|
||||
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
|
||||
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
|
||||
completing initial setup. Disable the local systemd-resolved stub through `profile_aegis` before
|
||||
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
|
||||
persisted in `/var/lib/icloudpd/config`.
|
||||
|
||||
207
README.md
207
README.md
@@ -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_*`.
|
||||
|
||||
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
|
||||
|
||||
- `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
|
||||
services, and firewalld. The manually activated `podman-compose-server` unit contains the existing
|
||||
Nginx Proxy Manager and Gitea services. The desired Compose file no longer includes Navidrome,
|
||||
Syncthing, or the obsolete Navidrome PostgreSQL database. Navidrome and Syncthing belong to Atlas;
|
||||
official Navidrome uses SQLite instead. Applying the profile does not stop or remove legacy
|
||||
containers and does not delete `/opt/postgres/data`.
|
||||
Syncthing, or the obsolete Navidrome PostgreSQL database; their temporary Atlas deployment is managed
|
||||
by `profile_backend_phase1`. Applying the profile does not stop or remove legacy containers and does
|
||||
not delete `/opt/postgres/data`.
|
||||
|
||||
Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only
|
||||
`80/tcp` and `443/tcp`; its administration interface is bound to `127.0.0.1:81` and can be reached
|
||||
from Ikaros or Nymph with the `npm-tunnel` Bash alias. Nextcloud remains disabled and the profile
|
||||
does not provision any `/srv/nextcloud` directories.
|
||||
|
||||
The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard
|
||||
and the Atlas services are active, configure the current NPM proxy hosts with Navidrome upstream
|
||||
`http://10.0.0.2:4533` and Syncthing GUI upstream `http://10.0.0.2:8384`. Only the Syncthing web GUI
|
||||
uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas
|
||||
WireGuard address. Configure both Syncthing authentication and an appropriate NPM access policy before
|
||||
publishing its GUI.
|
||||
NPM remains managed only by `profile_server`. Its WireGuard peer is Aegis (`10.0.0.2`), which forwards
|
||||
selected requests to LAN addresses and source-NATs them so no static route is required on the router.
|
||||
Use an Atlas LAN address for any current NAS-backed upstream; when Uranus receives its VIP, add that VIP
|
||||
to Prometheus' Aegis peer `AllowedIPs` and declare the corresponding proxy target separately.
|
||||
|
||||
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
|
||||
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
|
||||
firewalld rules, SSH key-only access for `pi`, and `wake-ikaros`. Set the host-local
|
||||
`aegis_lan_subnet` and `aegis_adguard_web_port` values before applying it. The playbook permits
|
||||
firewalld rules, SSH key-only access for `pi`, the `nfs-utils` and `wireguard-tools` rpm-ostree layers,
|
||||
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
|
||||
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
|
||||
for AdGuard while retaining DNS learned from the router. Define
|
||||
stub and points `/etc/resolv.conf` to its full resolver data, freeing port 53 for AdGuard. LAN clients
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
`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
|
||||
`vault_atlas_immich_db_password`. Bootstrap the host through its
|
||||
existing administrator. Open `51820/udp` towards Prometheus in the provider firewall first, then
|
||||
include both WireGuard peers in the same idempotent playbook run:
|
||||
|
||||
```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.
|
||||
existing administrator. The explicit pool gate is safe to repeat: the role creates the RAIDZ2 pool only when
|
||||
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.
|
||||
|
||||
`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
|
||||
@@ -239,11 +255,11 @@ paths, and the Immich database secret are validated. Atlas reads its declared SS
|
||||
separate files below `~/.ssh/authorized_keys.d/`.
|
||||
|
||||
With storage management enabled, Atlas creates the complete dataset hierarchy below the existing or
|
||||
explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the separate `archive/app_data/navidrome` and
|
||||
`archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`,
|
||||
`backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`,
|
||||
while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G`
|
||||
refreservation. Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords,
|
||||
explicitly bootstrapped `zpool`: SMB-shared `archive`, private `services/data` with separate
|
||||
`services/data/navidrome` and `services/data/syncthing` application datasets, `media`, `media/music`,
|
||||
`media/photobook`, and `backup/hosts/prometheus`. Application/archive datasets use `zstd`, while media,
|
||||
Syncthing, and host-backup datasets use `lz4`; `backup` has a `500G` reservation covering its descendants.
|
||||
Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords,
|
||||
agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed
|
||||
Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific
|
||||
exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using
|
||||
@@ -255,53 +271,93 @@ 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
|
||||
administration interface remains restricted to `127.0.0.1:81` for SSH-tunnel access.
|
||||
|
||||
Phase 1 is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. It is enabled in the
|
||||
Atlas host configuration and can be set to `false` only for a deliberate suspension. Official Navidrome `0.63.2` uses its SQLite database below `/data`; it does
|
||||
not support `ND_DATABASE_URL` or an external PostgreSQL backend. The obsolete `navidromedb` service
|
||||
was therefore removed from Prometheus instead of being reproduced on Atlas. The role derives all
|
||||
storage paths from the `zpool` mounted at `/zpool`: music is read-only at
|
||||
`/zpool/media/music`, Navidrome application state and `navidrome.db` are stored at
|
||||
`/zpool/archive/app_data/navidrome`, and Syncthing persists at
|
||||
`/zpool/archive/app_data/syncthing`. `profile_atlas` creates these datasets when
|
||||
`atlas_manage_storage` is enabled; the backend role verifies their exact mountpoints before starting
|
||||
containers. The backend role never creates the pool. The separate `wireguard_overlay` role manages `wg0`
|
||||
between Prometheus (`10.0.0.1`) and Atlas (`10.0.0.2`), generating private keys once
|
||||
on their respective hosts and exchanging only public keys through Ansible. Prometheus alone opens
|
||||
`51820/udp` publicly. When the WireGuard zone is created, Ansible reloads firewalld and immediately
|
||||
reloads Prometheus' rootful Podman networks so the existing proxy stack retains container DNS and
|
||||
connectivity. Backend ports are admitted only in the WireGuard firewalld zone.
|
||||
Atlas temporarily hosts rootless Navidrome and Syncthing until Uranus replaces them. They bind only to
|
||||
Atlas' LAN address (`192.168.178.55`); WireGuard remains exclusively between Prometheus (`10.0.0.1`)
|
||||
and Aegis (`10.0.0.2`). Their state is initialized ex novo in `/zpool/services/data/navidrome` and
|
||||
`/zpool/services/data/syncthing`; no source application state is migrated. The music library at
|
||||
`/zpool/media/music` is populated separately.
|
||||
|
||||
`backend_phase1_start_services` stays false during the application-state transfer, so the first real
|
||||
backend run renders the Quadlets without creating an empty Atlas database. After stopping Navidrome
|
||||
on Prometheus, copy the complete `/opt/navidrome/data/` directory into
|
||||
`/zpool/archive/app_data/navidrome/`, preserving `navidrome.db` and any SQLite sidecar files. Then set
|
||||
this variable to true and rerun the role to enable and start Navidrome and Syncthing. The playbook
|
||||
never copies or deletes application data.
|
||||
The separate `wireguard_overlay` role manages `wg0` between Prometheus (`10.0.0.1`) and Aegis
|
||||
(`10.0.0.2`), generating private keys once on their respective hosts and exchanging only public keys
|
||||
through Ansible. Prometheus alone opens `51820/udp`. Aegis forwards only the declared overlay-to-LAN
|
||||
traffic and source-NATs it, so Atlas and future Uranus nodes require neither a VPN interface nor a router
|
||||
static route. Atlas permits Navidrome (`4533/tcp`) and the Syncthing GUI (`8384/tcp`) only from Aegis;
|
||||
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
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit atlas --tags storage
|
||||
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit prometheus,atlas --tags wireguard
|
||||
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff
|
||||
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1
|
||||
ansible-playbook ansible/site.yml --limit prometheus,aegis --tags wireguard --check --diff
|
||||
```
|
||||
|
||||
For the cutover, stop the old Navidrome writer before copying its data directory, verify ownership by
|
||||
the Atlas `admin` account and confirm that the copied SQLite database is present before changing
|
||||
`backend_phase1_start_services: true` in `host_vars/atlas.yml`. Keep the source data and the stopped
|
||||
legacy `navidromedb` container until Navidrome on Atlas and a restore test have been validated.
|
||||
The first real WireGuard run must include both peers. If Fedora IoT has just layered `wireguard-tools`,
|
||||
reboot Aegis manually and rerun the command without `--check`; the role then waits for a real peer
|
||||
handshake.
|
||||
|
||||
Snapshot retention, Syncthing topology, WireGuard/firewall validation, Prometheus backup pulls,
|
||||
encrypted Borg backups to a Hetzner Storage Box, USB backup, monitoring, and disaster-recovery tests
|
||||
remain follow-up work. The detailed operational backlog is kept in `AGENTS.md`.
|
||||
Atlas declares recursive, systemd-timed ZFS snapshots for the complete pool hierarchy: 24 hourly
|
||||
snapshots at minute 05, 30 daily snapshots at 00:15, 8 weekly snapshots on Sunday at 01:00, and 12
|
||||
monthly snapshots on the first day at 02:00. The retention helper prunes only snapshots carrying its
|
||||
managed `atlas-auto` prefix and never rolls back a dataset. The OpenZFS monthly scrub timer is scheduled
|
||||
for the first Sunday at 03:00; the conflicting weekly scrub timer is disabled explicitly. The first recursive
|
||||
hourly snapshot completed successfully on Atlas; retention pruning and the first scheduled scrub still await
|
||||
live runtime evidence. Validate this layer independently with:
|
||||
|
||||
```bash
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit atlas --tags snapshots,scrub --check --diff
|
||||
```
|
||||
|
||||
Atlas also declares an encrypted Borg backup to the dedicated Hetzner Storage Box sub-account
|
||||
`u660064-sub1`. The repository is the sub-account-relative `./borg-data` path and uses the explicitly
|
||||
selected remote Borg 1.4 binary over SSH port 23. The ED25519 server key is pinned; a dedicated client
|
||||
key is generated for the locked, non-login `borg` system account, and its private half never leaves
|
||||
`/etc/atlas-borg`. The account has no sudo or supplementary groups and owns only its SSH identity,
|
||||
passphrase, cache, and Borg state. Borg receives its passphrase through a mode `0600` file rendered from
|
||||
`vault_atlas_borg_passphrase`.
|
||||
|
||||
The daily backup starts at 04:30 with up to 30 minutes of randomized delay. It creates a temporary,
|
||||
recursive ZFS snapshot and reconstructs every dataset below `/zpool` as a read-only bind-mounted tree,
|
||||
so parent and child datasets enter one consistent Borg archive. Cleanup always removes the temporary
|
||||
mounts and managed snapshot. Only the root wrapper performs snapshot and mount operations; it launches
|
||||
the Borg client as `borg` with temporary read-search capability and no ZFS, sudo, or pool-management
|
||||
privileges. Borg retains 30 daily, 8 weekly, and 12 monthly archives, then compacts the standard
|
||||
read-write repository. A full metadata and repository check runs as `borg` on the fifteenth day of each
|
||||
month at 06:00. Both operations use a common lock, journal logging, and bounded systemd retries.
|
||||
|
||||
Initial activation remains explicit:
|
||||
|
||||
1. Add a strong unique `vault_atlas_borg_passphrase` with `ansible-vault edit secrets/vault.yml`.
|
||||
2. Generate and display only the dedicated public key with
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags borg_key`.
|
||||
3. Install that public key in the Hetzner sub-account, then apply with
|
||||
`ansible-playbook ansible/site.yml --limit atlas --tags packages,borg`.
|
||||
4. Copy the ignored `secrets/recovery/atlas-borg-repokey.export` file to genuinely offline storage.
|
||||
The controller-side copy is not an offline backup by itself.
|
||||
|
||||
The role initializes only the missing `repokey` repository and never accepts an unpinned host key or
|
||||
password authentication. It does not start the first backup manually. Validate the rendered state with:
|
||||
|
||||
```bash
|
||||
ANSIBLE_LOCAL_TEMP=/tmp/ansible-local \
|
||||
ansible-playbook ansible/site.yml --limit atlas --tags packages,borg --check --diff
|
||||
```
|
||||
|
||||
Atlas runtime activation is complete: the initial backup and repository check succeeded, a full restore
|
||||
to a temporary directory was validated against the live `Archive` tree, the recovery-key export was copied
|
||||
to offline storage, and the temporary snapshot and bind mounts were cleaned up.
|
||||
|
||||
A temporary Nextcloud deployment on Atlas is also planned before Uranus: it requires separately
|
||||
declared persistent application, database, and cache storage, Vault-backed credentials, NPM-only
|
||||
publishing through Aegis, and defined backup, upgrade, and eventual migration procedures. Do not deploy
|
||||
it before the data-protection checklist is complete.
|
||||
|
||||
Prometheus backup pulls, USB backup, monitoring, and disaster-recovery tests remain follow-up work. The
|
||||
prioritized operational backlog is kept in `AGENTS.md`.
|
||||
|
||||
## How layering works
|
||||
|
||||
@@ -334,9 +390,10 @@ ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=t
|
||||
## AI coding agents
|
||||
|
||||
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
|
||||
lifecycle flags in `ansible/inventory/group_vars/all.yml`, so one agent can be
|
||||
installed, configured, or removed without affecting the others:
|
||||
GitHub Copilot; IBM Bob is also managed on `deadalus`. Codex Relay is installed
|
||||
only on `ikaros`. Each agent has its own lifecycle flags in
|
||||
`ansible/inventory/group_vars/all.yml`, so one agent can be installed,
|
||||
configured, or removed without affecting the others:
|
||||
|
||||
```yaml
|
||||
ai_agents:
|
||||
@@ -387,8 +444,8 @@ ansible-playbook ansible/site.yml --limit deadalus --tags ai_agents --check --di
|
||||
| `profile_workstation_dev_wsl` | WSL development setup. |
|
||||
| `profile_server` | Server setup. |
|
||||
| `profile_atlas` | Rocky Linux 9 NAS setup. |
|
||||
| `profile_backend_phase1` | Rootless Navidrome and Syncthing on Atlas. |
|
||||
| `wireguard_overlay` | Prometheus/Atlas WireGuard overlay. |
|
||||
| `profile_backend_phase1` | Temporary rootless Atlas Navidrome and Syncthing services. |
|
||||
| `wireguard_overlay` | Prometheus/Aegis WireGuard LAN gateway. |
|
||||
| `profile_aegis` | Fedora IoT always-on LAN node. |
|
||||
| `dotfiles_common` | Shared user dotfiles. |
|
||||
|
||||
@@ -400,8 +457,8 @@ platform_void -> packages_void + services_runit
|
||||
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
|
||||
platform_fedora -> packages_fedora + services_systemd
|
||||
platform_rocky -> packages_rocky + services_systemd
|
||||
wireguard_overlay -> wireguard_overlay (after platform_rocky)
|
||||
role_aegis -> profile_aegis
|
||||
wireguard_overlay -> wireguard_overlay (after Aegis profile and platform_rocky)
|
||||
atlas -> profile_atlas
|
||||
role_backend_phase1 -> profile_backend_phase1 (after atlas)
|
||||
rocky_server -> dotfiles_common + profile_server (after platform_rocky)
|
||||
@@ -506,7 +563,7 @@ ansible-playbook ansible/site.yml --list-tags
|
||||
| `sharing` | Atlas NFSv4 and SMB3 configuration. |
|
||||
| `storage` | Atlas child ZFS datasets. |
|
||||
| `tmux` | tmux configuration and plugins. |
|
||||
| `wireguard` | Prometheus/Atlas WireGuard overlay. |
|
||||
| `wireguard` | Prometheus/Aegis WireGuard LAN gateway. |
|
||||
| `wsl` | WSL bootstrap and configuration. |
|
||||
|
||||
## Bootstrapping a new machine
|
||||
|
||||
@@ -93,6 +93,8 @@ common_dotfiles:
|
||||
# AI coding-agent lifecycle controls are independent for every agent.
|
||||
# Installation and removal are mutually exclusive per agent; the common
|
||||
# pre-task enforces this before any changes are made.
|
||||
codex_relay_install_enabled: false
|
||||
|
||||
ai_agents:
|
||||
opencode:
|
||||
npm_package: opencode-ai
|
||||
@@ -109,6 +111,11 @@ ai_agents:
|
||||
install_enabled: true
|
||||
deploy_enabled: true
|
||||
uninstall_enabled: false
|
||||
codex_relay:
|
||||
npm_package: codex-relay
|
||||
install_enabled: "{{ codex_relay_install_enabled }}"
|
||||
deploy_enabled: false
|
||||
uninstall_enabled: false
|
||||
gemini_cli:
|
||||
npm_package: "@google/gemini-cli"
|
||||
install_enabled: true
|
||||
|
||||
@@ -9,6 +9,7 @@ fedora_packages_base:
|
||||
- jq
|
||||
- make
|
||||
- nodejs
|
||||
- openssl
|
||||
- ripgrep
|
||||
|
||||
fedora_manage_docker_repo: true
|
||||
@@ -58,3 +59,13 @@ fedora_dev_packages:
|
||||
|
||||
fedora_flatpak_remote_name: flathub
|
||||
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
|
||||
|
||||
@@ -40,4 +40,3 @@ server_firewalld_services:
|
||||
server_firewalld_ports: []
|
||||
server_sshd_service_name: sshd
|
||||
server_compose_selinux_mount_option: Z
|
||||
server_syncthing_enabled: false
|
||||
|
||||
@@ -75,21 +75,6 @@ server_directories:
|
||||
group: root
|
||||
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:
|
||||
PermitRootLogin: "no"
|
||||
|
||||
|
||||
@@ -3,14 +3,19 @@ enabled_services:
|
||||
- docker
|
||||
|
||||
workstation_dev_wsl_packages:
|
||||
- buildah
|
||||
- helm
|
||||
# Fedora ships the kubectl client binary in the kubernetes-client package.
|
||||
- kubernetes-client
|
||||
- mise
|
||||
- pinentry-curses
|
||||
- python3-pip
|
||||
- tmux
|
||||
|
||||
# Java 11 and Maven are managed by Mise on this Fedora WSL profile. Keep their
|
||||
# versions pinned; update them deliberately.
|
||||
# Java 11, Java 25 and Maven are managed by Mise on this Fedora WSL profile.
|
||||
# Keep their versions pinned; update them deliberately.
|
||||
workstation_mise_java_version: temurin-11.0.31+11
|
||||
workstation_mise_java_25_version: 25.0.2
|
||||
workstation_mise_maven_version: 3.9.16
|
||||
|
||||
workstation_is_wsl: true
|
||||
|
||||
@@ -9,6 +9,32 @@ ansible_ssh_use_tty: false
|
||||
|
||||
aegis_lan_subnet: 192.168.178.0/24
|
||||
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:
|
||||
- name: ikaros
|
||||
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
|
||||
|
||||
@@ -35,35 +35,65 @@ atlas_zpool_disks:
|
||||
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6A6VJK
|
||||
- /dev/disk/by-id/ata-ST4000VN006-3CW104_WW6AC1LM
|
||||
atlas_create_pool: false
|
||||
atlas_zfs_dataset_work: work
|
||||
atlas_zfs_dataset_archive: archive
|
||||
atlas_zfs_dataset_app_data: archive/app_data
|
||||
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
|
||||
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
|
||||
atlas_zfs_dataset_services: services
|
||||
atlas_zfs_dataset_app_data: services/data
|
||||
atlas_zfs_dataset_navidrome: services/data/navidrome
|
||||
atlas_zfs_dataset_syncthing: services/data/syncthing
|
||||
atlas_zfs_dataset_media: media
|
||||
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_backups: backups
|
||||
atlas_zfs_dataset_service_backups: backups/services
|
||||
atlas_mount_root: /zpool
|
||||
atlas_manage_storage: true
|
||||
atlas_manage_zfs_snapshots: true
|
||||
atlas_zfs_snapshot_prefix: atlas-auto
|
||||
atlas_zfs_snapshot_policies:
|
||||
- name: hourly
|
||||
calendar: "*-*-* *:05:00"
|
||||
keep: 24
|
||||
- name: daily
|
||||
calendar: "*-*-* 00:15:00"
|
||||
keep: 30
|
||||
- name: weekly
|
||||
calendar: "Sun *-*-* 01:00:00"
|
||||
keep: 8
|
||||
- name: monthly
|
||||
calendar: "*-*-01 02:00:00"
|
||||
keep: 12
|
||||
atlas_manage_zfs_scrub: true
|
||||
atlas_zfs_scrub_calendar: "Sun *-*-01..07 03:00:00"
|
||||
atlas_manage_borg_backup: true
|
||||
atlas_borg_repository_host: u660064-sub1.your-storagebox.de
|
||||
atlas_borg_repository_user: u660064-sub1
|
||||
atlas_borg_repository_port: 23
|
||||
atlas_borg_repository_path: ./borg-data
|
||||
atlas_borg_remote_path: borg-1.4
|
||||
# Verified against Hetzner's published ED25519 fingerprint on 2026-09-17:
|
||||
# SHA256:XqONwb1S0zuj5A1CDxpOSuD2hnAArV1A3wKY7Z3sdgM
|
||||
atlas_borg_host_key: >-
|
||||
[u660064-sub1.your-storagebox.de]:23 ssh-ed25519
|
||||
AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs
|
||||
atlas_borg_backup_calendar: "*-*-* 04:30:00"
|
||||
atlas_borg_check_calendar: "*-*-15 06:00:00"
|
||||
atlas_borg_randomized_delay: 30m
|
||||
atlas_borg_keep_daily: 30
|
||||
atlas_borg_keep_weekly: 8
|
||||
atlas_borg_keep_monthly: 12
|
||||
atlas_manage_sharing: true
|
||||
atlas_manage_media_stack: false
|
||||
|
||||
wireguard_overlay_enabled: true
|
||||
wireguard_address: 10.0.0.2/24
|
||||
wireguard_peers:
|
||||
- name: prometheus
|
||||
host: prometheus
|
||||
endpoint: "{{ hostvars['prometheus']['ansible_host'] }}:{{ hostvars['prometheus']['wireguard_listen_port'] }}"
|
||||
allowed_ips:
|
||||
- 10.0.0.0/24
|
||||
persistent_keepalive: 25
|
||||
|
||||
# WireGuard is retired on Atlas. These rootless services are a temporary home
|
||||
# until Uranus replaces them.
|
||||
backend_phase1_enabled: true
|
||||
backend_phase1_start_services: false
|
||||
backend_phase1_wireguard_address: 10.0.0.2
|
||||
backend_phase1_start_services: true
|
||||
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_syncthing_binary: false
|
||||
@@ -80,6 +110,7 @@ host_packages:
|
||||
- samba
|
||||
- samba-client
|
||||
- samba-common-tools
|
||||
- borgbackup
|
||||
- zfs
|
||||
|
||||
atlas_nfs_exports:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
hostname: ikaros
|
||||
|
||||
codex_relay_install_enabled: true
|
||||
chatgpt_desktop_enabled: true
|
||||
|
||||
host_packages:
|
||||
- openssh-clients
|
||||
- openssh-server
|
||||
@@ -229,7 +232,7 @@ gnome_gsettings_settings:
|
||||
- schema: org.gnome.shell
|
||||
key: app-picker-layout
|
||||
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
|
||||
key: disabled-extensions
|
||||
value: >-
|
||||
@@ -241,7 +244,7 @@ gnome_gsettings_settings:
|
||||
- schema: org.gnome.shell
|
||||
key: favorite-apps
|
||||
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
|
||||
key: remember-mount-password
|
||||
value: "true"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
hostname: nymph
|
||||
|
||||
chatgpt_desktop_enabled: true
|
||||
|
||||
# Fedora Workstation/GNOME laptop target. GNOME is provided by the base Fedora
|
||||
# Workstation install; Ansible deploys shared desktop dotfiles but leaves GNOME
|
||||
# settings unmanaged for now.
|
||||
|
||||
@@ -21,10 +21,12 @@ wireguard_listen_port: 51820
|
||||
wireguard_enable_ipv4_forwarding: true
|
||||
wireguard_reload_rootful_podman_networks: true
|
||||
wireguard_peers:
|
||||
- name: atlas
|
||||
host: atlas
|
||||
- name: aegis
|
||||
host: aegis
|
||||
# TODO: Aggiungere VIP di Uranus in futuro
|
||||
allowed_ips:
|
||||
- 10.0.0.2/32
|
||||
- 192.168.178.0/24
|
||||
|
||||
host_packages:
|
||||
- cockpit
|
||||
|
||||
@@ -99,7 +99,7 @@ all:
|
||||
wireguard_overlay:
|
||||
hosts:
|
||||
prometheus:
|
||||
atlas:
|
||||
aegis:
|
||||
|
||||
role_backend_phase1:
|
||||
hosts:
|
||||
|
||||
@@ -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-----
|
||||
@@ -130,6 +130,56 @@
|
||||
loop_control:
|
||||
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
|
||||
tags: [packages]
|
||||
ansible.builtin.set_fact:
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
---
|
||||
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_icloudpd_image: docker.io/boredazfcuk/icloudpd:latest
|
||||
aegis_icloudpd_folder_structure: '{:%Y/%m/%d}'
|
||||
|
||||
@@ -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
|
||||
tags: [aegis, icloudpd]
|
||||
ansible.builtin.assert:
|
||||
@@ -8,15 +22,86 @@
|
||||
no_log: true
|
||||
|
||||
- name: Require completed Aegis network placeholders
|
||||
tags: [aegis, firewall, services]
|
||||
tags: [aegis, dns, firewall, network, services]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aegis_lan_subnet != 'CHANGEME_LAN_SUBNET'
|
||||
- aegis_firewalld_zone | length > 0
|
||||
- aegis_adguard_web_port | int > 0
|
||||
- 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
|
||||
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
|
||||
tags: [aegis, services]
|
||||
|
||||
@@ -47,31 +47,67 @@ atlas_hardening_sysctls:
|
||||
net.ipv6.conf.all.accept_source_route: "0"
|
||||
net.ipv6.conf.default.accept_source_route: "0"
|
||||
atlas_zfs_pool: CHANGEME_ZFS_POOL
|
||||
atlas_zfs_dataset_work: work
|
||||
atlas_zfs_dataset_archive: archive
|
||||
atlas_zfs_dataset_app_data: archive/app_data
|
||||
atlas_zfs_dataset_navidrome: archive/app_data/navidrome
|
||||
atlas_zfs_dataset_syncthing: archive/app_data/syncthing
|
||||
atlas_zfs_dataset_services: services
|
||||
atlas_zfs_dataset_app_data: services/data
|
||||
atlas_zfs_dataset_navidrome: services/data/navidrome
|
||||
atlas_zfs_dataset_syncthing: services/data/syncthing
|
||||
atlas_zfs_dataset_media: media
|
||||
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_backups: backups
|
||||
atlas_zfs_dataset_service_backups: backups/services
|
||||
atlas_zfs_service_backups_refreservation: 500G
|
||||
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
|
||||
|
||||
atlas_manage_zfs_snapshots: false
|
||||
atlas_zfs_snapshot_prefix: atlas-auto
|
||||
atlas_zfs_snapshot_policies: []
|
||||
atlas_manage_zfs_scrub: false
|
||||
atlas_zfs_scrub_calendar: ""
|
||||
|
||||
atlas_manage_borg_backup: false
|
||||
atlas_borg_username: borg
|
||||
atlas_borg_group: borg
|
||||
atlas_borg_home: /var/lib/atlas-borg
|
||||
atlas_borg_repository_host: CHANGEME_BORG_HOST
|
||||
atlas_borg_repository_user: CHANGEME_BORG_USER
|
||||
atlas_borg_repository_port: 23
|
||||
atlas_borg_repository_path: ./borg-data
|
||||
atlas_borg_remote_path: borg-1.4
|
||||
atlas_borg_host_key: ""
|
||||
atlas_borg_ssh_private_key_path: /etc/atlas-borg/id_ed25519
|
||||
atlas_borg_known_hosts_path: /etc/atlas-borg/known_hosts
|
||||
atlas_borg_passphrase_path: /etc/atlas-borg/passphrase
|
||||
atlas_borg_ssh_wrapper_path: /usr/local/libexec/atlas-borg-ssh
|
||||
atlas_borg_passphrase: "{{ vault_atlas_borg_passphrase | default('') }}"
|
||||
atlas_borg_encryption_mode: repokey
|
||||
atlas_borg_archive_prefix: atlas
|
||||
atlas_borg_snapshot_prefix: atlas-borg
|
||||
atlas_borg_compression: auto,zstd,3
|
||||
atlas_borg_backup_calendar: ""
|
||||
atlas_borg_check_calendar: ""
|
||||
atlas_borg_randomized_delay: 30m
|
||||
atlas_borg_keep_daily: 30
|
||||
atlas_borg_keep_weekly: 8
|
||||
atlas_borg_keep_monthly: 12
|
||||
atlas_borg_config_dir: /var/lib/atlas-borg
|
||||
atlas_borg_cache_dir: /var/cache/atlas-borg
|
||||
atlas_borg_lock_path: /var/lib/atlas-borg/backup.lock
|
||||
atlas_borg_recovery_export_path: "{{ playbook_dir }}/../secrets/recovery/atlas-borg-repokey.export"
|
||||
|
||||
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_navidrome_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
|
||||
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
|
||||
atlas_media_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_media }}"
|
||||
atlas_music_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
|
||||
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
|
||||
atlas_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_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_file: /etc/yum.repos.d/45drives-enterprise.repo
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
name: smb
|
||||
state: restarted
|
||||
|
||||
- name: Restart Atlas Borg timers
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
loop:
|
||||
- atlas-borg-backup.timer
|
||||
- atlas-borg-check.timer
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Restart Atlas media Quadlets
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
|
||||
518
ansible/roles/profile_atlas/tasks/borg_backup.yml
Normal file
518
ansible/roles/profile_atlas/tasks/borg_backup.yml
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
- name: Validate Atlas Borg backup configuration
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_manage_storage | bool
|
||||
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
|
||||
- atlas_mount_root.startswith('/')
|
||||
- atlas_borg_username is match('^[a-z_][a-z0-9_-]*$')
|
||||
- atlas_borg_group is match('^[a-z_][a-z0-9_-]*$')
|
||||
- atlas_borg_username not in ['root', atlas_admin_username]
|
||||
- atlas_borg_group != 'wheel'
|
||||
- atlas_borg_home.startswith('/var/lib/')
|
||||
- atlas_borg_repository_host is match('^[A-Za-z0-9.-]+$')
|
||||
- atlas_borg_repository_user is match('^[A-Za-z0-9_-]+$')
|
||||
- atlas_borg_repository_port | int > 0
|
||||
- atlas_borg_repository_port | int < 65536
|
||||
- atlas_borg_repository_path is match('^\./[A-Za-z0-9][A-Za-z0-9._/-]*$')
|
||||
- "'/../' not in ('/' ~ atlas_borg_repository_path ~ '/')"
|
||||
- atlas_borg_remote_path is match('^borg-[0-9]+\.[0-9]+$')
|
||||
- atlas_borg_host_key.startswith(
|
||||
'[' ~ atlas_borg_repository_host ~ ']:' ~ (atlas_borg_repository_port | string) ~ ' ssh-ed25519 '
|
||||
)
|
||||
- atlas_borg_ssh_private_key_path.startswith('/etc/atlas-borg/')
|
||||
- atlas_borg_known_hosts_path.startswith('/etc/atlas-borg/')
|
||||
- atlas_borg_passphrase_path.startswith('/etc/atlas-borg/')
|
||||
- atlas_borg_ssh_wrapper_path.startswith('/usr/local/libexec/')
|
||||
- atlas_borg_encryption_mode == 'repokey'
|
||||
- atlas_borg_archive_prefix is match('^[a-z0-9][a-z0-9_-]*$')
|
||||
- atlas_borg_snapshot_prefix is match('^[a-z0-9][a-z0-9_-]*$')
|
||||
- atlas_borg_keep_daily | int > 0
|
||||
- atlas_borg_keep_weekly | int > 0
|
||||
- atlas_borg_keep_monthly | int > 0
|
||||
fail_msg: >-
|
||||
Atlas Borg needs a safe relative repository path, a pinned ED25519 host
|
||||
key, positive retention counts, and valid dedicated SSH settings.
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create the Atlas Borg system group
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.group:
|
||||
name: "{{ atlas_borg_group }}"
|
||||
system: true
|
||||
state: present
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create the least-privilege Atlas Borg account
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.user:
|
||||
name: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
groups: []
|
||||
append: false
|
||||
comment: Atlas Borg backup service
|
||||
home: "{{ atlas_borg_home }}"
|
||||
create_home: false
|
||||
shell: /sbin/nologin
|
||||
password_lock: true
|
||||
system: true
|
||||
state: present
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Read Atlas Borg account group membership
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- id
|
||||
- -nG
|
||||
- "{{ atlas_borg_username }}"
|
||||
register: atlas_borg_account_groups
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Require the Atlas Borg account to have no supplementary groups
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_borg_account_groups.stdout.split() == [atlas_borg_group]
|
||||
fail_msg: >-
|
||||
The Atlas Borg service account must belong only to its private primary
|
||||
group and must never receive wheel or other supplementary membership.
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Validate Atlas Borg systemd calendars
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemd-analyze
|
||||
- calendar
|
||||
- "{{ item }}"
|
||||
loop:
|
||||
- "{{ atlas_borg_backup_calendar }}"
|
||||
- "{{ atlas_borg_check_calendar }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create Atlas Borg configuration directory
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.file:
|
||||
path: /etc/atlas-borg
|
||||
state: directory
|
||||
owner: root
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0750"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Generate the dedicated Atlas Borg SSH identity
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ssh-keygen
|
||||
- -q
|
||||
- -t
|
||||
- ed25519
|
||||
- -N
|
||||
- ""
|
||||
- -C
|
||||
- atlas-borg@atlas
|
||||
- -f
|
||||
- "{{ atlas_borg_ssh_private_key_path }}"
|
||||
creates: "{{ atlas_borg_ssh_private_key_path }}"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Protect the Atlas Borg private SSH identity
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_ssh_private_key_path }}"
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0600"
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Set permissions on the Atlas Borg public SSH identity
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_ssh_private_key_path }}.pub"
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0644"
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Read the dedicated Atlas Borg public SSH identity
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ atlas_borg_ssh_private_key_path }}.pub"
|
||||
register: atlas_borg_public_key
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Report the public SSH identity to install in the Hetzner sub-account
|
||||
tags: [atlas, storage, backup, borg, borg_key]
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ atlas_borg_public_key.content | b64decode | trim }}"
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Pin the Hetzner Storage Box SSH host key
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.copy:
|
||||
content: "{{ atlas_borg_host_key }}\n"
|
||||
dest: "{{ atlas_borg_known_hosts_path }}"
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0600"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create Atlas Borg state directories
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0700"
|
||||
loop:
|
||||
- "{{ atlas_borg_config_dir }}"
|
||||
- "{{ atlas_borg_cache_dir }}"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create the shared Atlas Borg operation lock
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.copy:
|
||||
content: ""
|
||||
dest: "{{ atlas_borg_lock_path }}"
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0600"
|
||||
force: false
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Require the Atlas Borg encryption passphrase from Vault
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_borg_passphrase | length >= 20
|
||||
fail_msg: >-
|
||||
Define vault_atlas_borg_passphrase with a strong unique value in the
|
||||
encrypted Vault before activating the Borg repository.
|
||||
no_log: true
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Install the Atlas Borg passphrase
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.copy:
|
||||
content: "{{ atlas_borg_passphrase }}\n"
|
||||
dest: "{{ atlas_borg_passphrase_path }}"
|
||||
owner: "{{ atlas_borg_username }}"
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0600"
|
||||
diff: false
|
||||
no_log: true
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Install the Atlas Borg backup helper
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.template:
|
||||
src: atlas-borg-backup.sh.j2
|
||||
dest: /usr/local/sbin/atlas-borg-backup
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Install the Atlas Borg check helper
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.template:
|
||||
src: atlas-borg-check.sh.j2
|
||||
dest: /usr/local/sbin/atlas-borg-check
|
||||
owner: root
|
||||
group: "{{ atlas_borg_group }}"
|
||||
mode: "0750"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Create the local libexec directory for the Atlas Borg SSH wrapper
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_ssh_wrapper_path | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Install the capability-dropping Atlas Borg SSH wrapper
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.template:
|
||||
src: atlas-borg-ssh.sh.j2
|
||||
dest: "{{ atlas_borg_ssh_wrapper_path }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Install Atlas Borg systemd units
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "/etc/systemd/system/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop:
|
||||
- atlas-borg-backup.service
|
||||
- atlas-borg-backup.timer
|
||||
- atlas-borg-check.service
|
||||
- atlas-borg-check.timer
|
||||
notify: Restart Atlas Borg timers
|
||||
when: atlas_manage_borg_backup | bool
|
||||
|
||||
- name: Verify dedicated SSH access to the Hetzner Storage Box
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ssh
|
||||
- -T
|
||||
- -i
|
||||
- "{{ atlas_borg_ssh_private_key_path }}"
|
||||
- -p
|
||||
- "{{ atlas_borg_repository_port | string }}"
|
||||
- -o
|
||||
- BatchMode=yes
|
||||
- -o
|
||||
- IdentitiesOnly=yes
|
||||
- -o
|
||||
- StrictHostKeyChecking=yes
|
||||
- -o
|
||||
- "UserKnownHostsFile={{ atlas_borg_known_hosts_path }}"
|
||||
- "{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}"
|
||||
- pwd
|
||||
register: atlas_borg_ssh_probe
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Require the dedicated public key on the Hetzner sub-account
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_borg_ssh_probe.rc == 0
|
||||
fail_msg: >-
|
||||
Install the reported Atlas Borg public key in the Hetzner sub-account
|
||||
before rerunning the Borg tasks. Password authentication is never used.
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Probe the remote Atlas Borg repository path
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ssh
|
||||
- -T
|
||||
- -i
|
||||
- "{{ atlas_borg_ssh_private_key_path }}"
|
||||
- -p
|
||||
- "{{ atlas_borg_repository_port | string }}"
|
||||
- -o
|
||||
- BatchMode=yes
|
||||
- -o
|
||||
- IdentitiesOnly=yes
|
||||
- -o
|
||||
- StrictHostKeyChecking=yes
|
||||
- -o
|
||||
- "UserKnownHostsFile={{ atlas_borg_known_hosts_path }}"
|
||||
- "{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}"
|
||||
- stat
|
||||
- "{{ atlas_borg_repository_path }}"
|
||||
register: atlas_borg_repository_path_probe
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Probe the Atlas Borg repository
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/borg
|
||||
- --remote-path
|
||||
- "{{ atlas_borg_remote_path }}"
|
||||
- info
|
||||
- >-
|
||||
ssh://{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}:
|
||||
{{- atlas_borg_repository_port }}/{{ atlas_borg_repository_path }}
|
||||
environment:
|
||||
BORG_CACHE_DIR: "{{ atlas_borg_cache_dir }}"
|
||||
BORG_CONFIG_DIR: "{{ atlas_borg_config_dir }}"
|
||||
BORG_PASSCOMMAND: "cat {{ atlas_borg_passphrase_path }}"
|
||||
BORG_RSH: "{{ atlas_borg_ssh_wrapper_path }}"
|
||||
register: atlas_borg_repository_probe
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
- atlas_borg_repository_path_probe.rc == 0
|
||||
|
||||
- name: Reject an existing path that is not the configured Borg repository
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_borg_repository_probe.rc == 0
|
||||
fail_msg: >-
|
||||
The remote repository path already exists but Borg could not open it.
|
||||
Refusing to initialize over existing data; verify the path, passphrase,
|
||||
and repository state manually.
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
- atlas_borg_repository_path_probe.rc == 0
|
||||
|
||||
- name: Initialize the encrypted Atlas Borg repository
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/borg
|
||||
- --remote-path
|
||||
- "{{ atlas_borg_remote_path }}"
|
||||
- init
|
||||
- --encryption
|
||||
- "{{ atlas_borg_encryption_mode }}"
|
||||
- >-
|
||||
ssh://{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}:
|
||||
{{- atlas_borg_repository_port }}/{{ atlas_borg_repository_path }}
|
||||
environment:
|
||||
BORG_CACHE_DIR: "{{ atlas_borg_cache_dir }}"
|
||||
BORG_CONFIG_DIR: "{{ atlas_borg_config_dir }}"
|
||||
BORG_PASSCOMMAND: "cat {{ atlas_borg_passphrase_path }}"
|
||||
BORG_RSH: "{{ atlas_borg_ssh_wrapper_path }}"
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
- atlas_borg_repository_path_probe.rc != 0
|
||||
|
||||
- name: Verify the encrypted Atlas Borg repository
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/borg
|
||||
- --remote-path
|
||||
- "{{ atlas_borg_remote_path }}"
|
||||
- info
|
||||
- >-
|
||||
ssh://{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}:
|
||||
{{- atlas_borg_repository_port }}/{{ atlas_borg_repository_path }}
|
||||
environment:
|
||||
BORG_CACHE_DIR: "{{ atlas_borg_cache_dir }}"
|
||||
BORG_CONFIG_DIR: "{{ atlas_borg_config_dir }}"
|
||||
BORG_PASSCOMMAND: "cat {{ atlas_borg_passphrase_path }}"
|
||||
BORG_RSH: "{{ atlas_borg_ssh_wrapper_path }}"
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
changed_when: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Check for the local Atlas Borg recovery-key export
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.stat:
|
||||
path: "{{ atlas_borg_recovery_export_path }}"
|
||||
register: atlas_borg_recovery_export
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Export the Atlas Borg recovery key for offline preservation
|
||||
tags: [atlas, storage, backup, borg]
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
- not atlas_borg_recovery_export.stat.exists
|
||||
no_log: true
|
||||
block:
|
||||
- name: Create the local recovery-material directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_recovery_export_path | dirname }}"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
|
||||
- name: Export the encrypted Borg repository key on Atlas
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- /usr/bin/borg
|
||||
- --remote-path
|
||||
- "{{ atlas_borg_remote_path }}"
|
||||
- key
|
||||
- export
|
||||
- >-
|
||||
ssh://{{ atlas_borg_repository_user }}@{{ atlas_borg_repository_host }}:
|
||||
{{- atlas_borg_repository_port }}/{{ atlas_borg_repository_path }}
|
||||
- "{{ atlas_borg_config_dir }}/atlas-borg-repokey.export"
|
||||
environment:
|
||||
BORG_CACHE_DIR: "{{ atlas_borg_cache_dir }}"
|
||||
BORG_CONFIG_DIR: "{{ atlas_borg_config_dir }}"
|
||||
BORG_PASSCOMMAND: "cat {{ atlas_borg_passphrase_path }}"
|
||||
BORG_RSH: "{{ atlas_borg_ssh_wrapper_path }}"
|
||||
become: true
|
||||
become_user: "{{ atlas_borg_username }}"
|
||||
|
||||
- name: Fetch the encrypted Borg recovery key from Atlas
|
||||
ansible.builtin.fetch:
|
||||
src: "{{ atlas_borg_config_dir }}/atlas-borg-repokey.export"
|
||||
dest: "{{ atlas_borg_recovery_export_path }}"
|
||||
flat: true
|
||||
|
||||
- name: Protect the local Borg recovery-key export
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_recovery_export_path }}"
|
||||
mode: "0600"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
always:
|
||||
- name: Remove the temporary recovery-key export from Atlas
|
||||
ansible.builtin.file:
|
||||
path: "{{ atlas_borg_config_dir }}/atlas-borg-repokey.export"
|
||||
state: absent
|
||||
|
||||
- name: Enable Atlas Borg backup and check timers
|
||||
tags: [atlas, storage, backup, borg]
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
loop:
|
||||
- atlas-borg-backup.timer
|
||||
- atlas-borg-check.timer
|
||||
when:
|
||||
- atlas_manage_borg_backup | bool
|
||||
- not ansible_check_mode
|
||||
@@ -14,6 +14,12 @@
|
||||
- name: Import Atlas storage tasks
|
||||
ansible.builtin.import_tasks: storage.yml
|
||||
|
||||
- name: Import Atlas ZFS maintenance tasks
|
||||
ansible.builtin.import_tasks: zfs_maintenance.yml
|
||||
|
||||
- name: Import Atlas Borg backup tasks
|
||||
ansible.builtin.import_tasks: borg_backup.yml
|
||||
|
||||
- name: Import Atlas file sharing tasks
|
||||
ansible.builtin.import_tasks: sharing.yml
|
||||
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
tags: [atlas, storage]
|
||||
ansible.builtin.set_fact:
|
||||
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 }}"
|
||||
mountpoint: "{{ atlas_archive_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
@@ -16,6 +10,13 @@
|
||||
properties:
|
||||
compression: zstd
|
||||
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 }}"
|
||||
mountpoint: "{{ atlas_app_data_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
@@ -51,11 +52,27 @@
|
||||
properties:
|
||||
compression: lz4
|
||||
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 }}"
|
||||
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
group: "{{ atlas_admin_group }}"
|
||||
properties:
|
||||
compression: lz4
|
||||
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
|
||||
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_photobook }}"
|
||||
mountpoint: "{{ atlas_photobook_mountpoint }}"
|
||||
@@ -64,21 +81,6 @@
|
||||
properties:
|
||||
compression: lz4
|
||||
mountpoint: "{{ atlas_photobook_mountpoint }}"
|
||||
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backups }}"
|
||||
mountpoint: "{{ atlas_backups_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
group: "{{ atlas_admin_group }}"
|
||||
properties:
|
||||
compression: lz4
|
||||
mountpoint: "{{ atlas_backups_mountpoint }}"
|
||||
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_service_backups }}"
|
||||
mountpoint: "{{ atlas_service_backups_mountpoint }}"
|
||||
owner: "{{ atlas_admin_username }}"
|
||||
group: "{{ atlas_admin_group }}"
|
||||
properties:
|
||||
compression: lz4
|
||||
mountpoint: "{{ atlas_service_backups_mountpoint }}"
|
||||
refreservation: "{{ atlas_zfs_service_backups_refreservation }}"
|
||||
|
||||
- name: Require completed Atlas storage placeholders
|
||||
tags: [atlas, storage]
|
||||
|
||||
175
ansible/roles/profile_atlas/tasks/zfs_maintenance.yml
Normal file
175
ansible/roles/profile_atlas/tasks/zfs_maintenance.yml
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
- name: Validate Atlas ZFS snapshot policy
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_manage_storage | bool
|
||||
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
|
||||
- atlas_zfs_snapshot_prefix is match('^[a-z0-9][a-z0-9_-]*$')
|
||||
- atlas_zfs_snapshot_policies | length > 0
|
||||
- >-
|
||||
(atlas_zfs_snapshot_policies | map(attribute='name') | unique | list | length)
|
||||
== (atlas_zfs_snapshot_policies | length)
|
||||
fail_msg: >-
|
||||
Enable Atlas storage and declare a non-empty snapshot policy with a safe
|
||||
prefix and unique policy names before managing automatic snapshots.
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Validate Atlas ZFS snapshot policy entries
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.name is match('^[a-z][a-z0-9_-]*$')
|
||||
- item.keep | int > 0
|
||||
- item.calendar | length > 0
|
||||
fail_msg: >-
|
||||
Every Atlas snapshot policy needs a safe name, a positive retention
|
||||
count, and a systemd calendar expression.
|
||||
loop: "{{ atlas_zfs_snapshot_policies }}"
|
||||
loop_control:
|
||||
label: "{{ item.name | default('unnamed') }}"
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Validate Atlas ZFS snapshot calendars
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemd-analyze
|
||||
- calendar
|
||||
- "{{ item.calendar }}"
|
||||
loop: "{{ atlas_zfs_snapshot_policies }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}: {{ item.calendar }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Install Atlas ZFS snapshot and retention helper
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.template:
|
||||
src: atlas-zfs-snapshot.sh.j2
|
||||
dest: /usr/local/sbin/atlas-zfs-snapshot
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Install Atlas ZFS snapshot systemd service
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.template:
|
||||
src: atlas-zfs-snapshot@.service.j2
|
||||
dest: /etc/systemd/system/atlas-zfs-snapshot@.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Install Atlas ZFS snapshot systemd timers
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.template:
|
||||
src: atlas-zfs-snapshot.timer.j2
|
||||
dest: "/etc/systemd/system/atlas-zfs-snapshot-{{ item.name }}.timer"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop: "{{ atlas_zfs_snapshot_policies }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: atlas_manage_zfs_snapshots | bool
|
||||
|
||||
- name: Enable Atlas ZFS snapshot systemd timers
|
||||
tags: [atlas, storage, snapshots]
|
||||
ansible.builtin.systemd:
|
||||
name: "atlas-zfs-snapshot-{{ item.name }}.timer"
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
loop: "{{ atlas_zfs_snapshot_policies }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when:
|
||||
- atlas_manage_zfs_snapshots | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Validate Atlas ZFS scrub policy
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- atlas_manage_storage | bool
|
||||
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
|
||||
- atlas_zfs_scrub_calendar | length > 0
|
||||
fail_msg: >-
|
||||
Enable Atlas storage and declare a systemd calendar expression before
|
||||
managing periodic ZFS scrubs.
|
||||
when: atlas_manage_zfs_scrub | bool
|
||||
|
||||
- name: Validate Atlas ZFS scrub calendar
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemd-analyze
|
||||
- calendar
|
||||
- "{{ atlas_zfs_scrub_calendar }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: atlas_manage_zfs_scrub | bool
|
||||
|
||||
- name: Require OpenZFS scrub systemd units
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- systemctl
|
||||
- cat
|
||||
- "{{ item }}"
|
||||
loop:
|
||||
- "zfs-scrub@{{ atlas_zfs_pool }}.service"
|
||||
- "zfs-scrub-monthly@{{ atlas_zfs_pool }}.timer"
|
||||
- "zfs-scrub-weekly@{{ atlas_zfs_pool }}.timer"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: atlas_manage_zfs_scrub | bool
|
||||
|
||||
- name: Create Atlas ZFS scrub timer override directory
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/zfs-scrub-monthly@{{ atlas_zfs_pool }}.timer.d"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: atlas_manage_zfs_scrub | bool
|
||||
|
||||
- name: Configure Atlas ZFS monthly scrub schedule
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.template:
|
||||
src: atlas-zfs-scrub-timer.conf.j2
|
||||
dest: >-
|
||||
/etc/systemd/system/zfs-scrub-monthly@{{ atlas_zfs_pool }}.timer.d/override.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: atlas_manage_zfs_scrub | bool
|
||||
|
||||
- name: Disable the conflicting weekly OpenZFS scrub timer
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.systemd:
|
||||
name: "zfs-scrub-weekly@{{ atlas_zfs_pool }}.timer"
|
||||
enabled: false
|
||||
state: stopped
|
||||
daemon_reload: true
|
||||
when:
|
||||
- atlas_manage_zfs_scrub | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Enable the Atlas monthly OpenZFS scrub timer
|
||||
tags: [atlas, storage, scrub]
|
||||
ansible.builtin.systemd:
|
||||
name: "zfs-scrub-monthly@{{ atlas_zfs_pool }}.timer"
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
when:
|
||||
- atlas_manage_zfs_scrub | bool
|
||||
- not ansible_check_mode
|
||||
@@ -0,0 +1,40 @@
|
||||
[Unit]
|
||||
Description=Back up Atlas ZFS datasets to the encrypted Borg repository
|
||||
Documentation=man:borg-create(1) man:borg-prune(1) man:borg-compact(1)
|
||||
Requires=zfs.target
|
||||
Wants=network-online.target
|
||||
After=zfs.target network-online.target
|
||||
StartLimitIntervalSec=6h
|
||||
StartLimitBurst=3
|
||||
ConditionFileIsExecutable=/usr/local/sbin/atlas-borg-backup
|
||||
ConditionPathExists={{ atlas_borg_passphrase_path }}
|
||||
ConditionPathExists={{ atlas_borg_ssh_private_key_path }}
|
||||
ConditionPathExists={{ atlas_borg_known_hosts_path }}
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/atlas-borg-backup
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
SuccessExitStatus=1
|
||||
Restart=on-failure
|
||||
RestartSec=30m
|
||||
TimeoutStartSec=infinity
|
||||
RuntimeDirectory=atlas-borg
|
||||
RuntimeDirectoryMode=0750
|
||||
Nice=15
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
NoNewPrivileges=true
|
||||
PrivateMounts=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths={{ atlas_borg_cache_dir }} {{ atlas_borg_config_dir }} /run/atlas-borg /run/lock
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
176
ansible/roles/profile_atlas/templates/atlas-borg-backup.sh.j2
Normal file
176
ansible/roles/profile_atlas/templates/atlas-borg-backup.sh.j2
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export LC_ALL=C
|
||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export BORG_CACHE_DIR={{ atlas_borg_cache_dir | quote }}
|
||||
export BORG_CONFIG_DIR={{ atlas_borg_config_dir | quote }}
|
||||
export BORG_PASSCOMMAND={{ ('cat ' ~ atlas_borg_passphrase_path) | quote }}
|
||||
export BORG_RSH={{ atlas_borg_ssh_wrapper_path | quote }}
|
||||
|
||||
readonly pool={{ atlas_zfs_pool | quote }}
|
||||
readonly mount_root={{ atlas_mount_root | quote }}
|
||||
readonly repository={{ ('ssh://' ~ atlas_borg_repository_user ~ '@' ~ atlas_borg_repository_host
|
||||
~ ':' ~ (atlas_borg_repository_port | string) ~ '/' ~ atlas_borg_repository_path) | quote }}
|
||||
readonly remote_path={{ atlas_borg_remote_path | quote }}
|
||||
readonly archive_prefix={{ atlas_borg_archive_prefix | quote }}
|
||||
readonly snapshot_prefix={{ atlas_borg_snapshot_prefix | quote }}
|
||||
readonly compression={{ atlas_borg_compression | quote }}
|
||||
readonly stage=/run/atlas-borg/source
|
||||
readonly borg_user={{ atlas_borg_username | quote }}
|
||||
readonly borg_group={{ atlas_borg_group | quote }}
|
||||
readonly borg_home={{ atlas_borg_home | quote }}
|
||||
readonly borg_lock={{ atlas_borg_lock_path | quote }}
|
||||
|
||||
snapshot_name=""
|
||||
snapshot_created=false
|
||||
mounted_targets=()
|
||||
|
||||
# Invoked through the EXIT trap below.
|
||||
# shellcheck disable=SC2329
|
||||
cleanup() {
|
||||
local status=$?
|
||||
local cleanup_status=0
|
||||
local index
|
||||
trap - EXIT HUP INT TERM
|
||||
set +e
|
||||
|
||||
{% raw %}
|
||||
for ((index = ${#mounted_targets[@]} - 1; index >= 0; index--)); do
|
||||
{% endraw %}
|
||||
if mountpoint -q "${mounted_targets[$index]}"; then
|
||||
umount "${mounted_targets[$index]}" || cleanup_status=2
|
||||
fi
|
||||
done
|
||||
rm -rf "$stage" || cleanup_status=2
|
||||
|
||||
if [[ "$snapshot_created" == true ]]; then
|
||||
flock 9
|
||||
zfs destroy -r "${pool}@${snapshot_name}" || cleanup_status=2
|
||||
flock -u 9
|
||||
fi
|
||||
|
||||
if ((status == 0 && cleanup_status != 0)); then
|
||||
status=$cleanup_status
|
||||
fi
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'exit 143' HUP INT TERM
|
||||
|
||||
run_as_borg() {
|
||||
setpriv \
|
||||
--reuid "$borg_user" \
|
||||
--regid "$borg_group" \
|
||||
--clear-groups \
|
||||
--inh-caps=-all,+dac_read_search \
|
||||
--ambient-caps=-all,+dac_read_search \
|
||||
--bounding-set=-all,+dac_read_search \
|
||||
-- env HOME="$borg_home" USER="$borg_user" LOGNAME="$borg_user" "$@"
|
||||
}
|
||||
|
||||
exec 8>"$borg_lock"
|
||||
flock 8
|
||||
exec 9>/run/lock/atlas-zfs-snapshot.lock
|
||||
|
||||
zpool list -H -o name "$pool" >/dev/null
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
chown root:"$borg_group" /run/atlas-borg "$stage"
|
||||
chmod 0750 /run/atlas-borg "$stage"
|
||||
|
||||
flock 9
|
||||
while IFS= read -r stale_snapshot; do
|
||||
stale_suffix="${stale_snapshot#"${pool}@${snapshot_prefix}-"}"
|
||||
if [[ "$stale_suffix" =~ ^[0-9]{8}T[0-9]{6}Z$ ]]; then
|
||||
zfs destroy -r "$stale_snapshot"
|
||||
printf 'Removed stale Borg source snapshot %s\n' "$stale_snapshot"
|
||||
fi
|
||||
done < <(
|
||||
zfs list -H -t snapshot -o name -r "$pool" |
|
||||
grep -E "^${pool}@${snapshot_prefix}-[0-9]{8}T[0-9]{6}Z$" || true
|
||||
)
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
readonly timestamp
|
||||
snapshot_name="${snapshot_prefix}-${timestamp}"
|
||||
readonly snapshot_name
|
||||
zfs snapshot -r "${pool}@${snapshot_name}"
|
||||
snapshot_created=true
|
||||
flock -u 9
|
||||
printf 'Created recursive Borg source snapshot %s@%s\n' "$pool" "$snapshot_name"
|
||||
|
||||
while IFS=$'\t' read -r dataset dataset_mountpoint mounted; do
|
||||
if [[ "$mounted" != yes ]]; then
|
||||
printf 'Dataset %s is not mounted; refusing an incomplete backup\n' "$dataset" >&2
|
||||
exit 65
|
||||
fi
|
||||
if [[ "$dataset_mountpoint" != "$mount_root" && "$dataset_mountpoint" != "$mount_root/"* ]]; then
|
||||
printf 'Dataset %s has unexpected mountpoint %s\n' "$dataset" "$dataset_mountpoint" >&2
|
||||
exit 65
|
||||
fi
|
||||
|
||||
dataset_suffix="${dataset#"$pool"}"
|
||||
source_path="${dataset_mountpoint}/.zfs/snapshot/${snapshot_name}"
|
||||
target_path="${stage}${dataset_suffix}"
|
||||
mkdir -p "$target_path"
|
||||
mount --bind "$source_path" "$target_path"
|
||||
mount -o remount,bind,ro "$target_path"
|
||||
mounted_targets+=("$target_path")
|
||||
done < <(zfs list -H -o name,mountpoint,mounted -s name -r "$pool")
|
||||
|
||||
archive="${archive_prefix}-${timestamp}"
|
||||
readonly archive
|
||||
borg_status=0
|
||||
|
||||
set +e
|
||||
(
|
||||
cd /run/atlas-borg
|
||||
run_as_borg borg --remote-path "$remote_path" --lock-wait 600 create \
|
||||
--show-rc \
|
||||
--stats \
|
||||
--checkpoint-interval 900 \
|
||||
--compression "$compression" \
|
||||
"${repository}::${archive}" \
|
||||
source
|
||||
)
|
||||
create_status=$?
|
||||
set -e
|
||||
if ((create_status >= 2)); then
|
||||
exit "$create_status"
|
||||
fi
|
||||
borg_status=$create_status
|
||||
|
||||
set +e
|
||||
run_as_borg borg --remote-path "$remote_path" --lock-wait 600 prune \
|
||||
--show-rc \
|
||||
--list \
|
||||
--glob-archives "${archive_prefix}-*" \
|
||||
--keep-daily {{ atlas_borg_keep_daily | int }} \
|
||||
--keep-weekly {{ atlas_borg_keep_weekly | int }} \
|
||||
--keep-monthly {{ atlas_borg_keep_monthly | int }} \
|
||||
"$repository"
|
||||
prune_status=$?
|
||||
set -e
|
||||
if ((prune_status >= 2)); then
|
||||
exit "$prune_status"
|
||||
fi
|
||||
if ((prune_status > borg_status)); then
|
||||
borg_status=$prune_status
|
||||
fi
|
||||
|
||||
set +e
|
||||
run_as_borg borg --remote-path "$remote_path" --lock-wait 600 compact \
|
||||
--show-rc \
|
||||
"$repository"
|
||||
compact_status=$?
|
||||
set -e
|
||||
if ((compact_status >= 2)); then
|
||||
exit "$compact_status"
|
||||
fi
|
||||
if ((compact_status > borg_status)); then
|
||||
borg_status=$compact_status
|
||||
fi
|
||||
|
||||
exit "$borg_status"
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Schedule the encrypted Atlas Borg backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{ atlas_borg_backup_calendar }}
|
||||
Persistent=true
|
||||
RandomizedDelaySec={{ atlas_borg_randomized_delay }}
|
||||
AccuracySec=1min
|
||||
Unit=atlas-borg-backup.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,32 @@
|
||||
[Unit]
|
||||
Description=Check the encrypted Atlas Borg repository
|
||||
Documentation=man:borg-check(1)
|
||||
Wants=network-online.target
|
||||
After=network-online.target atlas-borg-backup.service
|
||||
ConditionFileIsExecutable=/usr/local/sbin/atlas-borg-check
|
||||
ConditionPathExists={{ atlas_borg_passphrase_path }}
|
||||
ConditionPathExists={{ atlas_borg_ssh_private_key_path }}
|
||||
ConditionPathExists={{ atlas_borg_known_hosts_path }}
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/atlas-borg-check
|
||||
User={{ atlas_borg_username }}
|
||||
Group={{ atlas_borg_group }}
|
||||
UMask=0077
|
||||
SuccessExitStatus=1
|
||||
TimeoutStartSec=infinity
|
||||
Nice=15
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths={{ atlas_borg_cache_dir }} {{ atlas_borg_config_dir }}
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
23
ansible/roles/profile_atlas/templates/atlas-borg-check.sh.j2
Normal file
23
ansible/roles/profile_atlas/templates/atlas-borg-check.sh.j2
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
export LC_ALL=C
|
||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
export BORG_CACHE_DIR={{ atlas_borg_cache_dir | quote }}
|
||||
export BORG_CONFIG_DIR={{ atlas_borg_config_dir | quote }}
|
||||
export BORG_PASSCOMMAND={{ ('cat ' ~ atlas_borg_passphrase_path) | quote }}
|
||||
export BORG_RSH={{ atlas_borg_ssh_wrapper_path | quote }}
|
||||
|
||||
readonly repository={{ ('ssh://' ~ atlas_borg_repository_user ~ '@' ~ atlas_borg_repository_host
|
||||
~ ':' ~ (atlas_borg_repository_port | string) ~ '/' ~ atlas_borg_repository_path) | quote }}
|
||||
readonly remote_path={{ atlas_borg_remote_path | quote }}
|
||||
readonly archive_prefix={{ atlas_borg_archive_prefix | quote }}
|
||||
readonly borg_lock={{ atlas_borg_lock_path | quote }}
|
||||
|
||||
exec 8>"$borg_lock"
|
||||
flock 8
|
||||
|
||||
exec borg --remote-path "$remote_path" --lock-wait 600 check \
|
||||
--show-rc \
|
||||
--glob-archives "${archive_prefix}-*" \
|
||||
"$repository"
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Schedule checks of the encrypted Atlas Borg repository
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{ atlas_borg_check_calendar }}
|
||||
Persistent=true
|
||||
RandomizedDelaySec={{ atlas_borg_randomized_delay }}
|
||||
AccuracySec=1min
|
||||
Unit=atlas-borg-check.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
19
ansible/roles/profile_atlas/templates/atlas-borg-ssh.sh.j2
Normal file
19
ansible/roles/profile_atlas/templates/atlas-borg-ssh.sh.j2
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Borg receives CAP_DAC_READ_SEARCH only for local snapshot traversal. Drop it
|
||||
# before starting the network transport so SSH runs as the plain service user.
|
||||
exec setpriv \
|
||||
--inh-caps=-all \
|
||||
--ambient-caps=-all \
|
||||
-- /usr/bin/ssh \
|
||||
-i {{ atlas_borg_ssh_private_key_path | quote }} \
|
||||
-p {{ atlas_borg_repository_port | int }} \
|
||||
-o BatchMode=yes \
|
||||
-o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile={{ atlas_borg_known_hosts_path | quote }} \
|
||||
-o ConnectTimeout=30 \
|
||||
-o ServerAliveInterval=60 \
|
||||
-o ServerAliveCountMax=3 \
|
||||
"$@"
|
||||
@@ -0,0 +1,6 @@
|
||||
[Timer]
|
||||
OnCalendar=
|
||||
OnCalendar={{ atlas_zfs_scrub_calendar }}
|
||||
Persistent=true
|
||||
RandomizedDelaySec=0
|
||||
AccuracySec=1min
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export LC_ALL=C
|
||||
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
printf 'Usage: %s <policy>\n' "$0" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
readonly pool={{ atlas_zfs_pool | quote }}
|
||||
readonly prefix={{ atlas_zfs_snapshot_prefix | quote }}
|
||||
readonly period="$1"
|
||||
|
||||
case "$period" in
|
||||
{% for policy in atlas_zfs_snapshot_policies %}
|
||||
{{ policy.name | quote }})
|
||||
keep={{ policy.keep | int }}
|
||||
;;
|
||||
{% endfor %}
|
||||
*)
|
||||
printf 'Unknown Atlas ZFS snapshot policy: %s\n' "$period" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
readonly keep
|
||||
|
||||
zpool list -H -o name "$pool" >/dev/null
|
||||
|
||||
exec 9>/run/lock/atlas-zfs-snapshot.lock
|
||||
flock 9
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
readonly timestamp
|
||||
readonly snapshot_prefix="${pool}@${prefix}-${period}-"
|
||||
readonly snapshot="${snapshot_prefix}${timestamp}"
|
||||
|
||||
zfs snapshot -r "$snapshot"
|
||||
printf 'Created recursive ZFS snapshot %s\n' "$snapshot"
|
||||
|
||||
snapshot_listing="$(zfs list -H -t snapshot -o name -s creation -r "$pool")"
|
||||
managed_snapshots=()
|
||||
while IFS= read -r snapshot_name; do
|
||||
if [[ "$snapshot_name" == "$snapshot_prefix"* ]]; then
|
||||
snapshot_suffix="${snapshot_name#"$snapshot_prefix"}"
|
||||
if [[ "$snapshot_suffix" =~ ^[0-9]{8}T[0-9]{6}Z$ ]]; then
|
||||
managed_snapshots+=("$snapshot_name")
|
||||
fi
|
||||
fi
|
||||
done <<< "$snapshot_listing"
|
||||
|
||||
{% raw %}
|
||||
managed_snapshot_count="${#managed_snapshots[@]}"
|
||||
{% endraw %}
|
||||
prune_count=$((managed_snapshot_count - keep))
|
||||
if ((prune_count <= 0)); then
|
||||
printf 'Retaining %d of %d managed %s snapshots\n' \
|
||||
"$managed_snapshot_count" "$keep" "$period"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for ((index = 0; index < prune_count; index++)); do
|
||||
candidate="${managed_snapshots[$index]}"
|
||||
if [[ "$candidate" != "$snapshot_prefix"* ]]; then
|
||||
printf 'Refusing to destroy unexpected snapshot: %s\n' "$candidate" >&2
|
||||
exit 65
|
||||
fi
|
||||
|
||||
zfs destroy -r "$candidate"
|
||||
printf 'Pruned recursive ZFS snapshot %s\n' "$candidate"
|
||||
done
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Schedule {{ item.name }} ZFS snapshots for {{ atlas_zfs_pool }}
|
||||
|
||||
[Timer]
|
||||
OnCalendar={{ item.calendar }}
|
||||
Persistent=true
|
||||
AccuracySec=1min
|
||||
Unit=atlas-zfs-snapshot@{{ item.name }}.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,25 @@
|
||||
[Unit]
|
||||
Description=Create and retain %i ZFS snapshots for {{ atlas_zfs_pool }}
|
||||
Documentation=man:zfs-snapshot(8) man:zfs-destroy(8)
|
||||
Requires=zfs.target
|
||||
After=zfs.target
|
||||
ConditionFileIsExecutable=/usr/local/sbin/atlas-zfs-snapshot
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/atlas-zfs-snapshot %i
|
||||
User=root
|
||||
Group=root
|
||||
UMask=0077
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
ProtectSystem=strict
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictRealtime=true
|
||||
LockPersonality=true
|
||||
@@ -5,14 +5,18 @@ backend_phase1_username: "{{ atlas_admin_username }}"
|
||||
backend_phase1_user_group: "{{ atlas_admin_group }}"
|
||||
backend_phase1_user_home: "{{ atlas_admin_home }}"
|
||||
backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd"
|
||||
backend_phase1_wireguard_interface: wg0
|
||||
backend_phase1_wireguard_address: CHANGEME_ATLAS_WIREGUARD_ADDRESS
|
||||
backend_phase1_wireguard_firewalld_zone: wireguard
|
||||
backend_phase1_bind_address: CHANGEME_ATLAS_BIND_ADDRESS
|
||||
backend_phase1_firewalld_zone: public
|
||||
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_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_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"
|
||||
backend_phase1_syncthing_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
|
||||
backend_phase1_music_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_music }}"
|
||||
backend_phase1_archive_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
|
||||
backend_phase1_app_data_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
|
||||
backend_phase1_navidrome_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_navidrome }}"
|
||||
backend_phase1_syncthing_dataset: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
|
||||
|
||||
@@ -7,17 +7,22 @@
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- not (atlas_manage_media_stack | bool)
|
||||
- backend_phase1_wireguard_interface in ansible_facts.interfaces
|
||||
- backend_phase1_wireguard_address != 'CHANGEME_ATLAS_WIREGUARD_ADDRESS'
|
||||
- backend_phase1_bind_address != 'CHANGEME_ATLAS_BIND_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_archive_dir.startswith('/')
|
||||
- backend_phase1_syncthing_folder_path.startswith(backend_phase1_archive_dir + '/')
|
||||
- backend_phase1_app_data_root.startswith('/')
|
||||
- backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/')
|
||||
- backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/')
|
||||
fail_msg: >-
|
||||
Disable the rootful media-stack gate and provide the active
|
||||
WireGuard interface/address and absolute ZFS-backed paths before
|
||||
Disable the rootful media-stack gate and provide the Atlas LAN bind
|
||||
address, firewall sources, and absolute ZFS-backed paths before
|
||||
enabling phase one. This role does not manage Prometheus or migrate
|
||||
Navidrome application data.
|
||||
application data.
|
||||
|
||||
- name: Read the rootless service account
|
||||
ansible.builtin.getent:
|
||||
@@ -49,6 +54,8 @@
|
||||
loop:
|
||||
- dataset: "{{ backend_phase1_music_dataset }}"
|
||||
mountpoint: "{{ backend_phase1_music_dir }}"
|
||||
- dataset: "{{ backend_phase1_archive_dataset }}"
|
||||
mountpoint: "{{ backend_phase1_archive_dir }}"
|
||||
- dataset: "{{ backend_phase1_app_data_dataset }}"
|
||||
mountpoint: "{{ backend_phase1_app_data_root }}"
|
||||
- dataset: "{{ backend_phase1_navidrome_dataset }}"
|
||||
@@ -101,6 +108,8 @@
|
||||
mode: "0750"
|
||||
- path: "{{ backend_phase1_syncthing_root }}"
|
||||
mode: "0750"
|
||||
- path: "{{ backend_phase1_syncthing_folder_path }}"
|
||||
mode: "0770"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
@@ -132,19 +141,35 @@
|
||||
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Permit phase-one services only through the WireGuard zone
|
||||
- name: Permit NPM access to phase-one web interfaces through Aegis
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ item }}"
|
||||
zone: "{{ backend_phase1_wireguard_firewalld_zone }}"
|
||||
rich_rule: >-
|
||||
rule family="ipv4" source address="{{ backend_phase1_npm_source_ip }}"
|
||||
port port="{{ item }}" protocol="tcp" accept
|
||||
zone: "{{ backend_phase1_firewalld_zone }}"
|
||||
state: enabled
|
||||
permanent: true
|
||||
immediate: true
|
||||
loop:
|
||||
- "{{ backend_phase1_navidrome_port }}/tcp"
|
||||
- "{{ backend_phase1_syncthing_gui_port }}/tcp"
|
||||
- "{{ backend_phase1_syncthing_transfer_port }}/tcp"
|
||||
- "{{ backend_phase1_syncthing_transfer_port }}/udp"
|
||||
- "{{ backend_phase1_syncthing_discovery_port }}/udp"
|
||||
- "{{ backend_phase1_navidrome_port }}"
|
||||
- "{{ backend_phase1_syncthing_gui_port }}"
|
||||
|
||||
- name: Permit native Syncthing traffic from the LAN
|
||||
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
|
||||
become_user: "{{ backend_phase1_username }}"
|
||||
|
||||
@@ -8,7 +8,7 @@ Image={{ backend_phase1_navidrome_image }}
|
||||
UserNS=keep-id
|
||||
User={{ backend_phase1_uid }}
|
||||
Group={{ backend_phase1_gid }}
|
||||
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_navidrome_port }}:4533
|
||||
PublishPort={{ backend_phase1_bind_address }}:{{ backend_phase1_navidrome_port }}:4533
|
||||
Environment=ND_LOGLEVEL=info
|
||||
Environment=ND_SCANSCHEDULE=1h
|
||||
Environment=ND_SESSIONTIMEOUT=24h
|
||||
|
||||
@@ -9,15 +9,17 @@ HostName=atlas-syncthing
|
||||
UserNS=keep-id
|
||||
User={{ backend_phase1_uid }}
|
||||
Group={{ backend_phase1_gid }}
|
||||
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }}
|
||||
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}
|
||||
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_transfer_port }}:{{ backend_phase1_syncthing_transfer_port }}/udp
|
||||
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
|
||||
PublishPort={{ backend_phase1_bind_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }}
|
||||
PublishPort={{ backend_phase1_bind_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 }}/udp
|
||||
PublishPort={{ backend_phase1_bind_address }}:{{ backend_phase1_syncthing_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
|
||||
Environment=HOME=/var/syncthing
|
||||
Environment=STHOMEDIR=/var/syncthing/config
|
||||
Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }}
|
||||
Environment=TZ={{ backend_phase1_timezone }}
|
||||
Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z
|
||||
# Archive is intentionally shared with Samba; use a shared SELinux container label.
|
||||
Volume={{ backend_phase1_archive_dir }}:/data:z
|
||||
NoNewPrivileges=true
|
||||
DropCapability=all
|
||||
|
||||
|
||||
@@ -20,15 +20,7 @@
|
||||
owner: "{{ item.owner }}"
|
||||
group: "{{ item.group }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop: >-
|
||||
{{
|
||||
(server_directories | default([]))
|
||||
+ (
|
||||
server_syncthing_directories | default([])
|
||||
if server_syncthing_enabled | default(true) | bool
|
||||
else []
|
||||
)
|
||||
}}
|
||||
loop: "{{ server_directories | default([]) }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
|
||||
@@ -66,6 +66,17 @@
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Check whether the pinned Java 25 version is installed with Mise
|
||||
tags: [packages, mise, java, wsl]
|
||||
ansible.builtin.command:
|
||||
cmd: "mise where java@{{ workstation_mise_java_25_version }}"
|
||||
become_user: "{{ username }}"
|
||||
environment:
|
||||
HOME: "{{ user_home }}"
|
||||
register: workstation_mise_java_25_where
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Check whether the pinned Maven version is installed with Mise
|
||||
tags: [packages, mise, maven, wsl]
|
||||
ansible.builtin.command:
|
||||
@@ -86,6 +97,7 @@
|
||||
HOME: "{{ user_home }}"
|
||||
when: >-
|
||||
workstation_mise_java_where.rc != 0 or
|
||||
workstation_mise_java_25_where.rc != 0 or
|
||||
workstation_mise_maven_where.rc != 0
|
||||
|
||||
- name: Ensure WSL boot configuration file exists
|
||||
|
||||
@@ -10,6 +10,12 @@ wireguard_mtu: 1420
|
||||
wireguard_firewalld_zone: wireguard
|
||||
wireguard_public_firewalld_zone: public
|
||||
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_handshake_retries: 12
|
||||
wireguard_handshake_delay: 5
|
||||
|
||||
@@ -14,10 +14,44 @@
|
||||
Configure this host's WireGuard address and peers, and run the first
|
||||
key bootstrap against every peer in the same play.
|
||||
|
||||
- name: Install WireGuard userspace tools
|
||||
- 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:
|
||||
name: "{{ wireguard_packages }}"
|
||||
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
|
||||
ansible.builtin.file:
|
||||
@@ -124,12 +158,33 @@
|
||||
permanent: true
|
||||
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:
|
||||
name: firewalld.service
|
||||
state: reloaded
|
||||
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
|
||||
|
||||
- name: Restore rootful Podman networking after firewalld reload
|
||||
@@ -142,7 +197,7 @@
|
||||
register: wireguard_podman_network_reload
|
||||
changed_when: wireguard_podman_network_reload.stdout_lines | length > 0
|
||||
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
|
||||
- not ansible_check_mode
|
||||
|
||||
@@ -154,6 +209,14 @@
|
||||
permanent: 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
|
||||
ansible.posix.firewalld:
|
||||
port: "{{ wireguard_listen_port }}/udp"
|
||||
|
||||
@@ -11,7 +11,8 @@ ListenPort = {{ wireguard_listen_port }}
|
||||
{% for peer in wireguard_peers %}
|
||||
[Peer]
|
||||
# {{ 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(', ') }}
|
||||
{% if peer.endpoint is defined %}
|
||||
Endpoint = {{ peer.endpoint }}
|
||||
|
||||
@@ -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>
|
||||
@@ -73,13 +73,6 @@
|
||||
- packages_rocky
|
||||
- services_systemd
|
||||
|
||||
- name: Configure WireGuard overlay
|
||||
hosts: wireguard_overlay
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- wireguard_overlay
|
||||
|
||||
- name: Configure Aegis Fedora IoT profile
|
||||
hosts: role_aegis
|
||||
become: true
|
||||
@@ -87,6 +80,13 @@
|
||||
roles:
|
||||
- profile_aegis
|
||||
|
||||
- name: Configure WireGuard overlay
|
||||
hosts: wireguard_overlay
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- wireguard_overlay
|
||||
|
||||
- name: Configure Atlas NAS profile
|
||||
hosts: atlas
|
||||
become: true
|
||||
|
||||
@@ -56,24 +56,6 @@ services:
|
||||
- "3000:3000"
|
||||
- "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:
|
||||
web:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[tools]
|
||||
java = "temurin-11.0.31+11"
|
||||
java = ["25.0.2", "temurin-11.0.31+11"]
|
||||
maven = "3.9.16"
|
||||
|
||||
[env]
|
||||
JAVA_HOME = "{{ env.HOME }}/.local/share/mise/installs/java/25.0.2"
|
||||
|
||||
1
secrets/.gitignore
vendored
1
secrets/.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
.vault_pass*
|
||||
vault.local.yml
|
||||
vault.local.yml*
|
||||
/recovery/
|
||||
|
||||
@@ -1,78 +1,83 @@
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
31386434333363613930316363393564373332303236306233643365386639346631336232373361
|
||||
3732633931376438313835373537666438383664306266380a633531316432343166323361303465
|
||||
39306136386664356163346266303963373839373763343136346135633236333333326331313063
|
||||
6264306265373865310a373735393632373861333433326632303931633732636535326264346537
|
||||
39633766326361666432396432623132616666363462373237623664363664373362346366383230
|
||||
65326665303762356233356531306331333231376163353638363936626562623937323166623065
|
||||
62633165383033303362336238343037353930396430336537363565333132666532323662653262
|
||||
65373531313264353938666161373064393239653165666462336665323962336134343432316134
|
||||
32373361613539356262313333663964623034313230356439626139383539326261663765373034
|
||||
36633933366662656464306561393130373238313730336638663932633935313037363133636266
|
||||
36663935303735623132663464376633306431396662306166393831313566323238363865303162
|
||||
66303732376530653435613966373832333161333137366230613166303061333433336461386234
|
||||
63393030616662373666643164663862653037383336323766646330653131623930353265336231
|
||||
39646335353635616465663763306262353931316562616437313362616136623735646535616431
|
||||
33363439623361343231363663363535663265366166346439623935323632366336363135373462
|
||||
62646662316538323734333334393566333463653833326162663065646634363336373865623932
|
||||
33646533636638386534366561663930613536663935663638313137646537626431393035653466
|
||||
36623435613966323166363630383531656436373366343364346439343034333934623836336636
|
||||
38653534343563393434646430306662343435653465363439326261373537666233353731306436
|
||||
35323739336361343665633239366633393530346335396635316238323435663466316235376536
|
||||
38303839313433643038353236616632626363653339346334303136306138336461383831626536
|
||||
39303966393034333739363061363865326165303236656438316537396463383935393764346135
|
||||
66323632323833353833643434316566326366633562666262386232653730303038613336663264
|
||||
62636332383834393861373665393364653362636339346632303463386565633830336363393065
|
||||
31383530613161366436323163393366386635316562633436383134623061353937326363396439
|
||||
38636438396637616362373937666361303536376164636533653536306338356263313965623336
|
||||
32633461623861643138613734313164633562613932386163643062353636376266633166373838
|
||||
39613438653531316333663736366161313832613830353566356461393435656234383037353335
|
||||
32343661376266353538643531313239306432646335383734623233373063316430383362643531
|
||||
35393263343534363936373361316265653934383735663933663365363564663966646335353337
|
||||
63366365383466393765336533363130643236313331633537346237353631623334633330376164
|
||||
32616530623032663761643437336536323332343130393339313232623364656334613233323833
|
||||
64346637343738386335343631333035636337633732333662663763326362636432646136353064
|
||||
30393061326435323837633632333765623931663265326137373135303035623464333366653566
|
||||
66613133396339376264313964353932396136613538383264363865633536373839663465333437
|
||||
37613765626131343330653063663764346364316264363635653438396331363263623562333735
|
||||
30646563303439663639376430336336383761363365353838613036333032306434623661636138
|
||||
63616439636437663732386164346365643834376631303263376563323662373734653631613730
|
||||
31373039656238303462643930303531613931373065316435303661343862623034653364653736
|
||||
37613835616630316133333130633631653639336266313438633166316539373433333661613839
|
||||
32356139366232313336343062323265366563633735383661393335386163653133313664613264
|
||||
63303739323863333439316461366137656434666366306466356633306663623730653939663430
|
||||
39313764663534316562326435316264633236373834323665356266346532323565323532333438
|
||||
33306237623430613463393164383332356533303433343465343930636563363862373330613632
|
||||
38643937613263313435626166313464316133373338373261666331653436373063393162636339
|
||||
66356434663239646334383433626566336433653265336332323866633666646132663836313537
|
||||
66663766306438396131623533613834626331653731323137303539303835306632663132616363
|
||||
64303438353836613136623562626664326364656133383865383730373762666539383036396337
|
||||
66393636343561333435313032353939316138306336316337353861383238326136373265663433
|
||||
62366566343866626336646466386561353061343735306565383437333931613635393034363430
|
||||
37633765663239643435623066313331353862393966306563393838386334393162306562363062
|
||||
61633363386564373834383432323861323364623365306439353631323463326461383039356466
|
||||
34396464653233626435656463326439303665633532656639376633353932666630626564616564
|
||||
34326638383634353033326232646339393638663637313136653763336265616635326666336530
|
||||
33663261376262626136346265656130653831636662306132393837306135643831353534626636
|
||||
34383762386665666363313932336632326230646439663366663037323562633630373137333232
|
||||
37393164656137303131313738396131613561306332356436303436636338623233343637363332
|
||||
36613137316337666461333237373266326238303531396432383461616239316630346230303735
|
||||
63636535383766353338643932366339333130663632386337643932636630316432313465393766
|
||||
39613065623631316165313134386231616165366266323634643632626230303861376461323433
|
||||
35346137326563646332303334313530383464373838373633363635373766333362386466323836
|
||||
32613236646561343365656239346239353866663336393930356238653463336361333033643331
|
||||
61323430663031383239353363616666336637666230663633616464616638303966343631386632
|
||||
32333735643936353638666336303133356435396338653465323234316234626536666635333238
|
||||
38313661303466333464666538363938656631396666643566343763396638336663376430646532
|
||||
38666234653330646262633062356238343536336637356432313137313561363937363936383364
|
||||
30643937383833663339643862363234643765386164316138636565643434373734383339363138
|
||||
36323163633837326132393365333236633264386664373234313061373835346634663137383837
|
||||
62666561353532303663346365343131316233633163323938623066656332383030393864363536
|
||||
38383939383935613432613837333863313239653831333438383133343763633838353964353161
|
||||
61323462343835613937653465633563306462613631323762656437626133336638396663646362
|
||||
30323661383134653336366234663333336261353162373030626266656336356233316265636661
|
||||
34303865313433633138363936373561636537353831373033303163646436303932626138356633
|
||||
63656364353163313037613262396338636230646330666331616534313466306361363433656132
|
||||
66633231626665303165346339373764666264313838313063323732653837383736633235363064
|
||||
37353632336238623366313432376163653535656134633634313065356533343933666135396633
|
||||
30613134646132613637656461303431613064393438363231383464663765316638
|
||||
61353065386233646137323235306631353635663530363237636231316265643562353465323430
|
||||
6165646466623962313835313537633137633766373930380a316335323962616265643136346666
|
||||
63336133336131346336383534356637623831363138323165633262386333363535393365383233
|
||||
6234393835653439370a313963313365373633323464343263383661383336363662633133643232
|
||||
34366634383862363635653034313531623330396639616462343630326162316535643465653532
|
||||
36326534333637376462353561343964633636366331363833313263353133383636623537303663
|
||||
35393032316439336666343161653439643638376134363535656262343963393365623432336433
|
||||
35383934313762313037326430316666363731666231336534326661353034333063643364343230
|
||||
65333739303566366263333565333465613136646237623937393733623438613832393634663463
|
||||
39376131313234333039633735613233373931613232653036663665316636303961653834366339
|
||||
36353730316132316233303964303839363161346564396163336137663134353062363733656430
|
||||
37643339326661653031376265646132623162373562393437373437313732396537383939333666
|
||||
62353036316633306666313461663033303830393765396131643035353730383931646239663935
|
||||
32626461316364386135303761383837613063336466363162323332663764616464373565383231
|
||||
61346463336566346533326535376439643133613762383633396131323632356533636139336365
|
||||
62393838316634623932643034376631333539343965383436613364643962363834346337353334
|
||||
32656439366439313734353963343133333533653839613632323338336131373566613835393536
|
||||
31663433616334373432376531346435336530303936356461303163646463613661643161313661
|
||||
66663866343565616631616338353737356164353562366164383736346131666662623132333466
|
||||
39383865653631373232393433663430643961646265386166333137643966303834363262373636
|
||||
62396434373363353636376133666133663162653265313139313732353639336232333862643036
|
||||
64386231336561396537326139346566306434633934343038663165396665363032383466633662
|
||||
62336163633964363435386630343966333162333730336138333239646631633132663931376462
|
||||
33663139356261313065376636613930353735396131306538306664646135636336643032623131
|
||||
38346264333331353633326535326431626563323036313665643337353563333339646430386564
|
||||
31613435383036313430316366323636663735326336393338353835323861333564363832656462
|
||||
35336435623261326363633033316130393062616339353263643062633331646137376135656365
|
||||
35636139336564346164616235616431326531333433646330386134323932373339646536356464
|
||||
66343533326534326165323564663533653666633035343163633832393361336462343937623165
|
||||
62383931326630363036396333313931393836366439653433623165666166356338653364336534
|
||||
35333936653833386163633738326164386166613561333530633937343230363366333662666539
|
||||
39333361633933663735303438663239303536363433313962643137386533633539326365383765
|
||||
37636538386339333935386132353265353031643662616330316463623661663738353433313830
|
||||
36373963633166333464653338343830373063323536383364393033393235326639613662343737
|
||||
38663362636331343061646465313237313431373433353361353265333766633463353632646536
|
||||
31323231306138323031396630656538363930373439336234343963616334363632653738316465
|
||||
63653938373830336362313238656266613362636634616537653863336132343931616262396130
|
||||
66393239303866656232653832343132366537333537343635666563343639323433383163613335
|
||||
39613533376634316133633430303535306266656333626264343733666335393661666561396633
|
||||
39346265316137326465326635396362333565393133623637633132616232326263663662343137
|
||||
33363733306135363361643031306265363733656362386666306334333035393839636533343363
|
||||
35396638616636633639343930373136376339346162393061393765363837646365383866636131
|
||||
33653465666239393133616232636231333332396138376332393664343364643835306530393238
|
||||
34663237303530303837663535646263393931373531393039356336316561653130356262636562
|
||||
38336362326639653237626634376334666565653036353236313634376364626338646538386536
|
||||
38626636386466373566646166393963643164343536373236396138303532393161363335386638
|
||||
32633032393737626363613463323366366637616361313537356136626661626633613739323338
|
||||
35383963666431343566356562333234663936376562616638636261303466633539376334303331
|
||||
39303834663234663063356233313962326664383839393832303462643636393034383434303465
|
||||
64333635376135326333356435373734643430623736373234643335343130383066326436356664
|
||||
63346663326364343634303930343338336139313864316165366232643537366635653764353763
|
||||
31363863633261643263303433373330366161323166366462336332313135366338393334653764
|
||||
66353733653137663835663731373364613030373334663061313433373861613665363236633130
|
||||
65613965366636343465336533613438373466383737373366653965633437323562643966396431
|
||||
39303033643438633762633263326132663466643438656366363431616237633031333936313831
|
||||
30323930383233313032323638356333626230333764363662313662646536643839353032353462
|
||||
30326166653937353130623133303533343934633565393831623033303234316330353432313266
|
||||
30636536633933376365623665616262663236383731633633346232613366333137396139306363
|
||||
35633336643266326335303261666666653536666630613639376336373237646134306462616537
|
||||
33343561373162666332613634643837343566646161373065366637653135613632353334636363
|
||||
63363232303963646530333366663862323264326536643337323266396566316233613630303637
|
||||
66646366376466373931613734363931316230323063373666653062373364396433633762633762
|
||||
38613933323733653238383935623230383562646563363833653838636165626365646537383639
|
||||
33666535656363393562316336633439636138373365623431393965653765306138646234663938
|
||||
65653133663663393731646337386535333261643932336132396237323930306136643534353930
|
||||
65636438396432623034626561613137336138623265393064383034623863303166356138393564
|
||||
37373164626634653662326234333539663735323464613334616130643937373730363263633366
|
||||
31393937326432386165343338313031376565313866363731643534313233303064373935303538
|
||||
31343832336230393636653432653162336361383963633766343461653466316337353931333363
|
||||
63313137303564336630343937356564643763383764613362366634373362666465626334336539
|
||||
64366533376165306532343461613265366266383862323032333465336161663161376630316465
|
||||
30306562666163646235656664653635366461366435663961623635383437663564356563346462
|
||||
31636234663765623838333237393239373564366262613637363938653463396530613963643837
|
||||
38636634376637366332623035313465393762653865623130336263343663303066366135616639
|
||||
63333964356466613038303263366462346261353030646532366361393965306435613131316463
|
||||
65366266376637323764643239323730366565633335666638666334663635373961303637383861
|
||||
35313431646434656562333937663837393038386361616630626532636339306432353434656165
|
||||
33663261383166386432383465666136376237346565303164363461666663346130346162316338
|
||||
62373061353034316234303835663439396434343738303764376665336239626238386436386234
|
||||
61306166383637366266393730323732386163366261393630336431633862353761343763363665
|
||||
61323039396234393835303633363339373633653334343766653032313230343464326664356566
|
||||
3462623830666664626633373966363866333337383730313066
|
||||
|
||||
@@ -14,3 +14,4 @@ vault_ikaros_authorized_ssh_keys:
|
||||
vault_atlas_admin_password_hash: "REPLACE_WITH_A_SHADOW_COMPATIBLE_HASH"
|
||||
vault_atlas_samba_password: "REPLACE_ME"
|
||||
vault_atlas_immich_db_password: "REPLACE_ME"
|
||||
vault_atlas_borg_passphrase: "REPLACE_WITH_A_STRONG_UNIQUE_PASSPHRASE"
|
||||
|
||||
Reference in New Issue
Block a user