summaryrefslogtreecommitdiff
path: root/roles/bridge_firewall
diff options
context:
space:
mode:
Diffstat (limited to 'roles/bridge_firewall')
-rw-r--r--roles/bridge_firewall/defaults/main.yml4
-rw-r--r--roles/bridge_firewall/meta/argument_specs.yml163
-rw-r--r--roles/bridge_firewall/meta/main.yml11
-rw-r--r--roles/bridge_firewall/tasks/main.yml35
-rw-r--r--roles/bridge_firewall/tasks/verify.yml27
-rw-r--r--roles/bridge_firewall/templates/bridge_cli.j272
-rw-r--r--roles/bridge_firewall/templates/ipv4_rules.yml.j254
-rw-r--r--roles/bridge_firewall/vars/main.yml11
8 files changed, 377 insertions, 0 deletions
diff --git a/roles/bridge_firewall/defaults/main.yml b/roles/bridge_firewall/defaults/main.yml
new file mode 100644
index 0000000..f656cb5
--- /dev/null
+++ b/roles/bridge_firewall/defaults/main.yml
@@ -0,0 +1,4 @@
+---
+bridge_firewall_state_rules: true
+bridge_firewall_bridge_forward_default_action: drop
+bridge_firewall_ipv4_forward_default_action: drop
diff --git a/roles/bridge_firewall/meta/argument_specs.yml b/roles/bridge_firewall/meta/argument_specs.yml
new file mode 100644
index 0000000..6d5493e
--- /dev/null
+++ b/roles/bridge_firewall/meta/argument_specs.yml
@@ -0,0 +1,163 @@
+---
+argument_specs:
+ main:
+ short_description: Bridges with bridge and IPv4 firewall (docs blueprint "Bridge and firewall example")
+ description:
+ - Per bridge, creates the bridge and its members, an interface group
+ C(<bridge>-ifaces), bridge-firewall rulesets C(<bridge>-pre) (prerouting)
+ and C(<bridge>-fwd) (forward) with the jump rules that select them, and
+ optionally an IPv4 ruleset C(ip-<bridge>-fwd) for routed traffic and an
+ input rule for access to the router - the structure used on the docs page.
+ - Bridges, interface groups and the bridge firewall use
+ vyos.vyos.vyos_config, because vyos.vyos has no resource modules for
+ them yet. Member descriptions and the IPv4 firewall use resource modules.
+ options:
+ bridge_firewall_bridges:
+ type: list
+ elements: dict
+ required: true
+ description: Bridges, in order; their position sets the jump rule numbers (10, 20, ... and 110, 120, ...).
+ options:
+ name:
+ type: str
+ required: true
+ description: Bridge interface, e.g. C(br1).
+ description:
+ type: str
+ description: Bridge description.
+ addresses:
+ type: list
+ elements: str
+ default: []
+ description: Bridge addresses; omit for a pure layer-2 bridge.
+ members:
+ type: list
+ elements: dict
+ required: true
+ description: Member interfaces.
+ options:
+ interface:
+ type: str
+ required: true
+ description: Member interface, e.g. C(eth3).
+ description:
+ type: str
+ description: Member interface description.
+ prerouting:
+ type: dict
+ description: Bridge ruleset C(<bridge>-pre), applied in the prerouting chain.
+ options: &bridge_ruleset
+ default_action:
+ type: str
+ choices: [accept, drop]
+ description: Ruleset default action (C(accept) for prerouting, C(drop) for forward by default).
+ rules:
+ type: list
+ elements: dict
+ default: []
+ description: Bridge firewall rules.
+ options:
+ number:
+ type: int
+ required: true
+ description: Rule number.
+ action:
+ type: str
+ choices: [accept, drop]
+ default: accept
+ description: Rule action.
+ description:
+ type: str
+ description: Rule description.
+ ethernet_type:
+ type: str
+ description: Ethernet type, e.g. C(ipv4), C(ipv6), C(arp).
+ protocol:
+ type: str
+ description: IP protocol, e.g. C(udp).
+ source:
+ type: dict
+ description: Source match (C(address), C(port), C(mac_address)).
+ destination:
+ type: dict
+ description: Destination match (C(address), C(port), C(mac_address)).
+ inbound_interface:
+ type: str
+ description: Inbound bridge port, e.g. C(eth6).
+ state:
+ type: list
+ elements: str
+ default: []
+ description: Connection states, e.g. C([new]).
+ log:
+ type: bool
+ default: false
+ description: Log matches.
+ forward:
+ type: dict
+ description: Bridge ruleset C(<bridge>-fwd), applied in the bridge forward chain.
+ options: *bridge_ruleset
+ ip_forward:
+ type: dict
+ description: IPv4 ruleset C(ip-<bridge>-fwd) for traffic routed out of the bridge.
+ options:
+ default_action:
+ type: str
+ choices: [accept, drop, reject]
+ default: drop
+ description: Ruleset default action.
+ rules:
+ type: list
+ elements: dict
+ default: []
+ description: IPv4 rules.
+ options:
+ number:
+ type: int
+ required: true
+ description: Rule number.
+ action:
+ type: str
+ choices: [accept, drop, reject]
+ default: accept
+ description: Rule action.
+ description:
+ type: str
+ description: Rule description.
+ outbound_interface:
+ type: str
+ description: Outbound interface, e.g. the internet uplink.
+ outbound_bridge:
+ type: str
+ description: Another bridge from this list, matched via its interface group.
+ router_access:
+ type: str
+ choices: [accept, drop]
+ description: Accept or drop IPv4 traffic from this bridge to the router itself.
+ bridge_firewall_state_rules:
+ type: bool
+ default: true
+ description: Add the page's established/related and invalid rules to the forward and input chains.
+ bridge_firewall_bridge_forward_default_action:
+ type: str
+ choices: [accept, drop]
+ default: drop
+ description: Default action of the bridge forward chain.
+ bridge_firewall_ipv4_forward_default_action:
+ type: str
+ choices: [accept, drop, reject]
+ default: drop
+ description: Default action of the IPv4 forward chain.
+ 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 bridge_firewall role
+ description: Run with C(tasks_from=verify). Checks bridge membership and that the bridge rulesets are active.
+ options:
+ bridge_firewall_bridges:
+ type: list
+ elements: dict
+ required: true
+ description: Same value as for C(main).
diff --git a/roles/bridge_firewall/meta/main.yml b/roles/bridge_firewall/meta/main.yml
new file mode 100644
index 0000000..4e556cb
--- /dev/null
+++ b/roles/bridge_firewall/meta/main.yml
@@ -0,0 +1,11 @@
+---
+galaxy_info:
+ author: VyOS maintainers and contributors
+ description: Bridges with bridge-layer and IPv4 firewall policies per bridge
+ license: GPL-3.0-or-later
+ min_ansible_version: "2.16"
+ platforms:
+ - name: GenericLinux
+ versions: [all]
+ galaxy_tags: [vyos, networking, bridge, firewall]
+dependencies: []
diff --git a/roles/bridge_firewall/tasks/main.yml b/roles/bridge_firewall/tasks/main.yml
new file mode 100644
index 0000000..d947258
--- /dev/null
+++ b/roles/bridge_firewall/tasks/main.yml
@@ -0,0 +1,35 @@
+---
+- name: Set member interface descriptions
+ vyos.vyos.vyos_interfaces:
+ config: "{{ _bridge_firewall_member_descriptions }}"
+ state: "{{ _bridge_firewall_state }}"
+ register: _bridge_firewall_r_ifaces
+ when: _bridge_firewall_member_descriptions | length > 0
+
+- name: Create bridges, interface groups and the bridge firewall
+ # No vyos.vyos resource module covers bridges, interface groups or the
+ # bridge firewall yet, so these go through vyos_config.
+ vyos.vyos.vyos_config:
+ lines: "{{ _bridge_firewall_cli_lines }}"
+ register: _bridge_firewall_r_cli
+ when: not (vyos_blueprints_render_only | default(false) | bool)
+
+- name: Configure the IPv4 input and forward filters
+ vyos.vyos.vyos_firewall_rules:
+ config: "{{ _bridge_firewall_ipv4 }}"
+ state: "{{ _bridge_firewall_state }}"
+ vars:
+ _bridge_firewall_ipv4: "{{ lookup('ansible.builtin.template', 'ipv4_rules.yml.j2') | from_yaml }}"
+ register: _bridge_firewall_r_ipv4
+ when: >-
+ bridge_firewall_bridges | selectattr('ip_forward', 'defined') | list | length > 0
+ or bridge_firewall_bridges | selectattr('router_access', 'defined') | list | length > 0
+
+- 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([])
+ + (_bridge_firewall_r_ifaces.rendered | default([]))
+ + _bridge_firewall_cli_lines
+ + (_bridge_firewall_r_ipv4.rendered | default([])) }}
+ when: vyos_blueprints_render_only | default(false) | bool
diff --git a/roles/bridge_firewall/tasks/verify.yml b/roles/bridge_firewall/tasks/verify.yml
new file mode 100644
index 0000000..295b8af
--- /dev/null
+++ b/roles/bridge_firewall/tasks/verify.yml
@@ -0,0 +1,27 @@
+---
+- name: Read bridges and bridge firewall
+ vyos.vyos.vyos_command:
+ commands:
+ - show configuration commands
+ - show firewall bridge
+ register: _bridge_firewall_v
+
+- name: Every member interface is in its bridge
+ ansible.builtin.assert:
+ that: >-
+ _bridge_firewall_v.stdout[0] is search('set interfaces bridge ' ~ (item.0.name | regex_escape)
+ ~ ' member interface ' ~ (item.1.interface | regex_escape) ~ '\b')
+ fail_msg: "{{ item.1.interface }} is not a member of {{ item.0.name }}"
+ quiet: true
+ loop: "{{ bridge_firewall_bridges | subelements('members') }}"
+ loop_control:
+ label: "{{ item.0.name }} {{ item.1.interface }}"
+
+- name: Every bridge ruleset is active
+ ansible.builtin.assert:
+ that: _bridge_firewall_v.stdout[1] is search('name ' ~ item)
+ fail_msg: "bridge ruleset {{ item }} is not active"
+ quiet: true
+ loop: >-
+ {{ (bridge_firewall_bridges | selectattr('prerouting', 'defined') | map(attribute='name') | map('regex_replace', '$', '-pre') | list)
+ + (bridge_firewall_bridges | selectattr('forward', 'defined') | map(attribute='name') | map('regex_replace', '$', '-fwd') | list) }}
diff --git a/roles/bridge_firewall/templates/bridge_cli.j2 b/roles/bridge_firewall/templates/bridge_cli.j2
new file mode 100644
index 0000000..18cb1f3
--- /dev/null
+++ b/roles/bridge_firewall/templates/bridge_cli.j2
@@ -0,0 +1,72 @@
+{#- Bridges, interface groups and the bridge firewall: no vyos.vyos resource
+ module covers these yet. Rule numbers for the jump rules follow the page. -#}
+{%- macro bridge_rule(prefix, r) -%}
+{{ prefix }} rule {{ r.number }} action '{{ r.action | default('accept') }}'
+{% if r.description is defined %}
+{{ prefix }} rule {{ r.number }} description '{{ r.description }}'
+{% endif %}
+{% for k in ['ethernet_type', 'protocol'] if r[k] is defined %}
+{{ prefix }} rule {{ r.number }} {{ k | replace('_', '-') }} '{{ r[k] }}'
+{% endfor %}
+{% for side in ['source', 'destination'] if r[side] is defined %}
+{% for k in ['address', 'port', 'mac_address'] if r[side][k] is defined %}
+{{ prefix }} rule {{ r.number }} {{ side }} {{ k | replace('_', '-') }} '{{ r[side][k] }}'
+{% endfor %}
+{% endfor %}
+{% if r.inbound_interface is defined %}
+{{ prefix }} rule {{ r.number }} inbound-interface name '{{ r.inbound_interface }}'
+{% endif %}
+{% for s in r.state | default([]) %}
+{{ prefix }} rule {{ r.number }} state '{{ s }}'
+{% endfor %}
+{% if r.log | default(false) | bool %}
+{{ prefix }} rule {{ r.number }} log
+{% endif %}
+{%- endmacro -%}
+{% for b in bridge_firewall_bridges %}
+{% set i = loop.index %}
+{% set grp = b.name ~ '-ifaces' %}
+{% if b.description is defined %}
+set interfaces bridge {{ b.name }} description '{{ b.description }}'
+{% endif %}
+{% for a in b.addresses | default([]) %}
+set interfaces bridge {{ b.name }} address '{{ a }}'
+{% endfor %}
+{% for m in b.members %}
+set interfaces bridge {{ b.name }} member interface {{ m.interface }}
+{% endfor %}
+set firewall group interface-group {{ grp }} interface '{{ b.name }}'
+{% for m in b.members %}
+set firewall group interface-group {{ grp }} interface '{{ m.interface }}'
+{% endfor %}
+{% if b.prerouting is defined %}
+set firewall bridge prerouting filter rule {{ i * 10 }} action 'jump'
+set firewall bridge prerouting filter rule {{ i * 10 }} description '{{ b.name }} traffic'
+set firewall bridge prerouting filter rule {{ i * 10 }} inbound-interface group '{{ grp }}'
+set firewall bridge prerouting filter rule {{ i * 10 }} jump-target '{{ b.name }}-pre'
+set firewall bridge name {{ b.name }}-pre default-action '{{ b.prerouting.default_action | default('accept') }}'
+{% for r in b.prerouting.rules | default([]) %}
+{{ bridge_rule('set firewall bridge name ' ~ b.name ~ '-pre', r) }}
+{% endfor %}
+{% endif %}
+{% if b.forward is defined %}
+set firewall bridge forward filter rule {{ 100 + i * 10 }} action 'jump'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} description '{{ b.name }} traffic'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} inbound-interface group '{{ grp }}'
+set firewall bridge forward filter rule {{ 100 + i * 10 }} jump-target '{{ b.name }}-fwd'
+set firewall bridge name {{ b.name }}-fwd default-action '{{ b.forward.default_action | default('drop') }}'
+{% for r in b.forward.rules | default([]) %}
+{{ bridge_rule('set firewall bridge name ' ~ b.name ~ '-fwd', r) }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% if bridge_firewall_bridges | selectattr('forward', 'defined') | list %}
+{% if bridge_firewall_state_rules | bool %}
+set firewall bridge forward filter rule 5 action 'accept'
+set firewall bridge forward filter rule 5 state 'established'
+set firewall bridge forward filter rule 5 state 'related'
+set firewall bridge forward filter rule 10 action 'drop'
+set firewall bridge forward filter rule 10 state 'invalid'
+{% endif %}
+set firewall bridge forward filter default-action '{{ bridge_firewall_bridge_forward_default_action }}'
+{% endif %}
diff --git a/roles/bridge_firewall/templates/ipv4_rules.yml.j2 b/roles/bridge_firewall/templates/ipv4_rules.yml.j2
new file mode 100644
index 0000000..dec5520
--- /dev/null
+++ b/roles/bridge_firewall/templates/ipv4_rules.yml.j2
@@ -0,0 +1,54 @@
+{#- IPv4 firewall for routed traffic and router access, via vyos_firewall_rules. -#}
+{% set routed = bridge_firewall_bridges | selectattr('ip_forward', 'defined') | list %}
+{% set access = bridge_firewall_bridges | selectattr('router_access', 'defined') | list %}
+- afi: ipv4
+ rule_sets:
+{% if access %}
+ - filter: input
+ rules:
+{% if bridge_firewall_state_rules | bool %}
+ - {number: 10, action: accept, state: {established: true, related: true}}
+ - {number: 20, action: drop, state: {invalid: true}}
+{% endif %}
+{% for b in access %}
+ - number: {{ 100 + loop.index * 10 }}
+ action: {{ 'accept' if b.router_access == 'accept' else 'drop' }}
+ description: {{ ((b.router_access == 'accept') | ternary('Accept access from ', 'Deny access from ') ~ b.name) | to_json }}
+ inbound_interface: {group: {{ (b.name ~ '-ifaces') | to_json }}}
+{% endfor %}
+{% endif %}
+{% if routed %}
+ - filter: forward
+ default_action: {{ bridge_firewall_ipv4_forward_default_action }}
+ rules:
+{% if bridge_firewall_state_rules | bool %}
+ - {number: 5, action: accept, state: {established: true, related: true}}
+ - {number: 10, action: drop, state: {invalid: true}}
+{% endif %}
+{% for b in routed %}
+ - number: {{ 100 + loop.index * 10 }}
+ action: jump
+ description: {{ (b.name ~ ' traffic') | to_json }}
+ inbound_interface: {group: {{ (b.name ~ '-ifaces') | to_json }}}
+ jump_target: {{ ('ip-' ~ b.name ~ '-fwd') | to_json }}
+{% endfor %}
+{% for b in routed %}
+ - name: {{ ('ip-' ~ b.name ~ '-fwd') | to_json }}
+ default_action: {{ b.ip_forward.default_action | default('drop') }}
+{% if b.ip_forward.rules | default([]) %}
+ rules:
+{% for r in b.ip_forward.rules %}
+ - number: {{ r.number | int }}
+ action: {{ r.action | default('accept') }}
+{% if r.description is defined %}
+ description: {{ r.description | to_json }}
+{% endif %}
+{% if r.outbound_interface is defined %}
+ outbound_interface: {name: {{ r.outbound_interface | to_json }}}
+{% elif r.outbound_bridge is defined %}
+ outbound_interface: {group: {{ (r.outbound_bridge ~ '-ifaces') | to_json }}}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/roles/bridge_firewall/vars/main.yml b/roles/bridge_firewall/vars/main.yml
new file mode 100644
index 0000000..7299574
--- /dev/null
+++ b/roles/bridge_firewall/vars/main.yml
@@ -0,0 +1,11 @@
+---
+_bridge_firewall_state: "{{ 'rendered' if (vyos_blueprints_render_only | default(false) | bool) else 'merged' }}"
+_bridge_firewall_cli_lines: "{{ (lookup('ansible.builtin.template', 'bridge_cli.j2') or '').splitlines() | map('trim') | select | list }}"
+_bridge_firewall_member_descriptions: >-
+ {%- set out = [] -%}
+ {%- for b in bridge_firewall_bridges -%}
+ {%- for m in b.members if m.description is defined -%}
+ {%- set _ = out.append({'name': m.interface, 'description': m.description}) -%}
+ {%- endfor -%}
+ {%- endfor -%}
+ {{ out }}