Add Rocky 9 Atlas NAS profile

This commit is contained in:
Fabio Scotto di Santolo
2026-08-30 14:27:07 +02:00
parent bc5a7572a7
commit 8ed439317a
22 changed files with 981 additions and 4 deletions

View File

@@ -16,6 +16,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS
- Void desktop profile is also the base for other future/reference hosts via `platform_void + graphical_desktop`
- Workstation: `deadalus` is Windows + Fedora WSL.
- Ubuntu server: `prometheus`
- NAS: `atlas` (Rocky Linux 9, 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.
@@ -40,6 +41,7 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, FreeBS
- Fedora laptop work: `ansible-playbook ansible/site.yml --limit nymph --check --diff`
- 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`
- 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`
- Mail bootstrap: `sh -n scripts/bootstrap_mail.sh` and `shellcheck scripts/bootstrap_mail.sh`
@@ -83,6 +85,46 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i
- Fedora WSL belongs to `platform_fedora`, `workstation_dev_fedora`, and the shared WSL layer. It must not receive Flatpak or Snap runtimes.
- Windows applications are installed manually and are not managed from the WSL profile.
## Atlas NAS Notes
- `atlas` is a remote Rocky Linux 9 NAS. Keep its connection, LAN, pool and mountpoint values in
`host_vars/atlas.yml`. Bootstrap it once with `-e atlas_connection_username=<existing-admin>`;
subsequent runs use the dedicated Atlas account.
- The pool is pre-existing: never add pool creation, disk partitioning, RAIDZ creation, rollback,
or destruction to the Atlas profile.
- `atlas_manage_storage` and `atlas_manage_firewall` remain false until their placeholders are
replaced; only then may the profile manage datasets, shares and LAN-restricted firewall rules.
- Atlas requires `vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` for Cockpit
and, when storage is enabled, `vault_atlas_samba_password`. Never print these values.
- Atlas uses NFSv4 for Linux and SMB for Windows/WSL, restricted to the configured LAN. Snapshot,
Rclone, Prometheus pull and USB backup automation are intentionally deferred.
## Atlas NAS TODO
- Replace every Atlas `CHANGEME` value, provide the required Vault variables and validate the first
remote bootstrap on the real Rocky Linux 9 host. Enable `atlas_manage_storage` first and
`atlas_manage_firewall` only after confirming the pool, mountpoints, LAN subnet and firewalld zone.
- 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.
- 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.
- Add Tailscale or WireGuard and corresponding LAN/VPN-only firewalld rules before enabling remote
services; never expose SSH, Cockpit, NFS, SMB or Syncthing through public port forwarding.
- Add the least-privilege Prometheus backup flow: remote dump generation, dedicated SSH identity,
pinned host key, atomic pull, verification, retention and an Atlas systemd service/timer.
- Add the encrypted Google Drive backup with Rclone Drive plus Crypt remotes, Vault-managed secrets,
snapshot-consistent sources, retries, logging, retention policy and a tested restore procedure.
- Add the UUID-bound offline USB backup with versioned rsync, locking, capacity checks, verification,
safe unmounting and a tested restore procedure; never trigger it for an arbitrary USB disk.
- Add monitoring and alerting for pool health, scrub/resilver, SMART data, temperatures, free space and
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/cloud, preserve Vault and Rclone 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.
## Coding Agent Notes
- Shared agent packages live in `ai_agents_npm_packages` in `ansible/inventory/group_vars/all.yml`.
- Shared agent dotfiles live in `ai_agents_dotfiles`; rendered configs live in `ai_agents_templates`.

View File

@@ -56,8 +56,8 @@ Il repository è diviso in due componenti principali:
# Macchine gestite
Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL e un server Ubuntu.
La composizione resta separata in assi indipendenti:
Il repository modella attualmente host Fedora/GNOME, una workstation Fedora WSL, un server Ubuntu e
un NAS Rocky Linux 9. La composizione resta separata in assi indipendenti:
```text
common user environment
@@ -75,6 +75,7 @@ Matrice target:
| nymph | Fedora | Desktop laptop | GNOME |
| deadalus | Fedora WSL | Workstation dev | — |
| prometheus | Ubuntu | Server | — |
| atlas | Rocky 9 | NAS | — |
Regola operativa:
@@ -93,6 +94,7 @@ Nota sullo stato attuale del playbook principale:
- `ansible/site.yml` applica oggi in automatico Fedora/GNOME su `ikaros` e `nymph`
- `ansible/site.yml` applica il profilo Fedora WSL alla workstation `deadalus`
- `ansible/site.yml` applica anche il profilo `ubuntu_server` con baseline apt, systemd, dotfiles server e firewall UFW
- `ansible/site.yml` applica il profilo NAS Rocky su `atlas` tramite SSH remoto
## Desktop
@@ -200,6 +202,33 @@ ansible-playbook ansible/site.yml --limit prometheus -e server_username=myuser -
---
## NAS
`atlas` e un NAS Rocky Linux 9 raggiunto tramite SSH. Il pool ZFS esiste gia: il profilo gestisce
solo i dataset figli e non deve mai creare, partizionare, distruggere, fare rollback o modificare il
pool. I client Linux usano NFSv4, quelli Windows/WSL SMB; entrambi restano limitati alla LAN
configurata.
Per il primo avvio sostituire i placeholder Atlas e fornire
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash` e
`vault_atlas_samba_password`. Eseguire il bootstrap tramite l'amministratore esistente:
```bash
ansible-playbook ansible/site.yml --limit atlas \
-e atlas_connection_username=<existing-admin>
```
`vault_atlas_admin_password_hash` deve essere un hash compatibile con `/etc/shadow`, non una
password Cockpit in chiaro. Le esecuzioni successive usano `atlas_admin_username`. Abilitare
`atlas_manage_storage` solo dopo aver verificato pool e mountpoint esistenti; abilitare
`atlas_manage_firewall` solo dopo aver verificato subnet LAN e zona firewalld attiva.
Restano da implementare retention delle snapshot, topologia Syncthing, VPN, pull da Prometheus,
Rclone, backup USB, monitoraggio e test di disaster recovery. Il backlog operativo dettagliato e in
`AGENTS.md`.
---
# Composizione della configurazione
Emacs è abilitato sui profili Fedora/GNOME e workstation; la configurazione canonica è distribuita da `dotfiles_common`, con Org in `~/Org/`, template versionati e export PDF/HTML/Markdown/DOCX/ODT. Per abilitarlo temporaneamente su un altro profilo:
@@ -247,6 +276,7 @@ I principali ruoli attualmente presenti sono:
| packages_freebsd | installazione pacchetti su FreeBSD via pkg |
| packages_ubuntu | installazione pacchetti su Ubuntu |
| packages_fedora | installazione pacchetti su Fedora |
| packages_rocky | installazione pacchetti su Rocky Linux 9 |
| services_runit | gestione servizi runit |
| services_systemd | gestione servizi systemd |
| services_freebsd | gestione servizi FreeBSD dichiarati per host |
@@ -259,6 +289,7 @@ I principali ruoli attualmente presenti sono:
| profile_workstation_dev_common | configurazione dev workstation condivisa |
| profile_workstation_dev_wsl | configurazione WSL condivisa per sviluppo |
| profile_server | configurazione server |
| profile_atlas | configurazione NAS Rocky Linux 9 |
| dotfiles_common | distribuzione dotfiles comuni |
| dotfiles | distribuzione configurazioni utente |
@@ -274,6 +305,8 @@ platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_freebsd -> packages_freebsd + services_freebsd
platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd
atlas -> profile_atlas
platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common
@@ -288,6 +321,7 @@ Questo significa che, allo stato attuale:
- il profilo Void resta selezionabile tramite `platform_void + graphical_desktop` per host futuri
- `deadalus` riceve il profilo Fedora WSL tramite play dev dedicati
- il server Ubuntu (`prometheus`) e gestito con pacchetti, servizi, dotfiles server e firewall
- il NAS Rocky (`atlas`) usa un pool ZFS gia esistente, condivisioni NFSv4/SMB limitate alla LAN e Cockpit/45Drives
- lo stack container server include `navidrome`, `postgres`, `gitea`, `nginx-proxy-manager` e `syncthing`, con GUI Syncthing raggiungibile tramite la rete Docker `web`
# Dotfiles
@@ -364,6 +398,7 @@ Allo stato attuale questo comando:
- per `platform_fedora & desktop_gnome` applica il profilo GNOME a `ikaros` e `nymph`
- per `workstation_dev_wsl` applica i tweak WSL dopo il layer Fedora a `deadalus`, escludendo Flatpak e Snap
- per gli host `ubuntu_server` applica pacchetti Ubuntu, servizi systemd, profilo server, UFW, dotfiles e template dedicati
- per `platform_rocky` applica pacchetti Rocky e servizi systemd ad `atlas`, quindi il profilo NAS dedicato
- non riavvia automaticamente il display manager
- carica `secrets/vault.yml` solo se presente
- carica `secrets/vault.local.yml` solo se presente, dopo `vault.yml`, cosi gli override locali hanno precedenza
@@ -377,6 +412,7 @@ ansible-playbook ansible/site.yml --limit ikaros --check --diff
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-lint ansible/site.yml
ansible-lint ansible/roles
yamllint ansible/

View File

@@ -36,7 +36,8 @@ infra/
## Managed machines
The repo currently covers Fedora/GNOME desktops, one Fedora WSL workstation, and an Ubuntu server. Configuration is layered instead of being tied to host names:
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:
```text
common user environment
@@ -52,6 +53,7 @@ common user environment
| `nymph` | Fedora | Desktop laptop | GNOME |
| `deadalus` | Fedora WSL | Development workstation | — |
| `prometheus` | Ubuntu | Server | — |
| `atlas` | Rocky 9 | NAS | — |
```text
ikaros must be boring
@@ -110,6 +112,31 @@ ansible-playbook ansible/site.yml --limit prometheus \
-e server_user_home=/srv/myuser
```
## NAS
`atlas` is a Rocky Linux 9 NAS reached through SSH. Its pool already exists: the profile only
manages child datasets and must never create, partition, destroy, roll back, or otherwise alter the
pool itself. Linux clients use NFSv4 and Windows/WSL clients use SMB; both are restricted to the
configured LAN.
For the first run, replace the Atlas placeholders and provide
`vault_atlas_authorized_ssh_keys`, `vault_atlas_admin_password_hash`, and
`vault_atlas_samba_password`. Bootstrap the host through its existing administrator:
```bash
ansible-playbook ansible/site.yml --limit atlas \
-e atlas_connection_username=<existing-admin>
```
`vault_atlas_admin_password_hash` must be an `/etc/shadow`-compatible hash, not a clear-text
Cockpit password. Subsequent runs use `atlas_admin_username`. Enable
`atlas_manage_storage` only after checking the existing pool and mountpoints; enable
`atlas_manage_firewall` only after checking the LAN subnet and active firewalld zone.
Snapshot retention, Syncthing topology, VPN access, Prometheus pulls, Rclone, USB backup,
monitoring, and disaster-recovery tests remain follow-up work. The detailed operational backlog is
kept in `AGENTS.md`.
## How layering works
A host can intentionally belong to more than one inventory group. The final configuration is the combination of the host and its groups, not a one-host/one-play mapping.
@@ -146,6 +173,7 @@ ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=t
| `packages_freebsd` | Installs packages on FreeBSD with pkg. |
| `packages_ubuntu` | Installs packages on Ubuntu. |
| `packages_fedora` | Installs packages on Fedora. |
| `packages_rocky` | Installs packages on Rocky Linux 9. |
| `services_runit` | Manages runit services. |
| `services_systemd` | Manages systemd services. |
| `services_freebsd` | Manages declared FreeBSD rc services. |
@@ -158,6 +186,7 @@ ansible-playbook ansible/site.yml --limit <host> --tags emacs -e emacs_enabled=t
| `profile_workstation_dev_common` | Shared workstation development setup. |
| `profile_workstation_dev_wsl` | WSL development setup. |
| `profile_server` | Server setup. |
| `profile_atlas` | Rocky Linux 9 NAS setup. |
| `dotfiles_common` | Shared user dotfiles. |
## What `site.yml` runs
@@ -168,6 +197,8 @@ platform_void -> packages_void + services_runit
platform_void & graphical_desktop -> profile_desktop_common + profile_desktop_sway + profile_desktop_niri + profile_desktop_host
platform_freebsd -> packages_freebsd + services_freebsd
platform_fedora -> packages_fedora + services_systemd
platform_rocky -> packages_rocky + services_systemd
atlas -> profile_atlas
platform_fedora & role_personal_workstation -> profile_personal_workstation
platform_fedora & desktop_gnome -> profile_desktop_gnome
workstation_dev_fedora -> profile_workstation_dev_common
@@ -180,6 +211,7 @@ So, in practice:
- `platform_fedora` configures `ikaros`, `nymph`, and `deadalus`.
- `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.
- Empty `platform_void` and `platform_freebsd` 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.
@@ -218,6 +250,7 @@ ansible-playbook ansible/site.yml --limit ikaros --check --diff
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-lint ansible/site.yml
ansible-lint ansible/roles
yamllint ansible/

View File

@@ -0,0 +1,32 @@
---
platform_profile: rocky
platform_family: redhat
platform_package_manager: dnf
platform_service_manager: systemd
rocky_openzfs_release_rpm: https://zfsonlinux.org/epel/zfs-release-3-0.el9.noarch.rpm
rocky_syncthing_version: 2.1.3
rocky_syncthing_archive_checksum: sha256:f929eb8e5b72a85543eeeefb2c38f34a68e0c530e70758a2905b78840c76602c
rocky_syncthing_archive_url: >-
{{
'https://github.com/syncthing/syncthing/releases/download/v'
~ rocky_syncthing_version
~ '/syncthing-linux-amd64-v'
~ rocky_syncthing_version
~ '.tar.gz'
}}
enabled_services:
- firewalld
rocky_packages_base:
- cockpit
- dnf-plugins-core
- firewalld
- nfs-utils
- openssh-server
- policycoreutils-python-utils
- rsync
- samba
- sudo
- zfs

View File

@@ -0,0 +1,50 @@
---
# Replace every CHANGEME value before enabling Atlas storage management.
hostname: atlas
ansible_host: CHANGEME_ATLAS_HOST
ansible_user: "{{ atlas_connection_username }}"
ansible_port: 22
ansible_ssh_private_key_file: CHANGEME_ATLAS_SSH_PRIVATE_KEY_PATH
atlas_admin_username: CHANGEME_ATLAS_ADMIN
atlas_connection_username: "{{ atlas_admin_username }}"
atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000
atlas_admin_gid: 1000
atlas_admin_ssh_keys: "{{ vault_atlas_authorized_ssh_keys | default([]) }}"
atlas_admin_password_hash: "{{ vault_atlas_admin_password_hash | default('!') }}"
atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_manage_firewall: false
atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_manage_storage: false
atlas_nfs_exports:
- path: "{{ atlas_work_mountpoint }}"
clients: "{{ atlas_lan_subnet }}(rw,sync,no_subtree_check,root_squash)"
atlas_samba_share_name: work
atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users:
- "{{ atlas_admin_username }}"
atlas_samba_password: "{{ vault_atlas_samba_password | default('') }}"
atlas_firewalld_rich_rules:
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="ssh" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="cockpit" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="2049" protocol="tcp" accept'
- 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept'
host_enabled_services:
- sshd
- cockpit.socket
- nfs-server
- smb
- zfs.target

View File

@@ -16,6 +16,10 @@ all:
platform_freebsd:
hosts: {}
platform_rocky:
hosts:
atlas:
void:
children:
platform_void:
@@ -24,6 +28,10 @@ all:
children:
platform_fedora:
rocky:
children:
platform_rocky:
graphical_desktop:
hosts:
ikaros:

View File

@@ -0,0 +1,151 @@
---
- name: Require Rocky Linux 9
tags: [packages]
ansible.builtin.assert:
that:
- ansible_facts['distribution'] == 'Rocky'
- ansible_facts['distribution_major_version'] == '9'
fail_msg: This platform role supports Rocky Linux 9 only.
- name: Install DNF configuration plugin
tags: [packages]
ansible.builtin.dnf:
name: dnf-plugins-core
state: present
- name: Enable Rocky CodeReady Builder repository
tags: [packages]
community.general.dnf_config_manager:
name:
- crb
state: enabled
when: rocky_enable_crb | default(true) | bool
- name: Install EPEL repository package
tags: [packages]
ansible.builtin.dnf:
name: epel-release
state: present
- name: Install official OpenZFS repository package
tags: [packages, storage]
ansible.builtin.dnf:
name: "{{ rocky_openzfs_release_rpm }}"
state: present
- name: Disable OpenZFS DKMS repository
tags: [packages, storage]
community.general.dnf_config_manager:
name:
- zfs
state: disabled
- name: Enable OpenZFS kmod repository
tags: [packages, storage]
community.general.dnf_config_manager:
name:
- zfs-kmod
state: enabled
- name: Refresh Rocky package metadata
tags: [packages]
ansible.builtin.dnf:
update_cache: true
- name: Install packages on Rocky Linux
tags: [packages]
ansible.builtin.dnf:
name: >-
{{
(
(common_packages | default([]))
+ (rocky_packages_base | default([]))
+ (profile_packages | default([]))
+ (host_packages | default([]))
) | unique
}}
state: present
- name: Require supported architecture for Syncthing
tags: [packages, syncthing]
ansible.builtin.assert:
that:
- ansible_facts['architecture'] == 'x86_64'
fail_msg: The pinned Atlas Syncthing binary currently supports x86_64 only.
- name: Read installed Syncthing version
tags: [packages, syncthing]
ansible.builtin.command:
argv:
- /usr/local/bin/syncthing
- --version
register: rocky_syncthing_version_check
changed_when: false
failed_when: false
check_mode: false
- name: Determine whether Syncthing must be installed
tags: [packages, syncthing]
ansible.builtin.set_fact:
rocky_syncthing_install_required: >-
{{
('syncthing v' ~ rocky_syncthing_version ~ ' ')
not in (rocky_syncthing_version_check.stdout | default(''))
}}
- name: Create temporary Syncthing extraction directory
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: directory
owner: root
group: root
mode: "0755"
when: rocky_syncthing_install_required | bool
- name: Download pinned Syncthing release
tags: [packages, syncthing]
ansible.builtin.get_url:
url: "{{ rocky_syncthing_archive_url }}"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
checksum: "{{ rocky_syncthing_archive_checksum }}"
owner: root
group: root
mode: "0644"
when: rocky_syncthing_install_required | bool
- name: Extract pinned Syncthing release
tags: [packages, syncthing]
ansible.builtin.unarchive:
src: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
dest: "/tmp/syncthing-{{ rocky_syncthing_version }}"
remote_src: true
when: rocky_syncthing_install_required | bool
- name: Install pinned Syncthing binary
tags: [packages, syncthing]
ansible.builtin.copy:
src: >-
{{
'/tmp/syncthing-' ~ rocky_syncthing_version
~ '/syncthing-linux-amd64-v' ~ rocky_syncthing_version
~ '/syncthing'
}}
dest: /usr/local/bin/syncthing
remote_src: true
owner: root
group: root
mode: "0755"
when: rocky_syncthing_install_required | bool
- name: Remove Syncthing release archive
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}.tar.gz"
state: absent
- name: Remove Syncthing extraction directory
tags: [packages, syncthing]
ansible.builtin.file:
path: "/tmp/syncthing-{{ rocky_syncthing_version }}"
state: absent

View File

@@ -0,0 +1,51 @@
---
atlas_manage_storage: false
atlas_admin_username: CHANGEME_ATLAS_ADMIN
atlas_admin_group: "{{ atlas_admin_username }}"
atlas_admin_home: "/home/{{ atlas_admin_username }}"
atlas_admin_uid: 1000
atlas_admin_gid: 1000
atlas_admin_ssh_keys: []
atlas_admin_password_hash: "!"
atlas_lan_subnet: CHANGEME_LAN_SUBNET
atlas_manage_firewall: false
atlas_firewalld_zone: public
atlas_zfs_pool: CHANGEME_ZFS_POOL
atlas_zfs_dataset_work: work
atlas_zfs_dataset_syncthing: syncthing
atlas_zfs_dataset_backup_prometheus: backup_prometheus
atlas_zfs_dataset_icloud_photos: icloud_photos
atlas_mount_root: /CHANGEME_ATLAS_MOUNT_ROOT
atlas_work_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_work }}"
atlas_syncthing_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_syncthing }}"
atlas_backup_prometheus_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_backup_prometheus }}"
atlas_icloud_photos_mountpoint: "{{ atlas_mount_root }}/{{ atlas_zfs_dataset_icloud_photos }}"
atlas_syncthing_config_dir: "{{ atlas_admin_home }}/.local/state/syncthing"
atlas_syncthing_default_dir: "{{ atlas_syncthing_mountpoint }}/Sync"
atlas_45drives_repo_url: https://repo.45drives.com/repofiles/rocky/45drives-enterprise.repo
atlas_45drives_repo_file: /etc/yum.repos.d/45drives-enterprise.repo
atlas_45drives_packages:
- cockpit-file-sharing
- cockpit-navigator
- cockpit-identities
- cockpit-zfs
- cockpit-scheduler
atlas_nfs_exports: []
atlas_samba_share_name: work
atlas_samba_workgroup: WORKGROUP
atlas_samba_valid_users: []
atlas_samba_password: ""
atlas_samba_password_marker: /var/lib/samba/private/.atlas-password.sha256
atlas_firewalld_rich_rules: []
atlas_firewalld_restricted_services:
- ssh
- cockpit
- nfs
- samba
atlas_selinux_booleans:
- samba_export_all_rw
- nfs_export_all_rw

View File

@@ -0,0 +1,34 @@
---
- name: Reload SSH service
ansible.builtin.systemd:
name: sshd
state: reloaded
- name: Reload NFS exports
ansible.builtin.command:
argv:
- exportfs
- -ra
changed_when: true
when: not ansible_check_mode
- name: Restart NFS server
ansible.builtin.systemd:
name: nfs-server
state: restarted
- name: Restart NFS mount daemon
ansible.builtin.systemd:
name: nfs-mountd
state: restarted
daemon_reload: true
- name: Restart Samba service
ansible.builtin.systemd:
name: smb
state: restarted
- name: Restart Atlas Syncthing service
ansible.builtin.systemd:
name: atlas-syncthing
state: restarted

View File

@@ -0,0 +1,118 @@
---
- name: Reject incomplete Atlas account configuration
tags: [atlas, services]
ansible.builtin.assert:
that:
- atlas_admin_username != 'CHANGEME_ATLAS_ADMIN'
- (atlas_admin_ssh_keys | default([])) | length > 0
- atlas_admin_password_hash not in ['', '!', '*']
fail_msg: >-
Define atlas_admin_username, vault_atlas_authorized_ssh_keys and
vault_atlas_admin_password_hash before applying the Atlas profile.
no_log: true
- name: Create Atlas administrator group
tags: [atlas, services]
ansible.builtin.group:
name: "{{ atlas_admin_group }}"
gid: "{{ atlas_admin_gid }}"
state: present
- name: Create Atlas administrator account
tags: [atlas, services]
ansible.builtin.user:
name: "{{ atlas_admin_username }}"
uid: "{{ atlas_admin_uid }}"
group: "{{ atlas_admin_group }}"
home: "{{ atlas_admin_home }}"
shell: /bin/bash
password: "{{ atlas_admin_password_hash }}"
create_home: true
state: present
no_log: true
- name: Grant Atlas administrator passwordless sudo
tags: [atlas, services]
ansible.builtin.copy:
content: "{{ atlas_admin_username }} ALL=(ALL) NOPASSWD: ALL\n"
dest: "/etc/sudoers.d/{{ atlas_admin_username }}"
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
- name: Manage Atlas administrator authorized SSH keys exclusively
tags: [atlas, services]
ansible.posix.authorized_key:
user: "{{ atlas_admin_username }}"
key: "{{ atlas_admin_ssh_keys | join('\n') }}"
state: present
exclusive: true
- name: Check whether the Atlas SSH host key exists
tags: [atlas, services]
ansible.builtin.stat:
path: /etc/ssh/ssh_host_ed25519_key
register: atlas_ssh_host_ed25519_key
- name: Generate missing Atlas SSH host keys
tags: [atlas, services]
ansible.builtin.command:
argv:
- ssh-keygen
- -A
changed_when: true
when: not atlas_ssh_host_ed25519_key.stat.exists
- name: Ensure Atlas SSH configuration drop-in directory exists
tags: [atlas, services]
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
- name: Ensure Atlas SSH drop-ins are loaded before other settings
tags: [atlas, services]
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^\s*Include\s+/etc/ssh/sshd_config\.d/\*\.conf\s*$'
line: Include /etc/ssh/sshd_config.d/*.conf
insertbefore: BOF
state: present
validate: "sshd -t -f %s"
- name: Render Atlas SSH hardening drop-in
tags: [atlas, services]
ansible.builtin.template:
src: 00-atlas-hardening.conf.j2
dest: /etc/ssh/sshd_config.d/00-atlas-hardening.conf
owner: root
group: root
mode: "0600"
notify: Reload SSH service
- name: Read effective Atlas SSH daemon configuration
tags: [atlas, services]
ansible.builtin.command:
argv:
- sshd
- -T
- -C
- "user={{ atlas_admin_username }},host=atlas,addr=127.0.0.1"
register: atlas_sshd_effective_configuration
changed_when: false
when: not ansible_check_mode
- name: Verify effective Atlas SSH hardening
tags: [atlas, services]
ansible.builtin.assert:
that:
- "'permitrootlogin no' in atlas_sshd_effective_configuration.stdout_lines"
- "'pubkeyauthentication yes' in atlas_sshd_effective_configuration.stdout_lines"
- "'passwordauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "'kbdinteractiveauthentication no' in atlas_sshd_effective_configuration.stdout_lines"
- "('allowusers ' ~ atlas_admin_username) in atlas_sshd_effective_configuration.stdout_lines"
fail_msg: The effective Atlas SSH configuration does not match the required hardening.
when: not ansible_check_mode

View File

@@ -0,0 +1,15 @@
---
- name: Configure the official 45Drives repository
tags: [atlas, packages]
ansible.builtin.get_url:
url: "{{ atlas_45drives_repo_url }}"
dest: "{{ atlas_45drives_repo_file }}"
owner: root
group: root
mode: "0644"
- name: Install 45Drives Cockpit plugins
tags: [atlas, packages]
ansible.builtin.dnf:
name: "{{ atlas_45drives_packages }}"
state: present

View File

@@ -0,0 +1,15 @@
---
- name: Import Atlas account tasks
ansible.builtin.import_tasks: account.yml
- name: Import Atlas 45Drives tasks
ansible.builtin.import_tasks: cockpit.yml
- name: Import Atlas storage tasks
ansible.builtin.import_tasks: storage.yml
- name: Import Atlas file sharing tasks
ansible.builtin.import_tasks: sharing.yml
- name: Import Atlas Syncthing tasks
ansible.builtin.import_tasks: syncthing.yml

View File

@@ -0,0 +1,211 @@
---
- name: Render Atlas NFS exports
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas.exports.j2
dest: /etc/exports.d/atlas.exports
owner: root
group: root
mode: "0644"
notify: Reload NFS exports
when: atlas_manage_storage | bool
- name: Configure Atlas NFSv4-only service
tags: [atlas, sharing]
ansible.builtin.template:
src: atlas-nfs.conf.j2
dest: /etc/nfs.conf.d/atlas.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS server
when: atlas_manage_storage | bool
- name: Mask Atlas NFSv3 RPC services
tags: [atlas, sharing, services]
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
masked: true
loop:
- rpc-statd.service
- rpcbind.service
- rpcbind.socket
loop_control:
label: "{{ item }}"
when: atlas_manage_storage | bool
- name: Ensure Atlas NFS mount daemon drop-in directory exists
tags: [atlas, sharing, services]
ansible.builtin.file:
path: /etc/systemd/system/nfs-mountd.service.d
state: directory
owner: root
group: root
mode: "0755"
when: atlas_manage_storage | bool
- name: Disable Atlas NFSv3 mount daemon listeners
tags: [atlas, sharing, services]
ansible.builtin.template:
src: nfs-mountd-v4only.conf.j2
dest: /etc/systemd/system/nfs-mountd.service.d/v4only.conf
owner: root
group: root
mode: "0644"
notify: Restart NFS mount daemon
when: atlas_manage_storage | bool
- name: Enable SELinux access for Atlas file sharing
tags: [atlas, sharing, services]
ansible.posix.seboolean:
name: "{{ item }}"
state: true
persistent: true
loop: "{{ atlas_selinux_booleans }}"
loop_control:
label: "{{ item }}"
when:
- atlas_manage_storage | bool
- (ansible_facts['selinux'] | default({})).get('status', 'disabled') == 'enabled'
- name: Render Atlas Samba configuration
tags: [atlas, sharing]
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: "0644"
validate: "testparm --suppress-prompt %s"
notify: Restart Samba service
when: atlas_manage_storage | bool
- name: Require Atlas Samba password
tags: [atlas, sharing]
ansible.builtin.assert:
that:
- atlas_samba_password | length > 0
fail_msg: Define vault_atlas_samba_password before enabling Atlas storage.
no_log: true
when: atlas_manage_storage | bool
- name: Read Atlas Samba account
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- pdbedit
- --list
- --user
- "{{ atlas_admin_username }}"
register: atlas_samba_account
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Ensure Atlas Samba private state directory exists
tags: [atlas, sharing]
ansible.builtin.file:
path: "{{ atlas_samba_password_marker | dirname }}"
state: directory
owner: root
group: root
mode: "0700"
when: atlas_manage_storage | bool
- name: Check Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.stat:
path: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_stat
when: atlas_manage_storage | bool
- name: Read Atlas Samba password marker
tags: [atlas, sharing]
ansible.builtin.slurp:
src: "{{ atlas_samba_password_marker }}"
register: atlas_samba_password_marker_content
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_marker_stat.stat.exists
- name: Determine whether Atlas Samba credentials must change
tags: [atlas, sharing]
ansible.builtin.set_fact:
atlas_samba_password_digest: "{{ atlas_samba_password | hash('sha256') }}"
atlas_samba_password_update_required: >-
{{
atlas_samba_account.rc != 0
or not atlas_samba_password_marker_stat.stat.exists
or (
atlas_samba_password_marker_content.content | default('')
| b64decode | trim
) != (atlas_samba_password | hash('sha256'))
}}
no_log: true
when: atlas_manage_storage | bool
- name: Set Atlas Samba account password
tags: [atlas, sharing]
ansible.builtin.command:
argv:
- smbpasswd
- -s
- -a
- "{{ atlas_admin_username }}"
stdin: "{{ atlas_samba_password }}\n{{ atlas_samba_password }}"
changed_when: true
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Record managed Atlas Samba password digest
tags: [atlas, sharing]
ansible.builtin.copy:
content: "{{ atlas_samba_password_digest }}\n"
dest: "{{ atlas_samba_password_marker }}"
owner: root
group: root
mode: "0600"
no_log: true
when:
- atlas_manage_storage | bool
- atlas_samba_password_update_required | bool
- name: Require completed Atlas firewall placeholders
tags: [atlas, sharing, services]
ansible.builtin.assert:
that:
- atlas_lan_subnet != 'CHANGEME_LAN_SUBNET'
- atlas_firewalld_zone | length > 0
fail_msg: Replace the Atlas LAN subnet and firewall zone placeholders.
when: atlas_manage_firewall | bool
- name: Apply Atlas firewalld rich rules
tags: [atlas, sharing, services]
ansible.posix.firewalld:
rich_rule: "{{ item }}"
permanent: true
immediate: true
state: enabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_rich_rules }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_firewall | bool
- name: Remove unrestricted Atlas services from firewalld zone
tags: [atlas, sharing, services]
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: disabled
zone: "{{ atlas_firewalld_zone }}"
loop: "{{ atlas_firewalld_restricted_services }}"
loop_control:
label: "{{ item }}"
when: atlas_manage_firewall | bool

View File

@@ -0,0 +1,77 @@
---
- name: Define Atlas ZFS datasets
tags: [atlas, storage]
ansible.builtin.set_fact:
atlas_zfs_datasets:
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_work }}"
mountpoint: "{{ atlas_work_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_syncthing }}"
mountpoint: "{{ atlas_syncthing_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_backup_prometheus }}"
mountpoint: "{{ atlas_backup_prometheus_mountpoint }}"
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_icloud_photos }}"
mountpoint: "{{ atlas_icloud_photos_mountpoint }}"
- name: Require completed Atlas storage placeholders
tags: [atlas, storage]
ansible.builtin.assert:
that:
- atlas_zfs_pool != 'CHANGEME_ZFS_POOL'
- atlas_mount_root != '/CHANGEME_ATLAS_MOUNT_ROOT'
- atlas_mount_root.startswith('/')
- (atlas_zfs_datasets | map(attribute='name') | unique | list | length) == (atlas_zfs_datasets | length)
- (atlas_zfs_datasets | map(attribute='mountpoint') | unique | list | length) == (atlas_zfs_datasets | length)
fail_msg: >-
Replace the Atlas ZFS pool and mount-root placeholders, then set
atlas_manage_storage to true before managing datasets.
when: atlas_manage_storage | bool
- name: Check whether the configured Atlas ZFS pool exists
tags: [atlas, storage]
ansible.builtin.command:
argv:
- zpool
- list
- -H
- -o
- name
- "{{ atlas_zfs_pool }}"
register: atlas_zfs_pool_check
changed_when: false
failed_when: false
when: atlas_manage_storage | bool
- name: Require the configured Atlas ZFS pool
tags: [atlas, storage]
ansible.builtin.assert:
that:
- atlas_zfs_pool_check.rc == 0
fail_msg: >-
The configured Atlas ZFS pool does not exist. The Atlas profile only
manages child datasets and never creates pools.
when: atlas_manage_storage | bool
- name: Manage Atlas ZFS datasets declaratively
tags: [atlas, storage]
community.general.zfs:
name: "{{ item.name }}"
state: present
extra_zfs_properties:
mountpoint: "{{ item.mountpoint }}"
loop: "{{ atlas_zfs_datasets }}"
loop_control:
label: "{{ item.name }}"
when: atlas_manage_storage | bool
- name: Set Atlas dataset ownership
tags: [atlas, storage]
ansible.builtin.file:
path: "{{ item.mountpoint }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0770"
loop: "{{ atlas_zfs_datasets }}"
loop_control:
label: "{{ item.mountpoint }}"
when: atlas_manage_storage | bool

View File

@@ -0,0 +1,40 @@
---
- name: Create Atlas Syncthing configuration directory
tags: [atlas, syncthing]
ansible.builtin.file:
path: "{{ atlas_syncthing_config_dir }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0700"
when: atlas_manage_storage | bool
- name: Create Atlas Syncthing default data directory
tags: [atlas, syncthing]
ansible.builtin.file:
path: "{{ atlas_syncthing_default_dir }}"
state: directory
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0770"
when: atlas_manage_storage | bool
- name: Render Atlas Syncthing systemd service
tags: [atlas, syncthing]
ansible.builtin.template:
src: atlas-syncthing.service.j2
dest: /etc/systemd/system/atlas-syncthing.service
owner: root
group: root
mode: "0644"
notify: Restart Atlas Syncthing service
when: atlas_manage_storage | bool
- name: Enable Atlas Syncthing service
tags: [atlas, syncthing]
ansible.builtin.systemd:
name: atlas-syncthing
enabled: true
state: started
daemon_reload: true
when: atlas_manage_storage | bool

View File

@@ -0,0 +1,6 @@
# Managed by Ansible. Do not edit manually.
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowUsers {{ atlas_admin_username }}

View File

@@ -0,0 +1,3 @@
# Managed by Ansible. Do not edit manually.
[nfsd]
vers3 = n

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Atlas Syncthing service
After=network-online.target
Wants=network-online.target
[Service]
User={{ atlas_admin_username }}
Group={{ atlas_admin_group }}
WorkingDirectory={{ atlas_syncthing_mountpoint }}
Environment=HOME={{ atlas_syncthing_mountpoint }}
ExecStart=/usr/local/bin/syncthing serve --no-browser --no-restart --no-upgrade --home={{ atlas_syncthing_config_dir }}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,4 @@
# Managed by Ansible. Do not edit manually.
{% for export in atlas_nfs_exports %}
{{ export.path }} {{ export.clients }}
{% endfor %}

View File

@@ -0,0 +1,3 @@
[Service]
ExecStart=
ExecStart=/usr/sbin/rpc.mountd --no-tcp --no-udp

View File

@@ -0,0 +1,16 @@
# Managed by Ansible. Do not edit manually.
[global]
workgroup = {{ atlas_samba_workgroup }}
security = user
map to guest = Never
server min protocol = SMB2
include = registry
[{{ atlas_samba_share_name }}]
path = {{ atlas_work_mountpoint }}
browseable = yes
read only = no
valid users = {{ atlas_samba_valid_users | join(' ') }}
force group = {{ atlas_admin_group }}
create mask = 0660
directory mask = 0770

View File

@@ -20,7 +20,8 @@
errors='ignore', wantlist=True) | length > 0
roles:
- dotfiles_common
- role: dotfiles_common
when: "'platform_rocky' not in group_names"
- name: Configure Void platform
hosts: platform_void
@@ -56,6 +57,21 @@
- packages_fedora
- services_systemd
- name: Configure Rocky Linux platform
hosts: platform_rocky
become: true
roles:
- packages_rocky
- services_systemd
- name: Configure Atlas NAS profile
hosts: atlas
become: true
roles:
- profile_atlas
- name: Configure personal workstation role on Fedora
hosts: platform_fedora:&role_personal_workstation
become: true