mirror of
https://github.com/fscotto/infra.git
synced 2026-09-27 11:02:47 +00:00
Compare commits
2 Commits
160d63c02d
...
e46a2b22d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e46a2b22d7 | ||
|
|
a97c64b08f |
46
AGENTS.md
46
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`
|
||||
@@ -168,19 +170,24 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
|
||||
service ports only in the WireGuard firewalld zone.
|
||||
|
||||
## 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.
|
||||
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--Atlas WireGuard path are operational. Aegis has validated NFSv4.2 read, write, delete,
|
||||
and `all_squash` mapping to UID/GID `1100` end-to-end.
|
||||
- Complete the Phase 1 Navidrome cutover: stop the Prometheus writer, copy and verify its complete
|
||||
`/opt/navidrome/data/` directory (including SQLite sidecars) under
|
||||
`/zpool/archive/app_data/navidrome/`, then set `backend_phase1_start_services: true` and validate
|
||||
Navidrome on Atlas through WireGuard. Do not delete the source until a restore test succeeds.
|
||||
- Start and validate the rendered Syncthing Quadlet only after its device IDs, star topology, folders,
|
||||
folder modes, ignore rules, and GUI/API protection are declared. Validate its GUI and native transfer
|
||||
ports through WireGuard only.
|
||||
- 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.
|
||||
- Keep `atlas_manage_media_stack` disabled until the future Immich deployment has validated `/dev/dri`,
|
||||
container paths, and the required Vault database secret.
|
||||
- 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
|
||||
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.
|
||||
@@ -193,8 +200,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
|
||||
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.
|
||||
- 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 +229,18 @@ 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` rpm-ostree layer used as the Atlas NFS client and reports the
|
||||
required reboot without initiating it. 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`.
|
||||
|
||||
28
README.md
28
README.md
@@ -193,12 +193,17 @@ 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` rpm-ostree layer required by the
|
||||
Atlas NFS client, and `wake-ikaros`. 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.
|
||||
|
||||
@@ -213,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 Atlas NFS client package 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
|
||||
|
||||
@@ -9,6 +9,12 @@ 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_ssh_authorized_keys:
|
||||
- name: ikaros
|
||||
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIxXjA3ffPwziKGR5gzc4gAoBehQPlnEMcXF4Wl0ZS ikaros"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
---
|
||||
aegis_hostname: aegis
|
||||
aegis_layered_packages:
|
||||
- nfs-utils
|
||||
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]
|
||||
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]
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user