457 lines
17 KiB
YAML
457 lines
17 KiB
YAML
---
|
|||
|
|
# 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' }}
|