From b4b3bd10b8b5822f05f6e43aa261607928025fb8 Mon Sep 17 00:00:00 2001 From: Fabio Scotto di Santolo Date: Sun, 13 Sep 2026 00:01:48 +0200 Subject: [PATCH] Harden Atlas network, SSH, firewall, and sharing --- AGENTS.md | 11 +- README.it.md | 11 +- README.md | 13 +- ansible/inventory/host_vars/atlas.yml | 14 +- ansible/roles/profile_atlas/defaults/main.yml | 23 +++ ansible/roles/profile_atlas/tasks/account.yml | 13 ++ .../roles/profile_atlas/tasks/hardening.yml | 45 +++++ ansible/roles/profile_atlas/tasks/main.yml | 3 + ansible/roles/profile_atlas/tasks/sharing.yml | 169 +++++++++++++----- .../templates/00-atlas-hardening.conf.j2 | 12 ++ .../roles/profile_atlas/templates/smb.conf.j2 | 4 + .../templates/atlas-syncthing.container.j2 | 7 +- 12 files changed, 260 insertions(+), 65 deletions(-) create mode 100644 ansible/roles/profile_atlas/tasks/hardening.yml diff --git a/AGENTS.md b/AGENTS.md index 49f561f..7dd36fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,8 @@ Ansible-driven personal infrastructure repo for Fedora and Void desktops, Fedora - Server compose render: `podman-compose -f /opt/docker/server/docker-compose.yml config` and `systemctl status podman-compose-server` - Atlas media stack: `ansible-playbook ansible/site.yml --limit atlas --tags storage,sharing,containers --check --diff` + - Atlas network/share hardening: + `ansible-playbook ansible/site.yml --limit atlas --tags hardening,sharing --check --diff` - Atlas phase-one rootless services: `ansible-playbook ansible/site.yml --limit atlas --tags backend_phase1 --check --diff` - Prometheus/Atlas WireGuard overlay: @@ -142,8 +144,9 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i - The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives only the `video` and `render` supplementary groups. Immich's rootful Quadlets run as `1100:1100`; Server and ML receive `/dev/dri`, while the Photobook external library is read-only at `/external/photobook`. -- Atlas exports Photobook only to the configured Aegis IP with all access squashed to UID/GID `1100`. SMB3 exposes - `Archive` to Vault-backed authorized accounts and admits the configured LAN without host-specific exclusions. +- Atlas applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, agent and remote forwarding + are disabled, while local forwarding remains available for private administrative tunnels. Photobook is exported only to the configured Aegis IP with all access squashed to UID/GID + `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. - `profile_backend_phase1` is limited to rootless Navidrome and Syncthing user Quadlets on Atlas. Official Navidrome @@ -155,8 +158,8 @@ The dotfile vars follow the same split: `desktop_common_dotfiles` carries mode-i Atlas and its SQLite database verified. The playbook renders the target but never migrates or deletes application data; after cutover, set the flag true to enable and start Navidrome and Syncthing. - Phase 1 must not change Prometheus' existing NPM deployment. NPM continues to be managed exactly by `profile_server`; - use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Native Syncthing transfer/discovery does - not use the HTTP proxy. + use `10.0.0.2:4533` for Navidrome and `10.0.0.2:8384` for the Syncthing GUI. Syncthing does not use host networking: + its GUI, transfer, QUIC and discovery ports are explicitly published only on `10.0.0.2`; native transfer/discovery does not use the HTTP proxy. - `wireguard_overlay` manages the required `wg0` path between Prometheus and Atlas, persists private keys only on their respective hosts, and exchanges only derived public keys. The initial run must include both hosts. Prometheus opens `51820/udp`; the Atlas backend role admits service ports only in the WireGuard firewalld zone. diff --git a/README.it.md b/README.it.md index 9e89f69..1022b4f 100644 --- a/README.it.md +++ b/README.it.md @@ -196,8 +196,8 @@ Nextcloud resta disabilitato e il profilo non crea directory `/srv/nextcloud`. La fase 1 su Atlas non modifica questo deployment NPM ne i suoi dati persistenti. Dopo aver attivato WireGuard e i servizi Atlas, configurare i proxy host NPM correnti con upstream Navidrome `http://10.0.0.2:4533` e upstream per la GUI Syncthing `http://10.0.0.2:8384`. Solo la GUI web di -Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native limitate a WireGuard. -Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI. +Syncthing usa NPM; il traffico di sincronizzazione resta sulle porte native pubblicate esplicitamente solo +sull'indirizzo WireGuard di Atlas. Configurare l'autenticazione Syncthing e una policy di accesso NPM adeguata prima di pubblicare la GUI. ### DuckDNS @@ -274,9 +274,10 @@ Con la gestione storage attiva, Atlas crea l'intera gerarchia sotto il pool `zpo `media/photobook`, `backups`, `backups/services` e `backup_prometheus`. I dataset applicativi e di archivio usano `zstd`; media, Syncthing e backup dei servizi usano `lz4`; `backups/services` mantiene inoltre una `refreservation` di `500G`. -SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e ammette la LAN -configurata senza esclusioni specifiche per host. NFSv4 esporta soltanto -`media/photobook` all'IP configurato di Aegis con `all_squash` verso UID/GID anonimi `1100`. +Atlas impone SELinux targeted in modo persistente e segnala, senza avviarlo, l’eventuale reboot necessario per attivarlo. Assegna esplicitamente l’interfaccia LAN primaria alla zona firewalld gestita e applica hardening persistente del kernel di rete: rifiuta redirect e source-route, registra i martian, usa reverse-path filtering loose per WireGuard e disabilita il forwarding IPv4. SSH consente solo l’amministratore dichiarato tramite chiave pubblica; root, password, agent e forwarding +remoto sono disabilitati, mentre il forwarding locale resta disponibile per tunnel amministrativi privati. SMB3 pubblica `Archive` solo agli account Samba configurati con password in Vault e +ammette la LAN configurata su SMB3 cifrato e firmato, esclusivamente su TCP/445. NFSv4 esporta soltanto +`media/photobook` all'IP configurato di Aegis su TCP/2049, con `all_squash` verso UID/GID anonimi `1100`. L'account di sistema `immich` usa UID/GID `1100`, shell senza login, nessuna appartenenza a `wheel` e i gruppi supplementari `video` e `render`. I Quadlet rootful di Immich Server, ML, cache compatibile diff --git a/README.md b/README.md index da30340..190ceae 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,9 @@ does not provision any `/srv/nextcloud` directories. The Atlas phase-one work does not change this NPM deployment or its persistent data. Once WireGuard and the Atlas services are active, configure the current NPM proxy hosts with Navidrome upstream `http://10.0.0.2:4533` and Syncthing GUI upstream `http://10.0.0.2:8384`. Only the Syncthing web GUI -uses NPM; synchronization traffic remains on its native WireGuard-restricted ports. Configure both -Syncthing authentication and an appropriate NPM access policy before publishing its GUI. +uses NPM; synchronization traffic remains on explicitly published native ports bound only to the Atlas +WireGuard address. Configure both Syncthing authentication and an appropriate NPM access policy before +publishing its GUI. Server identity comes from `server_username`, `server_user_group`, and `server_user_home` in `ansible/inventory/group_vars/server.yml`. `server_username` defaults to `username`, but it can be overridden, for example: @@ -230,9 +231,11 @@ explicitly bootstrapped `zpool`: `work`, `archive`, `archive/app_data`, the sepa `archive/app_data/syncthing` application datasets, `media`, `media/music`, `media/photobook`, `backups`, `backups/services`, and `backup_prometheus`. Application/archive datasets use `zstd`, while media, Syncthing and service-backup datasets use `lz4`; `backups/services` also has a `500G` -refreservation. SMB3 exposes `Archive` only to the configured Vault-backed Samba accounts and admits -the configured LAN without host-specific exclusions. NFSv4 exports only -`media/photobook` to the configured Aegis IP, using `all_squash` with anonymous UID/GID `1100`. +refreservation. Atlas enforces targeted SELinux persistently and reports, without initiating, any reboot required to activate it. It assigns its primary LAN interface explicitly to the managed firewalld zone and applies persistent kernel network hardening: redirects and source routes are rejected, martians logged, reverse-path filtering remains loose for WireGuard, and IPv4 forwarding is disabled. SSH permits only the declared administrator using public-key authentication; root login, passwords, +agent and remote forwarding are disabled, while local forwarding remains available for private administrative tunnels. SMB3 exposes `Archive` only to the configured Vault-backed +Samba accounts on encrypted, signed SMB3 over TCP/445 only and admits the configured LAN without host-specific +exclusions. NFSv4 exports only `media/photobook` to the configured Aegis IP over TCP/2049, using +`all_squash` with anonymous UID/GID `1100`. The `immich` system account is fixed to UID/GID `1100`, has no login shell or `wheel` membership, and receives `video` and `render` access. The rootful Immich Server, ML, Redis-compatible cache, PostgreSQL, diff --git a/ansible/inventory/host_vars/atlas.yml b/ansible/inventory/host_vars/atlas.yml index 79c4ae2..c131589 100644 --- a/ansible/inventory/host_vars/atlas.yml +++ b/ansible/inventory/host_vars/atlas.yml @@ -14,6 +14,7 @@ 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_ssh_allow_tcp_forwarding: local atlas_lan_subnet: 192.168.178.0/24 atlas_aegis_ip: 192.168.178.54 @@ -63,7 +64,9 @@ rocky_podman_packages: host_packages: - cockpit - nfs-utils + - policycoreutils - policycoreutils-python-utils + - python3-libselinux - samba - samba-client - samba-common-tools @@ -82,6 +85,7 @@ atlas_nfs_exports: atlas_samba_share_name: Archive atlas_samba_workgroup: WORKGROUP +atlas_samba_encryption: required # Append any pre-existing laptop Unix account here and keep its Samba password in Vault. atlas_samba_accounts: - username: "{{ atlas_admin_username }}" @@ -91,12 +95,10 @@ atlas_samba_valid_users: "{{ atlas_samba_accounts | map(attribute='username') | atlas_immich_db_password: "{{ vault_atlas_immich_db_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_aegis_ip }}" service name="nfs" accept' - - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="samba" accept' - - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="http" accept' - - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" service name="https" accept' + - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="22" protocol="tcp" accept' + - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="9090" protocol="tcp" accept' + - 'rule family="ipv4" source address="{{ atlas_aegis_ip }}" port port="2049" protocol="tcp" accept' + - 'rule family="ipv4" source address="{{ atlas_lan_subnet }}" port port="445" protocol="tcp" accept' host_enabled_services: - sshd diff --git a/ansible/roles/profile_atlas/defaults/main.yml b/ansible/roles/profile_atlas/defaults/main.yml index bf563a4..571d16a 100644 --- a/ansible/roles/profile_atlas/defaults/main.yml +++ b/ansible/roles/profile_atlas/defaults/main.yml @@ -11,6 +11,8 @@ atlas_admin_uid: 1000 atlas_admin_gid: 1000 atlas_admin_ssh_keys: [] atlas_admin_password_hash: "!" +# Local forwarding permits an administrator to tunnel a private service without allowing remote forwards. +atlas_ssh_allow_tcp_forwarding: local atlas_immich_username: immich atlas_immich_group: immich @@ -24,6 +26,25 @@ atlas_lan_subnet: CHANGEME_LAN_SUBNET atlas_aegis_ip: CHANGEME_AEGIS_IP atlas_manage_firewall: false atlas_firewalld_zone: public +atlas_hardening_sysctls: + net.ipv4.conf.all.accept_redirects: "0" + net.ipv4.conf.default.accept_redirects: "0" + net.ipv4.conf.all.send_redirects: "0" + net.ipv4.conf.default.send_redirects: "0" + net.ipv4.conf.all.accept_source_route: "0" + net.ipv4.conf.default.accept_source_route: "0" + net.ipv4.conf.all.log_martians: "1" + net.ipv4.conf.default.log_martians: "1" + net.ipv4.conf.all.rp_filter: "2" + net.ipv4.conf.default.rp_filter: "2" + net.ipv4.tcp_syncookies: "1" + net.ipv4.icmp_echo_ignore_broadcasts: "1" + net.ipv4.icmp_ignore_bogus_error_responses: "1" + net.ipv4.ip_forward: "0" + net.ipv6.conf.all.accept_redirects: "0" + net.ipv6.conf.default.accept_redirects: "0" + net.ipv6.conf.all.accept_source_route: "0" + net.ipv6.conf.default.accept_source_route: "0" atlas_zfs_pool: CHANGEME_ZFS_POOL atlas_zfs_dataset_work: work atlas_zfs_dataset_archive: archive @@ -72,6 +93,8 @@ atlas_nfs_exports: - "anongid={{ atlas_immich_gid }}" atlas_samba_share_name: Archive atlas_samba_workgroup: WORKGROUP +# Keep encryption mandatory unless a verified client-compatibility exception is explicitly required. +atlas_samba_encryption: required atlas_samba_valid_users: [] atlas_samba_accounts: [] atlas_samba_password_marker_dir: /var/lib/samba/private/ansible-passwords diff --git a/ansible/roles/profile_atlas/tasks/account.yml b/ansible/roles/profile_atlas/tasks/account.yml index 483bce2..6909685 100644 --- a/ansible/roles/profile_atlas/tasks/account.yml +++ b/ansible/roles/profile_atlas/tasks/account.yml @@ -6,6 +6,7 @@ - atlas_admin_username != 'CHANGEME_ATLAS_ADMIN' - (atlas_admin_ssh_keys | default([])) | length > 0 - atlas_admin_password_hash not in ['', '!', '*'] + - atlas_ssh_allow_tcp_forwarding in ['no', 'yes', 'local', 'remote', 'all'] - "'wheel' not in atlas_immich_supplementary_groups" fail_msg: >- Define atlas_admin_username, vault_atlas_authorized_ssh_keys and @@ -149,6 +150,18 @@ - "'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" + - "'authenticationmethods publickey' in atlas_sshd_effective_configuration.stdout_lines" + - "'x11forwarding no' in atlas_sshd_effective_configuration.stdout_lines" + - "('allowtcpforwarding ' ~ atlas_ssh_allow_tcp_forwarding) in atlas_sshd_effective_configuration.stdout_lines" + - "'allowagentforwarding no' in atlas_sshd_effective_configuration.stdout_lines" + - "'gatewayports no' in atlas_sshd_effective_configuration.stdout_lines" + - "'permittunnel no' in atlas_sshd_effective_configuration.stdout_lines" + - "'permituserenvironment no' in atlas_sshd_effective_configuration.stdout_lines" + - "'maxauthtries 3' in atlas_sshd_effective_configuration.stdout_lines" + - "'logingracetime 30' in atlas_sshd_effective_configuration.stdout_lines" + - "'clientaliveinterval 300' in atlas_sshd_effective_configuration.stdout_lines" + - "'clientalivecountmax 2' in atlas_sshd_effective_configuration.stdout_lines" + - "'loglevel VERBOSE' 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 diff --git a/ansible/roles/profile_atlas/tasks/hardening.yml b/ansible/roles/profile_atlas/tasks/hardening.yml new file mode 100644 index 0000000..c2bcd51 --- /dev/null +++ b/ansible/roles/profile_atlas/tasks/hardening.yml @@ -0,0 +1,45 @@ +--- +- name: Enforce targeted SELinux on Atlas + tags: [atlas, hardening, security] + ansible.posix.selinux: + policy: targeted + state: enforcing + update_kernel_param: true + register: atlas_selinux_enforcement + +- name: Report when Atlas requires a reboot for SELinux enforcement + tags: [atlas, hardening, security] + ansible.builtin.debug: + msg: Reboot Atlas before relying on SELinux enforcement. + when: atlas_selinux_enforcement.reboot_required | default(false) + +- name: Apply Atlas network-kernel hardening + tags: [atlas, hardening, security] + ansible.posix.sysctl: + name: "{{ item.key }}" + value: "{{ item.value }}" + state: present + sysctl_set: true + reload: true + loop: "{{ atlas_hardening_sysctls | dict2items }}" + loop_control: + label: "{{ item.key }}={{ item.value }}" + +- name: Read active Atlas SELinux mode + tags: [atlas, hardening, security] + ansible.builtin.command: + argv: + - getenforce + register: atlas_selinux_mode + changed_when: false + when: not ansible_check_mode + +- name: Require active SELinux enforcement on Atlas + tags: [atlas, hardening, security] + ansible.builtin.assert: + that: + - atlas_selinux_mode.stdout == 'Enforcing' + fail_msg: >- + Atlas SELinux is not enforcing. Reboot if requested by the preceding + SELinux task, then rerun the hardening role before enabling services. + when: not ansible_check_mode diff --git a/ansible/roles/profile_atlas/tasks/main.yml b/ansible/roles/profile_atlas/tasks/main.yml index df649ad..a422a9b 100644 --- a/ansible/roles/profile_atlas/tasks/main.yml +++ b/ansible/roles/profile_atlas/tasks/main.yml @@ -5,6 +5,9 @@ - name: Import Atlas 45Drives tasks ansible.builtin.import_tasks: cockpit.yml +- name: Import Atlas network hardening tasks + ansible.builtin.import_tasks: hardening.yml + - name: Import Atlas pool bootstrap tasks ansible.builtin.import_tasks: pool.yml diff --git a/ansible/roles/profile_atlas/tasks/sharing.yml b/ansible/roles/profile_atlas/tasks/sharing.yml index 6c83d52..a44ae63 100644 --- a/ansible/roles/profile_atlas/tasks/sharing.yml +++ b/ansible/roles/profile_atlas/tasks/sharing.yml @@ -1,4 +1,50 @@ ---- +- name: Require completed Atlas firewall placeholders + tags: [atlas, sharing, services] + ansible.builtin.assert: + that: + - atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' + - atlas_aegis_ip != 'CHANGEME_AEGIS_IP' + - atlas_firewalld_zone | length > 0 + - ansible_facts.default_ipv4.interface | default('') | length > 0 + fail_msg: Replace the Atlas LAN, Aegis 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: Assign primary Atlas LAN interface to managed firewalld zone + tags: [atlas, sharing, services] + ansible.posix.firewalld: + interface: "{{ ansible_facts.default_ipv4.interface }}" + zone: "{{ atlas_firewalld_zone }}" + state: enabled + permanent: true + immediate: true + 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 + - name: Render Atlas NFS exports tags: [atlas, sharing] ansible.builtin.template: @@ -10,19 +56,6 @@ notify: Reload NFS exports when: atlas_manage_sharing | bool -- name: Enable Atlas file-sharing services - tags: [atlas, sharing, services] - ansible.builtin.systemd: - name: "{{ item }}" - enabled: true - state: started - loop: - - nfs-server.service - - smb.service - loop_control: - label: "{{ item }}" - when: atlas_manage_sharing | bool - - name: Configure Atlas NFSv4-only service tags: [atlas, sharing] ansible.builtin.template: @@ -99,6 +132,7 @@ tags: [atlas, sharing] ansible.builtin.assert: that: + - atlas_samba_encryption in ['required', 'desired'] - atlas_samba_accounts | length > 0 - >- atlas_samba_accounts | map(attribute='username') | list @@ -131,38 +165,87 @@ no_log: true when: atlas_manage_sharing | bool -- name: Require completed Atlas firewall placeholders +- name: Enable Atlas file-sharing services tags: [atlas, sharing, services] + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + loop: + - nfs-server.service + - smb.service + loop_control: + label: "{{ item }}" + when: atlas_manage_sharing | bool + +- name: Gather effective Atlas firewalld zone configuration + tags: [atlas, sharing, services, security] + ansible.posix.firewalld_info: + zones: + - "{{ atlas_firewalld_zone }}" + register: atlas_firewalld_info + when: + - atlas_manage_firewall | bool + - not ansible_check_mode + +- name: Verify effective Atlas firewalld restrictions + tags: [atlas, sharing, services, security] ansible.builtin.assert: that: - - atlas_lan_subnet != 'CHANGEME_LAN_SUBNET' - - atlas_aegis_ip != 'CHANGEME_AEGIS_IP' - - atlas_firewalld_zone | length > 0 - fail_msg: Replace the Atlas LAN, Aegis and firewall-zone placeholders. - when: atlas_manage_firewall | bool + - atlas_firewalld_zone in atlas_firewalld_info.firewalld_info.zones + - >- + ansible_facts.default_ipv4.interface + in atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].interfaces + - >- + atlas_firewalld_restricted_services + | intersect(atlas_firewalld_info.firewalld_info.zones[atlas_firewalld_zone].services) + | length == 0 + fail_msg: >- + The effective Atlas firewalld zone does not restrict the primary LAN + interface and unmanaged service exposure as declared. + when: + - atlas_manage_firewall | bool + - not ansible_check_mode -- 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: Apply pending Atlas sharing handlers before verification + tags: [atlas, sharing, services, security] + ansible.builtin.meta: flush_handlers + when: + - atlas_manage_sharing | bool + - not ansible_check_mode -- 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 }}" +- name: Read Atlas file-sharing service state + tags: [atlas, sharing, services, security] + ansible.builtin.service_facts: + when: + - atlas_manage_sharing | bool + - not ansible_check_mode + +- name: Require active Atlas file-sharing services + tags: [atlas, sharing, services, security] + ansible.builtin.assert: + that: + - ansible_facts.services['nfs-server.service'] is defined + - ansible_facts.services['nfs-server.service'].state == 'running' + - ansible_facts.services['smb.service'] is defined + - ansible_facts.services['smb.service'].state == 'running' + fail_msg: Atlas NFSv4 or SMB3 did not start after its managed configuration was applied. + when: + - atlas_manage_sharing | bool + - not ansible_check_mode + +- name: Require Atlas file-sharing TCP listeners + tags: [atlas, sharing, services, security] + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ item }}" + state: started + timeout: 15 + loop: + - 2049 + - 445 loop_control: - label: "{{ item }}" - when: atlas_manage_firewall | bool + label: "{{ item }}/tcp" + when: + - atlas_manage_sharing | bool + - not ansible_check_mode diff --git a/ansible/roles/profile_atlas/templates/00-atlas-hardening.conf.j2 b/ansible/roles/profile_atlas/templates/00-atlas-hardening.conf.j2 index 90c8386..cf2f51c 100644 --- a/ansible/roles/profile_atlas/templates/00-atlas-hardening.conf.j2 +++ b/ansible/roles/profile_atlas/templates/00-atlas-hardening.conf.j2 @@ -3,4 +3,16 @@ PermitRootLogin no PubkeyAuthentication yes PasswordAuthentication no KbdInteractiveAuthentication no +AuthenticationMethods publickey +X11Forwarding no +AllowTcpForwarding {{ atlas_ssh_allow_tcp_forwarding }} +AllowAgentForwarding no +GatewayPorts no +PermitTunnel no +PermitUserEnvironment no +MaxAuthTries 3 +LoginGraceTime 30 +ClientAliveInterval 300 +ClientAliveCountMax 2 +LogLevel VERBOSE AllowUsers {{ atlas_admin_username }} diff --git a/ansible/roles/profile_atlas/templates/smb.conf.j2 b/ansible/roles/profile_atlas/templates/smb.conf.j2 index 519cfa3..decd29f 100644 --- a/ansible/roles/profile_atlas/templates/smb.conf.j2 +++ b/ansible/roles/profile_atlas/templates/smb.conf.j2 @@ -4,6 +4,10 @@ security = user map to guest = Never server min protocol = SMB3 + server signing = mandatory + smb encrypt = {{ atlas_samba_encryption }} + disable netbios = yes + smb ports = 445 hosts allow = {{ atlas_lan_subnet }} include = registry 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 e14eb97..b594ef7 100644 --- a/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 +++ b/ansible/roles/profile_backend_phase1/templates/atlas-syncthing.container.j2 @@ -6,13 +6,16 @@ Description=Atlas rootless Syncthing backend ContainerName=atlas-syncthing Image={{ backend_phase1_syncthing_image }} HostName=atlas-syncthing -Network=host 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 Environment=HOME=/var/syncthing Environment=STHOMEDIR=/var/syncthing/config -Environment=STGUIADDRESS={{ backend_phase1_wireguard_address }}:{{ backend_phase1_syncthing_gui_port }} +Environment=STGUIADDRESS=0.0.0.0:{{ backend_phase1_syncthing_gui_port }} Environment=TZ={{ backend_phase1_timezone }} Volume={{ backend_phase1_syncthing_root }}:/var/syncthing:Z NoNewPrivileges=true