If we have a large group of servers, it is important to be able to centrally manage firewall rules. Ansible is a good option since we can create groups and apply specific rules to one group or another.
We install the iptables userlands:
We load the iptables rules at startup:
We install the necessary python libraries:
We create the necessary directories for Ansible:
chown -R root:kr0m /etc/ansible/
chmod 775 /etc/ansible/
We create a server group called test:
[test]
kr0mtest
We write the playbook:
- hosts: test
vars:
allowed_hosts:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
tasks:
- name: Firewall rule - allow INPUT custom hosts
iptables:
action: insert
chain: INPUT
rule_num: 1
source: "{{ item }}"
jump: ACCEPT
with_items:
- "{{ allowed_hosts }}"
- name: Firewall rule - allow OUTPUT custom hosts
iptables:
action: insert
chain: OUTPUT
rule_num: 1
destination: "{{ item }}"
jump: ACCEPT
with_items:
- "{{ allowed_hosts }}"
- name: Save iptables rules
shell: /etc/init.d/iptables save
We execute it:
We check that the config has been applied correctly:
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT all -- 3.3.3.3 0.0.0.0/0
ACCEPT all -- 2.2.2.2 0.0.0.0/0
ACCEPT all -- 1.1.1.1 0.0.0.0/0
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
ACCEPT all -- 0.0.0.0/0 3.3.3.3
ACCEPT all -- 0.0.0.0/0 2.2.2.2
ACCEPT all -- 0.0.0.0/0 1.1.1.1
NOTE: I have not been able to apply a default DROP, the only way I have been able to do it is by leaving DROP as the last ACL in both INPUT and OUTPUT.
It is very common to have some generic rules and some more specific ones. To be able to reuse playbooks, we will use includes:
- hosts: "{{hostlist}}"
vars:
input_ports:
ssh32002:
port: 32002
protocol: tcp
ssh22:
port: 22
protocol: tcp
http:
port: 80
protocol: tcp
https:
port: 443
protocol: tcp
dns:
port: 53
protocol: tcp
dnsUdp:
port: 53
protocol: udp
output_ports:
http:
port: 80
protocol: tcp
https:
port: 443
protocol: tcp
dns:
port: 53
protocol: tcp
dnsUdp:
port: 53
protocol: udp
traffic_direction:
- INPUT
- OUTPUT
tasks:
- name: Firewall rule - flush previous INPUT ACLs
iptables:
flush: true
chain: "{{ item }}"
with_items: "{{ traffic_direction }}"
- name: Firewall rule - allow all INPUT loopback traffic
iptables:
action: append
chain: INPUT
in_interface: lo
jump: ACCEPT
- name: Firewall rule - allow all OUTPUT loopback traffic
iptables:
action: append
chain: OUTPUT
out_interface: lo
jump: ACCEPT
- name: Firewall rule - allow INPUT ICMP traffic
iptables:
action: append
chain: "{{ item }}"
protocol: icmp
jump: ACCEPT
with_items: "{{ traffic_direction }}"
- name: Firewall rule - allow INPUT ports
iptables:
action: append
chain: INPUT
protocol: "{{ item.value.protocol }}"
destination_port: "{{ item.value.port }}"
jump: ACCEPT
loop: "{{ lookup('dict', input_ports) }}"
- name: Firewall rule - allow INPUT2 ports
iptables:
action: append
chain: OUTPUT
protocol: "{{ item.value.protocol }}"
source_port: "{{ item.value.port }}"
jump: ACCEPT
loop: "{{ lookup('dict', input_ports) }}"
- name: Firewall rule - allow OUTPUT ports
iptables:
action: append
chain: INPUT
protocol: "{{ item.value.protocol }}"
source_port: "{{ item.value.port }}"
jump: ACCEPT
loop: "{{ lookup('dict', output_ports) }}"
- name: Firewall rule - allow OUTPUT2 ports
iptables:
action: append
chain: OUTPUT
protocol: "{{ item.value.protocol }}"
destination_port: "{{ item.value.port }}"
jump: ACCEPT
loop: "{{ lookup('dict', output_ports) }}"
- name: Firewall rule - deny all traffic
iptables:
action: append
chain: "{{ item }}"
jump: DROP
with_items: "{{ traffic_direction }}"
Now we include the generic playbook in our playbook:
- name: Load generic fw rules
import_playbook: genericFw.yml hostlist=test
- hosts: test
vars:
allowed_hosts:
- 1.1.1.1
- 2.2.2.2
- 3.3.3.3
tasks:
- name: Firewall rule - allow INPUT custom hosts
iptables:
action: insert
chain: INPUT
rule_num: 1
source: "{{ item }}"
jump: ACCEPT
with_items:
- "{{ allowed_hosts }}"
- name: Firewall rule - allow OUTPUT custom hosts
iptables:
action: insert
chain: OUTPUT
rule_num: 1
destination: "{{ item }}"
jump: ACCEPT
with_items:
- "{{ allowed_hosts }}"
- name: Save iptables rules
shell: /etc/init.d/iptables save
We run the playbook:
We check that it has worked:
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT all -- 3.3.3.3 0.0.0.0/0
2 ACCEPT all -- 2.2.2.2 0.0.0.0/0
3 ACCEPT all -- 1.1.1.1 0.0.0.0/0
4 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
5 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
6 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:32002
7 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
8 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:53
9 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
10 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
11 ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:53
12 ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp spt:53
13 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:80
14 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:53
15 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:443
16 DROP all -- 0.0.0.0/0 0.0.0.0/0
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
Chain OUTPUT (policy ACCEPT)
num target prot opt source destination
1 ACCEPT all -- 0.0.0.0/0 3.3.3.3
2 ACCEPT all -- 0.0.0.0/0 2.2.2.2
3 ACCEPT all -- 0.0.0.0/0 1.1.1.1
4 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
5 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
6 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:32002
7 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:80
8 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:53
9 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:22
10 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:443
11 ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp spt:53
12 ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:53
13 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
14 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:53
15 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:443
16 DROP all -- 0.0.0.0/0 0.0.0.0/0
Indeed, the ports from the generic playbook and the IPs from the normal playbook are there.