From 764d329a8f3c7832eabb69e56ce646237a0ba3a5 Mon Sep 17 00:00:00 2001 From: Fabio Scotto di Santolo Date: Tue, 1 Sep 2026 22:38:49 +0200 Subject: [PATCH] feat: add Aegis Fedora CoreOS profile --- AGENTS.md | 16 +++- README.md | 26 ++++++- ansible/bootstrap/aegis.bu | 16 ++++ .../group_vars/platform_fedora_coreos.yml | 2 + ansible/inventory/host_vars/aegis.yml | 8 ++ ansible/inventory/hosts.yml | 9 +++ ansible/roles/profile_aegis/defaults/main.yml | 9 +++ ansible/roles/profile_aegis/handlers/main.yml | 11 +++ ansible/roles/profile_aegis/tasks/main.yml | 77 +++++++++++++++++++ .../templates/adguardhome.container.j2 | 18 +++++ .../templates/icloudpd.container.j2 | 20 +++++ .../profile_aegis/templates/wake-ikaros.j2 | 21 +++++ ansible/site.yml | 11 ++- 13 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 ansible/bootstrap/aegis.bu create mode 100644 ansible/inventory/group_vars/platform_fedora_coreos.yml create mode 100644 ansible/inventory/host_vars/aegis.yml create mode 100644 ansible/roles/profile_aegis/defaults/main.yml create mode 100644 ansible/roles/profile_aegis/handlers/main.yml create mode 100644 ansible/roles/profile_aegis/tasks/main.yml create mode 100644 ansible/roles/profile_aegis/templates/adguardhome.container.j2 create mode 100644 ansible/roles/profile_aegis/templates/icloudpd.container.j2 create mode 100644 ansible/roles/profile_aegis/templates/wake-ikaros.j2 diff --git a/AGENTS.md b/AGENTS.md index 0e237b4..80e9eb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md -Ansible-driven personal infrastructure repo for Fedora and Void desktops, WSL, and an Ubuntu server. +Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora CoreOS, WSL, and servers. ## Source Of Truth - Main orchestration: `ansible/site.yml` @@ -17,6 +17,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, WSL, a - Workstation: `deadalus` is Windows + Fedora WSL. - Ubuntu server: `prometheus` - NAS: `atlas` (Rocky Linux 9, reached through SSH) +- Always-on LAN node: `aegis` (Fedora CoreOS on Raspberry Pi 4, reached through SSH) - Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions. - Inventory axes are independent: `platform_*`, `role_*`, and `desktop_*`. Legacy `void` and `desktop` remain compatibility parents. @@ -24,7 +25,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, WSL, a - Preserve layering `all -> platform -> role -> desktop -> host`. - Keep `ansible/site.yml` small; orchestration belongs there, implementation belongs in roles. - Prefer minimal, targeted edits. Preserve idempotency and existing ordering. -- All hosts use `ansible_connection: local`. +- Desktop and WSL hosts use `ansible_connection: local`; remote infrastructure hosts use SSH. - Treat `secrets/` as sensitive. Never print secret values. - Tmux plugins are bootstrapped by TPM on the host; the repo only keeps tmux config and custom helper scripts. - Read the relevant role tasks, templates, vars, and deployed dotfiles before editing. @@ -42,6 +43,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, WSL, a - WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff` - Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff` - Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --check --diff` + - Aegis CoreOS: `ansible-playbook ansible/site.yml --limit aegis --check --diff` - Focused checks: - Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit --tags emacs --check --diff -e emacs_enabled=true` - AI coding agents: `ansible-playbook ansible/site.yml --limit --tags ai_agents --check --diff` @@ -148,3 +150,13 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i - Keep `README.md` and `AGENTS.md` aligned when workflows materially change. - If you add a new operational area, also add the narrowest validation command for it. - Call out checks you could not run and any follow-up verification needed. + +## Aegis Fedora CoreOS Notes +- `aegis` is a remote Fedora CoreOS Raspberry Pi 4 node. Bootstrap it once with + `ansible/bootstrap/aegis.bu`; the remaining configuration is applied by `profile_aegis` over SSH. +- Fedora CoreOS 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`, and the + Podman auto-update timer. Keep Apple IDs and other credentials in Vault and use `no_log` for their + rendering. +- iCloudPD requires post-deployment interactive MFA initialization; its cookie/configuration state is + persisted in `/var/lib/icloudpd/config`. diff --git a/README.md b/README.md index 649a44c..e4b8e6a 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ infra/ ## Managed machines -The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, an Ubuntu server, and -a Rocky Linux 9 NAS. Configuration is layered instead of being tied to host names: +The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora CoreOS LAN +node, an Ubuntu server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied to +host names: ```text common user environment @@ -52,6 +53,7 @@ common user environment | `ikaros` | Fedora | Personal workstation | GNOME | | `nymph` | Fedora | Desktop laptop | GNOME | | `deadalus` | Fedora WSL | Development workstation | — | +| `aegis` | Fedora CoreOS | Always-on LAN node | — | | `prometheus` | Ubuntu | Server | — | | `atlas` | Rocky 9 | NAS | — | @@ -112,6 +114,21 @@ ansible-playbook ansible/site.yml --limit prometheus \ -e server_user_home=/srv/myuser ``` +## Aegis + +`aegis` is a Raspberry Pi 4 running Fedora CoreOS. Provision it once with +`ansible/bootstrap/aegis.bu`, after replacing the SSH public-key placeholder: + +```bash +butane --strict --pretty --output aegis.ign ansible/bootstrap/aegis.bu +``` + +The controller then manages it remotely as `core@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, and +`wake-ikaros`. Define `vault_aegis_icloudpd_apple_id` in Vault before applying it. iCloudPD still +requires interactive MFA initialization after its first deployment. + ## NAS `atlas` is a Rocky Linux 9 NAS reached through SSH. Its pool already exists: the profile only @@ -222,6 +239,7 @@ 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_aegis` | Fedora CoreOS always-on LAN node. | | `dotfiles_common` | Shared user dotfiles. | ## What `site.yml` runs @@ -232,6 +250,7 @@ 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 +role_aegis -> profile_aegis atlas -> profile_atlas platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & desktop_gnome -> profile_desktop_gnome @@ -246,6 +265,8 @@ So, in practice: - `deadalus` gets the Fedora development layer followed by the WSL layer. - `ubuntu_server` configures `prometheus`. - `atlas` receives the Rocky platform layer and the NAS profile through SSH. +- `aegis` receives only the immutable Fedora CoreOS profile through SSH; it does not receive + mutable Fedora package or common dotfile roles. - Empty `platform_void` groups do nothing until they get a host. - The playbook never restarts the display manager during a run. - `secrets/vault.yml` and then `secrets/vault.local.yml` are loaded only when present. @@ -285,6 +306,7 @@ ansible-playbook ansible/site.yml --limit nymph --check --diff ansible-playbook ansible/site.yml --limit deadalus --check --diff ansible-playbook ansible/site.yml --limit prometheus --check --diff ansible-playbook ansible/site.yml --limit atlas --check --diff +ansible-playbook ansible/site.yml --limit aegis --check --diff ansible-lint ansible/site.yml ansible-lint ansible/roles yamllint ansible/ diff --git a/ansible/bootstrap/aegis.bu b/ansible/bootstrap/aegis.bu new file mode 100644 index 0000000..c283a11 --- /dev/null +++ b/ansible/bootstrap/aegis.bu @@ -0,0 +1,16 @@ +# Bootstrap monouso per Fedora CoreOS su Aegis. +# Sostituire la chiave SSH prima di generare Ignition con butane --strict. +variant: fcos +version: 1.6.0 +passwd: + users: + - name: core + ssh_authorized_keys: + - "ssh-ed25519 CHANGEME_AEGIS_SSH_PUBLIC_KEY" +storage: + files: + - path: /etc/hostname + mode: 0644 + contents: + inline: | + aegis diff --git a/ansible/inventory/group_vars/platform_fedora_coreos.yml b/ansible/inventory/group_vars/platform_fedora_coreos.yml new file mode 100644 index 0000000..bccea1e --- /dev/null +++ b/ansible/inventory/group_vars/platform_fedora_coreos.yml @@ -0,0 +1,2 @@ +--- +# Fedora CoreOS is immutable: do not attach the mutable Fedora package roles. diff --git a/ansible/inventory/host_vars/aegis.yml b/ansible/inventory/host_vars/aegis.yml new file mode 100644 index 0000000..d1e4a95 --- /dev/null +++ b/ansible/inventory/host_vars/aegis.yml @@ -0,0 +1,8 @@ +--- +ansible_host: aegis +ansible_connection: ssh +ansible_user: core +ansible_become: true +ansible_python_interpreter: /usr/bin/python3 + +aegis_icloudpd_apple_id: "{{ vault_aegis_icloudpd_apple_id | default('') }}" diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index 9740dd4..ceb697e 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -13,6 +13,10 @@ all: deadalus: ansible_connection: local + platform_fedora_coreos: + hosts: + aegis: + platform_rocky: hosts: atlas: @@ -24,6 +28,7 @@ all: fedora: children: platform_fedora: + platform_fedora_coreos: rocky: children: @@ -45,6 +50,10 @@ all: ikaros: ansible_connection: local + role_aegis: + hosts: + aegis: + desktop_sway: hosts: {} diff --git a/ansible/roles/profile_aegis/defaults/main.yml b/ansible/roles/profile_aegis/defaults/main.yml new file mode 100644 index 0000000..34a7525 --- /dev/null +++ b/ansible/roles/profile_aegis/defaults/main.yml @@ -0,0 +1,9 @@ +--- +aegis_hostname: aegis +aegis_adguard_image: docker.io/adguard/adguardhome:latest +aegis_icloudpd_image: docker.io/boredazfcuk/icloudpd:latest +aegis_icloudpd_folder_structure: '{:%Y/%m/%d}' +aegis_icloudpd_synchronisation_interval: 86400 +aegis_icloudpd_apple_id: "" +aegis_ikaros_mac_address: aa:bb:cc:dd:ee:ff +aegis_wol_port: 9 diff --git a/ansible/roles/profile_aegis/handlers/main.yml b/ansible/roles/profile_aegis/handlers/main.yml new file mode 100644 index 0000000..304f675 --- /dev/null +++ b/ansible/roles/profile_aegis/handlers/main.yml @@ -0,0 +1,11 @@ +--- +- name: Restart Aegis Quadlet services + ansible.builtin.systemd: + name: "{{ item }}" + state: restarted + daemon_reload: true + loop: + - adguardhome.service + - icloudpd.service + loop_control: + label: "{{ item }}" diff --git a/ansible/roles/profile_aegis/tasks/main.yml b/ansible/roles/profile_aegis/tasks/main.yml new file mode 100644 index 0000000..a1915e5 --- /dev/null +++ b/ansible/roles/profile_aegis/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Require Aegis iCloudPD Apple ID + tags: [aegis, icloudpd] + ansible.builtin.assert: + that: + - aegis_icloudpd_apple_id | length > 0 + fail_msg: Define vault_aegis_icloudpd_apple_id before applying the Aegis profile. + no_log: true + +- name: Set Aegis hostname + tags: [aegis, services] + ansible.builtin.hostname: + name: "{{ aegis_hostname }}" + +- name: Create Aegis persistent container directories + tags: [aegis, containers] + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: "0755" + loop: + - /var/lib/adguard/work + - /var/lib/adguard/conf + - /var/lib/icloudpd/data + - /var/lib/icloudpd/config + +- name: Create Quadlet configuration directory + tags: [aegis, containers] + ansible.builtin.file: + path: /etc/containers/systemd + state: directory + owner: root + group: root + mode: "0755" + +- name: Render Aegis Quadlet units + tags: [aegis, containers] + ansible.builtin.template: + src: "{{ item.src }}" + dest: "/etc/containers/systemd/{{ item.dest }}" + owner: root + group: root + mode: "0644" + loop: + - src: adguardhome.container.j2 + dest: adguardhome.container + - src: icloudpd.container.j2 + dest: icloudpd.container + loop_control: + label: "{{ item.dest }}" + no_log: "{{ item.dest == 'icloudpd.container' }}" + notify: Restart Aegis Quadlet services + +- name: Install Wake-on-LAN helper for Ikaros + tags: [aegis, wol] + ansible.builtin.template: + src: wake-ikaros.j2 + dest: /usr/local/bin/wake-ikaros + owner: root + group: root + mode: "0755" + +- name: Enable Aegis Quadlet services and automatic updates + tags: [aegis, containers, services] + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + daemon_reload: true + loop: + - adguardhome.service + - icloudpd.service + - podman-auto-update.timer + loop_control: + label: "{{ item }}" diff --git a/ansible/roles/profile_aegis/templates/adguardhome.container.j2 b/ansible/roles/profile_aegis/templates/adguardhome.container.j2 new file mode 100644 index 0000000..7f680b7 --- /dev/null +++ b/ansible/roles/profile_aegis/templates/adguardhome.container.j2 @@ -0,0 +1,18 @@ +# Managed by Ansible. Do not edit manually. +[Unit] +Description=AdGuard Home DNS sinkhole +Wants=network-online.target +After=network-online.target + +[Container] +Image={{ aegis_adguard_image }} +Network=host +Volume=/var/lib/adguard/work:/opt/adguardhome/work:Z +Volume=/var/lib/adguard/conf:/opt/adguardhome/conf:Z +AutoUpdate=registry + +[Service] +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/profile_aegis/templates/icloudpd.container.j2 b/ansible/roles/profile_aegis/templates/icloudpd.container.j2 new file mode 100644 index 0000000..e8f6aeb --- /dev/null +++ b/ansible/roles/profile_aegis/templates/icloudpd.container.j2 @@ -0,0 +1,20 @@ +# Managed by Ansible. Do not edit manually. +[Unit] +Description=iCloud Photos Downloader +Wants=network-online.target +After=network-online.target + +[Container] +Image={{ aegis_icloudpd_image }} +Environment=apple_id={{ aegis_icloudpd_apple_id }} +Environment=folder_structure={{ aegis_icloudpd_folder_structure }} +Environment=synchronisation_interval={{ aegis_icloudpd_synchronisation_interval }} +Volume=/var/lib/icloudpd/data:/home/root/iCloud:Z +Volume=/var/lib/icloudpd/config:/config:Z +AutoUpdate=registry + +[Service] +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/profile_aegis/templates/wake-ikaros.j2 b/ansible/roles/profile_aegis/templates/wake-ikaros.j2 new file mode 100644 index 0000000..0c4e5a2 --- /dev/null +++ b/ansible/roles/profile_aegis/templates/wake-ikaros.j2 @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Managed by Ansible. Do not edit manually. +set -euo pipefail + +mac='{{ aegis_ikaros_mac_address }}' +port='{{ aegis_wol_port }}' +mac_hex="${mac//:/}" + +if [[ ! $mac_hex =~ ^[[:xdigit:]]{12}$ ]]; then + printf 'Indirizzo MAC non valido: %s\n' "$mac" >&2 + exit 1 +fi + +packet='\xFF\xFF\xFF\xFF\xFF\xFF' +for _ in {1..16}; do + packet+="$(printf '\\x%s\\x%s\\x%s\\x%s\\x%s\\x%s' \ + "${mac_hex:0:2}" "${mac_hex:2:2}" "${mac_hex:4:2}" \ + "${mac_hex:6:2}" "${mac_hex:8:2}" "${mac_hex:10:2}")" +done + +printf '%b' "$packet" > "/dev/udp/255.255.255.255/${port}" diff --git a/ansible/site.yml b/ansible/site.yml index 4907611..b2faafb 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -33,7 +33,9 @@ roles: - role: dotfiles_common - when: "'platform_rocky' not in group_names" + when: + - "'platform_rocky' not in group_names" + - "'platform_fedora_coreos' not in group_names" - name: Configure Void platform hosts: platform_void @@ -69,6 +71,13 @@ - packages_rocky - services_systemd +- name: Configure Aegis Fedora CoreOS profile + hosts: role_aegis + become: true + + roles: + - profile_aegis + - name: Configure Atlas NAS profile hosts: atlas become: true