Merge pull request #5 from fscotto/copilot/featureadd-pi-user-and-encrypt-filesystem

Configure Aegis with a pi user and Btrfs root filesystem
This commit is contained in:
Copilot
2026-09-04 01:04:28 +02:00
committed by GitHub
13 changed files with 262 additions and 5 deletions

View File

@@ -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,10 @@ 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`.
- Use Git Flow branch prefixes: `feature/` for new functionality, `bugfix/` for non-urgent fixes,
`hotfix/` for urgent production fixes, `release/` for release preparation, and `support/` for
maintained release lines. Do not use abbreviated prefixes such as `feat/`.
- 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 +46,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 <host> --tags emacs --check --diff -e emacs_enabled=true`
- AI coding agents: `ansible-playbook ansible/site.yml --limit <host> --tags ai_agents --check --diff`
@@ -148,3 +153,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`.

View File

@@ -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,25 @@ 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 `pi` user receives the configured SSH key, and partition 5 on `/dev/mmcblk0` is formatted as
Btrfs and mounted as the root filesystem (`/`) on first boot. Formatting is destructive for that
partition.
The controller then 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, 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 +243,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 +254,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 +269,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 +310,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/

View File

@@ -0,0 +1,32 @@
# Bootstrap monouso per Fedora CoreOS su Aegis.
# Sostituire le chiavi SSH prima di generare Ignition con butane --strict.
variant: fcos
version: 1.6.0
passwd:
users:
- name: pi
ssh_authorized_keys:
- "ssh-ed25519 CHANGEME_AEGIS_SSH_PUBLIC_KEY"
storage:
disks:
- device: /dev/mmcblk0
wipe_table: false
partitions:
- label: aegis-data
number: 5
size_mib: 0
filesystems:
- device: /dev/disk/by-partlabel/aegis-data
format: btrfs
label: aegis-data
wipe_filesystem: false
mount:
path: /
options:
- compress=zstd
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: pi
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:
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: {}

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