Centralized iptables Management with Ansible: Handling Global Rules and Host-Specific Exceptions


2 views

Managing iptables across hundreds of servers presents a unique dichotomy: we need both centralized control for baseline security policies and flexibility for host-specific exceptions. Traditional template-based approaches often fail when server configurations diverge significantly.

The include-based approach you mentioned actually aligns with how many large-scale operations handle firewall management. Here's why it works:

#!/bin/bash
# /etc/iptables.d/main.iptables (managed by Ansible)

# Common rules for all hosts
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Include base protections
. /etc/iptables.d/base_protections.inc

# Include host-specific rules if exists
[ -f /etc/iptables.d/local_rules.inc ] && . /etc/iptables.d/local_rules.inc

COMMIT

Here's how to structure the Ansible components:

# playbooks/roles/iptables/tasks/main.yml
- name: Deploy base iptables configuration
  template:
    src: main.iptables.j2
    dest: /etc/iptables.d/main.iptables
    mode: 0640

- name: Deploy common rules
  template:
    src: base_protections.inc.j2
    dest: /etc/iptables.d/base_protections.inc
    mode: 0640

- name: Ensure local rules directory exists
  file:
    path: /etc/iptables.d/local_rules.inc
    state: touch
    mode: 0640

For servers requiring custom rules, we can use Ansible's host_vars:

# host_vars/webserver01.yml
iptables_local_rules: |
  # Allow HTTP/HTTPS
  -A INPUT -p tcp --dport 80 -j ACCEPT
  -A INPUT -p tcp --dport 443 -j ACCEPT
  # Special monitoring access
  -A INPUT -s 10.10.1.100 -p tcp --dport 5666 -j ACCEPT

Combine these elements in a playbook:

# playbooks/configure_iptables.yml
- hosts: all
  become: yes
  roles:
    - iptables
  tasks:
    - name: Push host-specific rules when defined
      template:
        src: local_rules.inc.j2
        dest: /etc/iptables.d/local_rules.inc
      when: iptables_local_rules is defined

To prevent locking yourself out:

# playbooks/roles/iptables/handlers/main.yml
- name: flush all temporary rules
  command: iptables -F
  listen: "flush iptables"

- name: apply new rules
  command: iptables-restore < /etc/iptables.d/main.iptables
  listen: "apply iptables rules"

- name: test rules before applying
  command: iptables-restore -t < /etc/iptables.d/main.iptables
  register: test_result
  failed_when: test_result.rc != 0
  listen: "test iptables rules"

Always maintain SSH access during updates:

# templates/base_protections.inc.j2
# Emergency SSH access
-A INPUT -p tcp --dport {{ ansible_ssh_port | default(22) }} -j ACCEPT
# Allow established connections
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

Managing iptables across hundreds of servers with varying requirements presents a unique operational challenge. We need to maintain:

  • Centralized control for common firewall rules
  • Local flexibility for host-specific configurations
  • An audit trail of all changes
  • Minimal service disruption during updates

Here's a battle-tested approach combining Ansible's power with local flexibility:

inventory/
├── group_vars/
│   ├── all.yml          # Common rules for all hosts
│   ├── web_servers.yml  # Web-specific rules
│   └── db_servers.yml   # Database-specific rules
└── host_vars/
    ├── host1.yml        # Host-specific overrides
    └── host2.yml

Create a modular iptables template system:

# roles/iptables/templates/iptables.rules.j2
# Common rules
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Include common rules
{% include 'common.rules.j2' %}

# Include group-specific rules
{% if 'web_servers' in group_names %}
{% include 'web.rules.j2' %}
{% endif %}

# Include host-specific rules
{% if iptables_custom_rules is defined %}
{% for rule in iptables_custom_rules %}
{{ rule }}
{% endfor %}
{% endif %}
COMMIT

For servers requiring local modifications:

# playbooks/iptables.yml
- hosts: all
  tasks:
    - name: Deploy main iptables rules
      template:
        src: iptables.rules.j2
        dest: /etc/iptables.rules
      notify: reload iptables
    
    - name: Check for local overrides
      stat:
        path: /etc/iptables.local
      register: local_rules
    
    - name: Apply local rules if present
      command: /sbin/iptables-restore /etc/iptables.local
      when: local_rules.stat.exists

Sample playbook demonstrating rule deployment with variable precedence:

# playbooks/deploy_firewall.yml
- hosts: all
  vars_files:
    - "{{ inventory_dir }}/group_vars/all.yml"
    - "{{ inventory_dir }}/group_vars/{{ group }}.yml"
  tasks:
    - name: Gather host-specific variables
      include_vars:
        file: "{{ inventory_dir }}/host_vars/{{ inventory_hostname }}.yml"
      when: hostvars[inventory_hostname].iptables_custom is defined
    
    - include_role:
        name: iptables

For environments with complex rule requirements:

# roles/iptables/tasks/dynamic_rules.yml
- name: Generate dynamic rules
  template:
    src: dynamic.rules.j2
    dest: /etc/iptables.d/{{ item }}.rules
  with_items: "{{ iptables_rule_sets }}"
  notify: assemble rules

- name: Assemble final ruleset
  command: cat /etc/iptables.d/*.rules > /etc/iptables.rules
  args:
    creates: /etc/iptables.rules
  notify: reload iptables
  • Always deploy changes with --check first
  • Implement a rollback mechanism
  • Use ansible vault for sensitive rules
  • Maintain rule documentation in YAML comments