--- # Narrow reconciliation for the audited us4 public zone. This playbook never # reloads or restarts firewalld and deliberately does not manage Docker rules. - name: Safely remove audited stale firewalld allowances from us4 hosts: wireguard become: true gather_facts: false serial: 1 any_errors_fatal: true vars: us4_firewalld_confirm: false us4_console_confirm: false us4_firewalld_zone: public us4_firewalld_keep_services: - dhcpv6-client - http - https - smtp - ssh us4_firewalld_stale_services: - imap - imaps - smtp-submission - smtps us4_firewalld_stale_ports: - 24/tcp - 6443/tcp - 8443/tcp us4_firewalld_expected_containers: - nghttpx-proxy - semaphoreui-postgres-1 - semaphoreui-semaphore-1 - squid-backend - traefik - trlm-server-trilium-1 - wireguard us4_firewalld_backup_root: /var/backups/us4-firewall tasks: - name: Require the audited host and explicit apply confirmations ansible.builtin.assert: that: - inventory_hostname == 'us4' - ansible_host == 'us4.wsvc.info' - ansible_host_ipv4 == '185.201.226.122' - ansible_check_mode or (us4_firewalld_confirm | bool) - ansible_check_mode or (us4_console_confirm | bool) fail_msg: >- Apply is allowed only for audited host us4 after the provider console has been tested. Set both us4_firewalld_confirm=true and us4_console_confirm=true. Check mode does not require confirmation. - name: Verify the remote host identity ansible.builtin.command: argv: [hostname, -f] check_mode: false changed_when: false register: us4_firewalld_hostname - name: Reject an unexpected remote host ansible.builtin.assert: that: - us4_firewalld_hostname.stdout == 'us4.wsvc.info' - name: Verify required services are active ansible.builtin.command: argv: [systemctl, is-active, --quiet, "{{ item }}"] check_mode: false changed_when: false loop: - atd - firewalld - name: Verify firewalld Python bindings used by ansible.posix ansible.builtin.command: argv: [python3, -c, "import dbus, firewall, firewall.client"] check_mode: false changed_when: false - name: Verify the default firewalld zone ansible.builtin.command: argv: [firewall-cmd, --get-default-zone] check_mode: false changed_when: false register: us4_firewalld_default_zone - name: Read runtime public-zone services ansible.builtin.command: argv: [firewall-cmd, --zone=public, --list-services] check_mode: false changed_when: false register: us4_firewalld_runtime_services - name: Read permanent public-zone services ansible.builtin.command: argv: [firewall-cmd, --permanent, --zone=public, --list-services] check_mode: false changed_when: false register: us4_firewalld_permanent_services - name: Read runtime public-zone ports ansible.builtin.command: argv: [firewall-cmd, --zone=public, --list-ports] check_mode: false changed_when: false register: us4_firewalld_runtime_ports - name: Read permanent public-zone ports ansible.builtin.command: argv: [firewall-cmd, --permanent, --zone=public, --list-ports] check_mode: false changed_when: false register: us4_firewalld_permanent_ports - name: Normalize the audited public-zone state ansible.builtin.set_fact: us4_firewalld_pre_services: "{{ us4_firewalld_runtime_services.stdout.split() | sort }}" us4_firewalld_pre_permanent_services: "{{ us4_firewalld_permanent_services.stdout.split() | sort }}" us4_firewalld_pre_ports: "{{ us4_firewalld_runtime_ports.stdout.split() | sort }}" us4_firewalld_pre_permanent_ports: "{{ us4_firewalld_permanent_ports.stdout.split() | sort }}" - name: Fail closed on public-zone drift or unknown allowances ansible.builtin.assert: that: - us4_firewalld_default_zone.stdout == us4_firewalld_zone - us4_firewalld_pre_services == us4_firewalld_pre_permanent_services - us4_firewalld_pre_ports == us4_firewalld_pre_permanent_ports - us4_firewalld_keep_services | difference(us4_firewalld_pre_services) | length == 0 - us4_firewalld_pre_services | difference(us4_firewalld_keep_services + us4_firewalld_stale_services) | length == 0 - us4_firewalld_pre_ports | difference(us4_firewalld_stale_ports) | length == 0 fail_msg: >- The public zone differs from the audited baseline. Stop and review it; this playbook will not infer whether an unknown allowance is required. - name: Select only audited stale entries that currently exist ansible.builtin.set_fact: us4_firewalld_cleanup_services: >- {{ us4_firewalld_stale_services | intersect(us4_firewalld_pre_services) | sort }} us4_firewalld_cleanup_ports: >- {{ us4_firewalld_stale_ports | intersect(us4_firewalld_pre_ports) | sort }} - name: Report the proposed reconciliation ansible.builtin.debug: msg: keep_services: "{{ us4_firewalld_keep_services }}" remove_services: "{{ us4_firewalld_cleanup_services }}" remove_ports: "{{ us4_firewalld_cleanup_ports }}" reload_or_restart: false - name: Create rollback material when cleanup is required when: - not ansible_check_mode - us4_firewalld_cleanup_services | length > 0 or us4_firewalld_cleanup_ports | length > 0 block: - name: Create the protected firewall backup root ansible.builtin.file: path: "{{ us4_firewalld_backup_root }}" state: directory owner: root group: root mode: "0700" - name: Create a backup timestamp ansible.builtin.command: argv: [date, +%Y%m%dT%H%M%S%z] changed_when: false register: us4_firewalld_backup_timestamp - name: Set the protected backup directory ansible.builtin.set_fact: us4_firewalld_backup_dir: >- {{ us4_firewalld_backup_root }}/{{ us4_firewalld_backup_timestamp.stdout }} us4_firewalld_rollback_command: >- {{ us4_firewalld_backup_root }}/{{ us4_firewalld_backup_timestamp.stdout }}/rollback-phase1.sh - name: Create the protected backup directory ansible.builtin.file: path: "{{ us4_firewalld_backup_dir }}" state: directory owner: root group: root mode: "0700" - name: Back up the complete firewalld configuration ansible.builtin.command: argv: - tar - --create - --gzip - "--file={{ us4_firewalld_backup_dir }}/firewalld.tgz" - --directory=/etc - firewalld changed_when: true - name: Capture the pre-change runtime ruleset ansible.builtin.shell: cmd: >- umask 077 && nft list ruleset > {{ us4_firewalld_backup_dir | quote }}/nft-ruleset.txt executable: /bin/bash changed_when: true - name: Install the exact pre-change rollback script ansible.builtin.copy: dest: "{{ us4_firewalld_rollback_command }}" owner: root group: root mode: "0700" content: | #!/bin/sh set -eu exec >>/var/log/us4-firewalld-phase1-rollback.log 2>&1 printf '%s rollback start\n' "$(date -Is)" add_service() { service=$1 /usr/bin/firewall-cmd --permanent --zone=public \ --query-service="$service" >/dev/null 2>&1 || /usr/bin/firewall-cmd --permanent --zone=public \ --add-service="$service" /usr/bin/firewall-cmd --zone=public \ --query-service="$service" >/dev/null 2>&1 || /usr/bin/firewall-cmd --zone=public --add-service="$service" } add_port() { port=$1 /usr/bin/firewall-cmd --permanent --zone=public \ --query-port="$port" >/dev/null 2>&1 || /usr/bin/firewall-cmd --permanent --zone=public \ --add-port="$port" /usr/bin/firewall-cmd --zone=public \ --query-port="$port" >/dev/null 2>&1 || /usr/bin/firewall-cmd --zone=public --add-port="$port" } {% for service in us4_firewalld_cleanup_services %} add_service {{ service }} {% endfor %} {% for port in us4_firewalld_cleanup_ports %} add_port {{ port }} {% endfor %} /usr/bin/firewall-cmd --check-config printf '%s rollback complete\n' "$(date -Is)" - name: Schedule the 15-minute automatic rollback ansible.builtin.shell: cmd: | set -euo pipefail output=$(printf '%s\n' {{ us4_firewalld_rollback_command | quote }} | at now + 15 minutes 2>&1) job_id=$(printf '%s\n' "$output" | sed -n 's/^job \([0-9][0-9]*\).*/\1/p') test -n "$job_id" printf '%s\n' "$job_id" executable: /bin/bash changed_when: true register: us4_firewalld_rollback_job - name: Record the automatic rollback job ansible.builtin.set_fact: us4_firewalld_rollback_job_id: "{{ us4_firewalld_rollback_job.stdout }}" us4_firewalld_rollback_cancelled: false - name: Persist the rollback job ID beside the backup ansible.builtin.copy: dest: "{{ us4_firewalld_backup_dir }}/phase1-at-job-id" owner: root group: root mode: "0600" content: "{{ us4_firewalld_rollback_job_id }}\n" - name: Reconcile and verify the audited public zone block: - name: Remove audited stale firewalld services ansible.posix.firewalld: zone: "{{ us4_firewalld_zone }}" service: "{{ item }}" state: disabled permanent: true immediate: true loop: "{{ us4_firewalld_stale_services }}" - name: Remove audited stale firewalld ports ansible.posix.firewalld: zone: "{{ us4_firewalld_zone }}" port: "{{ item }}" state: disabled permanent: true immediate: true loop: "{{ us4_firewalld_stale_ports }}" - name: Verify the permanent firewalld configuration ansible.builtin.command: argv: [firewall-cmd, --check-config] when: not ansible_check_mode changed_when: false - name: Read reconciled runtime services ansible.builtin.command: argv: [firewall-cmd, --zone=public, --list-services] changed_when: false when: not ansible_check_mode register: us4_firewalld_after_runtime_services - name: Read reconciled permanent services ansible.builtin.command: argv: [firewall-cmd, --permanent, --zone=public, --list-services] changed_when: false when: not ansible_check_mode register: us4_firewalld_after_permanent_services - name: Read reconciled runtime ports ansible.builtin.command: argv: [firewall-cmd, --zone=public, --list-ports] changed_when: false when: not ansible_check_mode register: us4_firewalld_after_runtime_ports - name: Read reconciled permanent ports ansible.builtin.command: argv: [firewall-cmd, --permanent, --zone=public, --list-ports] changed_when: false when: not ansible_check_mode register: us4_firewalld_after_permanent_ports - name: Require the exact audited post-change public zone ansible.builtin.assert: that: - us4_firewalld_after_runtime_services.stdout.split() | sort == us4_firewalld_keep_services | sort - us4_firewalld_after_permanent_services.stdout.split() | sort == us4_firewalld_keep_services | sort - us4_firewalld_after_runtime_ports.stdout.split() | length == 0 - us4_firewalld_after_permanent_ports.stdout.split() | length == 0 when: not ansible_check_mode - name: Verify a fresh independent SSH and sudo path ansible.builtin.command: argv: - ssh - -4 - -o - BatchMode=yes - -o - ConnectTimeout=10 - -o - ControlMaster=no - -o - ControlPath=none - windy@us4.wsvc.info - sudo -n true delegate_to: localhost become: false changed_when: false when: not ansible_check_mode vars: ansible_become: false - name: Verify public HTTPS routes ansible.builtin.uri: url: "{{ item.url }}" follow_redirects: all status_code: "{{ item.status }}" validate_certs: true use_proxy: false loop: - {url: https://update.wsvc.info/, status: 200} - {url: https://us4-gate.wsvc.info/, status: 401} - {url: https://trlm.wsvc.info/, status: 200} delegate_to: localhost become: false when: not ansible_check_mode vars: ansible_become: false - name: Verify the secondary MX TCP listener externally ansible.builtin.wait_for: host: "{{ ansible_host_ipv4 }}" port: 25 state: started connect_timeout: 5 timeout: 10 delegate_to: localhost become: false when: not ansible_check_mode vars: ansible_become: false - name: Verify all expected containers are running ansible.builtin.command: argv: [docker, ps, --format, "{{ '{{.Names}}' }}"] changed_when: false when: not ansible_check_mode register: us4_firewalld_running_containers - name: Reject missing application containers ansible.builtin.assert: that: - us4_firewalld_expected_containers | difference(us4_firewalld_running_containers.stdout_lines) | length == 0 when: not ansible_check_mode - name: Verify Fail2ban remains active ansible.builtin.command: argv: [fail2ban-client, status] changed_when: false when: not ansible_check_mode register: us4_firewalld_fail2ban - name: Require all audited Fail2ban jails ansible.builtin.assert: that: - item in us4_firewalld_fail2ban.stdout loop: - postfix-postscreen - postfix-sasl - recidive - sshd when: not ansible_check_mode - name: Verify the deployed WireGuard health check ansible.builtin.command: argv: [/usr/local/lib/vps-health/run] changed_when: false when: not ansible_check_mode register: us4_firewalld_wireguard_health - name: Cancel automatic rollback only after all checks pass ansible.builtin.command: argv: [at, -r, "{{ us4_firewalld_rollback_job_id }}"] changed_when: true when: - not ansible_check_mode - us4_firewalld_cleanup_services | length > 0 or us4_firewalld_cleanup_ports | length > 0 - name: Mark the automatic rollback as cancelled ansible.builtin.set_fact: us4_firewalld_rollback_cancelled: true when: - not ansible_check_mode - us4_firewalld_cleanup_services | length > 0 or us4_firewalld_cleanup_ports | length > 0 rescue: - name: Preserve the automatic rollback and stop ansible.builtin.fail: msg: >- A reconciliation or verification task failed. No reload was attempted. If cleanup was required, its automatic rollback remains scheduled; do not remove it manually. always: - name: Report backup and rollback disposition ansible.builtin.debug: msg: backup: >- {{ us4_firewalld_backup_dir | default('not-created-in-check-mode' if ansible_check_mode else 'not-required') }} automatic_rollback: >- {{ 'not-created-in-check-mode' if ansible_check_mode else ('cancelled-after-success' if (us4_firewalld_rollback_cancelled | default(false)) else 'scheduled-or-executed') if (us4_firewalld_cleanup_services | length > 0 or us4_firewalld_cleanup_ports | length > 0) else 'not-required' }}