infra/ansible/roles/firewall/tasks/main.yml
Graham McIntire 80943fc684
fix idempotency across roles
- firewall: remove ufw reset on every run; pre-check state before enable/disable/rule tasks
- debian: add regexp to fstab lineinfile; clean stale mariadb 12.2 repo
- librenms: use mariadb 11.8 (trixie-compatible)
- mailcow: idempotent git config, docker compose down/pull/up
- tailscale: don't redownload gpg key every run
2026-06-02 11:54:34 -05:00

197 lines
5.3 KiB
YAML

---
# Firewall configuration using ufw
# Default deny incoming, allow outgoing
# Trusted subnets get full access
# Per-host rules for publicly exposed services
- name: Install ufw
apt:
name: ufw
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install firewalld
dnf:
name: firewalld
state: present
when: ansible_os_family == "RedHat"
- name: Ensure firewalld is started and enabled
systemd:
name: firewalld
state: started
enabled: true
when: ansible_os_family == "RedHat"
# --- Firewalld (RedHat/AlmaLinux) ---
- name: Allow trusted subnets in firewalld
ansible.posix.firewalld:
zone: public
source: "{{ item.subnet }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_trusted_subnets }}"
when: ansible_os_family == "RedHat"
- name: Allow SSH from specific IPs in firewalld
ansible.posix.firewalld:
zone: public
rich_rule: 'rule family="ipv4" source address="{{ item.ip }}" port port="22" protocol="tcp" accept'
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_ssh_allow_from }}"
when:
- ansible_os_family == "RedHat"
- firewall_ssh_allow_from | length > 0
- name: Allow per-host public services in firewalld
ansible.posix.firewalld:
zone: public
port: "{{ item.port | string }}/{{ item.proto }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_allow_rules }}"
when:
- ansible_os_family == "RedHat"
- firewall_allow_rules | length > 0
# --- UFW (Debian/Ubuntu) - Check current state ---
- name: "Check ufw status"
ansible.builtin.command: ufw status
register: _fw_ufw_active
changed_when: false
failed_when: false
check_mode: false
when: ansible_os_family == "Debian"
# --- UFW (Debian/Ubuntu) - Disable when firewall not managed ---
- name: Disable ufw
ufw:
state: disabled
when:
- ansible_os_family == "Debian"
- not (firewall_enabled | default(true))
- _fw_ufw_active.stdout is defined
- "'Status: active' in _fw_ufw_active.stdout"
# --- UFW (Debian/Ubuntu) - Configure when firewall managed ---
- name: Set default incoming policy to deny
ufw:
direction: incoming
policy: "{{ firewall_default_incoming }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- name: Set default outgoing policy to allow
ufw:
direction: outgoing
policy: "{{ firewall_default_outgoing }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- name: Set default routed policy
ufw:
direction: routed
policy: "{{ firewall_default_routed }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- name: "Capture existing ufw rules"
ansible.builtin.command: ufw status verbose
register: _fw_ufw_rules
changed_when: false
failed_when: false
check_mode: false
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- _fw_ufw_active.stdout is defined
- "'Status: active' in _fw_ufw_active.stdout"
- name: Allow all traffic from trusted subnets
ufw:
rule: allow
from_ip: "{{ item.subnet }}"
comment: "{{ item.comment }}"
loop: "{{ firewall_trusted_subnets }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- _fw_ufw_rules.skipped | default(true) or item.subnet not in _fw_ufw_rules.stdout
- name: Allow SSH from specific IPs
ufw:
rule: allow
port: "22"
proto: tcp
from_ip: "{{ item.ip }}"
comment: "SSH from {{ item.comment }}"
loop: "{{ firewall_ssh_allow_from }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- firewall_ssh_allow_from | length > 0
- _fw_ufw_rules.skipped | default(true) or item.ip not in _fw_ufw_rules.stdout
- name: Allow per-host public services
ufw:
rule: allow
port: "{{ item.port | string }}"
proto: "{{ item.proto }}"
from_ip: "{{ item.from_ip | default('any') }}"
comment: "{{ item.comment }}"
loop: "{{ firewall_allow_rules }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- firewall_allow_rules | length > 0
- _fw_ufw_rules.skipped | default(true) or (item.port | string + '/' + item.proto) not in _fw_ufw_rules.stdout
- name: Enable IP forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
state: present
reload: true
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- firewall_ip_forward
- name: Configure NAT masquerade rules in ufw before.rules
ansible.builtin.blockinfile:
path: /etc/ufw/before.rules
insertbefore: "\\*filter"
marker: "# {mark} ANSIBLE MANAGED - {{ item.comment }}"
block: |
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s {{ item.source }} -o {{ item.out_interface }} -j MASQUERADE -m comment --comment "{{ item.comment }}"
COMMIT
loop: "{{ firewall_masquerade_rules }}"
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- firewall_masquerade_rules | length > 0
notify: Reload ufw
- name: Enable ufw
ufw:
state: enabled
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true)
- _fw_ufw_active.stdout is defined
- "'Status: active' not in _fw_ufw_active.stdout"