--- # Firewall configuration using ufw # Default deny incoming, allow outgoing # Trusted subnets get full access # Per-host rules for publicly exposed services - name: Install ufw ansible.builtin.apt: name: ufw state: present update_cache: true when: ansible_facts['os_family'] == "Debian" - name: Install firewalld ansible.builtin.dnf: name: firewalld state: present when: ansible_facts['os_family'] == "RedHat" - name: Ensure firewalld is started and enabled ansible.builtin.systemd: name: firewalld state: started enabled: true when: ansible_facts['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_facts['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_facts['os_family'] == "RedHat" - firewall_ssh_allow_from | length > 0 - name: Allow per-host public services in firewalld (unrestricted) ansible.posix.firewalld: zone: public port: "{{ item.port | string }}/{{ item.proto }}" permanent: true immediate: true state: enabled loop: "{{ firewall_allow_rules | rejectattr('sources', 'defined') | list }}" when: - ansible_facts['os_family'] == "RedHat" - firewall_allow_rules | rejectattr('sources', 'defined') | list | length > 0 - name: Remove port-level rules for source-restricted services in firewalld ansible.posix.firewalld: zone: public port: "{{ item.port | string }}/{{ item.proto }}" permanent: true immediate: true state: disabled loop: "{{ firewall_allow_rules | selectattr('sources', 'defined') | list }}" when: - ansible_facts['os_family'] == "RedHat" - firewall_allow_rules | selectattr('sources', 'defined') | list | length > 0 - name: Allow source-restricted services in firewalld (IPv4 rich rules) ansible.posix.firewalld: zone: public rich_rule: 'rule family="ipv4" source address="{{ item.1 }}" port port="{{ item.0.port | string }}" protocol="{{ item.0.proto }}" accept' permanent: true immediate: true state: enabled with_subelements: - "{{ firewall_allow_rules | selectattr('sources', 'defined') | list }}" - sources when: - ansible_facts['os_family'] == "RedHat" - firewall_allow_rules | selectattr('sources', 'defined') | list | length > 0 - name: Allow source-restricted services in firewalld (IPv6 rich rules) ansible.posix.firewalld: zone: public rich_rule: 'rule family="ipv6" source address="{{ item.1 }}" port port="{{ item.0.port | string }}" protocol="{{ item.0.proto }}" accept' permanent: true immediate: true state: enabled with_subelements: - "{{ firewall_allow_rules | selectattr('ipv6_sources', 'defined') | list }}" - ipv6_sources when: - ansible_facts['os_family'] == "RedHat" - firewall_allow_rules | selectattr('ipv6_sources', 'defined') | list | 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_facts['os_family'] == "Debian" # --- UFW (Debian/Ubuntu) - Disable when firewall not managed --- - name: Disable ufw community.general.ufw: state: disabled when: - ansible_facts['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 community.general.ufw: direction: incoming policy: "{{ firewall_default_incoming }}" when: - ansible_facts['os_family'] == "Debian" - firewall_enabled | default(true) - name: Set default outgoing policy to allow community.general.ufw: direction: outgoing policy: "{{ firewall_default_outgoing }}" when: - ansible_facts['os_family'] == "Debian" - firewall_enabled | default(true) - name: Set default routed policy community.general.ufw: direction: routed policy: "{{ firewall_default_routed }}" when: - ansible_facts['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_facts['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 community.general.ufw: rule: allow from_ip: "{{ item.subnet }}" comment: "{{ item.comment }}" loop: "{{ firewall_trusted_subnets }}" when: - ansible_facts['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 community.general.ufw: rule: allow port: "22" proto: tcp from_ip: "{{ item.ip }}" comment: "SSH from {{ item.comment }}" loop: "{{ firewall_ssh_allow_from }}" when: - ansible_facts['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 community.general.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_facts['os_family'] == "Debian" - firewall_enabled | default(true) - firewall_allow_rules | length > 0 - _fw_ufw_rules.skipped | default(true) or (item.from_ip | default('any') + ' ' + 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_facts['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_facts['os_family'] == "Debian" - firewall_enabled | default(true) - firewall_masquerade_rules | length > 0 notify: Reload ufw - name: Enable ufw community.general.ufw: state: enabled when: - ansible_facts['os_family'] == "Debian" - firewall_enabled | default(true) - _fw_ufw_active.stdout is defined - "'Status: active' not in _fw_ufw_active.stdout"