diff --git a/.travis.yml b/.travis.yml index 3e4ab90..123f4db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ --- +sudo: True language: 'python' python: '2.7' diff --git a/CHANGES.rst b/CHANGES.rst index 74bcecd..156f9eb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,15 +8,21 @@ Changelog **debops.ferm** This project adheres to `Semantic Versioning `__ -and `human-readable changelog `__. +and `human-readable changelog `__. -The current role maintainer_ is drybjed_ +The current role maintainer_ is drybjed_. `debops.ferm master`_ - unreleased ---------------------------------- -.. _debops.ferm master: https://github.com/debops/ansible-ferm/compare/v0.2.2...master +.. _debops.ferm master: https://github.com/debops/ansible-ferm/compare/v0.3.0...master + + +`debops.ferm v0.3.0`_ - 2017-07-12 +---------------------------------- + +.. _debops.ferm v0.3.0: https://github.com/debops/ansible-ferm/compare/v0.2.2...v0.3.0 Added ~~~~~ @@ -24,6 +30,8 @@ Added - Add a variable which can be used to restrict what network interfaces can be used for connections from Ansible Controller. [gaudenz] +- Update the Ansible facts automatically if they have been changed. [drybjed_] + Changed ~~~~~~~ @@ -33,6 +41,49 @@ Changed - Packets blocked due to rate limits will be now dropped instead of being rejected by default. [gaudenz] +- The data format of the firewall rules has been redesigned. Rules can now be + defined as nested YAML lists, existing default or dependent rules can + be easily modified through the Ansible inventory, multiple firewall rules can + be included in one configuration file. [drybjed_] + +- The firewall rules are now read from the :file:`/etc/ferm/rules.d/` directory + to help with transition to the new data format and avoid tab-completion + collision with the :file:`/etc/ferm/ferm.conf` file. [drybjed_] + +- Use of multiple rule parameters that define the final filename of the + configuration files has been dropped, now only the ``item.name`` parameter is + used to define the filename. [drybjed_] + +- The role automatically removes duplicate configuration files (based on the + ``name`` parameter) when the weight of a given rule is changed to make + modifications easier. [drybjed_] + +- The scale of the "weight" used to sort the rules in the directory has been + changed from 00-99 to 000-999. [drybjed_] + +- The ``item.weight`` parameter is now relative to the "weight class" or rule + type defined for a given firewall rule. You can use negative weight values + for better control over rule order. [drybjed_] + +- Run the ``debconf`` task only when APT is the package manager. This should + allow the role to be used on OSes other than Debian/Ubuntu. [drybjed_] + +- The :file:`/etc/ferm/ferm.conf` configuration file will be now properly + diverted to preserve the original. [drybjed_] + +Removed +~~~~~~~ + +- The ``ferm__default_weight`` variable has been removed. The default rule + weight is defined in the weight map directly. [drybjed_] + +- The role will no longer create the :file:`/etc/ferm/ferm.d/` directory by + default. Existing directories are not removed. [drybjed_] + +- The ``item.when`` and ``item.delete`` parameters are no longer supported. You + can control rule presence conditionally using ``item.rule_state`` or + ``item.state`` parameters. [drybjed_] + `debops.ferm v0.2.2`_ - 2016-12-01 ---------------------------------- @@ -44,7 +95,7 @@ Added - Write missing role documentation. [ganto_, ypid_, drybjed_] -- Allow to disable :envvar:`ferm__rules_forward` using +- Allow to disable ``ferm__rules_forward`` using :envvar:`ferm__forward_accept`. [ypid_] Changed @@ -53,7 +104,7 @@ Changed - Use the `Ansible package module`_ which requires Ansible v2.0. [ypid_] - Be more precise about the expected format of ``item.by_role`` in - :ref:`default_rules`. [ypid_] + :ref:`ferm__ref_default_rules`. [ypid_] - Move kernel parameters to enable reverse path filtering to the debops.sysctl_ role. [ypid_] @@ -76,7 +127,7 @@ Deprecated compatibility. [ypid_] - Deprecated ``item.role``, use ``item.by_role`` instead. Applies for: - :ref:`default_rules`. [ypid_] + :ref:`ferm__ref_default_rules`. [ypid_] `debops.ferm v0.2.1`_ - 2016-04-21 diff --git a/COPYRIGHT b/COPYRIGHT index d5f50a8..0f44145 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,9 +1,9 @@ debops.ferm - Manage iptables firewall using ferm -Copyright (C) 2013-2016 Maciej Delmanowski -Copyright (C) 2015-2016 Robin Schneider +Copyright (C) 2013-2017 Maciej Delmanowski +Copyright (C) 2015-2017 Robin Schneider Copyright (C) 2016 Reto Gantenbein -Copyright (C) 2014-2016 DebOps https://debops.org/ +Copyright (C) 2014-2017 DebOps https://debops.org/ This Ansible role is part of DebOps. diff --git a/README.md b/README.md index 930a63d..1ba09a0 100644 --- a/README.md +++ b/README.md @@ -51,4 +51,4 @@ License: [GPL-3.0](https://tldrlegal.com/license/gnu-general-public-license-v3-% *** -This role is part of the [DebOps](https://debops.org/) project. README generated by [ansigenome](https://github.com/nickjj/ansigenome/). +This role is part of [DebOps](https://debops.org/). README generated by [ansigenome](https://github.com/nickjj/ansigenome/). diff --git a/defaults/main.yml b/defaults/main.yml index 9b3337a..48eca91 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -88,7 +88,7 @@ ferm__ansible_controllers_ports: [ 'ssh' ] # # List of interfaces for the default Ansible Controllers rule. An empty list # means all interfaces. -ferm__ansible_controllers_interfaces: [ ] +ferm__ansible_controllers_interfaces: [] # ]]] # .. envvar:: ferm__default_policy_input [[[ @@ -240,8 +240,13 @@ ferm__log_burst: '5' ferm__log_group: '32' # ]]] # ]]] -# Rules configuration [[[ -# ----------------------- +# Firewall rules configuration [[[ +# -------------------------------- + +# The variables below define what rules should be present in the firewall. Each +# variable is a YAML dictionary with nested dictionaries. They are combined in +# the :envvar:`ferm__combined_rules` variable. See :ref:`ferm__ref_rules` for +# more details. # .. envvar:: ferm__include_legacy [[[ # @@ -249,31 +254,66 @@ ferm__log_group: '32' # transition to the new firewall rules in the future. ferm__include_legacy: True + # ]]] +# .. envvar:: ferm__dependent_rules [[[ +# +# YAML list which contains :command:`ferm` rules to manage defined by other +# Ansible roles using dependent variables. +ferm__dependent_rules: [] + + # ]]] +# .. envvar:: ferm__fix_dependent_rules [[[ +# +# For now, some rules defined by other Ansible roles are incomplete. This +# template makes sure that all required information is added if missing. This +# variable will be removed at some point in the future, therefore you should +# not rely on it. +ferm__fix_dependent_rules: '{{ lookup("template", + "lookup/ferm__fix_dependent_rules.j2", + convert_data=False) | from_json }}' + # ]]] # .. envvar:: ferm__rules [[[ # -# List of :command:`iptables` rules to manage, templates used by these rules are included -# in :file:`templates/etc/ferm/ferm.d/` directory. -# Additional variables are described below. +# YAML list which contains :command:`ferm` rules which should be defined on all +# hosts in the Ansible inventory. ferm__rules: [] # ]]] # .. envvar:: ferm__group_rules [[[ # -# List of :command:`iptables` rules to manage for a host group. +# YAML list which contains :command:`ferm` rules which should be defined on +# a group of hosts in the Ansible inventory. ferm__group_rules: [] # ]]] # .. envvar:: ferm__host_rules [[[ # -# List of :command:`iptables` rules to manage for an individual host. +# YAML list which contains :command:`ferm` rules which should be defined on +# specific hosts in the Ansible inventory. ferm__host_rules: [] # ]]] -# .. envvar:: ferm__dependent_rules [[[ +# .. envvar:: ferm__combined_rules [[[ # -# List of :command:`iptables` rules to manage depending on other rules. -ferm__dependent_rules: [] +# YAML list which defines the order in which firewall rules are defined and +# affect each other. This list is then passed to the parser template to +# generate final dictionary with rules for :command:`ferm`. +ferm__combined_rules: '{{ ferm__default_rules + + ferm__fix_dependent_rules + + ferm__rules + + ferm__group_rules + + ferm__host_rules }}' + + # ]]] +# .. envvar:: ferm__parsed_rules [[[ +# +# YAML dictionary which contains all of the defined :command:`ferm` rules +# combined together. This variable is used in the Ansible tasks that manage the +# rules on remote hosts. +ferm__parsed_rules: '{{ lookup("template", + "lookup/ferm__parsed_rules.j2", + convert_data=False) | from_yaml }}' # ]]] # .. envvar:: ferm_input_list [[[ @@ -306,161 +346,112 @@ ferm_input_host_list: [] ferm_input_dependent_list: [] # ]]] -# .. envvar:: ferm__default_weight [[[ +# .. envvar:: ferm__default_weight_map [[[ # -# Set the default rule weight for rules that do not specify it. -ferm__default_weight: '50' +# Dictionary with mapping between "rule classes" and their desired weight. +ferm__default_weight_map: + 'pre-hook': '00' + 'function': '00' + 'custom': '00' + 'loopback': '01' + 'default_policy': '05' + 'policy': '05' + 'ansible-controller': '05' + 'any-whitelist': '10' + 'filter-icmp': '15' + 'connection-tracking': '20' + 'filter-syn': '25' + 'any-blacklist': '30' + 'sshd-chain': '40' + 'any-forward': '60' + 'default': '100' + 'accept': '100' + 'any-service': '100' + 'reject': '900' + 'any-reject': '900' + 'post-hook': '950' # ]]] # .. envvar:: ferm__weight_map [[[ # -# Dictionary with mapping between "rule classes" and their desired weight. -ferm__weight_map: - 'loopback': '00' - 'ansible-controller': '01' - 'any-whitelist': '02' - 'filter-icmp': '03' - 'conntrack': '05' - 'filter-syn': '07' - 'any-blacklist': '09' - 'any-ssh-whitelist': '25' - 'any-ssh-accept': '30' - 'any-ssh-filter': '31' - 'any-service': '50' - 'any-reject': '99' +# Dictionary with additional mapping between "rule classes" and their desired +# weight. This variable can be used to override weight for specific weight +# classes. +ferm__weight_map: {} + + # ]]] +# .. envvar:: ferm__combined_weight_map [[[ +# +# YAML dictionary with the combined default and custom weight maps, used by the +# Ansible tasks. +ferm__combined_weight_map: '{{ ferm__default_weight_map + | combine(ferm__weight_map) }}' # ]]] # .. envvar:: ferm__default_rules [[[ # -# List of default firewall rules defined on each host. -ferm__default_rules: '{{ ferm__rules_default_policy + - ferm__rules_log + - ferm__rules_hooks + - ferm__rules_variables + - ferm__rules_filter_loopback + - ferm__rules_filter_ansible_controller + - ferm__rules_filter_icmp + - ferm__rules_filter_conntrack + - ferm__rules_filter_syn + - ferm__rules_filter_recent_badguys + - ferm__rules_accept_dhcpv6_client + - ferm__rules_filter_include_legacy + - ferm__rules_filter_recent_scanners + - ferm__rules_filter_reject_all + - ferm__rules_fail2ban + - ferm__rules_forward }}' - - # ]]] -# .. envvar:: ferm__rules_default_policy [[[ -# -# Configure default policies for built-in :command:`iptables` chains. -ferm__rules_default_policy: - - - type: 'default_policy' +# YAML dictionary with default firewall rules defined on each host. +ferm__default_rules: + + - name: 'policy_filter_input' + type: 'default_policy' chain: 'INPUT' - weight: '00' - name: 'filter_input' policy: '{{ ferm__default_policy_input }}' - - type: 'default_policy' + - name: 'policy_filter_forward' + type: 'default_policy' chain: 'FORWARD' - weight: '00' - name: 'filter_forward' policy: '{{ ferm__default_policy_forward }}' - - type: 'default_policy' + - name: 'policy_filter_output' + type: 'default_policy' chain: 'OUTPUT' - weight: '00' - name: 'filter_output' policy: '{{ ferm__default_policy_output }}' - # ]]] -# .. envvar:: ferm__rules_hooks [[[ -# -# Implement custom firewall hooks. -ferm__rules_hooks: - - - type: 'custom' - name: 'hooks' - weight: '00' + - name: 'firewall_hooks' + type: 'custom' comment: 'Run custom hooks at various firewall stages' rules: | @hook pre "run-parts /etc/ferm/hooks/pre.d"; @hook post "run-parts /etc/ferm/hooks/post.d"; @hook flush "run-parts /etc/ferm/hooks/flush.d"; -# ]]] -# .. envvar:: ferm__rules_variables [[[ -# -# Provide custom variables in the firewall configuration. -ferm__rules_variables: - - - type: 'custom' - name: 'variables' - weight: '00' + - name: 'firewall_variables' + type: 'custom' comment: 'Define custom variables available in the firewall' rules: | @def $domains = ({{ ferm__domains | unique | join(" ") }}); @def $ipv4_enabled = {{ "1" if "ip" in ferm__domains else "0" }}; @def $ipv6_enabled = {{ "1" if "ip6" in ferm__domains else "0" }}; -# ]]] -# .. envvar:: ferm__rules_log [[[ -# -# Create custom log function which is used by other firewall rules to log -# packets. -ferm__rules_log: - - - type: 'custom' - name: 'log' - weight: '00' + - name: 'firewall_log' + type: 'custom' comment: 'Custom log function used by other rules' - rule_state: '{{ "present" if (ferm__log | bool) else "absent" }}' rules: | @def &log($msg) = { mod limit limit {{ ferm__log_limit }} limit-burst {{ ferm__log_burst }} {{ ferm__log_target }}; } + rule_state: '{{ "present" if (ferm__log | bool) else "absent" }}' - # ]]] -# .. envvar:: ferm__rules_filter_loopback [[[ -# -# Allow connections from ``localhost``. -ferm__rules_filter_loopback: - - - type: 'accept' - weight: '00' + - name: 'accept_loopback' + type: 'accept' weight_class: 'loopback' - name: 'loopback' interface: 'lo' - # ]]] -# .. envvar:: ferm__rules_filter_ansible_controller [[[ -# -# Allow connections from Ansible Controllers. -ferm__rules_filter_ansible_controller: - - - type: 'ansible_controller' - weight: '01' + - name: 'accept_ansible_controller' + type: 'ansible_controller' weight_class: 'ansible-controller' comment: 'Accept SSH connections from Ansible Controllers' - name: 'rules' dport: '{{ ferm__ansible_controllers_ports }}' interface: '{{ ferm__ansible_controllers_interfaces }}' multiport: True accept_any: False - # ]]] -# .. envvar:: ferm__rules_filter_icmp [[[ -# -# Filter ICMP packets. -ferm__rules_filter_icmp: - - - type: 'hashlimit' - weight: '03' + - name: 'filter_icmp_flood' + type: 'hashlimit' weight_class: 'filter-icmp' - name: 'icmp-flood' protocol: 'icmp' enabled: '{{ ferm__filter_icmp | bool }}' hashlimit: '{{ ferm__filter_icmp_limit }}' @@ -469,27 +460,14 @@ ferm__rules_filter_icmp: hashlimit_target: 'ACCEPT' target: 'DROP' - # ]]] -# .. envvar:: ferm__rules_filter_conntrack [[[ -# -# Enable connection tracking for incoming, forwarded and outgoing traffic. -ferm__rules_filter_conntrack: - - - type: 'connection_tracking' - weight: '05' - weight_class: 'conntrack' + - name: 'connection_tracking' + type: 'connection_tracking' + weight_class: 'connection-tracking' chain: [ 'INPUT', 'OUTPUT', 'FORWARD' ] - # ]]] -# .. envvar:: ferm__rules_filter_syn [[[ -# -# Filter TCP SYN packets. -ferm__rules_filter_syn: - - - type: 'hashlimit' - weight: '07' + - name: 'filter_syn_flood' + type: 'hashlimit' weight_class: 'filter-syn' - name: 'syn-flood' protocol: 'tcp' protocol_syn: True rule_state: '{{ "present" if (ferm__filter_syn | bool) else "absent" }}' @@ -499,16 +477,9 @@ ferm__rules_filter_syn: hashlimit_target: 'RETURN' target: 'DROP' - # ]]] -# .. envvar:: ferm__rules_filter_recent_badguys [[[ -# -# Block all packets marked as "badguys" later in the firewall. -ferm__rules_filter_recent_badguys: - - - type: 'recent' - weight: '09' + - name: 'block_recent_badguys' + type: 'recent' weight_class: 'any-blacklist' - name: 'block-recent-badguys' comment: 'Reject packets marked as "badguys"' rule_state: '{{ "present" if (ferm__filter_recent | bool) else "absent" }}' recent_name: '{{ ferm__filter_recent_name }}' @@ -516,33 +487,18 @@ ferm__rules_filter_recent_badguys: recent_seconds: '{{ ferm__filter_recent_time }}' recent_target: 'REJECT' - - type: 'recent' - weight: '09' + - name: 'clean_recent_badguys' + type: 'recent' weight_class: 'any-blacklist' - name: 'clean-recent-badguys' comment: 'Reject packets marked as "badguys"' rule_state: '{{ "present" if (ferm__filter_recent | bool) else "absent" }}' recent_name: '{{ ferm__filter_recent_name }}' recent_remove: True recent_log: False - # ]]] -# .. envvar:: ferm__rules_accept_dhcpv6_client [[[ -# -# Allow DHCPv6 responses. -# -# Related to: -# -# * https://serverfault.com/questions/513772/getting-an-ip-from-an-ipv6-dhcp-server -# * https://bugzilla.redhat.com/show_bug.cgi?id=656334 -# * https://bugzilla.redhat.com/show_bug.cgi?id=591630 -# -ferm__rules_accept_dhcpv6_client: - - - type: 'accept' - weight: '50' + - name: 'accept_dhcpv6_client' + type: 'accept' weight_class: 'any-service' - filename: 'dhcpv6-client' comment: 'DHCPv6 responses seem to be neither RELATED nor ESTABLISHED.' domain: [ 'ip6' ] saddr: [ 'fe80::/10' ] @@ -551,113 +507,82 @@ ferm__rules_accept_dhcpv6_client: sport: [ 'dhcpv6-server' ] dport: [ 'dhcpv6-client' ] - # ]]] -# .. envvar:: ferm__rules_filter_include_legacy [[[ -# -# Include rules from legacy ferm directory. -ferm__rules_filter_include_legacy: - - - type: 'accept' - weight: '50' - weight_class: 'any-service' - filename: 'jump_legacy-rules' + - name: 'jump_to_legacy_input_rules' + type: 'accept' + weight: '-10' + weight_class: 'reject' comment: 'Jump to legacy firewall rules' target: 'debops-legacy-input-rules' rule_state: '{{ "present" if (ferm__include_legacy | bool) else "absent" }}' - - type: 'include' - weight: 'zz' + - name: 'include_legacy_input_rules' + type: 'include' + weight_class: 'post-hook' chain: 'debops-legacy-input-rules' comment: 'Include legacy firewall rules' - name: 'legacy-input-rules' include: '/etc/ferm/filter-input.d/' rule_state: '{{ "present" if (ferm__include_legacy | bool) else "absent" }}' - # ]]] -# .. envvar:: ferm__rules_filter_recent_scanners [[[ -# -# Block potential port scanners that try to access closed ports. -ferm__rules_filter_recent_scanners: - - - type: 'recent' + - name: 'block_portscans' + type: 'recent' weight: '85' - name: 'block-portscans' comment: 'Mark potential port scanners as bad guys' recent_set_name: '{{ ferm__filter_recent_name }}' rule_state: '{{ "present" if (ferm__mark_portscan | bool) else "absent" }}' - # ]]] -# .. envvar:: ferm__rules_filter_reject_all [[[ -# -# Reject all incoming packets not handled by previous rules. -ferm__rules_filter_reject_all: - - - type: 'reject' - weight: '99' - weight_class: 'any-reject' + - name: 'reject_all' + type: 'reject' - # ]]] -# .. envvar:: ferm__rules_fail2ban [[[ -# -# :program:`fail2ban` support rules for ferm. -ferm__rules_fail2ban: - - - type: 'fail2ban' - weight: 'zz' + - name: 'fail2ban-hook' + type: 'fail2ban' comment: 'Reload fail2ban rules' rule_state: '{{ "present" if (ferm__fail2ban | bool) else "absent" }}' + rules: | + @hook post "type fail2ban-server > /dev/null && (fail2ban-client ping > /dev/null && fail2ban-client reload > /dev/null || true) || true"; + @hook flush "type fail2ban-server > /dev/null && (fail2ban-client ping > /dev/null && fail2ban-client reload > /dev/null || true) || true"; + weight_class: 'post-hook' - # ]]] -# .. envvar:: ferm__rules_forward [[[ -# -# Manage packet forwarding to other hosts/containers. -ferm__rules_forward: - - - chain: 'FORWARD' + - name: 'forward_external_in' + chain: 'FORWARD' type: 'accept' + weight: '1' + weight_class: 'any-forward' interface_present: '{{ ferm__external_interfaces | unique }}' - weight: '10' - role: 'forward' - role_weight: '10' - name: 'external_in' comment: 'Forward incoming traffic to other hosts' - rule_state: '{{ "present" if ( - ferm__forward_accept|bool and ( - (ferm__forward|d(ferm_forward) | bool) or - (ansible_local|d() and ansible_local.ferm|d() and - ansible_local.ferm.forward | bool))) - else "absent" }}' - - - chain: 'FORWARD' + rule_state: '{{ "present" + if (ferm__forward_accept|bool and ferm__forward | bool) + else "ignore" }}' + + - name: 'forward_external_out' + chain: 'FORWARD' type: 'accept' + weight: '2' + weight_class: 'any-forward' outerface_present: '{{ ferm__external_interfaces | unique }}' - weight: '10' - role: 'forward' - role_weight: '20' - name: 'external_out' comment: 'Forward outgoing traffic to other hosts' - rule_state: '{{ "present" if ( - ferm__forward_accept|bool and ( - (ferm__forward|d(ferm_forward) | bool) or - (ansible_local|d() and ansible_local.ferm|d() and - ansible_local.ferm.forward | bool))) - else "absent" }}' - - - chain: 'FORWARD' + rule_state: '{{ "present" + if (ferm__forward_accept|bool and ferm__forward | bool) + else "ignore" }}' + + - name: 'forward_internal' + chain: 'FORWARD' type: 'accept' interface_present: '{{ ferm__internal_interfaces }}' outerface_present: '{{ ferm__internal_interfaces }}' - weight: '10' - role: 'forward' - role_weight: '30' - name: 'internal' + weight: '3' + weight_class: 'any-forward' comment: 'Forward internal traffic between hosts' - rule_state: '{{ "present" if ( - ferm__forward_accept|bool and ( - (ferm__forward|d(ferm_forward) | bool) or - (ansible_local|d() and ansible_local.ferm|d() and - ansible_local.ferm.forward | bool))) - else "absent" }}' + rule_state: '{{ "present" + if (ferm__forward_accept|bool and ferm__forward | bool) + else "ignore" }}' + + - name: 'fix_bootpc_checksum' + type: 'custom' + rules: | + # Add checksums to BOOTP packets from virtual machines and containers. + # https://www.redhat.com/archives/libvir-list/2010-August/msg00035.html + @hook post "iptables -A POSTROUTING -t mangle -p udp --dport bootpc -j CHECKSUM --checksum-fill"; + rule_state: 'ignore' # ]]] # .. envvar:: ferm__custom_files [[[ @@ -693,15 +618,20 @@ ferm__fail2ban: True # Enable forwarding in :command:`ip(6)tables`. This option can be used by other # roles and it's status is saved in Ansible local facts, which will override # variable status from role defaults or inventory. -ferm__forward: False +ferm__forward: '{{ ansible_local.ferm.forward + if (ansible_local|d() and ansible_local.ferm|d() and + ansible_local.ferm.forward|d()) + else "False" }}' # ]]] # .. envvar:: ferm__forward_accept [[[ # # Should traffic be forwarded between the other hosts/containers if # :envvar:`ferm__forward` is ``True``? -# Refer to :envvar:`ferm__rules_forward` for details. -ferm__forward_accept: False +ferm__forward_accept: '{{ ansible_local.ferm.forward + if (ansible_local|d() and ansible_local.ferm|d() and + ansible_local.ferm.forward|d()) + else "False" }}' # ]]] # .. envvar:: ferm__external_interfaces [[[ diff --git a/docs/defaults-detailed.rst b/docs/defaults-detailed.rst index d311848..bd6a1d0 100644 --- a/docs/defaults-detailed.rst +++ b/docs/defaults-detailed.rst @@ -11,6 +11,127 @@ them. :local: :depth: 1 +.. _ferm__ref_rules: + +ferm__rules +----------- + +The ``ferm__*_rules`` variables are YAML lists which define what +firewall rules are configured on a host. The rules are combined together in the +:envvar:`ferm__combined_rules` variable which defines the order of the rule +variables and therefore how they will affect each other. + +Each entry in the ``ferm__*_rules`` lists is a YAML dictionary. The entry needs +to have the ``name`` parameter that specifies the rule name, otherwise it will +be skipped. + +The result is stored as :envvar:`ferm__parsed_rules` variable. This order +allows modification of the default rules as well as rules defined by other +Ansible roles using Ansible inventory variables. + +The rules are stored in the :file:`/etc/ferm/rules.d/` directory and +the filename format is: + +.. code-block:: none + + /etc/ferm/rules.d/_rule_.conf + +The rule "weight" is determined by a given rule type which can be overridden if +needed, see the ``type``, ``weight`` and ``weight_class`` parameters for more +details. + +Each rule defined in a dictionary uses specific parameters. The parameters +described here are general ones, mostly usable on the main "level" and are +related to management of rule files. The parameters related to specific +:command:`ferm` rules are described in :ref:`ferm__ref_firewall_rules` +documentation. + +``name`` + Name of the firewall rule to configure. An example rule definition: + + .. code-block:: yaml + + ferm__rules: + - name: 'accept_all_connections' + type: 'accept' + accept_any: True + +``rules`` + Either a string or a YAML text block that contains raw :command:`ferm` + configuration options, or a list of YAML dictionaries which specify firewall + rules. If this parameter is not specified, role will try and generate rules + automatically based on other parameters specified on the "first level" of + a given rule definition. Most of the other parameters can be specified on the + "second level" rules and will apply to a given rule in the list. + + Example custom rule definition that restarts :command:`nginx` after firewall + is modified: + + .. code-block:: yaml + + ferm__rules: + - name: 'restart_nginx': + type: 'post-hook' + rules: '@hook post "type nginx > /dev/null && systemctl restart nginx || true";' + + Example list of rule definitions which will open access to different service + ports; rules will be present in the same file: + + .. code-block:: yaml + + ferm__rules: + - name: 'allow_http_https' + rules: + + - dport: 'http' + accept_any: True + + - dport: 'https' + accept_any: True + +``rule_state`` + Optional. Specify the state of the firewall rule file, or one of the + rules included in that file. Supported states: + + - ``present``: default. The rule file will be created if it doesn't exist, + a rule will be present in the file. + + - ``absent``: The rule file will be removed, a rule in the file will not be + generated. + + - ``ignore``: the role will not change the current state of the configuration + file. This value does not have an effect on the rules inside the file. + +``comment`` + Optional. Add a comment in the rule configuration file, either as a string or + as a YAML text block. + +``template`` + Optional. Name of the template to use to generate the firewall rule file. + Currently only one template is available, ``rule`` so this option is not + useful yet. + +``type`` + Optional. Specify the rule type as a name, for example ``accept`` or + ``reject``. Different rule types can use different rule parameters, the rule + type also affects the "weight" used to order the configuration files. Weight + of the different rules is specified in the :envvar:`ferm__default_weight_map` + variable and can be overridden using the :envvar:`ferm__weight_map` variable. + + List of known rule types can be found in the :ref:`ferm__ref_firewall_rules` + documentation. + +``weight_class`` + Optional. Override the rule type with another type, to change the sort order + of the configuration files. This parameter does not affect the + :command:`ferm` configuration template, only the resulting filename. + +``weight`` + Optional. Additional positive or negative number (for example ``2`` or + ``-2``) which will be added to the rule weight affecting the file sorting + order. + + .. _ferm__ref_input_list: ferm_input_list diff --git a/docs/guides.rst b/docs/guides.rst index 9fd109d..9b564c5 100644 --- a/docs/guides.rst +++ b/docs/guides.rst @@ -53,7 +53,7 @@ variables. ferm__default_policy_forward: DROP -* As soon as :envvar:`ferm__forward` is enabled, :envvar:`ferm__rules_forward` +* As soon as :envvar:`ferm__forward` is enabled, the default role configuration will create a default rule list which will accept every incoming or outgoing packet with a valid forward target. This can make sense if forwarding should be enabled on a virtualization host to allow packets in and out a separate @@ -84,12 +84,12 @@ variables. else "absent" }}' If there are multiple internal interfaces additional rules permitting packet - forwarding between those might be necessary. Check the ``internal`` rule of - the default :envvar:`ferm__rules_forward` for an example. + forwarding between those might be necessary. Check the ``forward_internal`` rule of + the default :envvar:`ferm__default_rules` for an example. * Once a packet was accepted by the firewall all related packets belonging to the same connection are accepted too. This is defined in the - :envvar:`ferm__rules_filter_conntrack` rule which is loaded as part of the + ``connection_tracking`` rule which is loaded as part of the :envvar:`ferm__default_rules` rule list. @@ -122,7 +122,7 @@ address of a network packet is rewritten to the internal host address. .. topic:: Note - The :ref:`dmz_template` rule template won't modify the source address of a + The :ref:`ferm__ref_type_dmz` rule template won't modify the source address of a forwarded packet. This means that the original source address can still be identified at the internal receiver, however the route leading back to the source address must traverse the gateway again in order to successfully @@ -198,7 +198,7 @@ any other purpose. * First create an Ansible list with an individually chosen name which will hold the custom output rules. For every outgoing connection which should be allowed to the internal or external network a rule needs to be added. Every - template described in the :ref:`rule_templates` chapter can be used for the + template described in the :ref:`ferm__ref_rule_types` chapter can be used for the custom rules. The definition below is just a minimal example to show the procedure:: @@ -248,7 +248,7 @@ any other purpose. name: 'reject_out' comment: 'Reject remaining outgoing traffic' - The last rule is using the :ref:`reject_template` template which will reject + The last rule is using the :ref:`ferm__ref_type_reject` which will reject every packet not explicitly allowed. This will make it easier to figure out missing rules than if the packets would simply be dropped. @@ -270,8 +270,8 @@ any other purpose. Block Port Scans ~~~~~~~~~~~~~~~~ -To block port scans there is a predefined rule list -:envvar:`ferm__rules_filter_recent_scanners` which is not enabled by default. +To block port scans there is a predefined rule ``block_portscans`` which is not +enabled by default. It will remember source addresses which try to reach closed ports and completely blocks access from those addresses for a while. This behaviour can be enabled by setting :envvar:`ferm__mark_portscan`:: diff --git a/docs/playbooks/ferm.yml b/docs/playbooks/ferm.yml index cae7cb7..822a644 100644 --- a/docs/playbooks/ferm.yml +++ b/docs/playbooks/ferm.yml @@ -4,8 +4,11 @@ hosts: [ 'debops_all_hosts', 'debops_service_ferm' ] become: True + environment: '{{ inventory__environment | d({}) + | combine(inventory__group_environment | d({})) + | combine(inventory__host_environment | d({})) }}' + roles: - role: debops.ferm tags: [ 'role::ferm' ] - diff --git a/docs/rules.rst b/docs/rules.rst index 755a949..3a6c39b 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -1,3 +1,5 @@ +.. _ferm__ref_firewall_rules: + Firewall Rule Definitions ========================= @@ -14,7 +16,7 @@ requirements. :local: :depth: 2 -.. _default_rules: +.. _ferm__ref_default_rules: Default rules ------------- @@ -30,7 +32,7 @@ In case a firewall is not required or preferred this behaviour can be disabled by setting :envvar:`ferm__enabled` to ``False`` in the inventory. -.. _custom_rules: +.. _ferm__ref_custom_rules: Custom rules ------------ @@ -41,18 +43,6 @@ predefined rule lists (:envvar:`ferm__rules`, :envvar:`ferm__group_rules`, inventory. Each rule has to be defined as a YAML dict using some of the following keys: -``type`` - Type of the rule template used for creating the corresponding ferm - configuration, required. See `Rule templates`_ for a description of - the available rule templates. - -``chain`` - Optional. :command:`iptables` chain to which the rule is added or from which it - is removed. Defaults to ``INPUT``. - -``comment`` - Optional. Comment which should be added to the generated rule configuration. - ``domain`` Optional. :command:`iptables` domain used for the firewall rule. Possible values: :command:`ip`, ``ip6``. Defaults to :envvar:`ferm__domains`. @@ -61,71 +51,37 @@ the following keys: Optional. :command:`iptables` table to which the rule is added or from which it is removed. Defaults to ``filter``. -``filename`` - Optional. Set custom filename for ferm rule definition instead of generated - one. - -``name`` - Optional. Set rule name in ferm configuration file when ``item.filename`` is - not set and other places where a custom rule name might be useful. +``chain`` + Optional. :command:`iptables` chain to which the rule is added or from which it + is removed. Defaults to ``INPUT``. ``by_role`` Optional. Name of the Ansible role in the format ``ROLE_OWNER.ROLE_NAME`` - which is responsable for the rule. - The sanitized name will be included in the autogenerated filename. - -``role`` - Deprecated. Use ``by_role`` instead. - -``role_weight`` - Optional. This allows to set the same ``item.weight`` for all rules of a - particular Ansible role. - -``rule_state`` - Optional. Specify if rule is to be added or removed. Possible values: - ``present`` or ``absent``. Defaults to ``present``. - -``delete`` - This option is deprecated, see `discussion `_. - Use ``rule_state`` instead. - Delete rule from :program:`ferm` configuration. Possible values ``True`` - or ``False``. Defaults to ``False``. - -``weight`` - Optional. Helps with file sorting in rule directory. - -``weight_class`` - Optional. Helps to manage order of firewall rules. The ``item.weight_class`` - will be checked in the :envvar:`ferm__weight_map` dictionary. If a corresponding - entry is found, its weight will be used for that rule, if not, the - ``item.weight`` specified in the rule will be used instead. + which is responsable for the rule. This will be included as a comment in the + generated rule file. Depending on the chosen type, many additional variables are supported. -Please check the individual template description below. - +Please check the individual rule type description below. -.. _rule_templates: -Rule templates --------------- +.. _ferm__ref_rule_types: -There exist a number of predefined rule templates for generating firewall -rules through ferm. Each rule definition is referencing the used template -through its ``item.type`` key. The templates are located in the -:file:`templates/etc/ferm/ferm.d/` directory. +Rule types +---------- -Following a list of the available rule templates which can be used to -create custom rules. +There exist a number of predefined rule types for generating firewall rules +through :command:`ferm`. Following a list of the available rule types which can +be used to create custom rules. -.. _accept_template: +.. _ferm__ref_type_accept: -accept -^^^^^^ +The 'accept' type +~~~~~~~~~~~~~~~~~ -Template to create rules that match interfaces, ports, remote IP -addresses/subnets and can accept the packets, reject, or redirect to a -different chain. The following template-specific YAML keys are supported: +This rule type can be used to create rules that match interfaces, ports, remote +IP addresses/subnets and can accept the packets, reject, or redirect to +a different chain. The following type-specific YAML keys are supported: ``accept_any`` Optional. Match all source addresses by default. Possible values: ``True`` @@ -141,13 +97,9 @@ different chain. The following template-specific YAML keys are supported: ``dport`` Optional. List of destination ports to which the rule is applied. -``enabled`` - Optional. Enable rule definition. Possible values: ``True`` or ``False``. - Defaults to ``True``. - ``include`` - Optional. Custom ferm configuration file to include. See `ferm include`_ - for more details. + Optional. Custom :command:`ferm` configuration file to include. + See `ferm include`_ for more details. ``interface`` Optional. List of network interfaces for incoming packets to which the @@ -206,25 +158,20 @@ different chain. The following template-specific YAML keys are supported: Optional. :command:`iptables` jump target. Possible values: ``ACCEPT``, ``DROP``, ``REJECT``, ``RETURN``, ``NOP`` or a custom target. Defaults to ``ACCEPT``. -``when`` - This option is deprecated, see `discussion `_. - Use ``rule_state`` instead. - Optional. Define condition for the rule to be disabled. - .. _ferm include: http://ferm.foo-projects.org/download/2.1/ferm.html#includes .. _ferm realgoto: http://ferm.foo-projects.org/download/2.1/ferm.html#realgoto_custom_chain_name .. _ferm subchain: http://ferm.foo-projects.org/download/2.1/ferm.html#_subchain -.. _ansible_controller_template: +.. _ferm__ref_type_ansible_controller: -ansible_controller -^^^^^^^^^^^^^^^^^^ +The 'ansible_controller' type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Similar to the `accept_template`_ template but defaults to the SSH target -port and sets the source address to the host running Ansible if not -overwritten through the ``item.ansible_controllers`` key. The following -template-specific YAML keys are supported: +Similar to the ``accept`` type but defaults to the SSH target port and sets the +source address to the host running Ansible if not overwritten through the +``item.ansible_controllers`` key. The following type-specific YAML keys are +supported: ``ansible_controllers`` Optional. List of source IP address which are added to ``item.saddr``. @@ -238,10 +185,6 @@ template-specific YAML keys are supported: Optional. List of destination ports to which the rule is applied. Defaults to :command:`ssh`. -``enabled`` - Optional. Enable rule definition. Possible values: ``True`` or ``False``. - Defaults to ``True``. - ``include`` Optional. Custom ferm configuration file to include. See `ferm include`_ for more details. @@ -295,19 +238,16 @@ template-specific YAML keys are supported: Optional. :command:`iptables` jump target. Possible values: ``ACCEPT``, ``DROP``, ``REJECT``, ``RETURN``, ``NOP`` or a custom target. Defaults to ``ACCEPT``. -This template is used in the default rule :envvar:`ferm__rules_filter_ansible_controller` -which enables SSH connections from the Ansible controller host. - .. _iptables multiport: http://ipset.netfilter.org/iptables-extensions.man.html#lbBM -.. connection_tracking_template: +.. _ferm__ref_type_connection_tracking: -connection_tracking -^^^^^^^^^^^^^^^^^^^ +The 'connection_tracking' type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Template to enable connection tracking using the `iptables conntrack`_ or -`iptables state`_ extension. The following template-specific YAML keys are +This type is used to enable connection tracking using the `iptables conntrack`_ +or `iptables state`_ extension. The following type-specific YAML keys are supported: ``active_target`` @@ -338,21 +278,17 @@ supported: Optional. List of network interfaces for outgoing packets which are excluded from the rule. -This template is used in the default rule :envvar:`ferm__rules_filter_conntrack` -which enables connection tracking in the ``INPUT``, ``OUTPUT`` and ``FORWARD`` -chain. - .. _iptables conntrack: http://ipset.netfilter.org/iptables-extensions.man.html#lbAO .. _iptables state: http://ipset.netfilter.org/iptables-extensions.man.html#lbCC -.. _custom_template: +.. _ferm__ref_type_custom: -custom -^^^^^^ +The 'custom' type +~~~~~~~~~~~~~~~~~ -Template to define custom ferm rules. The following additional YAML keys are -supported: +The type used to define custom :command:`ferm` rules. The following additional +YAML keys are supported: ``rules`` ferm rule definition, required. @@ -364,31 +300,26 @@ supported: This template is used among others in a debops.libvirtd_ custom ferm rule. -.. _default_policy_template: +.. _ferm__ref_type_default_policy: -default_policy -^^^^^^^^^^^^^^ +The 'default_policy' type +~~~~~~~~~~~~~~~~~~~~~~~~~ -Template to define :command:`iptables` default policies. The following -template-specific YAML keys are supported: +This type is used to define :command:`iptables` default policies. The following +type-specific YAML keys are supported: ``policy`` :command:`iptables` chain policy, required. -This template is used in the default rule :envvar:`ferm__rules_default_policy` -which sets the ``INPUT``, ``FORWARD`` and ``OUTPUT`` chain policies according -to :envvar:`ferm__default_policy_input`, :envvar:`ferm__default_policy_forward` -and :envvar:`ferm__default_policy_output`. +.. _ferm__ref_type_dmz: -.. _dmz_template: +The 'dmz' type +~~~~~~~~~~~~~~ -dmz -^^^ - -Template to enable connection forwarding to another host. If ``item.port`` -is not specified, all traffic is forwarded. The following template-specific -YAML keys are supported: +This type can be used to enable connection forwarding to another host. If +``item.port`` is not specified, all traffic is forwarded. The following +type-specific YAML keys are supported: ``multiport`` Optional. Use `iptables multiport`_ extension. Possible values: ``True`` @@ -411,25 +342,13 @@ YAML keys are supported: internal destination port is different from the original destination port. -.. _fail2ban_template: +.. _ferm__ref_type_hashlimit: -fail2ban -^^^^^^^^ +The 'hashlimit' type +~~~~~~~~~~~~~~~~~~~~ -Template to integrate fail2ban with :program:`ferm`. As the fail2ban service is -defining its own :command:`iptables` chains the template will make sure that they -are properly refreshed if the :program:`ferm` configuration changes. - -This template is used in the default rule :envvar:`ferm__rules_fail2ban`. - - -.. _hashlimit_template: - -hashlimit -^^^^^^^^^ - -Template to define rate limit rules using the `iptables hashlimit`_ extension. -The following template-specific YAML keys are supported: +This type is used to define rate limit rules using the `iptables hashlimit`_ +extension. The following type-specific YAML keys are supported: ``daddr`` Optional. List of destination IP addresses or networks to which the @@ -438,10 +357,6 @@ The following template-specific YAML keys are supported: ``dport`` Optional. List of destination ports to which the rule is applied. -``enabled`` - Optional. Enable rule definition. Possible values: ``True`` and ``False``. - Defaults to ``True``. - ``hashlimit_burst`` Optional. Number of packets to match within the expiration time. Defaults to ``5``. @@ -508,36 +423,32 @@ The following template-specific YAML keys are supported: Optional. :command:`iptables` jump target in case the rate limit is reached. Defaults to ``REJECT``. -This template is used in the default rules :envvar:`ferm__rules_filter_icmp` and -:envvar:`ferm__rules_filter_syn` which limits the packet rate for ICMP packets -and new connection attempts. - .. _iptables hashlimit: http://ipset.netfilter.org/iptables-extensions.man.html#lbAY -.. _include_template: +.. _ferm__ref_type_include: -include -^^^^^^^ +The 'include' type +~~~~~~~~~~~~~~~~~~ -Template to include custom ferm configuration files. The following -template-specific YAML keys are supported: +This type can be used to include custom :command:`ferm` configuration files. +The following type-specific YAML keys are supported: ``include`` Optional. Custom ferm configuration file to include. See `ferm include`_ for more details. -.. _log_template: +.. _ferm__ref_type_log: -log -^^^ +The 'log' type +~~~~~~~~~~~~~~ -Template to specify logging rules using the `iptables log`_ extension. -The following template-specific YAML keys are supported: +This type can be used to specify logging rules using the `iptables log`_ +extension. The following type-specific YAML keys are supported: ``include`` - Optional. Custom ferm configuration file to include. See + Optional. Custom :command:`ferm` configuration file to include. See `ferm include`_ for more details. ``log_burst`` @@ -587,13 +498,13 @@ The following template-specific YAML keys are supported: .. _iptables log: http://ipset.netfilter.org/iptables-extensions.man.html#lbDD -.. _recent_template: +.. _ferm__ref_type_recent: -recent -^^^^^^ +The 'recent' type +~~~~~~~~~~~~~~~~~ -Template to track connections and respond accordingly by using the -`iptables recent`_ extension. The following template-specific YAML keys are +This type can be used to track connections and respond accordingly by using the +`iptables recent`_ extension. The following type-specific YAML keys are supported: ``dport`` @@ -667,7 +578,7 @@ supported: separate subchain with the name given. See `ferm subchain`_ for more details. -When using the `recent_template`_ template make sure to always define two +When using the ``recent`` type make sure to always define two rules: * One for matching the packet against the address list using the @@ -677,22 +588,19 @@ rules: * To clear the source address from the list again in case the connection restrictions are not met, add a second role using ``item.recent_remove``. -This template is used in the default role :envvar:`ferm__rules_filter_recent_badguys` -which will block IP addresses which are doing excessive connection attempts. - .. _iptables recent: http://ipset.netfilter.org/iptables-extensions.man.html#lbBW -.. _reject_template: +.. _ferm__ref_type_reject: -reject -^^^^^^ +The 'reject' type +~~~~~~~~~~~~~~~~~ -Template to reject all traffic. It can be added for example as a final rule -in a custom chain. +This type is used to reject all traffic. It can be added for example as a final +rule in a custom chain. -.. _legacy_rules: +.. _ferm__ref_legacy_rules: Legacy rules ------------ diff --git a/tasks/main.yml b/tasks/main.yml index 561f072..186e643 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -6,6 +6,7 @@ question: 'ferm/enable' vtype: 'boolean' value: '{{ "yes" if ferm__enabled|bool else "no" }}' + when: ansible_pkg_mgr == 'apt' - name: Ensure ferm is installed package: @@ -24,7 +25,7 @@ group: 'adm' mode: '2750' with_items: - - '/etc/ferm/ferm.d' + - '/etc/ferm/rules.d' - '/etc/ferm/filter-input.d' - '/etc/ferm/hooks/pre.d' - '/etc/ferm/hooks/post.d' @@ -59,6 +60,12 @@ mode: '0644' notify: [ 'Restart ferm' ] +- name: Divert the original ferm configuration file + command: dpkg-divert --quiet --local --divert /etc/ferm/ferm.conf.dpkg-divert --rename /etc/ferm/ferm.conf + args: + creates: '/etc/ferm/ferm.conf.dpkg-divert' + when: ferm__enabled|bool + - name: Configure main ferm config file template: src: 'etc/ferm/ferm.conf.j2' @@ -67,55 +74,49 @@ group: 'adm' mode: '0644' notify: [ 'Restart ferm' ] + when: ferm__enabled|bool + +- name: Revert the original configuration file + shell: rm -f /etc/ferm/ferm.conf ; dpkg-divert --quiet --local --rename --remove /etc/ferm/ferm.conf + args: + removes: '/etc/ferm/ferm.conf.dpkg-divert' + warn: False + when: not ferm__enabled|bool -- name: Remove ip(6)tables rules if requested +- name: Remove firewall rules file: - dest: '/etc/ferm/ferm.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d(ferm__default_weight)) }}_{{ item.filename | d((((item.by_role|d(item.role)| replace(".", "_")) + "_" + ((item.role_weight + "_") if item.role_weight|d() else "")) if (item.by_role|d(item.role)|d()) else "") + item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' + dest: '/etc/ferm/rules.d/{{ "%03d" | format((ferm__combined_weight_map[item.value.weight_class | d(item.value.type | d("default"))] | d("80"))|int + (item.value.weight | d("0"))|int) }}_rule_{{ item.value.name | d(item.key) }}.conf' state: 'absent' - with_flattened: - - '{{ ferm_rules | d([]) | list }}' - - '{{ ferm_group_rules | d([]) | list }}' - - '{{ ferm_host_rules | d([]) | list }}' - - '{{ ferm_dependent_rules | d([]) | list }}' - - '{{ ferm_default_rules | d([]) | list }}' - - '{{ ferm__rules }}' - - '{{ ferm__group_rules }}' - - '{{ ferm__host_rules }}' - - '{{ ferm__dependent_rules }}' - - '{{ ferm__default_rules }}' - when: (ferm__enabled|bool and item.type|d() and - ((item.rule_state|d("present") == "absent") or - (item.delete|d() | bool))) - register: ferm__register_rules_del + with_dict: '{{ ferm__parsed_rules }}' + register: ferm__register_rules_removed + when: (item.value.rule_state|d(item.value.state|d('present')) == 'absent') tags: [ 'role::ferm:rules' ] -- name: Configure ip(6)tables rules +- name: Generate firewall rules template: - src: 'etc/ferm/ferm.d/{{ item.type }}.conf.j2' - dest: '/etc/ferm/ferm.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d(ferm__default_weight)) }}_{{ item.filename | d((((item.by_role|d(item.role)| replace(".", "_")) + "_" + ((item.role_weight + "_") if item.role_weight|d() else "")) if (item.by_role|d(item.role)|d()) else "") + item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' + src: 'etc/ferm/rules.d/{{ item.value.template | d("rule") }}.conf.j2' + dest: '/etc/ferm/rules.d/{{ "%03d" | format((ferm__combined_weight_map[item.value.weight_class | d(item.value.type | d("default"))] | d("80"))|int + (item.value.weight | d("0"))|int) }}_rule_{{ item.value.name | d(item.key) }}.conf' owner: 'root' group: 'adm' mode: '0644' - with_flattened: - - '{{ ferm_rules | d([]) | list }}' - - '{{ ferm_group_rules | d([]) | list }}' - - '{{ ferm_host_rules | d([]) | list }}' - - '{{ ferm_dependent_rules | d([]) | list }}' - - '{{ ferm_default_rules | d([]) | list }}' - - '{{ ferm__rules }}' - - '{{ ferm__group_rules }}' - - '{{ ferm__host_rules }}' - - '{{ ferm__dependent_rules }}' - - '{{ ferm__default_rules }}' - when: (ferm__enabled|bool and item.type|d() and - ((item.rule_state|d("present") == "present") and - not (item.delete|d() | bool))) - register: ferm__register_rules_add + with_dict: '{{ ferm__parsed_rules }}' + register: ferm__register_rules_created + when: (item.value.rule_state|d(item.value.state|d('present')) not in [ 'absent', 'ignore' ]) + tags: [ 'role::ferm:rules' ] + +- name: Remove unknown firewall rules + shell: find /etc/ferm/rules.d -maxdepth 1 -type f + -name '*_rule_{{ item.item.value.name | d(item.item.key) }}.conf' + ! -name '{{ "%03d" | format((ferm__combined_weight_map[item.item.value.weight_class | d(item.item.value.type | d("default"))] | d("80"))|int + (item.item.value.weight | d("0"))|int) }}_rule_{{ item.item.value.name | d(item.item.key) }}.conf' -exec rm -vf {} + + with_items: + - '{{ ferm__register_rules_removed.results }}' + - '{{ ferm__register_rules_created.results }}' + when: (item.item.key|d() and item.changed|bool) tags: [ 'role::ferm:rules' ] - name: Remove iptables INPUT rules if requested file: - path: '/etc/ferm/filter-input.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d(ferm__default_weight)) }}_{{ item.filename | d(item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' + path: '/etc/ferm/filter-input.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d("50")) }}_{{ item.filename | d(item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' state: 'absent' with_flattened: - '{{ ferm_input_list }}' @@ -129,7 +130,7 @@ - name: Configure iptables INPUT rules template: src: 'etc/ferm/filter-input.d/{{ item.type }}.conf.j2' - dest: '/etc/ferm/filter-input.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d(ferm__default_weight)) }}_{{ item.filename | d(item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' + dest: '/etc/ferm/filter-input.d/{{ ferm__weight_map[item.weight_class|d()] | d(item.weight | d("50")) }}_{{ item.filename | d(item.type + "_" + item.name | d((item.dport[0] if item.dport|d() else "rules"))) }}.conf' owner: 'root' group: 'adm' mode: '0644' @@ -147,7 +148,7 @@ name: 'ferm' state: 'restarted' when: (ferm__enabled | bool and (ferm__register_files.changed|bool or - ferm__register_rules_del.changed|bool or ferm__register_rules_add.changed|bool or + ferm__register_rules_created|changed or ferm__register_rules_removed|changed or ferm__register_input_rules_del.changed|bool or ferm__register_input_rules_add.changed|bool)) - name: Clear iptables rules if ferm is disabled @@ -212,3 +213,8 @@ group: 'root' mode: '0644' tags: [ 'role::ferm:rules' ] + register: ferm__register_facts + +- name: Update Ansible facts if they were modified + action: setup + when: ferm__register_facts|changed diff --git a/templates/etc/ansible/facts.d/ferm.fact.j2 b/templates/etc/ansible/facts.d/ferm.fact.j2 index 6d7c4df..cb71aa6 100644 --- a/templates/etc/ansible/facts.d/ferm.fact.j2 +++ b/templates/etc/ansible/facts.d/ferm.fact.j2 @@ -1,5 +1,5 @@ {% set ferm__tpl_forward = False %} -{% if ((ferm__forward|d(ferm_forward) | bool) or +{% if (ferm__forward | bool or (ansible_local|d() and ansible_local.ferm|d() and (ansible_local.ferm.forward|d() | bool))) %} {% set ferm__tpl_forward = True %} diff --git a/templates/etc/default/ferm.j2 b/templates/etc/default/ferm.j2 index c909f3e..9a0a15f 100644 --- a/templates/etc/default/ferm.j2 +++ b/templates/etc/default/ferm.j2 @@ -8,18 +8,17 @@ FAST=no # cache the output of ferm --lines in /var/cache/ferm? CACHE=no -# additional paramaters for ferm (like --def '=bar') +# additional parameters for ferm (like --def '=bar') OPTIONS= # Enable the ferm init script? (i.e. run on bootup) {% if ferm__enabled | bool %} ENABLED="yes" {% else %} -{% if ((ansible_local|d() and ansible_local.ferm|d() and ansible_local.ferm.enabled|d() and ansible_local.ferm.enabled | bool) and +{% if ((ansible_local|d() and ansible_local.ferm|d() and ansible_local.ferm.enabled|d() | bool) and not ferm__enabled | bool) %} ENABLED="yes" {% else %} ENABLED="no" {% endif %} {% endif %} - diff --git a/templates/etc/ferm/ferm.conf.j2 b/templates/etc/ferm/ferm.conf.j2 index 73d2644..676849a 100644 --- a/templates/etc/ferm/ferm.conf.j2 +++ b/templates/etc/ferm/ferm.conf.j2 @@ -1,5 +1,4 @@ # {{ ansible_managed }} # Load configuration from parts -@include 'ferm.d/'; - +@include 'rules.d/'; diff --git a/templates/etc/ferm/rules.d/rule.conf.j2 b/templates/etc/ferm/rules.d/rule.conf.j2 new file mode 100644 index 0000000..730e697 --- /dev/null +++ b/templates/etc/ferm/rules.d/rule.conf.j2 @@ -0,0 +1,559 @@ +# {{ ansible_managed }} +{% import 'templates/import/debops__tpl_macros.j2' as debops__tpl_macros with context %} + +{% macro print_list(elements, prefix='', postfix='') %}{% if elements | length == 1 %}{{ ((prefix + ' ') if prefix else '') + elements | join(' ') + ((' ' + postfix) if postfix else '') }}{% else %}{{ ((prefix + ' ') if prefix else '') + '(' + (elements | join(' ')) + ')' + ((' ' + postfix) if postfix else '') }}{% endif %}{% endmacro %} +{% macro print_rule(config) %} +{# Domain, table, chain #} +{# ==================== #} +{% set ferm__tpl_config = { + 'type': 'accept', + 'domain_args': [], + 'target': 'ACCEPT', + 'hashlimit_target': 'RETURN', + 'recent_target': 'NOP', + 'reject_with': 'icmp-admin-prohibited', + 'saddr': [], + 'daddr': [], + 'sport': [], + 'dport': [] +} %} +{% if config.comment|d() %} +{% set _ = ferm__tpl_config.update({'comment': config.comment }) %} +{% endif %} +{% if config.type|d() %} +{% set _ = ferm__tpl_config.update({'type': config.type }) %} +{% endif %} +{% set _ = ferm__tpl_config.update({'domain': (debops__tpl_macros.flattened(config.domain|d(config.domains|d(ferm__domains))) | from_json) }) %} +{% if ferm__tpl_config['type'] != 'dmz' %} +{% set _ = ferm__tpl_config.update({'table': (debops__tpl_macros.flattened(config.table|d(config.tables|d('filter'))) | from_json) }) %} +{% set _ = ferm__tpl_config.update({'chain': (debops__tpl_macros.flattened(config.chain|d(config.chains|d('INPUT'))) | from_json) }) %} +{% endif %} +{% if ferm__tpl_config['domain']|d() %} +{% set _ = ferm__tpl_config['domain_args'].append(print_list(ferm__tpl_config['domain'], prefix='domain')) %} +{% endif %} +{% if ferm__tpl_config['table']|d() %} +{% set _ = ferm__tpl_config['domain_args'].append(print_list(ferm__tpl_config['table'], prefix='table')) %} +{% endif %} +{% if ferm__tpl_config['chain']|d() %} +{% set _ = ferm__tpl_config['domain_args'].append(print_list(ferm__tpl_config['chain'], prefix='chain')) %} +{% endif %} +{# Rule arguments #} +{# ============== #} +{% if config.saddr|d() %} +{% set _ = ferm__tpl_config['saddr'].extend(([ config.saddr ] if config.saddr is string else config.saddr)) %} +{% endif %} +{% if config.daddr|d() %} +{% set _ = ferm__tpl_config['daddr'].extend(([ config.daddr ] if config.daddr is string else config.daddr)) %} +{% endif %} +{% if config.sport|d() %} +{% set _ = ferm__tpl_config['sport'].extend(([ config.sport ] if config.sport is string else config.sport)) %} +{% endif %} +{% if config.dport|d() %} +{% set _ = ferm__tpl_config['dport'].extend(([ config.dport ] if config.dport is string else config.dport)) %} +{% endif %} +{% if config.recent_name|d() %} +{% set _ = ferm__tpl_config.update({'recent_name': config.recent_name}) %} +{% elif config.recent_set_name|d() %} +{% set _ = ferm__tpl_config.update({'recent_set_name': config.recent_set_name}) %} +{% endif %} +{% if config.recent_update|d() and config.recent_update | bool %} +{% set _ = ferm__tpl_config.update({'recent_update': True}) %} +{% endif %} +{% if config.recent_remove|d() and config.recent_remove | bool %} +{% set _ = ferm__tpl_config.update({'recent_remove': True}) %} +{% endif %} +{% if config.recent_seconds|d() %} +{% set _ = ferm__tpl_config.update({'recent_seconds': config.recent_seconds | string}) %} +{% endif %} +{% if config.recent_hitcount|d() %} +{% set _ = ferm__tpl_config.update({'recent_hitcount': config.recent_hitcount | string}) %} +{% endif %} +{% if config.recent_target|d() %} +{% set _ = ferm__tpl_config.update({'recent_target': config.recent_target}) %} +{% endif %} +{% if config.subchain|d() %} +{% set _ = ferm__tpl_config.update({'subchain': (ferm__tpl_config['type'] + "-" + config.name | d((ferm__tpl_config['dport'][0] if ferm__tpl_config['dport']|d() else "rules")))}) %} +{% endif %} +{% set _ = ferm__tpl_config.update({'interface': (debops__tpl_macros.flattened(config.interface|d(config.interfaces)) | from_json) }) %} +{% if ferm__tpl_config['type'] == 'connection_tracking' %} +{% set _ = ferm__tpl_config.update({'tracking_invalid_target': (config.tracking_invalid_target | d('DROP'))}) %} +{% set _ = ferm__tpl_config.update({'tracking_active_target': (config.tracking_active_target | d('ACCEPT'))}) %} +{% set _ = ferm__tpl_config.update({'tracking_module': (config.tracking_module | d('conntrack'))}) %} +{% if ferm__tpl_config['tracking_module'] == 'state' %} +{% set _ = ferm__tpl_config.update({'tracking_module_command': 'mod state state'}) %} +{% else %} +{% set _ = ferm__tpl_config.update({'tracking_module_command': 'mod conntrack ctstate'}) %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['type'] == 'dmz' %} +{% if config.public_ip|d() %} +{% set _ = ferm__tpl_config.update({'public_ip': ([ config.public_ip ] if config.public_ip is string else config.public_ip) }) %} +{% endif %} +{% if config.private_ip|d() %} +{% set _ = ferm__tpl_config.update({'private_ip': ([ config.private_ip ] if config.private_ip is string else config.private_ip) }) %} +{% endif %} +{% if config.port|d() or config.ports|d() %} +{% set _ = ferm__tpl_config.update({'dmz_ports': (debops__tpl_macros.flattened(config.port|d(config.ports))) | from_json }) %} +{% endif %} +{% endif %} +{% for interface in (debops__tpl_macros.flattened(config.interface_present|d(config.interfaces_present)) | from_json) %} +{% if hostvars[inventory_hostname]["ansible_" + interface]|d() %} +{% set _ = ferm__tpl_config.update({'interface_present': [ interface ] }) %} +{% endif %} +{% endfor %} +{% if config.outerface|d() or config.outerfaces|d() %} +{% set _ = ferm__tpl_config.update({'outerface': (debops__tpl_macros.flattened(config.outerface|d(config.outerfaces)) | from_json) }) %} +{% endif %} +{% for outerface in (debops__tpl_macros.flattened(config.outerface_present|d(config.outerfaces_present)) | from_json) %} +{% if hostvars[inventory_hostname]["ansible_" + outerface]|d() %} +{% set _ = ferm__tpl_config.update({'outerface_present': [ outerface ] }) %} +{% endif %} +{% endfor %} +{% if config.protocol|d() or config.protocols|d() %} +{% set _ = ferm__tpl_config.update({'protocol': (debops__tpl_macros.flattened(config.protocol|d(config.protocols)) | from_json) }) %} +{% endif %} +{% if config.protocol_syn|d() %} +{% if config.protocol_syn | bool %} +{% set _ = ferm__tpl_config.update({'protocol_syn': [ 'syn' ] }) %} +{% elif not config.protocol_syn | bool %} +{% set _ = ferm__tpl_config.update({'protocol_syn': [ '! syn' ] }) %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['type'] == 'ansible_controller' %} +{% set ferm__tpl_ansible_controllers = [] %} +{% if ansible_local|d() and ansible_local.core|d() and ansible_local.core.ansible_controllers|d() %} +{% set _ = ferm__tpl_ansible_controllers.extend(ansible_local.core.ansible_controllers) %} +{% endif %} +{% if ansible_local|d() and ansible_local.ferm|d() and ansible_local.ferm.ansible_controllers|d() %} +{% set _ = ferm__tpl_ansible_controllers.extend(ansible_local.ferm.ansible_controllers) %} +{% endif %} +{% if ferm__ansible_controllers|d() %} +{% set _ = ferm__tpl_ansible_controllers.extend(([ ferm__ansible_controllers ] if ferm__ansible_controllers is string else ferm__ansible_controllers)) %} +{% endif %} +{% if config.ansible_controller|d() %} +{% set _ = ferm__tpl_ansible_controllers.extend(([ config.ansible_controller ] if config.ansible_controller is string else config.ansible_controller)) %} +{% elif config.ansible_controllers|d() %} +{% set _ = ferm__tpl_ansible_controllers.extend(([ config.ansible_controllers ] if config.ansible_controllers is string else config.ansible_controllers)) %} +{% endif %} +{% if ferm__tpl_ansible_controllers %} +{% set _ = ferm__tpl_config['saddr'].extend(ferm__tpl_ansible_controllers) %} +{% endif %} +{% endif %} +{% if config.state|d() %} +{% set _ = ferm__tpl_config.update({'state': ([ config.state ] if config.state is string else config.state) }) %} +{% endif %} +{% if config.target|d() %} +{% set _ = ferm__tpl_config.update({'target': config.target}) %} +{% endif %} +{% if config.hashlimit_target|d() %} +{% set _ = ferm__tpl_config.update({'hashlimit_target': config.hashlimit_target}) %} +{% endif %} +{% if config.reject_with|d() %} +{% set _ = ferm__tpl_config.update({'reject_with': config.reject_with}) %} +{% endif %} +{% if config.subchain is defined %} +{% if config.subchain | bool %} +{% set _ = ferm__tpl_config.update({'subchain': config.subchain}) %} +{% else %} +{% if config.hashlimit|d() %} +{% set _ = ferm__tpl_config.update({'subchain': (ferm__tpl_config['type'] + "-" + config.hashlimit_name | d(config.name | d(rule_name)))}) %} +{% endif %} +{% endif %} +{% endif %} +{% if config.hashlimit|d() %} +{% set _ = ferm__tpl_config.update({'subchain': (ferm__tpl_config['type'] + "-" + config.hashlimit_name | d(config.name | d(rule_name)))}) %} +{% endif %} +{% if config.limit|d() %} +{% set _ = ferm__tpl_config.update({'limit': config.limit}) %} +{% if config.limit_burst|d() %} +{% set _ = ferm__tpl_config.update({'limit_burst': config.limit_burst}) %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['type'] == 'log' %} +{% set _ = ferm__tpl_config.update({'log_target': 'LOG'}) %} +{% endif %} +{% if config.log_target|d() %} +{% set _ = ferm__tpl_config.update({'log_target': config.log_target}) %} +{% endif %} +{% if config.log_limit|d() %} +{% set _ = ferm__tpl_config.update({'limit': config.log_limit}) %} +{% endif %} +{% if config.log_burst|d() %} +{% set _ = ferm__tpl_config.update({'limit_burst': config.log_burst}) %} +{% endif %} +{% if config.log_ip_options|d() %} +{% set _ = ferm__tpl_config.update({'log_ip_options': config.log_ip_options | bool}) %} +{% endif %} +{% if config.log_tcp_options|d() %} +{% set _ = ferm__tpl_config.update({'log_tcp_options': config.log_tcp_options | bool}) %} +{% endif %} +{% if config.log_tcp_sequence|d() %} +{% set _ = ferm__tpl_config.update({'log_tcp_sequence': config.log_tcp_sequence | bool}) %} +{% endif %} +{% if config.log_prefix|d() %} +{% set _ = ferm__tpl_config.update({'log_prefix': config.log_prefix}) %} +{% endif %} +{% if config.log_level|d() %} +{% set _ = ferm__tpl_config.update({'log_level': config.log_level}) %} +{% endif %} +{% set ferm__tpl_log_args = [] %} +{% if ferm__tpl_config['type'] == 'log' %} +{% if ferm__tpl_config['log_target'] == 'LOG' %} +{% if ferm__tpl_config['log_ip_options']|d() and ferm__tpl_config['log_ip_options']|bool %} +{% set _ = ferm__tpl_log_args.append('log-ip-options') %} +{% endif %} +{% if ferm__tpl_config['log_tcp_options']|d() and ferm__tpl_config['log_tcp_options']|bool %} +{% set _ = ferm__tpl_log_args.append('log-tcp-options') %} +{% endif %} +{% if ferm__tpl_config['log_tcp_sequence']|d() and ferm__tpl_config['log_tcp_sequence']|bool %} +{% set _ = ferm__tpl_log_args.append('log-tcp-sequence') %} +{% endif %} +{% if ferm__tpl_config['log_level']|d() %} +{% set _ = ferm__tpl_log_args.append('log-level ' + ferm__tpl_config['log_level']) %} +{% endif %} +{% if ferm__tpl_config['log_prefix']|d() %} +{% set _ = ferm__tpl_log_args.append('log-prefix "' + ferm__tpl_config['log_prefix'] + '"') %} +{% endif %} +{% endif %} +{% endif %} +{% set ferm__tpl_recent_args = [] %} +{% if ferm__tpl_config['recent_name']|d() %} +{% set _ = ferm__tpl_recent_args.append('name "' + ferm__tpl_config['recent_name'] + '"') %} +{% elif ferm__tpl_config['recent_set_name']|d() %} +{% set _ = ferm__tpl_recent_args.append('set name "' + ferm__tpl_config['recent_set_name'] + '"') %} +{% endif %} +{% if (ferm__tpl_config['recent_update']|d(False)) | bool %} +{% set _ = ferm__tpl_recent_args.append('update') %} +{% endif %} +{% if (ferm__tpl_config['recent_remove']|d(False)) | bool %} +{% set _ = ferm__tpl_recent_args.append('remove') %} +{% endif %} +{% if ferm__tpl_config['recent_seconds']|d() %} +{% set _ = ferm__tpl_recent_args.append('seconds ' + ferm__tpl_config['recent_seconds']) %} +{% endif %} +{% if ferm__tpl_config['recent_hitcount']|d() %} +{% set _ = ferm__tpl_recent_args.append('hitcount ' + ferm__tpl_config['recent_hitcount']) %} +{% endif %} +{% set ferm__tpl_arguments = [] %} +{% if ferm__tpl_config['interface']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['interface'], prefix='interface')) %} +{% elif ferm__tpl_config['interface_present']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['interface_present'], prefix='interface')) %} +{% endif %} +{% if ferm__tpl_config['outerface']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['outerface'], prefix='outerface')) %} +{% elif ferm__tpl_config['outerface_present']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['outerface_present'], prefix='outerface')) %} +{% endif %} +{% if ferm__tpl_config['protocol']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['protocol'], prefix='protocol')) %} +{% elif not ferm__tpl_config['protocol']|d() and (ferm__tpl_config['sport']|d() or ferm__tpl_config['dport']|d()) %} +{% set _ = ferm__tpl_arguments.append("protocol tcp") %} +{% endif %} +{% if ferm__tpl_config['protocol_syn']|d() %} +{% set _ = ferm__tpl_arguments.extend(ferm__tpl_config['protocol_syn']) %} +{% endif %} +{% if ferm__tpl_config['sport']|d() %} +{% if config.multiport|d() and config.multiport | bool %} +{% if ferm__tpl_config['sport'] | length == 1 %} +{% set _ = ferm__tpl_arguments.append("sport " + ferm__tpl_config['sport'] | join(" ")) %} +{% else %} +{% set _ = ferm__tpl_arguments.append("mod multiport source-ports (" + ferm__tpl_config['sport'] | join(" ") + ")") %} +{% endif %} +{% else %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['sport'], prefix='sport')) %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['dport']|d() %} +{% if config.multiport|d() and config.multiport | bool %} +{% if ferm__tpl_config['dport'] | length == 1 %} +{% set _ = ferm__tpl_arguments.append("dport " + ferm__tpl_config['dport'] | join(" ")) %} +{% else %} +{% set _ = ferm__tpl_arguments.append("mod multiport destination-ports (" + ferm__tpl_config['dport'] | join(" ") + ")") %} +{% endif %} +{% else %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['dport'], prefix='dport')) %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['state']|d() %} +{% set _ = ferm__tpl_arguments.append(print_list(ferm__tpl_config['state'], prefix='mod state state')) %} +{% endif %} +{% if ferm__tpl_arguments and ((ferm__tpl_config['saddr']|d([])) | length > 3 or config.hashlimit|d()) %} +{% if ferm__tpl_config['subchain']|d() %} +{% set _ = ferm__tpl_arguments.append('@subchain "' + ferm__tpl_config['subchain'] + '"') %} +{% endif %} +{% endif %} +{% if ferm__tpl_config['limit']|d() %} +{% set _ = ferm__tpl_arguments.append("mod limit limit " + ferm__tpl_config['limit']) %} +{% if ferm__tpl_config['limit_burst']|d() %} +{% set _ = ferm__tpl_arguments.append("limit-burst " + ferm__tpl_config['limit_burst']) %} +{% endif %} +{% endif %} +{# This is where the configuration begins + ====================================== #} +{% if ferm__tpl_config['comment']|d() %} +{{ (ferm__tpl_config['comment'] if ferm__tpl_config['comment'] is string else ferm__tpl_config['comment'] | join('\n')) | regex_replace('\n$', '') | comment(prefix='', postfix='') -}} +{% endif %} +{% if ferm__tpl_config['type'] != 'dmz' %} +{% if ferm__tpl_config['domain_args'] %}{{ ferm__tpl_config['domain_args'] | join(" ") }} {% endif %}{ +{% endif %} +{% if ferm__tpl_config['type'] in [ 'policy', 'default_policy' ] %} + policy {{ config.policy }}; +{% elif ferm__tpl_config['type'] == 'include' %} + @include "{{ config.include }}"; +{% elif ferm__tpl_config['type'] == 'connection_tracking' %} + {% if ferm__tpl_arguments %}{{ ferm__tpl_arguments | join(" ") }} {% endif %}{ + {{ ferm__tpl_config['tracking_module_command'] }} INVALID {{ ferm__tpl_config['tracking_invalid_target'] }}; + {{ ferm__tpl_config['tracking_module_command'] }} (ESTABLISHED RELATED) {{ ferm__tpl_config['tracking_active_target'] }}; + } +{% elif ferm__tpl_config['type'] == 'log' %} + {% if ferm__tpl_arguments %}{{ ferm__tpl_arguments | join(" ") }} {% endif %}{ +{% if ferm__tpl_log_args %} + {{ ferm__tpl_config['log_target'] }} {{ ferm__tpl_log_args | join(' ') }}; +{% else %} + {{ ferm__tpl_config['log_target'] }}; +{% endif %} + } +{% elif ferm__tpl_config['type'] == 'recent' %} + {% if ferm__tpl_arguments %}{{ ferm__tpl_arguments | join(" ") }} {% endif %}{ + + mod recent {{ ferm__tpl_recent_args | join(" ") }} { +{% if ((config.recent_log is undefined or config.recent_log | bool) and ferm__log | bool) %} + + &log("{{ config.recent_log_prefix | d('ipt-recent-' + config.recent_name | d(config.recent_set_name) + ': ') }}"); +{% endif %} +{% if ferm__tpl_config['recent_target'] %} +{% if ferm__tpl_config['recent_target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + + jump "{{ ferm__tpl_config['recent_target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + + realgoto "{{ ferm__tpl_config['recent_target'] }}"; +{% endif %} +{% elif ferm__tpl_config['recent_target'] in [ 'REJECT' ] %} + + REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} + + {{ ferm__tpl_config['recent_target'] }}; +{% endif %} +{% endif %} + } + } +{% elif ferm__tpl_config['type'] == 'reject' %} + protocol udp REJECT reject-with icmp-port-unreachable; + protocol tcp REJECT reject-with tcp-reset; + @if @eq($DOMAIN, ip) { + REJECT reject-with icmp-proto-unreachable; + } + @if @eq($DOMAIN, ip6) { + REJECT; + } +{% elif ferm__tpl_config['type'] in [ 'custom', 'raw' ] %} +{% if ferm__tpl_config['domain_args'] %}{{ ferm__tpl_config['domain_args'] | join(" ") + " {" }} +{% endif %} +{% if config.rules|d() %} +{% if ferm__tpl_config['domain_args'] %} +{{ config.rules | indent(4,true) }} +{% else %} +{{ config.rules }} +{% endif %} +{% endif %} +{% if ferm__tpl_config['domain_args'] %}}{% endif %} +{% elif ferm__tpl_config['type'] == 'dmz' %} +{% if ferm__tpl_config['domain_args'] %}{{ ferm__tpl_config['domain_args'] | join(" ") }} {% endif %}{ + @def $PUBLIC_IP = ( @ipfilter( ({{ ferm__tpl_config['public_ip'] | unique | join(' ') }}) ) ); + @def $PRIVATE_IP = ( @ipfilter( ({{ ferm__tpl_config['private_ip'] | unique | join(' ') }}) ) ); + @if @ne($PUBLIC_IP,"") @if @ne($PRIVATE_IP,"") { + table filter chain FORWARD { +{% if ferm__tpl_config['dmz_ports']|d() %} + protocol ({{ ferm__tpl_config['protocols']|d([ 'tcp' ]) | join(" ") }}) { +{% if ferm__tpl_config['dmz_ports'] | length > 1 %} + mod multiport destination-ports ({{ ferm__tpl_config['dmz_ports'] | join(" ") }}) { +{% else %} + dport ({{ ferm__tpl_config['dmz_ports'] | join(" ") }}) { +{% endif %} + destination $PRIVATE_IP ACCEPT; + } + } +{% else %} + destination $PRIVATE_IP ACCEPT; +{% endif %} + } + + table nat { + chain PREROUTING { +{% if ferm__tpl_config['dmz_ports']|d() %} + protocol ({{ ferm__tpl_config['protocols']|d([ 'tcp' ]) | join(" ") }}) { +{% if ferm__tpl_config['dmz_ports'] | length > 1 %} + mod multiport destination-ports ({{ ferm__tpl_config['dmz_ports'] | join(" ") }}) { +{% else %} + dport ({{ ferm__tpl_config['dmz_ports'] | join(" ") }}) { +{% endif %} +{% if ferm__tpl_config['dport']|d() %} + destination $PUBLIC_IP DNAT to @cat($PRIVATE_IP, ":{{ ferm__tpl_config['dport'][0] }}"); +{% else %} + destination $PUBLIC_IP DNAT to $PRIVATE_IP; +{% endif %} + } + } +{% else %} + destination $PUBLIC_IP DNAT to $PRIVATE_IP; +{% endif %} + } + chain POSTROUTING { + source $PRIVATE_IP SNAT to $PUBLIC_IP; + } + } + } +} +{% else %} + {% if ferm__tpl_arguments %}{{ ferm__tpl_arguments | join(" ") }} {% endif %}{ +{% if config.hashlimit|d() %} + + mod hashlimit hashlimit {{ config.hashlimit }} +{% if config.hashlimit_burst|d() %} + hashlimit-burst {{ config.hashlimit_burst }} +{% endif %} + hashlimit-mode {{ config.hashlimit_mode | d("srcip") }} + hashlimit-name {{ config.hashlimit_name | d(config.name | d(rule_name)) }} +{% if config.hashlimit_expire is undefined or config.hashlimit_expire %} + hashlimit-htable-expire {{ ((config.hashlimit_expire|d("1800")) | int * 1000) }} +{% endif %} +{% if ferm__tpl_config['hashlimit_target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + + jump "{{ ferm__tpl_config['target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + + realgoto "{{ ferm__tpl_config['target'] }}"; +{% endif %} +{% elif ferm__tpl_config['hashlimit_target'] in [ 'REJECT' ] %} + + REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} + + {{ ferm__tpl_config['hashlimit_target'] }}; +{% endif %} +{% if ((config.log is undefined or config.log | bool) and (ferm__log | bool)) %} + + &log("{{ config.log_prefix | d('ipt-hashlimit-' + config.hashlimit_name | d(config.name | d(rule_name)) + ': ') }}"); + +{% endif %} +{% endif %} +{% if ferm__tpl_config['saddr']|d() %} + @def $SITEMS = ( @ipfilter( ({{ ferm__tpl_config['saddr'] | unique | join(" ") }}) ) ); + @if @ne($SITEMS,"") { +{% if ferm__tpl_config['target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + saddr $SITEMS jump "{{ ferm__tpl_config['target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + saddr $SITEMS realgoto "{{ ferm__tpl_config['target'] }}"; +{% endif %} +{% elif ferm__tpl_config['target'] in [ 'REJECT' ] %} + saddr $SITEMS REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} + saddr $SITEMS {{ ferm__tpl_config['target'] }}; +{% endif %} + } +{% elif ferm__tpl_config['daddr']|d() %} + @def $DITEMS = ( @ipfilter( ({{ ferm__tpl_config['daddr'] | unique | join(" ") }}) ) ); + @if @ne($DITEMS,"") { +{% if ferm__tpl_config['target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + daddr $DITEMS jump "{{ ferm__tpl_config['target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + daddr $DITEMS realgoto "{{ ferm__tpl_config['target'] }}"; +{% endif %} +{% elif ferm__tpl_config['target'] in [ 'REJECT' ] %} + daddr $DITEMS REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} + daddr $DITEMS {{ ferm__tpl_config['target'] }}; +{% endif %} + } +{% else %} +{% if config.accept_any is defined %} +{% if config.accept_any | bool %} +{% if ferm__tpl_config['target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + jump "{{ ferm__tpl_config['target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + realgoto "{{ ferm__tpl_config['target'] }}"; +{% endif %} +{% elif ferm__tpl_config['target'] in [ 'REJECT' ] %} + REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} + {{ ferm__tpl_config['target'] }}; +{% endif %} +{% elif not config.accept_any | bool %} + # Connections from any IP address not allowed +{% endif %} +{% else %} +{% if ferm__tpl_config['target'] not in [ 'ACCEPT', 'DROP', 'REJECT', 'RETURN', 'NOP' ] %} +{% if config.include|d() %} + @include "{{ config.include }}"; +{% elif config.realgoto is undefined or not config.realgoto | bool %} + jump "{{ ferm__tpl_config['target'] }}"; +{% elif config.realgoto|d() and config.realgoto | bool %} + realgoto "{{ ferm__tpl_config['target'] }}"; +{% endif %} +{% elif ferm__tpl_config['target'] in [ 'REJECT' ] %} + REJECT reject-with {{ ferm__tpl_config['reject_with'] }}; +{% else %} +{% if ferm__tpl_arguments %} + {{ ferm__tpl_config['target'] }}; +{% else %} + # No rule parameters specified +{% endif %} +{% endif %} +{% endif %} +{% endif %} + } +{% endif %} +{% if ferm__tpl_config['type'] != 'dmz' %} +} +{% endif %} +{% endmacro %} +{% set rule_name = (item.value.name | d(item.key)) %} +{% set rule = item.value %} +{% if rule.by_role|d() %} +{{ ("This firewall rule was generated by: " + (rule.by_role if rule.by_role is string else rule.by_role | join('\n'))) | regex_replace('\n$', '') | comment(prefix='', postfix='') -}} +{% endif %} +{% if rule.comment|d() %} +{{ (rule.comment if rule.comment is string else rule.comment | join('\n')) | regex_replace('\n$', '') | comment(prefix='', postfix='') -}} +{% endif %} +{% if rule.rules|d() %} +{% if rule.rules is string %} +{{ rule.rules -}} +{% elif rule.rules is mapping %} +{{ print_rule(rule.rules) -}} +{% elif rule.rules is not string and rule.rules is not mapping %} +{% for element in rule.rules %}{% if not loop.first %} + +{% endif %} +{% if element is string %} +{{ element -}} +{% elif element is mapping %} +{{ print_rule(element) -}} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} +{% if rule.debug|d() | bool or (ansible_local|d() and ansible_local.tags|d() and 'debug' in ansible_local.tags) %} + +{{ ("rule_name: " + (rule_name | to_nice_json)) | replace('\n$','') | comment(prefix='',postfix='') -}} +{{ ("rule: " + (rule | to_nice_json)) | replace('\n$','') | comment(prefix='',postfix='') -}} +{% endif %} diff --git a/templates/etc/network/if-pre-up.d/ferm-forward.j2 b/templates/etc/network/if-pre-up.d/ferm-forward.j2 index b5b2254..1fd8cb8 100644 --- a/templates/etc/network/if-pre-up.d/ferm-forward.j2 +++ b/templates/etc/network/if-pre-up.d/ferm-forward.j2 @@ -2,25 +2,18 @@ # {{ ansible_managed }} -{% if ferm__enabled | bool %} -{% if (ferm__forward|d(ferm_forward) | bool) or - (ansible_local|d() and ansible_local.ferm|d() and - (ansible_local.ferm.forward|d() | bool)) %} -{% set ferm__tpl_interfaces = (ferm__external_interfaces|d([]) | list) + - (ferm__internal_interfaces|d([]) | list) %} -{% for interface in ferm__tpl_interfaces | unique %} -{% if interface and hostvars[inventory_hostname]["ansible_" + interface] | d() %} +{% if ferm__enabled | bool and ferm__forward | bool %} +{% set ferm__tpl_interfaces = (ferm__external_interfaces|d([]) | list) + + (ferm__internal_interfaces|d([]) | list) %} +{% for interface in ferm__tpl_interfaces | unique %} +{% if interface and hostvars[inventory_hostname]["ansible_" + interface] | d() %} # Force Router Advertisement support on {{ interface }} interface if [ "$IFACE" = "{{ interface }}" ] ; then sysctl -w net/ipv6/conf/{{ interface }}/accept_ra=2 fi -{% endif %} -{% endfor %} -{% else %} -# Network forwarding in ip(6)tables is not enabled -{% endif %} +{% endif %} +{% endfor %} {% else %} # ferm support is disabled {% endif %} - diff --git a/templates/etc/sysctl.d/30-ferm.conf.j2 b/templates/etc/sysctl.d/30-ferm.conf.j2 index 94eac5f..9a7c80b 100644 --- a/templates/etc/sysctl.d/30-ferm.conf.j2 +++ b/templates/etc/sysctl.d/30-ferm.conf.j2 @@ -1,8 +1,6 @@ # {{ ansible_managed }} -{% if ferm__enabled | bool %} -{% if ((ferm__forward|d(ferm_forward) | bool) or - (ansible_local|d() and ansible_local.ferm|d() and (ansible_local.ferm.forward|d() | bool))) %} +{% if ferm__enabled | bool and ferm__forward|bool %} # Enable IPv4 forwarding net.ipv4.ip_forward = 1 @@ -13,11 +11,6 @@ net.ipv6.conf.all.forwarding = 1 # Enable IPv6 autoconfiguration (SLAAC) net.ipv6.conf.default.accept_ra = 1 net.ipv6.conf.all.accept_ra = 1 - -{% else %} -# Forwarding in ip(6)tables is not enabled - -{% endif %} {% else %} # ferm support is disabled {% endif %} diff --git a/templates/import/debops__tpl_macros.j2 b/templates/import/debops__tpl_macros.j2 new file mode 100644 index 0000000..5a1cc15 --- /dev/null +++ b/templates/import/debops__tpl_macros.j2 @@ -0,0 +1,236 @@ +{# vim: foldmarker=[[[,]]]:foldmethod=marker +# Commonly used set of macros in DebOps. +# It can be imported in repositories as needed. +# Changes to this file should go upstream: https://github.com/debops/debops-playbooks/blob/master/templates/debops__tpl_macros.j2 +# +# Copyright [[[ +# ============= +# +# Copyright (C) 2014-2017 Maciej Delmanowski +# Copyright (C) 2015-2017 Robin Schneider +# Copyright (C) 2014-2017 DebOps https://debops.org/ +# +# This file is part of DebOps. +# +# DebOps is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# DebOps is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with DebOps. If not, see https://www.gnu.org/licenses/. +# +# ]]] +# +# Usage [[[ +# ========= +# +# Copy the template file to `./templates/import/debops__tpl_macros.j2` of your +# role and import it from there into various other templates or even use it +# in templates which are called by {{ lookup("template", ...) }}. +# +# Make sure to retain the filename of this file so that automatic updates of +# this file can be implemented. +# +# To use the macros in your own template, this file needs to be imported like so: +# +# {% import 'templates/import/debops__tpl_macros.j2' as debops__tpl_macros with context %} +# +# Then you can start using the macros like this: +# +# {{ debops__tpl_macros.indent(some_content, 4) }} +# +# ]]] #} + +{% macro get_realm_yaml_list(domains, fallback_realm) %}{# [[[ #} +{% set custom_realm_list = [] %} +{% if domains and (ansible_local|d() and ansible_local.pki|d() and ansible_local.pki.known_realms|d()) %} +{% for domain in (get_yaml_list_for_elem(domains) | from_yaml) %} +{% if domain in ansible_local.pki.known_realms %} +{% set _ = custom_realm_list.append(domain) %} +{% elif (domain.split('.')[1:] | join('.')) in ansible_local.pki.known_realms %} +{% set _ = custom_realm_list.append(domain.split('.')[1:] | join('.')) %} +{% endif %} +{% endfor %} +{% endif %} +{% if custom_realm_list|length == 0 %} +{% set _ = custom_realm_list.append(fallback_realm) %} +{% endif %} +{{ custom_realm_list | to_nice_yaml }} +{% endmacro %}{# ]]] #} + +{% macro get_apache_version() %}{# [[[ #} +{{ ansible_local.apache.version + if (ansible_local|d() and ansible_local.apache|d() and + ansible_local.apache.version|d()) + else "2.4.0" -}} +{% endmacro %}{# ]]] #} + +{% macro get_apache_min_version() %}{# [[[ #} +{{ ansible_local.apache.min_version + if (ansible_local|d() and ansible_local.apache|d() and + ansible_local.apache.min_version|d()) + else "2.4.0" -}} +{% endmacro %}{# ]]] #} + +{% macro get_openssl_version() %}{# [[[ #} +{{ ansible_local.pki.openssl_version + if (ansible_local|d() and ansible_local.pki|d() and + ansible_local.pki.openssl_version|d()) + else "0.0.0" }} +{% endmacro %}{# ]]] #} + +{% macro get_gnutls_version() %}{# [[[ #} +{{ ansible_local.pki.gnutls_version + if (ansible_local|d() and ansible_local.pki|d() and + ansible_local.pki.gnutls_version|d()) + else "0.0.0" }} +{% endmacro %}{# ]]] #} + +{% macro indent(content, width=4, indentfirst=False) %}{# [[[ #} +{# +## Fixed version of the `indent` filter which does not insert trailing spaces on empty lines. +## Note that you can not use this macro like a filter but have to use it like a regular macro. +## Example: {{ debops__tpl_macros.indent(some_content, 4) }} +## +## Python re.sub seems to default to re.MULTILINE in Ansible. +#} +{{ content | indent(width, indentfirst) | regex_replace("[ \\t\\r\\f\\v]+(\\n|$)", "\\1") -}} +{% endmacro %}{# ]]] #} + +{% macro merge_dict(current_dict, to_merge_dict, dict_key='name') %}{# [[[ #} +{# +## Recursively merges nested dictionaries or a nested dictionary with a list of +## dictionaries. In the second case, a key name can be given whose value will +## be used as top-level key for the dictionary item. Note that for merging simple +## dictionaries you should use the regular `combined` Jinja filter. +## +## This can be used to define default variables as YAML dictionaries and then +## use YAML lists of dictionaries either in the inventory or in role dependent +## variables which lets you add multiple YAML lists together. +## +## Arguments: +## current_dict: Nested dictionary whose values might get overwritten if +## defined in `to_merge_dict`. +## to_merge_dict: Dictionary or list of dictionaries being merged into `current_dict`. +## dict_key: Key in the `to_merge_dict` item which is used to match the +## dictionary item being merged in `current_dict` in case `to_merge_dict` +## is a list of dictionaries. +## +## Return: JSON-formatted dictionary +## +## Examples: +## +## 1. Merge single-nested dictionaries: +## {{ debops__tmpl_macros.merge_dict({'item1': {'key1': 'val1', 'key2': 'val2'}, +## 'item2': {'key3': 'val3', 'key4': 'val4'}}, +## {'item1': {'key2': 'new_val2'}, +## 'item2': {'key5': 'val5' }}) }} +## +## Will output: {"item1": {"key1": "val1", "key2": "new_val2"}, +## "item2": {"key3": "val3", "key4": "val4", "key5": "val5"}} +## +## 2. Merge double-nested dictionaries: +## {{ debops__tmpl_macros.merge_dict({'item1': {'prop1': {'key1': 'val1'}}}, +## {'item1': {'prop1': {'key3': 'val3'}, +## 'prop2': {'key2': 'val2'}}}) }} +## +## Will output: {"item1": {"prop1": {"key1": "val1", "key3": "val3"}, +## "prop2": {"key2": "val2"}}} +## +## 3. Merge nested dictionary with list of dictionaries: +## {{ debops__tmpl_macros.merge_dict({'item1': {'key1': 'val1', 'key2': 'val2'}}, +## [{'name': 'item2', 'key3': 'val3'}, +## {'name': 'item3', 'key4': 'val4'}]) }} +## +## Will output: {"item1: {"key1": "val1", "key2": "val2"}, +## "item2: {"name": "item2", "key3": "val3"}, +## "item3: {"name": "item3", "key4": "val4"}} +#} +{% set merged_dict = current_dict %} +{% if to_merge_dict %} +{% if to_merge_dict is mapping %} +{% for dict_name in to_merge_dict.keys() | sort %} +{% if to_merge_dict[dict_name][dict_key]|d() %} +{% set _ = merged_dict.update({to_merge_dict[dict_name][dict_key]:(current_dict[to_merge_dict[dict_name][dict_key]]|d({}) | combine(to_merge_dict[dict_name], recursive=True))}) %} +{% elif to_merge_dict[dict_name][dict_key] is undefined %} +{% set _ = merged_dict.update({dict_name:(current_dict[dict_name]|d({}) | combine(to_merge_dict[dict_name], recursive=True))}) %} +{% endif %} +{% endfor %} +{% elif to_merge_dict is not string and to_merge_dict is not mapping %} +{% set flattened_dict = lookup("flattened", to_merge_dict) %} +{% for element in ([ flattened_dict ] if flattened_dict is mapping else flattened_dict) %} +{% if element[dict_key]|d() %} +{% set _ = merged_dict.update({element[dict_key]:(current_dict[element[dict_key]]|d({}) | combine(element, recursive=True))}) %} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} +{{ merged_dict | to_json }} +{% endmacro %}{# ]]] #} + +{% macro flattened() %}{# [[[ #} +{# This macro does what the flattened lookup from Ansible fails to do in Jinja templates as of Ansible 2.2. +## All macro arguments are flattened into one "flat" list. +## Uses a less known feature of Jinja for using the *args and *kwargs syntax as known from +## Python. Even if the macro does not declare any arguments, it will happyily +## flatten any non-key-value arguments you provide. +## Additional key-value arguments can be used to influence the behavoir of the macro. +## The macro uses recursion to flatten nested lists. +## Ref: https://stackoverflow.com/questions/13944751/args-kwargs-in-jinja2-macros +## Retruns flattened object as JSON which you will need to deserialize using `from_json`. +## Usage: +## +## {{ debops__tpl_macros.flattened(['list1', 55, ["deeplist1", "deeplist elem", True, False, undefined], {'mapping': True}], 'raw1', 22, True, False) }} +## → ["list1", 55, "deeplist1", "deeplist elem", true, false, "raw1", 22, true, false] +## +## {{ debops__tpl_macros.flattened(['list1', 55, ["deeplist1", "deeplist elem", True, False, undefined], {'mapping': True}], 'raw1', 22, True, False, filter_undef=False) }} +## → ["list1", 55, "deeplist1", "deeplist elem", true, false, null, "raw1", 22, true, false] +## +## {{ debops__tpl_macros.flattened(['list1', 55, ["deeplist1", "deeplist elem", True, False, undefined], {'mapping': True}], 'raw1', 22, True, False, filter_mapping=False) }} +## → ["list1", 55, "deeplist1", "deeplist elem", true, false, {"mapping": true}, "raw1", 22, true, false] +## +## Ansible versions tested with: 2.1, 2.2 +## Jinja versions tested with: 2.8.1 +#} +{% set filter_undef = kwargs.filter_undef|d(True) | bool %} +{% set filter_mapping = kwargs.filter_mapping|d(True) | bool %} +{# The following options are planned but currently don’t work: #} +{% set append_mapping_keys = kwargs.append_mapping_keys|d(False) | bool %} +{% set append_mapping_values = kwargs.append_mapping_values|d(False) | bool %} +{# +{{ "filter_undef:" + (filter_undef | string) }} +{{ "filter_mapping:" + (filter_mapping | string) }} +{{ "varargs:" + (varargs | string) }} +#} +{% set elem_flattened = [] %} +{% for arg in varargs %} +{# +{{ "arg: " + (arg | string) }} +#} +{% if filter_undef and (arg is undefined) %} +{# Filter out. #} +{% elif append_mapping_keys and (arg is mapping) %} +{# Does not work as of Jinja 2.8? #} +{% set _ = elem_flattened.extend(flattened(arg.keys(), filter_undef=filter_undef, filter_mapping=filter_mapping) | from_json) %} +{% elif append_mapping_values and (arg is mapping) %} +{# Does not work as of Jinja 2.8? #} +{% set _ = elem_flattened.extend(flattened(arg.values(), filter_undef=filter_undef, filter_mapping=filter_mapping) | from_json) %} +{% elif filter_mapping and (arg is mapping) %} +{# Filter out. #} +{% elif (arg is undefined) %} +{% set _ = elem_flattened.append(None) %} +{% elif (arg is string) or (arg is number) or (arg is sameas True) or (arg is sameas False) or (arg is mapping) %} +{% set _ = elem_flattened.append(arg) %} +{% elif (arg is iterable) %} +{% for element in arg %} +{% set _ = elem_flattened.extend(flattened(element, filter_undef=filter_undef, filter_mapping=filter_mapping) | from_json) %} +{% endfor %} +{% endif %} +{% endfor %} +{{ elem_flattened | to_json }} +{% endmacro %}{# ]]] #} diff --git a/templates/lookup/ferm__fix_dependent_rules.j2 b/templates/lookup/ferm__fix_dependent_rules.j2 new file mode 100644 index 0000000..0a01dcf --- /dev/null +++ b/templates/lookup/ferm__fix_dependent_rules.j2 @@ -0,0 +1,36 @@ +{% if ferm__dependent_rules %} +{% set ferm__tpl_dependent_rules = lookup('flattened', ferm__dependent_rules) %} +{% else %} +{% set ferm__tpl_dependent_rules = [] %} +{% endif %} +{% set ferm__tpl_fixed_rules = {} %} +{% set ferm__tpl_flattened_rules = [] %} +{% if ferm__tpl_dependent_rules %} +{% if ferm__tpl_dependent_rules is mapping %} +{% set fixed_dict = ferm__tpl_dependent_rules %} +{% if 'name' not in fixed_dict.keys() %} +{% if 'filename' in fixed_dict.keys() %} +{% set _ = fixed_dict.update({"name": ('filename_' + (fixed_dict.filename if fixed_dict.filename is string else fixed_dict.filename[0]))}) %} +{% elif 'dport' in fixed_dict.keys() %} +{% set _ = fixed_dict.update({"name": ('dport_' + (fixed_dict.dport if fixed_dict.dport is string else fixed_dict.dport[0]))}) %} +{% endif %} +{% endif %} +{% set _ = ferm__tpl_fixed_rules.update({fixed_dict['name']:fixed_dict}) %} +{% elif ferm__tpl_dependent_rules is not string and ferm__tpl_dependent_rules is not mapping %} +{% for element in ferm__tpl_dependent_rules %} +{% set fixed_dict = element %} +{% if 'name' not in fixed_dict.keys() %} +{% if 'filename' in fixed_dict.keys() %} +{% set _ = fixed_dict.update({"name": ('filename_' + (fixed_dict.filename if fixed_dict.filename is string else fixed_dict.filename[0]))}) %} +{% elif 'dport' in fixed_dict.keys() %} +{% set _ = fixed_dict.update({"name": ('dport_' + (fixed_dict.dport if fixed_dict.dport is string else fixed_dict.dport[0]))}) %} +{% endif %} +{% endif %} +{% set _ = ferm__tpl_fixed_rules.update({fixed_dict['name']:fixed_dict}) %} +{% endfor %} +{% endif %} +{% endif %} +{% for key, value in ferm__tpl_fixed_rules.iteritems() %} +{% set _ = ferm__tpl_flattened_rules.append(value) %} +{% endfor %} +{{ ferm__tpl_flattened_rules | to_json }} diff --git a/templates/lookup/ferm__parsed_rules.j2 b/templates/lookup/ferm__parsed_rules.j2 new file mode 100644 index 0000000..ce4f359 --- /dev/null +++ b/templates/lookup/ferm__parsed_rules.j2 @@ -0,0 +1,44 @@ +{% set ferm__tpl_filtered_rules = {} %} +{% for element in ferm__combined_rules %} +{% if element.name|d() and element.rule_state|d(element.state|d('present')) != 'ignore' %} +{% set rule_parameters = (ferm__tpl_filtered_rules[element.name].copy() if ferm__tpl_filtered_rules[element.name] is defined else {}) %} +{% if rule_parameters.rules is undefined and element.rules is undefined %} +{% set ferm__tpl_rule = {} %} +{% for parameter in element.keys() | sort %} +{% if parameter not in [ 'comment', 'rule_state', 'name', 'rules', 'template', 'weight', 'weight_class', 'role', 'role_weight', 'delete', 'when', 'enabled' ] %} +{% if parameter == 'state' %} +{% if element["state"] not in [ 'present', 'absent', 'ignore' ] %} +{% set _ = ferm__tpl_rule.update({parameter: element[parameter]}) %} +{% endif %} +{% else %} +{% set _ = ferm__tpl_rule.update({parameter: element[parameter]}) %} +{% endif %} +{% endif %} +{% endfor %} +{% if ferm__tpl_rule %} +{% set _ = rule_parameters.update({"rules":[]}) %} +{% set _ = rule_parameters["rules"].append(ferm__tpl_rule) %} +{% endif %} +{% endif %} +{% if element["state"] is undefined or element["state"] not in [ 'present', 'absent', 'ignore' ] %} +{% if element["enabled"]|d() %} +{% if element["enabled"]|bool %} +{% set _ = rule_parameters.update({"rule_state":"present"}) %} +{% else %} +{% set _ = rule_parameters.update({"rule_state":"absent"}) %} +{% endif %} +{% elif element["rule_state"]|d() %} +{% set _ = rule_parameters.update({"rule_state":element["rule_state"]}) %} +{% else %} +{% set _ = rule_parameters.update({"rule_state":"present"}) %} +{% endif %} +{% endif %} +{% for param_key in element.keys() %} +{% set _ = rule_parameters.update({ param_key: element[param_key] }) %} +{% endfor %} +{% if element.name|d() %} +{% set _ = ferm__tpl_filtered_rules.update({element.name: rule_parameters}) %} +{% endif %} +{% endif %} +{% endfor %} +{{ ferm__tpl_filtered_rules | to_yaml }}