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
| Parameter | Description | Example |
|---|---|---|
chain | Chain to operate on | INPUT, OUTPUT, FORWARD, PREROUTING, POSTROUTING |
table | Table | filter (default), nat, mangle, raw |
protocol | Protocol | tcp, udp, icmp |
source | Source IP/CIDR | 10.0.0.0/8 |
destination | Destination IP/CIDR | 192.168.1.0/24 |
source_port | Source port | 1024:65535 |
destination_port | Destination port | 80, 443, 8080:8090 |
jump | Target | ACCEPT, DROP, REJECT, LOG, DNAT, SNAT, MASQUERADE |
state | Rule state | present, absent |
action | Where to add | insert (top), append (bottom) |
in_interface | Input interface | eth0 |
out_interface | Output interface | eth1 |
ctstate | Connection state | NEW, ESTABLISHED, RELATED |
comment | Rule comment | "Allow SSH" |
limit | Rate limit | 25/minute |
limit_burst | Burst limit | 100 |
ip_version | IP version | ipv4, 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"
Related Articles
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.