Introduction

ansible.builtin.iptables manages Linux firewall rules directly — no firewalld or ufw abstraction layer. It's the module you need when managing servers that use raw iptables (common in containers, minimal installs, legacy systems, and custom network appliances). This guide covers every common pattern: allowing services, blocking traffic, NAT, port forwarding, rate limiting, and persisting rules across reboots.

Module Parameters

ParameterDescriptionExample
chainChain to operate onINPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING
tableTablefilter (default), nat, mangle, raw
protocolProtocoltcp, udp, icmp
sourceSource IP/CIDR10.0.0.0/8
destinationDestination IP/CIDR192.168.1.0/24
source_portSource port1024:65535
destination_portDestination port80, 443, 8080:8090
jumpTargetACCEPT, DROP, REJECT, LOG, DNAT, SNAT, MASQUERADE
stateRule statepresent, absent
actionWhere to addinsert (top), append (bottom)
in_interfaceInput interfaceeth0
out_interfaceOutput interfaceeth1
ctstateConnection stateNEW, ESTABLISHED, RELATED
commentRule comment"Allow SSH"
limitRate limit25/minute
limit_burstBurst limit100
ip_versionIP versionipv4, ipv6

Basic Firewall Rules

Allow SSH, HTTP, HTTPS

---
- name: Configure iptables firewall
  hosts: servers
  become: true
  tasks:
    - name: Allow established and related connections
      ansible.builtin.iptables:
        chain: INPUT
        ctstate: ESTABLISHED,RELATED
        jump: ACCEPT
        comment: "Allow established connections"

    - name: Allow loopback
      ansible.builtin.iptables:
        chain: INPUT
        in_interface: lo
        jump: ACCEPT
        comment: "Allow loopback"

    - name: Allow SSH
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "22"
        jump: ACCEPT
        comment: "Allow SSH"

    - name: Allow HTTP
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "80"
        jump: ACCEPT
        comment: "Allow HTTP"

    - name: Allow HTTPS
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "443"
        jump: ACCEPT
        comment: "Allow HTTPS"

    - name: Allow ICMP (ping)
      ansible.builtin.iptables:
        chain: INPUT
        protocol: icmp
        jump: ACCEPT
        comment: "Allow ping"

    - name: Drop all other input
      ansible.builtin.iptables:
        chain: INPUT
        jump: DROP
        comment: "Default deny"

Restrict SSH by Source IP

- name: Allow SSH only from office
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    source: 203.0.113.0/24
    destination_port: "22"
    jump: ACCEPT
    comment: "Allow SSH from office"

- name: Block SSH from everywhere else
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "22"
    jump: DROP
    comment: "Block SSH from others"

NAT and Port Forwarding

SNAT (Source NAT / Masquerade)

- name: Enable IP forwarding
  ansible.posix.sysctl:
    name: net.ipv4.ip_forward
    value: '1'
    sysctl_set: true
    reload: true

- name: Masquerade outbound traffic
  ansible.builtin.iptables:
    table: nat
    chain: POSTROUTING
    out_interface: eth0
    jump: MASQUERADE
    comment: "NAT outbound traffic"

DNAT (Port Forwarding)

- name: Forward port 80 to internal web server
  ansible.builtin.iptables:
    table: nat
    chain: PREROUTING
    protocol: tcp
    destination_port: "80"
    jump: DNAT
    to_destination: "10.0.1.10:80"
    comment: "Forward HTTP to web01"

- name: Allow forwarded traffic
  ansible.builtin.iptables:
    chain: FORWARD
    protocol: tcp
    destination: 10.0.1.10
    destination_port: "80"
    jump: ACCEPT
    comment: "Allow forwarded HTTP"

Port Redirect (Local)

- name: Redirect port 80 to 8080 locally
  ansible.builtin.iptables:
    table: nat
    chain: PREROUTING
    protocol: tcp
    destination_port: "80"
    jump: REDIRECT
    to_ports: "8080"
    comment: "Redirect 80 to 8080"

Rate Limiting

Limit SSH Connections

- name: Rate limit SSH (prevent brute force)
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "22"
    ctstate: NEW
    limit: "3/minute"
    limit_burst: "5"
    jump: ACCEPT
    comment: "Rate limit SSH"

- name: Drop excess SSH
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "22"
    ctstate: NEW
    jump: DROP
    comment: "Drop excess SSH"

Log and Drop

- name: Log dropped packets
  ansible.builtin.iptables:
    chain: INPUT
    jump: LOG
    log_prefix: "IPTables-Dropped: "
    log_level: "4"
    limit: "5/minute"
    comment: "Log dropped packets"

- name: Drop all other input
  ansible.builtin.iptables:
    chain: INPUT
    jump: DROP

IPv6 Rules

- name: Allow SSH over IPv6
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    destination_port: "22"
    jump: ACCEPT
    ip_version: ipv6
    comment: "Allow SSH IPv6"

- name: Allow ICMPv6 (required for IPv6)
  ansible.builtin.iptables:
    chain: INPUT
    protocol: ipv6-icmp
    jump: ACCEPT
    ip_version: ipv6
    comment: "Allow ICMPv6"

Persist Rules Across Reboots

iptables rules are lost on reboot unless saved:

- name: Install iptables-persistent (Debian/Ubuntu)
  ansible.builtin.apt:
    name: iptables-persistent
    state: present
  when: ansible_os_family == 'Debian'

- name: Save iptables rules (Debian/Ubuntu)
  ansible.builtin.shell: iptables-save > /etc/iptables/rules.v4
  changed_when: true

- name: Save ip6tables rules
  ansible.builtin.shell: ip6tables-save > /etc/iptables/rules.v6
  changed_when: true

- name: Save iptables rules (RHEL/CentOS)
  ansible.builtin.shell: iptables-save > /etc/sysconfig/iptables
  changed_when: true
  when: ansible_os_family == 'RedHat'

Using a Handler

tasks:
  - name: Allow HTTP
    ansible.builtin.iptables:
      chain: INPUT
      protocol: tcp
      destination_port: "80"
      jump: ACCEPT
    notify: save iptables

handlers:
  - name: save iptables
    ansible.builtin.shell: iptables-save > /etc/iptables/rules.v4

Complete Firewall Playbook

---
- name: Configure complete iptables firewall
  hosts: servers
  become: true
  vars:
    allowed_ssh_sources:
      - 10.0.0.0/8
      - 172.16.0.0/12
    open_tcp_ports:
      - "80"
      - "443"
  tasks:
    - name: Flush existing rules
      ansible.builtin.iptables:
        chain: "{{ item }}"
        flush: true
      loop: [INPUT, FORWARD, OUTPUT]

    - name: Set default policies
      ansible.builtin.iptables:
        chain: "{{ item.chain }}"
        policy: "{{ item.policy }}"
      loop:
        - { chain: INPUT, policy: DROP }
        - { chain: FORWARD, policy: DROP }
        - { chain: OUTPUT, policy: ACCEPT }

    - name: Allow loopback
      ansible.builtin.iptables:
        chain: INPUT
        in_interface: lo
        jump: ACCEPT

    - name: Allow established connections
      ansible.builtin.iptables:
        chain: INPUT
        ctstate: ESTABLISHED,RELATED
        jump: ACCEPT

    - name: Allow SSH from trusted sources
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        source: "{{ item }}"
        destination_port: "22"
        jump: ACCEPT
        comment: "SSH from {{ item }}"
      loop: "{{ allowed_ssh_sources }}"

    - name: Allow public TCP ports
      ansible.builtin.iptables:
        chain: INPUT
        protocol: tcp
        destination_port: "{{ item }}"
        jump: ACCEPT
        comment: "Allow port {{ item }}"
      loop: "{{ open_tcp_ports }}"

    - name: Allow ICMP
      ansible.builtin.iptables:
        chain: INPUT
        protocol: icmp
        jump: ACCEPT

    - name: Save rules
      ansible.builtin.shell: iptables-save > /etc/iptables/rules.v4
      changed_when: true

Troubleshooting

List Current Rules

- name: Show current iptables rules
  ansible.builtin.command: iptables -L -n -v --line-numbers
  register: rules
  changed_when: false

- ansible.builtin.debug:
    var: rules.stdout_lines

Rule Order Matters

# Use action: insert to add at top (before default deny)
- name: Emergency access rule
  ansible.builtin.iptables:
    chain: INPUT
    protocol: tcp
    source: 203.0.113.1
    destination_port: "22"
    jump: ACCEPT
    action: insert
    comment: "Emergency SSH access"

Conclusion

The ansible.builtin.iptables module manages Linux firewall rules directly — no firewalld or ufw layer. Use it for servers that need raw iptables control: define INPUT rules for allowed services, FORWARD rules for routing, NAT rules for port forwarding and masquerading, and rate limiting for brute-force protection. Always persist rules with iptables-save via a handler, and use action: insert for rules that must be evaluated before a default deny policy.