diff --git a/README.md b/README.md index 6828899..b0fcd16 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,11 @@ After I found out `UFW` was too limited in terms of functionalities, I tried sev - Simplicity (not having to learn how role variables would generate the rules) - Persistence (reload the rules at boot) -This role is an attempt to solve these requirements. It currently supports only ipv4 on Debian and RedHat distributions. +This role is an attempt to solve these requirements. + +It supports **ipv4** and **ipv6*** on Debian and RedHat distributions. + +*ipv6 support was brought up thanks to [@maloddon](https://github.com/maloddon). It is currently in early stages and knowledgable people should review the [default rules](https://github.com/mikegleasonjr/ansible-role-firewall/blob/ipv6/defaults/main.yml). ipv6 rules are not configured by default. If you which to use them, don't forget to set `firewall_v6_configure` to `true`. Requirements ------------ @@ -28,9 +32,12 @@ Installation Role Variables -------------- -There are only 3 dictionaries to override in `defaults/main.yml`: +`defaults/main.yml`: ``` +firewall_v4_configure: true +firewall_v6_configure: false + firewall_v4_default_rules: 001 default policies: - -P INPUT ACCEPT @@ -47,16 +54,34 @@ firewall_v4_default_rules: - -A INPUT -p tcp --dport ssh -j ACCEPT 999 drop everything: - -P INPUT DROP - firewall_v4_group_rules: {} - firewall_v4_host_rules: {} +firewall_v6_default_rules: + 001 default policies: + - -P INPUT ACCEPT + - -P OUTPUT ACCEPT + - -P FORWARD DROP + 002 allow loopback: + - -A INPUT -i lo -s ::1/128 -d ::1/128 -j ACCEPT + - -A INPUT -i lo -s fe80::/64 -d fe80::/64 -j ACCEPT + 003 allow ping replies: + - -A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT + - -A OUTPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT + 100 allow established related: + - -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + 200 allow ssh: + - -A INPUT -p tcp --dport ssh -j ACCEPT + 999 drop everything: + - -P INPUT DROP +firewall_v6_group_rules: {} +firewall_v6_host_rules: {} + ``` -The keys to the dictionaries (`001 default policies`, `002 allow loopback`, ...) can be anything. They are only used for rules **ordering** and **overriding** (explained later). On rules generation, the keys are sorted alphabetically. Hence the 001s and 999s. +The keys to the `*_rules` dictionaries (`001 default policies`, `002 allow loopback`, ...) can be anything. They are only used for rules **ordering** and **overriding**. On rules generation, the keys are sorted alphabetically. That's why I chose here the 001s and 999s. -Those defaults will generate the following script to be executed on the host: +Those defaults will generate the following script to be executed on the host (for ipv4): ``` #!/bin/sh @@ -94,7 +119,7 @@ iptables -A INPUT -p tcp --dport ssh -j ACCEPT iptables -P INPUT DROP ``` -As you can see, the rules are ordered by the dictionary key. You can also observe that you can do pretty much what you want with the rules. In fact, the rules defined in the variables are simply the same rules you would pass to the `iptables` command. You have complete control over the rules syntax. +As you can see, you have complete control over the rules syntax. `$ iptables -L -n` on the host then shows... @@ -116,11 +141,11 @@ ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 icmptype 0 Now that takes care of the default rules. What about overriding? -The role provides 2 more variables where you can define more rules. Rules defined in those variables will be merged with the default rules. In fact, rules in `firewall_v4_host_rules` will be merged with `firewall_v4_group_rules`, and then the result will be merged back with the defaults. +You can change the rules for specific hosts and groups instead of re-defining everything. Rules in `firewall_v4_host_rules` will be merged with `firewall_v4_group_rules`, and then the result will be merged back with the defaults. Same thing for ipv6. This allows 3 levels of rules definition and overriding. I simply chose the names to match how the variable precedence works in Ansible (`all` -> `group` -> `host`). See the example playbook below to see rules overriding in action. -Example Playbook +Example Playbook (ipv4) ---------------- ``` @@ -169,11 +194,11 @@ firewall_v4_host_rules: 200 allow ssh limiting brute force: [] ``` -That's right, to "delete" rules, you just assign an empty list to an existing dictionary key. +To "delete" rules, you just assign an empty list to an existing dictionary key. To summarize, rules in `firewall_v4_host_rules` will overwrite rules in `firewall_v4_group_rules`, and then rules in `firewall_v4_group_rules` will overwrite rules in `firewall_v4_default_rules`. -You can play with the rules and see the generated script on the host at the following location: `/etc/iptables.v4.generated`. +You can play with the rules and see the generated script on the host at the following location: `/etc/iptables.v4.generated` and `/etc/iptables.v6.generated`. Dependencies ------------ diff --git a/defaults/main.yml b/defaults/main.yml index 791194c..dceb944 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,4 +1,7 @@ --- +firewall_v4_configure: true +firewall_v6_configure: false + firewall_v4_default_rules: 001 default policies: - -P INPUT ACCEPT @@ -15,7 +18,24 @@ firewall_v4_default_rules: - -A INPUT -p tcp --dport ssh -j ACCEPT 999 drop everything: - -P INPUT DROP - firewall_v4_group_rules: {} - firewall_v4_host_rules: {} + +firewall_v6_default_rules: + 001 default policies: + - -P INPUT ACCEPT + - -P OUTPUT ACCEPT + - -P FORWARD DROP + 002 allow loopback: + - -A INPUT -i lo -s ::1/128 -d ::1/128 -j ACCEPT + 003 allow ping replies: + - -A INPUT -p icmpv6 --icmpv6-type echo-request -j ACCEPT + - -A OUTPUT -p icmpv6 --icmpv6-type echo-reply -j ACCEPT + 100 allow established related: + - -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + 200 allow ssh: + - -A INPUT -p tcp --dport ssh -j ACCEPT + 999 drop everything: + - -P INPUT DROP +firewall_v6_group_rules: {} +firewall_v6_host_rules: {} diff --git a/tasks/persist-debian.yml b/tasks/persist-debian.yml index 5d729b6..cb7d355 100644 --- a/tasks/persist-debian.yml +++ b/tasks/persist-debian.yml @@ -1,12 +1,10 @@ --- -- name: Remove any obsolete v4 save script - file: path=/etc/network/if-post-down.d/iptables-v4 state=absent - -- name: Remove any obsolete v4 restore script - file: path=/etc/network/if-pre-up.d/iptables-v4 state=absent - -- name: Remove any obsolete v4 saved rules - file: path=/etc/iptables.v4.saved state=absent +- name: Remove any obsolete scripts used by an old version of the role + file: path={{ item }} state=absent + with_items: + - /etc/network/if-post-down.d/iptables-v4 + - /etc/network/if-pre-up.d/iptables-v4 + - /etc/iptables.v4.saved - name: Install iptables-persistent apt: name=iptables-persistent state=present @@ -14,7 +12,7 @@ - name: Check if netfilter-persistent is present shell: which netfilter-persistent register: is_netfilter - when: v4_script|changed + when: v4_script|changed or v6_script|changed changed_when: false ignore_errors: yes @@ -25,3 +23,13 @@ - name: Save v4 rules (iptables-persistent) command: /etc/init.d/iptables-persistent save when: v4_script|changed and is_netfilter.rc == 1 + +- name: Save v6 rules (netfilter-persistent) + command: netfilter-persistent save + when: v6_script|changed and is_netfilter.rc == 0 + +- name: Save v6 rules (iptables-persistent) + command: /etc/init.d/iptables-persistent save + when: v6_script|changed and is_netfilter.rc == 1 + + diff --git a/tasks/persist-redhat.yml b/tasks/persist-redhat.yml index c88ca37..4fd0323 100644 --- a/tasks/persist-redhat.yml +++ b/tasks/persist-redhat.yml @@ -3,6 +3,10 @@ shell: iptables-save -c > /etc/sysconfig/iptables when: v4_script|changed +- name: Save v6 rules (/etc/sysconfig/ip6tables) + shell: iptables-save -c > /etc/sysconfig/ip6tables + when: v6_script|changed + - name: Ensure iptables service is installed yum: name=iptables-services state=present when: ansible_distribution_major_version >= '7' diff --git a/tasks/rules.yml b/tasks/rules.yml index 8573311..b9154e3 100644 --- a/tasks/rules.yml +++ b/tasks/rules.yml @@ -2,9 +2,21 @@ - name: Generate v4 rules template: src=generated.v4.j2 dest=/etc/iptables.v4.generated owner=root group=root mode=755 register: v4_script + when: firewall_v4_configure - name: Load v4 rules command: /etc/iptables.v4.generated register: v4_script_load_result failed_when: v4_script_load_result.rc != 0 or 'unknown option' in v4_script_load_result.stderr when: v4_script|changed + +- name: Generate v6 rules + template: src=generated.v6.j2 dest=/etc/iptables.v6.generated owner=root group=root mode=755 + register: v6_script + when: firewall_v6_configure + +- name: Load v6 rules + command: /etc/iptables.v6.generated + register: v6_script_load_result + failed_when: v6_script_load_result.rc != 0 or 'unknown option' in v6_script_load_result.stderr + when: v6_script|changed diff --git a/templates/generated.v6.j2 b/templates/generated.v6.j2 new file mode 100644 index 0000000..56a3237 --- /dev/null +++ b/templates/generated.v6.j2 @@ -0,0 +1,26 @@ +#!/bin/sh +# {{ ansible_managed }} +{% set merged = firewall_v6_default_rules.copy() %} +{% set _ = merged.update(firewall_v6_group_rules) %} +{% set _ = merged.update(firewall_v6_host_rules) %} + +# flush rules & delete user-defined chains +ip6tables -F +ip6tables -X +ip6tables -t raw -F +ip6tables -t raw -X +ip6tables -t nat -F +ip6tables -t nat -X +ip6tables -t mangle -F +ip6tables -t mangle -X + +{% for group, rules in merged|dictsort %} +# {{ group }} +{% if not rules %} +# (none) +{% endif %} +{% for rule in rules %} +ip6tables {{ rule }} +{% endfor %} + +{% endfor %} diff --git a/tests.yml b/tests.yml index 31aab3e..67332ac 100644 --- a/tests.yml +++ b/tests.yml @@ -4,6 +4,8 @@ roles: - role: . + firewall_v6_configure: true + firewall_v4_group_rules: 400 allow http: - -A INPUT -p tcp --dport http -j ACCEPT @@ -12,11 +14,22 @@ firewall_v4_host_rules: 400 allow 7890: [] + firewall_v6_group_rules: + 400 allow http: + - -A INPUT -p tcp --dport http -j ACCEPT + 400 allow 7890: + - -A INPUT -p tcp --dport 7890 -j ACCEPT + firewall_v6_host_rules: + 400 allow 7890: [] + tasks: - name: Retrieve v4 rules command: iptables -L -n changed_when: false register: v4_rules + - name: Check that INPUT policy has been applied + assert: + that: "'Chain INPUT (policy DROP' in v4_rules.stdout" - name: Check that a default rule has been applied assert: that: "'tcp dpt:22' in v4_rules.stdout" @@ -26,3 +39,20 @@ - name: Check that deleted rules are deleted assert: that: "'tcp dpt:7890' not in v4_rules.stdout" + + - name: Retrieve v6 rules + command: ip6tables -L -n + changed_when: false + register: v6_rules + - name: Check that INPUT policy has been applied + assert: + that: "'Chain INPUT (policy DROP' in v6_rules.stdout" + - name: Check that a default rule has been applied + assert: + that: "'tcp dpt:22' in v6_rules.stdout" + - name: Check that a group rule has been applied + assert: + that: "'tcp dpt:80' in v6_rules.stdout" + - name: Check that deleted rules are deleted + assert: + that: "'tcp dpt:7890' not in v6_rules.stdout"