feat: add Aegis Fedora CoreOS profile

This commit is contained in:
Fabio Scotto di Santolo
2026-09-01 22:38:49 +02:00
parent 1517c92866
commit 764d329a8f
13 changed files with 239 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
# AGENTS.md # 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 ## Source Of Truth
- Main orchestration: `ansible/site.yml` - 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. - Workstation: `deadalus` is Windows + Fedora WSL.
- Ubuntu server: `prometheus` - Ubuntu server: `prometheus`
- NAS: `atlas` (Rocky Linux 9, reached through SSH) - NAS: `atlas` (Rocky Linux 9, reached through SSH)
- Always-on LAN node: `aegis` (Fedora CoreOS on Raspberry Pi 4, reached through SSH)
- Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions. - Hosts intentionally belong to multiple groups; trust `ansible/site.yml` over hostname assumptions.
- Inventory axes are independent: `platform_*`, `role_*`, and `desktop_*`. Legacy `void` and `desktop` remain compatibility parents. - 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`. - Preserve layering `all -> platform -> role -> desktop -> host`.
- Keep `ansible/site.yml` small; orchestration belongs there, implementation belongs in roles. - Keep `ansible/site.yml` small; orchestration belongs there, implementation belongs in roles.
- Prefer minimal, targeted edits. Preserve idempotency and existing ordering. - 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. - 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. - 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. - 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` - WSL workstation dev: `ansible-playbook ansible/site.yml --limit deadalus --check --diff`
- Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff` - Server: `ansible-playbook ansible/site.yml --limit prometheus --check --diff`
- Atlas NAS: `ansible-playbook ansible/site.yml --limit atlas --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: - Focused checks:
- Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit <host> --tags emacs --check --diff -e emacs_enabled=true` - Emacs is disabled by default; temporary Emacs check: `ansible-playbook ansible/site.yml --limit <host> --tags emacs --check --diff -e emacs_enabled=true`
- AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff` - AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff`
@@ -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. - 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. - 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. - 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`.

View File

@@ -36,8 +36,9 @@ infra/
## Managed machines ## Managed machines
The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, an Ubuntu server, and The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, a Fedora CoreOS LAN
a Rocky Linux 9 NAS. Configuration is layered instead of being tied to host names: node, an Ubuntu server, and a Rocky Linux 9 NAS. Configuration is layered instead of being tied to
host names:
```text ```text
common user environment common user environment
@@ -52,6 +53,7 @@ common user environment
| `ikaros` | Fedora | Personal workstation | GNOME | | `ikaros` | Fedora | Personal workstation | GNOME |
| `nymph` | Fedora | Desktop laptop | GNOME | | `nymph` | Fedora | Desktop laptop | GNOME |
| `deadalus` | Fedora WSL | Development workstation | — | | `deadalus` | Fedora WSL | Development workstation | — |
| `aegis` | Fedora CoreOS | Always-on LAN node | — |
| `prometheus` | Ubuntu | Server | — | | `prometheus` | Ubuntu | Server | — |
| `atlas` | Rocky 9 | NAS | — | | `atlas` | Rocky 9 | NAS | — |
@@ -112,6 +114,21 @@ ansible-playbook ansible/site.yml --limit prometheus \
-e server_user_home=/srv/myuser -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 ## NAS
`atlas` is a Rocky Linux 9 NAS reached through SSH. Its pool already exists: the profile only `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_workstation_dev_wsl` | WSL development setup. |
| `profile_server` | Server setup. | | `profile_server` | Server setup. |
| `profile_atlas` | Rocky Linux 9 NAS setup. | | `profile_atlas` | Rocky Linux 9 NAS setup. |
| `profile_aegis` | Fedora CoreOS always-on LAN node. |
| `dotfiles_common` | Shared user dotfiles. | | `dotfiles_common` | Shared user dotfiles. |
## What `site.yml` runs ## 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_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_fedora -> packages_fedora + services_systemd platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd platform_rocky -> packages_rocky + services_systemd
role_aegis -> profile_aegis
atlas -> profile_atlas atlas -> profile_atlas
platform_fedora & role_personal_workstation -> profile_personal_workstation platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome platform_fedora & desktop_gnome -> profile_desktop_gnome
@@ -246,6 +265,8 @@ So, in practice:
- `deadalus` gets the Fedora development layer followed by the WSL layer. - `deadalus` gets the Fedora development layer followed by the WSL layer.
- `ubuntu_server` configures `prometheus`. - `ubuntu_server` configures `prometheus`.
- `atlas` receives the Rocky platform layer and the NAS profile through SSH. - `atlas` receives the Rocky platform layer and the NAS profile through SSH.
- `aegis` receives only the immutable Fedora 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. - Empty `platform_void` groups do nothing until they get a host.
- The playbook never restarts the display manager during a run. - The playbook never restarts the display manager during a run.
- `secrets/vault.yml` and then `secrets/vault.local.yml` are loaded only when present. - `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 deadalus --check --diff
ansible-playbook ansible/site.yml --limit prometheus --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 atlas --check --diff
ansible-playbook ansible/site.yml --limit aegis --check --diff
ansible-lint ansible/site.yml ansible-lint ansible/site.yml
ansible-lint ansible/roles ansible-lint ansible/roles
yamllint ansible/ yamllint ansible/

View File

@@ -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

View File

@@ -0,0 +1,2 @@
---
# Fedora CoreOS is immutable: do not attach the mutable Fedora package roles.

View File

@@ -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('') }}"

View File

@@ -13,6 +13,10 @@ all:
deadalus: deadalus:
ansible_connection: local ansible_connection: local
platform_fedora_coreos:
hosts:
aegis:
platform_rocky: platform_rocky:
hosts: hosts:
atlas: atlas:
@@ -24,6 +28,7 @@ all:
fedora: fedora:
children: children:
platform_fedora: platform_fedora:
platform_fedora_coreos:
rocky: rocky:
children: children:
@@ -45,6 +50,10 @@ all:
ikaros: ikaros:
ansible_connection: local ansible_connection: local
role_aegis:
hosts:
aegis:
desktop_sway: desktop_sway:
hosts: {} hosts: {}

View File

@@ -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

View File

@@ -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 }}"

View File

@@ -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 }}"

View File

@@ -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

View File

@@ -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

View File

@@ -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}"

View File

@@ -33,7 +33,9 @@
roles: roles:
- role: dotfiles_common - 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 - name: Configure Void platform
hosts: platform_void hosts: platform_void
@@ -69,6 +71,13 @@
- packages_rocky - packages_rocky
- services_systemd - services_systemd
- name: Configure Aegis Fedora CoreOS profile
hosts: role_aegis
become: true
roles:
- profile_aegis
- name: Configure Atlas NAS profile - name: Configure Atlas NAS profile
hosts: atlas hosts: atlas
become: true become: true