diff options
| author | omnom62 <omnom62@outlook.com> | 2026-10-05 20:56:05 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-10-05 20:56:05 +1000 |
| commit | 8ee802e41374942965b6b93cfb4534725ef45145 (patch) | |
| tree | ef3e5579bd767422caa6fe77cea6a7b5d2d8562c /roles/zone_firewall | |
| parent | 748df2bc1d35fa285dd3fe46916e1230408778c7 (diff) | |
| download | vyos.blueprints-main.tar.gz vyos.blueprints-main.zip | |
T9393: new rolesmain
Diffstat (limited to 'roles/zone_firewall')
| -rw-r--r-- | roles/zone_firewall/defaults/main.yml | 6 | ||||
| -rw-r--r-- | roles/zone_firewall/meta/argument_specs.yml | 151 | ||||
| -rw-r--r-- | roles/zone_firewall/meta/main.yml | 11 | ||||
| -rw-r--r-- | roles/zone_firewall/tasks/main.yml | 34 | ||||
| -rw-r--r-- | roles/zone_firewall/tasks/verify.yml | 24 | ||||
| -rw-r--r-- | roles/zone_firewall/templates/firewall_rules.yml.j2 | 47 | ||||
| -rw-r--r-- | roles/zone_firewall/templates/zones.yml.j2 | 28 | ||||
| -rw-r--r-- | roles/zone_firewall/vars/main.yml | 17 |
8 files changed, 318 insertions, 0 deletions
diff --git a/roles/zone_firewall/defaults/main.yml b/roles/zone_firewall/defaults/main.yml new file mode 100644 index 0000000..6bb4a1a --- /dev/null +++ b/roles/zone_firewall/defaults/main.yml @@ -0,0 +1,6 @@ +--- +zone_firewall_policies: [] +zone_firewall_ipv6: true +zone_firewall_all_pairs: true +zone_firewall_base_rules: true +zone_firewall_log_rules: true diff --git a/roles/zone_firewall/meta/argument_specs.yml b/roles/zone_firewall/meta/argument_specs.yml new file mode 100644 index 0000000..fa47473 --- /dev/null +++ b/roles/zone_firewall/meta/argument_specs.yml @@ -0,0 +1,151 @@ +--- +argument_specs: + main: + short_description: Zone-based firewall (docs blueprint "Zone-Policy example") + description: + - Defines zones, one ruleset per zone-pair-direction named + C(<from>-<to>) for IPv4 and C(<from>-<to>-6) for IPv6, and binds the + rulesets to the zones, following the practices on the docs page. + - Every ruleset gets C(default-action drop), C(default-log) and the two + base rules from the page (1 accept established/related, 2 drop and log + invalid). Rules 1 and 2 are therefore reserved. + - With C(zone_firewall_all_pairs), rulesets are created for every zone + pair, including pairs that never communicate, so attempts are logged. + - Uses vyos.vyos.vyos_firewall_rules and vyos.vyos.vyos_firewall_global + (zone support requires a vyos.vyos release that includes it). + - B(Lockout risk) as the page warns - put the interface you manage the + router through into a zone with rules allowing SSH to the local zone + before running this role. + options: + zone_firewall_zones: + type: list + elements: dict + required: true + description: Zones. Exactly one may be the local zone (the router itself). + options: + name: + type: str + required: true + description: Zone name, e.g. C(lan). + interfaces: + type: list + elements: str + default: [] + description: Member interfaces, e.g. C([eth0.20]). Not used for the local zone. + local: + type: bool + default: false + description: This zone is the router itself. + default_action: + type: str + choices: [drop, reject] + default: drop + description: Zone default action (zones cannot accept). + default_log: + type: bool + default: true + description: Log packets hitting the zone default action. + description: + type: str + description: Zone description. + zone_firewall_policies: + type: list + elements: dict + default: [] + description: Rules per zone-pair-direction (traffic from C(from) to C(to)). + options: + from: + type: str + required: true + description: Source zone. + to: + type: str + required: true + description: Destination zone. + default_action: + type: str + choices: [drop, reject, accept] + default: drop + description: Ruleset default action, e.g. C(accept) for a management zone. + rules: + type: list + elements: dict + default: [] + description: Rules added after the base rules. + options: + number: + type: int + required: true + description: Rule number (not 1 or 2 while base rules are on). + action: + type: str + choices: [accept, drop, reject] + default: accept + description: Rule action. + description: + type: str + description: Rule description. + protocol: + type: str + description: + - Protocol, e.g. C(tcp), C(udp), C(tcp_udp), C(icmp). + - C(icmp) becomes C(ipv6-icmp) in the IPv6 ruleset. + source: + type: dict + description: Source match. + options: + address: + type: str + description: Address or network. + port: + type: str + description: Port(s), e.g. C(22) or C(20,21). + destination: + type: dict + description: Destination match. + options: + address: + type: str + description: Address or network. + port: + type: str + description: Port(s), e.g. C(80,443). + family: + type: str + choices: [ipv4, ipv6, both] + description: + - Rulesets the rule goes into. Inferred when omitted - an + IPv6 address means C(ipv6), an IPv4 address C(ipv4), no + address C(both). + log: + type: bool + description: Log matches. Defaults to C(zone_firewall_log_rules). + zone_firewall_ipv6: + type: bool + default: true + description: Also create IPv6 rulesets and bind them. + zone_firewall_all_pairs: + type: bool + default: true + description: Create a (base rules only) ruleset for every zone pair not listed. + zone_firewall_base_rules: + type: bool + default: true + description: Add the page's rules 1 (established/related) and 2 (invalid) to every ruleset. + zone_firewall_log_rules: + type: bool + default: true + description: Default for C(log) on user rules, as the page recommends. + vyos_blueprints_render_only: + type: bool + default: false + description: Collect commands into C(vyos_blueprints_rendered) instead of configuring. + verify: + short_description: Post-deployment checks for the zone_firewall role + description: Run with C(tasks_from=verify). Checks every zone and its member interfaces are active. + options: + zone_firewall_zones: + type: list + elements: dict + required: true + description: Same value as for C(main). diff --git a/roles/zone_firewall/meta/main.yml b/roles/zone_firewall/meta/main.yml new file mode 100644 index 0000000..c83c9f4 --- /dev/null +++ b/roles/zone_firewall/meta/main.yml @@ -0,0 +1,11 @@ +--- +galaxy_info: + author: VyOS maintainers and contributors + description: Zone-based firewall with per zone-pair rulesets for IPv4 and IPv6 + license: GPL-3.0-or-later + min_ansible_version: "2.16" + platforms: + - name: GenericLinux + versions: [all] + galaxy_tags: [vyos, networking, firewall, zone] +dependencies: [] diff --git a/roles/zone_firewall/tasks/main.yml b/roles/zone_firewall/tasks/main.yml new file mode 100644 index 0000000..469dc24 --- /dev/null +++ b/roles/zone_firewall/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: Check zones and policies + ansible.builtin.assert: + that: + - zone_firewall_zones | selectattr('local', 'defined') | selectattr('local') | list | length <= 1 + - (zone_firewall_policies | map(attribute='from') | list + zone_firewall_policies | map(attribute='to') | list) + | difference(_zone_firewall_names) | length == 0 + - not (zone_firewall_base_rules | bool) + or (zone_firewall_policies | map(attribute='rules', default=[]) | flatten + | map(attribute='number') | select('in', [1, 2]) | list | length == 0) + fail_msg: >- + At most one local zone; every policy must reference defined zones; + rule numbers 1 and 2 are reserved for the base rules. + quiet: true + +- name: Create one ruleset per zone-pair-direction + vyos.vyos.vyos_firewall_rules: + config: "{{ lookup('ansible.builtin.template', 'firewall_rules.yml.j2') | from_yaml }}" + state: "{{ _zone_firewall_state }}" + register: _zone_firewall_r_rules + +- name: Define zones and bind the rulesets + vyos.vyos.vyos_firewall_global: + config: "{{ lookup('ansible.builtin.template', 'zones.yml.j2') | from_yaml }}" + state: "{{ _zone_firewall_state }}" + register: _zone_firewall_r_zones + +- name: Collect rendered commands # noqa: var-naming[no-role-prefix] - shared across roles by design + ansible.builtin.set_fact: + vyos_blueprints_rendered: >- + {{ vyos_blueprints_rendered | default([]) + + ([_zone_firewall_r_rules, _zone_firewall_r_zones] + | selectattr('rendered', 'defined') | map(attribute='rendered') | flatten) }} + when: vyos_blueprints_render_only | default(false) | bool diff --git a/roles/zone_firewall/tasks/verify.yml b/roles/zone_firewall/tasks/verify.yml new file mode 100644 index 0000000..110eeac --- /dev/null +++ b/roles/zone_firewall/tasks/verify.yml @@ -0,0 +1,24 @@ +--- +- name: Read zone policy + vyos.vyos.vyos_command: + commands: + - show firewall zone-policy + register: _zone_firewall_v + +- name: Every zone is active + ansible.builtin.assert: + that: _zone_firewall_v.stdout[0] is search('(?m)^\s*' ~ (item.name | regex_escape) ~ '\s') + fail_msg: "zone {{ item.name }} is missing from 'show firewall zone-policy'" + quiet: true + loop: "{{ zone_firewall_zones }}" + loop_control: + label: "{{ item.name }}" + +- name: Member interfaces are bound + ansible.builtin.assert: + that: _zone_firewall_v.stdout[0] is search('\b' ~ (item.1 | regex_escape) ~ '\b') + fail_msg: "interface {{ item.1 }} of zone {{ item.0.name }} is not bound" + quiet: true + loop: "{{ zone_firewall_zones | subelements('interfaces', skip_missing=true) }}" + loop_control: + label: "{{ item.0.name }} {{ item.1 }}" diff --git a/roles/zone_firewall/templates/firewall_rules.yml.j2 b/roles/zone_firewall/templates/firewall_rules.yml.j2 new file mode 100644 index 0000000..0e083f3 --- /dev/null +++ b/roles/zone_firewall/templates/firewall_rules.yml.j2 @@ -0,0 +1,47 @@ +{%- macro rule_family(r) -%} +{%- set addrs = [r.source.address | default('') if r.source is defined and r.source else '', + r.destination.address | default('') if r.destination is defined and r.destination else ''] | select | list -%} +{%- if r.family is defined and r.family -%}{{ r.family }} +{%- elif addrs | select('search', ':') | list -%}ipv6 +{%- elif addrs -%}ipv4 +{%- else -%}both +{%- endif -%} +{%- endmacro -%} +{% for afi in _zone_firewall_families %} +- afi: {{ afi }} + rule_sets: +{% for p in _zone_firewall_policies %} + - name: {{ (p['from'] ~ '-' ~ p['to'] ~ ('-6' if afi == 'ipv6' else '')) | to_json }} + default_action: {{ p.default_action | default('drop') }} + enable_default_log: true + rules: +{% if zone_firewall_base_rules | bool %} + - number: 1 + action: accept + state: {established: true, related: true} + - number: 2 + action: drop + log: enable + state: {invalid: true} +{% endif %} +{% for r in p.rules | default([]) if rule_family(r) in [afi, 'both'] %} + - number: {{ r.number | int }} + action: {{ r.action | default('accept') }} +{% if r.description is defined %} + description: {{ r.description | to_json }} +{% endif %} +{% if r.protocol is defined %} + protocol: {{ ('ipv6-icmp' if (afi == 'ipv6' and r.protocol == 'icmp') else r.protocol) | to_json }} +{% endif %} +{% if r.log | default(zone_firewall_log_rules) | bool %} + log: enable +{% endif %} +{% for side in ['source', 'destination'] if r[side] is defined and r[side] %} + {{ side }}: +{% for k in ['address', 'port'] if r[side][k] is defined %} + {{ k }}: {{ r[side][k] | string | to_json }} +{% endfor %} +{% endfor %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/roles/zone_firewall/templates/zones.yml.j2 b/roles/zone_firewall/templates/zones.yml.j2 new file mode 100644 index 0000000..3af0e99 --- /dev/null +++ b/roles/zone_firewall/templates/zones.yml.j2 @@ -0,0 +1,28 @@ +zone: +{% for z in zone_firewall_zones %} + - name: {{ z.name | to_json }} + default_action: {{ z.default_action | default('drop') }} +{% if z.default_log | default(true) | bool %} + default_log: true +{% endif %} +{% if z.description is defined %} + description: {{ z.description | to_json }} +{% endif %} +{% if z.local | default(false) | bool %} + local_zone: true +{% else %} + interfaces: {{ z.interfaces | default([]) | to_json }} +{% endif %} +{% set srcs = _zone_firewall_policies | selectattr('to', 'equalto', z.name) | list %} +{% if srcs %} + sources: +{% for p in srcs %} + - zone: {{ p['from'] | to_json }} + firewall: + name: {{ (p['from'] ~ '-' ~ z.name) | to_json }} +{% if zone_firewall_ipv6 | bool %} + ipv6_name: {{ (p['from'] ~ '-' ~ z.name ~ '-6') | to_json }} +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} diff --git a/roles/zone_firewall/vars/main.yml b/roles/zone_firewall/vars/main.yml new file mode 100644 index 0000000..14ffa31 --- /dev/null +++ b/roles/zone_firewall/vars/main.yml @@ -0,0 +1,17 @@ +--- +_zone_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}" +_zone_firewall_families: "{{ ['ipv4', 'ipv6'] if zone_firewall_ipv6 | bool else ['ipv4'] }}" +_zone_firewall_names: "{{ zone_firewall_zones | map(attribute='name') | list }}" +# Listed policies plus, with zone_firewall_all_pairs, an empty policy for every +# other ordered zone pair, so every zone-pair-direction has a logged ruleset. +_zone_firewall_policies: >- + {%- set out = zone_firewall_policies | list -%} + {%- if zone_firewall_all_pairs | bool -%} + {%- set listed = zone_firewall_policies | map(attribute='from') | zip(zone_firewall_policies | map(attribute='to')) | map('join', '>') | list -%} + {%- for a in _zone_firewall_names -%} + {%- for b in _zone_firewall_names if b != a and (a ~ '>' ~ b) not in listed -%} + {%- set _ = out.append({'from': a, 'to': b, 'rules': []}) -%} + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + {{ out }} |
