Move temporary Navidrome and Syncthing services to Atlas

This commit is contained in:
Fabio Scotto di Santolo
2026-09-17 10:18:23 +02:00
parent 77afdda0a3
commit bd13cb65cc
11 changed files with 71 additions and 80 deletions

View File

@@ -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. `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 - 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. 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 - `profile_backend_phase1` temporarily runs rootless Navidrome and Syncthing on Atlas until Uranus replaces
Navidrome or Syncthing Quadlets. Future application workloads belong to the Uranus K3s cluster. 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 - `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 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 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 - 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 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. Prometheus' Aegis peer when the cluster control plane is assigned.
- Keep Atlas application Quadlets disabled. Plan Navidrome, Syncthing, Nextcloud, and Immich as Uranus workloads, - Validate temporary Atlas Navidrome and Syncthing through Aegis before creating their NPM Proxy Hosts.
with their storage and routing declared separately from the NAS baseline. 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 - 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, 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. and POSIX ACL model and test the same files through both protocols.

View File

@@ -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 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 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, 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 Syncthing, or the obsolete Navidrome PostgreSQL database; their temporary Atlas deployment is managed
to Uranus rather than Atlas. Applying the profile does not stop or remove legacy containers and does by `profile_backend_phase1`. Applying the profile does not stop or remove legacy containers and does
not delete `/opt/postgres/data`. not delete `/opt/postgres/data`.
Firewalld enables SSH, Cockpit (`9090/tcp`), HTTP and HTTPS. Nginx Proxy Manager publishes only 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 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. 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 Atlas temporarily hosts rootless Navidrome and Syncthing until Uranus replaces them. They bind only to
`services/data` datasets remain storage namespaces, but no Atlas container service is enabled from this Atlas' LAN address (`192.168.178.55`); WireGuard remains exclusively between Prometheus (`10.0.0.1`)
playbook. Future application workloads belong to the Uranus K3s cluster. 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 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 (`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 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 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 static route. Atlas permits Navidrome (`4533/tcp`) and the Syncthing GUI (`8384/tcp`) only from Aegis;
exists. When the WireGuard zone is created, Ansible reloads firewalld and immediately reloads Syncthing native ports are limited to the LAN. Configure NPM manually with
Prometheus' rootful Podman networks so the existing proxy stack retains container DNS and connectivity. `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: 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_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_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. | | `wireguard_overlay` | Prometheus/Aegis WireGuard LAN gateway. |
| `profile_aegis` | Fedora IoT always-on LAN node. | | `profile_aegis` | Fedora IoT always-on LAN node. |
| `dotfiles_common` | Shared user dotfiles. | | `dotfiles_common` | Shared user dotfiles. |

View File

@@ -40,4 +40,3 @@ server_firewalld_services:
server_firewalld_ports: [] server_firewalld_ports: []
server_sshd_service_name: sshd server_sshd_service_name: sshd
server_compose_selinux_mount_option: Z server_compose_selinux_mount_option: Z
server_syncthing_enabled: false

View File

@@ -75,21 +75,6 @@ server_directories:
group: root group: root
mode: "0755" 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: server_sshd_settings:
PermitRootLogin: "no" PermitRootLogin: "no"

View File

@@ -52,9 +52,14 @@ atlas_manage_storage: true
atlas_manage_sharing: true atlas_manage_sharing: true
atlas_manage_media_stack: false atlas_manage_media_stack: false
# Atlas is a NAS-only host. WireGuard is retired manually on the host and is # WireGuard is retired on Atlas. These rootless services are a temporary home
# intentionally no longer managed here. # until Uranus replaces them.
backend_phase1_enabled: false 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_openzfs_repo: true
rocky_manage_syncthing_binary: false rocky_manage_syncthing_binary: false

View File

@@ -5,9 +5,10 @@ backend_phase1_username: "{{ atlas_admin_username }}"
backend_phase1_user_group: "{{ atlas_admin_group }}" backend_phase1_user_group: "{{ atlas_admin_group }}"
backend_phase1_user_home: "{{ atlas_admin_home }}" backend_phase1_user_home: "{{ atlas_admin_home }}"
backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd" backend_phase1_quadlet_dir: "{{ backend_phase1_user_home }}/.config/containers/systemd"
backend_phase1_wireguard_interface: wg0 backend_phase1_bind_address: CHANGEME_ATLAS_BIND_ADDRESS
backend_phase1_wireguard_address: CHANGEME_ATLAS_WIREGUARD_ADDRESS backend_phase1_firewalld_zone: public
backend_phase1_wireguard_firewalld_zone: wireguard 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_music_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_music }}"
backend_phase1_app_data_root: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_app_data }}" 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 }}" backend_phase1_navidrome_data_dir: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_navidrome }}"

View File

@@ -7,17 +7,20 @@
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- not (atlas_manage_media_stack | bool) - not (atlas_manage_media_stack | bool)
- backend_phase1_wireguard_interface in ansible_facts.interfaces - backend_phase1_bind_address != 'CHANGEME_ATLAS_BIND_ADDRESS'
- backend_phase1_wireguard_address != 'CHANGEME_ATLAS_WIREGUARD_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_music_dir.startswith('/')
- backend_phase1_app_data_root.startswith('/') - backend_phase1_app_data_root.startswith('/')
- backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/') - backend_phase1_navidrome_data_dir.startswith(backend_phase1_app_data_root + '/')
- backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/') - backend_phase1_syncthing_root.startswith(backend_phase1_app_data_root + '/')
fail_msg: >- fail_msg: >-
Disable the rootful media-stack gate and provide the active Disable the rootful media-stack gate and provide the Atlas LAN bind
WireGuard interface/address and absolute ZFS-backed paths before address, firewall sources, and absolute ZFS-backed paths before
enabling phase one. This role does not manage Prometheus or migrate enabling phase one. This role does not manage Prometheus or migrate
Navidrome application data. application data.
- name: Read the rootless service account - name: Read the rootless service account
ansible.builtin.getent: ansible.builtin.getent:
@@ -132,19 +135,35 @@
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus" DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ backend_phase1_uid }}/bus"
when: not ansible_check_mode 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: ansible.posix.firewalld:
port: "{{ item }}" rich_rule: >-
zone: "{{ backend_phase1_wireguard_firewalld_zone }}" rule family="ipv4" source address="{{ backend_phase1_npm_source_ip }}"
port port="{{ item }}" protocol="tcp" accept
zone: "{{ backend_phase1_firewalld_zone }}"
state: enabled state: enabled
permanent: true permanent: true
immediate: true immediate: true
loop: loop:
- "{{ backend_phase1_navidrome_port }}/tcp" - "{{ backend_phase1_navidrome_port }}"
- "{{ backend_phase1_syncthing_gui_port }}/tcp" - "{{ backend_phase1_syncthing_gui_port }}"
- "{{ backend_phase1_syncthing_transfer_port }}/tcp"
- "{{ backend_phase1_syncthing_transfer_port }}/udp" - name: Permit native Syncthing traffic from the LAN
- "{{ backend_phase1_syncthing_discovery_port }}/udp" 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 - name: Start rootless phase-one Quadlets
become_user: "{{ backend_phase1_username }}" become_user: "{{ backend_phase1_username }}"

View File

@@ -8,7 +8,7 @@ Image={{ backend_phase1_navidrome_image }}
UserNS=keep-id UserNS=keep-id
User={{ backend_phase1_uid }} User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }} 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_LOGLEVEL=info
Environment=ND_SCANSCHEDULE=1h Environment=ND_SCANSCHEDULE=1h
Environment=ND_SESSIONTIMEOUT=24h Environment=ND_SESSIONTIMEOUT=24h

View File

@@ -9,10 +9,10 @@ HostName=atlas-syncthing
UserNS=keep-id UserNS=keep-id
User={{ backend_phase1_uid }} User={{ backend_phase1_uid }}
Group={{ backend_phase1_gid }} Group={{ backend_phase1_gid }}
PublishPort={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }}:{{ backend_phase1_syncthing_gui_port }} PublishPort={{ backend_phase1_bind_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_bind_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_bind_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_discovery_port }}:{{ backend_phase1_syncthing_discovery_port }}/udp
Environment=HOME=/var/syncthing Environment=HOME=/var/syncthing
Environment=STHOMEDIR=/var/syncthing/config Environment=STHOMEDIR=/var/syncthing/config
Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }} Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }}

View File

@@ -20,15 +20,7 @@
owner: "{{ item.owner }}" owner: "{{ item.owner }}"
group: "{{ item.group }}" group: "{{ item.group }}"
mode: "{{ item.mode }}" mode: "{{ item.mode }}"
loop: >- loop: "{{ server_directories | default([]) }}"
{{
(server_directories | default([]))
+ (
server_syncthing_directories | default([])
if server_syncthing_enabled | default(true) | bool
else []
)
}}
loop_control: loop_control:
label: "{{ item.path }}" label: "{{ item.path }}"

View File

@@ -56,24 +56,6 @@ services:
- "3000:3000" - "3000:3000"
- "127.0.0.1:222:22" - "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: networks:
web: web: