Ansible Playbook for Ubuntu UFW Firewall Rules

Here is a simple Ansible UFW firewall playbook.

You will need to install the Ansible collection community.general if it is not already installed.

ansible-galaxy collection install community.general

Next create the playbook. Modify the ports to your liking. Also remember you can put these variables in the Ansible inventory file so each hosts can have different settings.

---
- hosts: ubuntu
  become: true
  vars:
    ssh_port: 22
    http_port: 80
    https_port: 443

  tasks:
  - name: Ensure UFW is installed
    apt:
      name: ufw
      state: present
      update_cache: yes

  - name: Restrict SSH access to local IP addresses (RFC 1918)
    block:
      - name: Allow SSH from 10.0.0.0/8
        community.general.ufw:
          rule: allow
          from_ip: 10.0.0.0/8
          port: "{{ ssh_port }}"
          proto: tcp
      - name: Allow SSH from 172.16.0.0/12
        community.general.ufw:
          rule: allow
          from_ip: 172.16.0.0/12
          port: "{{ ssh_port }}"
          proto: tcp
      - name: Allow SSH from 192.168.0.0/16
        community.general.ufw:
          rule: allow
          from_ip: 192.168.0.0/16
          port: "{{ ssh_port }}"
          proto: tcp

  - name: Allow HTTPS access from the world
    community.general.ufw:
      rule: allow
      port: "{{ https_port }}"
      proto: tcp

  - name: Allow HTTP access from the world
    community.general.ufw:
      rule: allow
      port: "{{ http_port }}"
      proto: tcp

  - name: Enable UFW and set default policy to deny incoming
    community.general.ufw:
      state: enabled
      policy: deny

Save the playbook as ubuntu_firewall.yaml

Run the playbook with:

ansible-playbook ubuntu_firewall.yaml -i inventory/hosts

Here is a more advanced playbook that will loop through multiple IP addresses and ports.

---
- hosts: ubuntu
  become: true
  vars:
    local_ports:
      - { port: '22', proto: 'tcp' }  # SSH
      - { port: '161', proto: 'udp' }  # SNMP
    public_ports:
      - { port: '80', proto: 'tcp' }  # HTTP
      - { port: '443', proto: 'tcp' } # HTTPS
    local_ips:
      - 10.0.0.0/8
      - 172.16.0.0/12
      - 192.168.0.0/16

  tasks:
    - name: Ensure UFW is installed
      apt:
        name: ufw
        state: present
        update_cache: yes

    - name: Allow public_ports
      community.general.ufw:
        rule: allow
        port: "{{ item.port }}"
        proto: "{{ item.proto }}"
      loop: "{{ public_ports }}"


    - name: Allow access to local_ports from RFC 1918 local addresses
      block:
        - name: Allow local_ports from RFC 1918 local IPs
          community.general.ufw:
            rule: allow
            from_ip: "{{ item.0 }} "
            port: "{{ item.1.port }}"
            proto: "{{ item.1.proto }}"
          loop: "{{ local_ips | product(local_ports) | list }}"

    - name: Enable UFW and set default policy to deny incoming
      community.general.ufw:
        state: enabled
        policy: deny

Check for backdoored version of xz (CVE-2024-3094) (Ansible/Bash)

Info on the xc backdoor

https://www.openwall.com/lists/oss-security/2024/03/29/4

https://tukaani.org/xz-backdoor/

https://www.tenable.com/blog/frequently-asked-questions-cve-2024-3094-supply-chain-backdoor-in-xz-utils

Kostas on Twitter posted a helpful one-liner to check the xz version without running the actual command.

https://twitter.com/kostastsale/status/1773890846250926445

Versions 5.6.0 and 5.6.1 are backdoored.

Bash one liner

The following Bash commands were taken and modified from the above Twitter link

Here is a one liner that will check the version of xz binaries and return if they are safe or vulnerable. You’ll need to run this in a Bash shell. May have issues in sh.

for xz_p in $(type -a xz | awk '{print $NF}' ); do  if ( strings "$xz_p" | grep "xz (XZ Utils)" | grep '5.6.0\|5.6.1' ); then echo $xz_p Vulnerable; else echo $xz_p Safe ; fi ; done 

Ansible Playbooks

Here are two different Ansible Playbooks to check if the xz package(s) are backdoored.

This one uses the above Bash commands to check the xz binaries.

---
- name: Check if XZ tools are compromised
# https://twitter.com/kostastsale/status/1773890846250926445
  hosts: all

  tasks: 
    - name: Run Bash command
      shell : 
        for xz_p in $(type -a xz | awk '{print $NF}' ); do 
          if ( strings "$xz_p" | grep "xz (XZ Utils)" | grep '5.6.0\|5.6.1' ); 
            then echo $xz_p Vulnerable!; 
          else 
            echo $xz_p Safe ; 
          fi ; 
        done
      args: 
        executable: /bin/bash
      register: result

    - name: Show output
      ansible.builtin.debug:
        msg: "{{ result.stdout_lines }}"

The following playbook uses the package manager to check the xz version. On RHEL/Fedora this is the xc package. On Debian/Ubuntu, it is part of the liblzma5 package.

---
- name: Check if XZ tools are compromised
  hosts: all

  tasks:
    - name: Collect package info
      ansible.builtin.package_facts:
        manager: auto

    - name: Check if liblzma5 is vulnerable (Ubuntu/Debian)
      ansible.builtin.debug:
        msg: "Installed version of liblzma5/xz: {{ ansible_facts.packages['liblzma5'] | map(attribute='version') | join(', ') }} Vulnerable!"
      when: ('liblzma5' in ansible_facts.packages) and (ansible_facts.packages['liblzma5'][0].version.split('-')[0] is version('5.6.0', '==') or ansible_facts.packages['liblzma5'][0].version.split('-')[0] is version('5.6.1', '=='))

    - name: Check if xz is vulnerable (RHEL/Fedora/Rocky/Alma)
      ansible.builtin.debug:
        msg: "Installed version of xz: {{ ansible_facts.packages['xz'] | map(attribute='version') | join(', ') }} is vulnerable"
      when: ('xz' in ansible_facts.packages) and (ansible_facts.packages['xz'][0].version is version('5.6.0', '==') or ansible_facts.packages['xz'][0].version is version('5.6.1', '=='))

Configuring Firewalld with Ansible

We’ll be using Ansible to change and maintain our firewall settings on a server.

The playbook will do the following.

  1. Set the default zone to drop (Drops all external traffic to server)
  2. Set a zone for internal access
  3. Allow access from RFC1918 addresses to internal zone (Any local IP address will be able to access the server)
  4. Enable the services and ports specified in the vars section
  5. Disable the services listed in firewall_disable_services variable

Modify the variables as needed for your server(s). You can also add or move the variables to the inventory or host_vars files.

If you need to create an inventory file, refer to the first part of this post

BE CAREFUL CHANGING FIREWALL SETTINGS!!! IMPROPER SETTINGS COULD RENDER THE SERVER INACCESSIBLE!!!

Playbook for firewalld

Change the variables under the vars section

---
- name: Configure firewalld
  hosts: rhel
  gather_facts: yes
  become: yes

  vars: 
    firewall_allowed_ips:
      - 10.0.0.0/8
      - 172.16.0.0/12
      - 192.168.0.0/16
    firewall_allowed_services:
      - ssh
      - https
      - snmp
    firewall_allowed_ports:
      - "2222/tcp"
    firewall_disable_services:
      - cockpit
      - dhcpv6-client
      - mdns
      - samba-client

  tasks: 
  - name: Set default zone to drop
    ansible.builtin.command: firewall-cmd --set-default-zone=drop
    register: default_zone_set
    changed_when:
      - '"ZONE_ALREADY_SET" not in default_zone_set.stderr'

  - name: Enable and allow access to internal zone from RFC1918 addresses
    ansible.posix.firewalld:
      source: "{{ item }}"
      zone: internal
      permanent: true
      immediate: true
      state: enabled
    with_items: "{{ firewall_allowed_ips }}"

  - name: Disable unused services for internal zone
    ansible.posix.firewalld:
      service: "{{ item }}"
      zone: internal
      permanent: true
      immediate: true
      state: disabled
    with_items: "{{ firewall_disable_services }}"


  - name: Set services for internal zone
    ansible.posix.firewalld:
      service: "{{ item }}"
      zone: internal
      permanent: true
      immediate: true
      state: enabled
    with_items: "{{ firewall_allowed_services }}"

  - name: Set custom ports for internal zone
    ansible.posix.firewalld:
      port: "{{ item }}"
      zone: internal
      permanent: true
      immediate: true
      state: enabled
    with_items: "{{ firewall_allowed_ports }}"

Helpful links

https://docs.ansible.com/ansible/latest/collections/ansible/posix/firewalld_module.html#parameter-source

https://stackoverflow.com/questions/51563643/how-to-change-firewalld-zone-using-ansible

https://www.middlewareinventory.com/blog/ansible-firewalld/