diff --git a/AGENTS.md b/AGENTS.md index 48271f8..059cbe6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,8 +149,10 @@ 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. -- Atlas is NAS-only. `profile_backend_phase1` is disabled in its host variables; do not reactivate its former - Navidrome or Syncthing Quadlets. Future application workloads belong to the Uranus K3s cluster. +- `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 @@ -167,8 +169,9 @@ and `all_squash` mapping to UID/GID `1100` end-to-end. - Validate the Prometheus--Aegis WireGuard gateway after migration: peer handshake and counters, Aegis IPv4 forwarding and masquerading, and an NPM request from Prometheus to an Atlas LAN address. Add the Uranus VIP to Prometheus' Aegis peer when the cluster control plane is assigned. -- Keep Atlas application Quadlets disabled. Plan Navidrome, Syncthing, Nextcloud, and Immich as Uranus workloads, - with their storage and routing declared separately from the NAS baseline. +- Validate temporary Atlas Navidrome and Syncthing through Aegis before creating their NPM Proxy Hosts. + Keep NPM host configuration manual; plan their eventual Uranus migration with storage and routing declared + separately from the NAS 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. diff --git a/README.md b/README.md index 0740b0b..639cfbb 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,8 @@ 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. Those future application workloads belong -to Uranus rather than Atlas. Applying the profile does not stop or remove legacy containers and does +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 @@ -271,17 +271,22 @@ 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. -Atlas is a NAS-only host; its former phase-one Navidrome and Syncthing role is disabled. The -`services/data` datasets remain storage namespaces, but no Atlas container service is enabled from this -playbook. Future application workloads belong to the Uranus K3s cluster. +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. 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. 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. +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 the gateway with: @@ -383,7 +388,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_backend_phase1` | Retired Atlas phase-one role; disabled pending Uranus replacement. | +| `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. | diff --git a/ansible/inventory/group_vars/rocky_server.yml b/ansible/inventory/group_vars/rocky_server.yml index 0713092..e5da756 100644 --- a/ansible/inventory/group_vars/rocky_server.yml +++ b/ansible/inventory/group_vars/rocky_server.yml @@ -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 diff --git a/ansible/inventory/group_vars/server.yml b/ansible/inventory/group_vars/server.yml index 5e16bb1..bc026f1 100644 --- a/ansible/inventory/group_vars/server.yml +++ b/ansible/inventory/group_vars/server.yml @@ -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" diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index dd1453f..2525d44 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -52,9 +52,14 @@ atlas_manage_storage: true atlas_manage_sharing: true atlas_manage_media_stack: false -# Atlas is a NAS-only host. WireGuard is retired manually on the host and is -# intentionally no longer managed here. -backend_phase1_enabled: false +# WireGuard is retired on Atlas. These rootless services are a temporary home +# until Uranus replaces them. +backend_phase1_enabled: true +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 diff --git a/ansible/roles/profile_backend_phase1/defaults/main.yml b/ansible/roles/profile_backend_phase1/defaults/main.yml index e50774b..c71015a 100644 --- a/ansible/roles/profile_backend_phase1/defaults/main.yml +++ b/ansible/roles/profile_backend_phase1/defaults/main.yml @@ -5,9 +5,10 @@ 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_app_data_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}" backend_phase1_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}" diff --git a/ansible/roles/profile_backend_phase1/tasks/main.yml b/ansible/roles/profile_backend_phase1/tasks/main.yml index d5c4703..1b4384c 100644 --- a/ansible/roles/profile_backend_phase1/tasks/main.yml +++ b/ansible/roles/profile_backend_phase1/tasks/main.yml @@ -7,17 +7,20 @@ 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_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: @@ -132,19 +135,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 }}" diff --git a/ansible/roles/profile_backend_phase1/templates/atlas-navidrome.container.j2 b/ansible/roles/profile_backend_phase1/templates/atlas-navidrome.container.j2 index eace113..84e887d 100644 --- a/ansible/roles/profile_backend_phase1/templates/atlas-navidrome.container.j2 +++ b/ansible/roles/profile_backend_phase1/templates/atlas-navidrome.container.j2 @@ -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 diff --git a/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 b/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 index b594ef7..5b96b0b 100644 --- a/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 +++ b/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 @@ -9,10 +9,10 @@ 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 }} diff --git a/ansible/roles/profile_server/tasks/main.yml b/ansible/roles/profile_server/tasks/main.yml index f5fba74..395a9b1 100644 --- a/ansible/roles/profile_server/tasks/main.yml +++ b/ansible/roles/profile_server/tasks/main.yml @@ -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 }}" diff --git a/ansible/templates/server/docker-compose.yml.j2 b/ansible/templates/server/docker-compose.yml.j2 index 1a47af9..86b9afb 100644 --- a/ansible/templates/server/docker-compose.yml.j2 +++ b/ansible/templates/server/docker-compose.yml.j2 @@ -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: