From dde07242a0b74796957317f48409c2469d6fb7c1 Mon Sep 17 00:00:00 2001 From: Michal Opala Date: Tue, 8 Sep 2026 15:29:51 +0200 Subject: [PATCH 1/3] F #274: Add simple firewall port opener role (helper/fw) - Disable helper/fw by default - Detect and support ufw and firewalld - Do not attempt full management, open ports only on a best-effort basis - Provide default rules for OpenNebula FEs/HVs Signed-off-by: Michal Opala --- playbooks/pre.yml | 3 + roles/helper/fw/README.md | 85 +++++++++++++ roles/helper/fw/defaults/main.yml | 117 +++++++++++++++++ roles/helper/fw/meta/main.yml | 3 + roles/helper/fw/tasks/main.yml | 204 ++++++++++++++++++++++++++++++ 5 files changed, 412 insertions(+) create mode 100644 roles/helper/fw/README.md create mode 100644 roles/helper/fw/defaults/main.yml create mode 100644 roles/helper/fw/meta/main.yml create mode 100644 roles/helper/fw/tasks/main.yml diff --git a/playbooks/pre.yml b/playbooks/pre.yml index 602465cb..ab83c98b 100644 --- a/playbooks/pre.yml +++ b/playbooks/pre.yml @@ -65,6 +65,9 @@ - role: helper/kernel tags: [kernel] + - role: helper/fw + tags: [fw] + - role: helper/keys tags: [keys] diff --git a/roles/helper/fw/README.md b/roles/helper/fw/README.md new file mode 100644 index 00000000..07ea6782 --- /dev/null +++ b/roles/helper/fw/README.md @@ -0,0 +1,85 @@ +Role: opennebula.deploy.helper.fw +================================= + +A role that opens TCP/UDP ports. + +Requirements +------------ + +N/A + +Role Variables +-------------- + +| Name | Type | Default | Description | +|---------------------------|--------|-------------------|--------------------------------------| +| `fw_ports` | `list` | `null` (disabled) | FW configuration to apply. | +| `fw_ports[].name` | `str` | undefined | Unique name of the rule (required). | +| `fw_ports[].ports` | `list` | undefined | List of ports to open. | +| `fw_ports[].groups.local` | `list` | undefined | List of groups to open FW ports on. | +| `fw_ports[].groups.peers` | `list` | undefined | List of groups to open FW ports for. | + +Dependencies +------------ + +N/A + +Example Playbook +---------------- + + # Enable helper/fw, use default rules only. + - hosts: frontend:node + vars: + fw_ports: [] + roles: + - role: opennebula.deploy.helper.facts + - role: opennebula.deploy.helper.fw + + # Enable helper/fw, skip OneXmlRpc rule (please check role defaults). + - hosts: frontend:node + vars: + fw_ports: + - name: OneXmlRpc + ports: [2633/tcp] + groups: + local: [frontend] + peers: null # `null` skips the rule + roles: + - role: opennebula.deploy.helper.facts + - role: opennebula.deploy.helper.fw + + # Enable helper/fw, open custom UDP port on FEs, but allow datagrams only from HVs. + - hosts: frontend:node + vars: + fw_ports: + - name: CustomPort1 + ports: [1234/udp] + groups: + local: [frontend] + peers: [node] + roles: + - role: opennebula.deploy.helper.facts + - role: opennebula.deploy.helper.fw + + # Enable helper/fw, open custom TCP port range everywhere for everybody. + - hosts: frontend:node + vars: + fw_ports: + - name: CustomRange1 + ports: [1234-4321/tcp] # 1234:4321/tcp is also allowed + groups: + local: [frontend, node] + peers: [] # `[]` means "any" or "0.0.0.0" + roles: + - role: opennebula.deploy.helper.facts + - role: opennebula.deploy.helper.fw + +License +------- + +Apache-2.0 + +Author Information +------------------ + +[OpenNebula Systems](https://opennebula.io/) diff --git a/roles/helper/fw/defaults/main.yml b/roles/helper/fw/defaults/main.yml new file mode 100644 index 00000000..43eea920 --- /dev/null +++ b/roles/helper/fw/defaults/main.yml @@ -0,0 +1,117 @@ +--- +fw_ports: null # disabled, set it at least to [] to enable + +fw_default_ports: + - name: OpenSSH + ports: [22/tcp] + groups: + local: [frontend, node] + peers: [] + + - name: OneFlow + ports: [2474/tcp] + groups: + local: [frontend] + peers: [] + + - name: FireEdge + ports: [2616/tcp] + groups: + local: [frontend] + peers: [] + + - name: OneXmlRpc + ports: [2633/tcp] + groups: + local: [frontend] + peers: [] + + - name: Grafana + ports: [3000/tcp] + groups: + local: [grafana] + peers: [] + + - name: OneMonitor + ports: [4124/tcp, 4124/udp] + groups: + local: [frontend] + peers: [node] + + - name: OneGate + ports: [5030/tcp] + groups: + local: [frontend] + peers: [node] + + - name: VncPorts + ports: [5900-65535/tcp] + groups: + local: [node] + peers: [frontend] + + - name: Prometheus + ports: [9090/tcp] + groups: + local: [frontend] + peers: [] + + - name: Alertmanager + ports: [9093/tcp] + groups: + local: [frontend] + peers: [] + + - name: NodeExporter + ports: [9100/tcp] + groups: + local: [frontend, node] + peers: [frontend] + + - name: DbExporter + ports: [9104/tcp] + groups: + local: [frontend] + peers: [frontend] + + - name: OvsExporter + ports: [9475/tcp] + groups: + local: [frontend, node] + peers: [frontend] + + - name: SmartExporter + ports: [9633/tcp] + groups: + local: [frontend, node] + peers: [frontend] + + - name: LvmExporter + ports: [9845/tcp] + groups: + local: [node] + peers: [frontend] + + - name: OneExporter + ports: [9925/tcp] + groups: + local: [frontend] + peers: [frontend] + + - name: LibvirtExporter + ports: [9926/tcp] + groups: + local: [node] + peers: [frontend] + + - name: NoVncProxy + ports: [29876/tcp] + groups: + local: [frontend] + peers: [] + + - name: LiveMigrations + ports: [49152-49215/tcp] + groups: + local: [node] + peers: [node] diff --git a/roles/helper/fw/meta/main.yml b/roles/helper/fw/meta/main.yml new file mode 100644 index 00000000..8fe6dc20 --- /dev/null +++ b/roles/helper/fw/meta/main.yml @@ -0,0 +1,3 @@ +--- +collections: + - opennebula.deploy diff --git a/roles/helper/fw/tasks/main.yml b/roles/helper/fw/tasks/main.yml new file mode 100644 index 00000000..6967fc77 --- /dev/null +++ b/roles/helper/fw/tasks/main.yml @@ -0,0 +1,204 @@ +--- +- when: + - fw_ports is defined + - fw_ports is sequence + vars: + _fw_dict: >- + {{ + {} | combine( + fw_default_ports | map(attribute='name') + | zip(fw_default_ports) + | items2dict(key_name=0, value_name=1), + + fw_ports | map(attribute='name') + | zip(fw_ports) + | items2dict(key_name=0, value_name=1), + + recursive=true + ) | dict2items + | rejectattr('value.groups.peers', 'none') + | items2dict + }} + + _supported: + firewalld: + Debian: firewalld.service + RedHat: firewalld.service + Suse: firewalld.service + ufw: + Debian: ufw.service + RedHat: ufw.service + Suse: ufw.service + + _enabled: >- + {{ systemd_fw_services.results | selectattr('status.LoadState', 'defined') + | rejectattr('status.LoadState', 'in', ['masked', 'not-found']) + | selectattr('status.UnitFileState', 'defined') + | selectattr('status.UnitFileState', 'in', ['enabled']) + | map(attribute='item') }} + + _detected: >- + {{ _enabled | first | d('') }} + block: + - name: Detect FW services (query raw status) + ansible.builtin.systemd_service: + name: "{{ _supported[item][ansible_os_family] }}" + loop: "{{ _supported.keys() }}" + register: systemd_fw_services + no_log: true + + - name: Assert a single FW-managing service is enabled + ansible.builtin.assert: + that: (_enabled | count) < 2 and _detected is truthy + fail_msg: More than one or none enabled FW-managing services detected, aborting.. + + - name: Open requested FW ports + ansible.builtin.shell: + cmd: "{{ _shell[_detected].cmd }}" + executable: /bin/bash + vars: + _shell: + firewalld: + cmd: | + set -o errexit -o pipefail + + BEFORE="$(firewall-cmd --permanent --zone=public --list-all | sort | cksum)" + + {% for v in _fw_dict.values() %} + + {%- set local = v.groups.local | map('extract', groups) + | map('d') + | flatten + | select -%} + {%- set peers = v.groups.peers | map('extract', groups) + | map('d') + | flatten + | select + | reject('==', inventory_hostname) -%} + {%- set ipv4s = peers | map('extract', hostvars, ['ansible_facts', + 'default_ipv4', + 'address']) + | map('d') + | flatten + | select -%} + {%- set ipv6s = peers | map('extract', hostvars, ['ansible_facts', + 'default_ipv6', + 'address']) + | map('d') + | flatten + | select -%} + + {% if inventory_hostname in local %} + + {% for p in v.ports %} + + {% if v.groups.peers | count > 0 %} + {% for h in ipv4s %} + firewall-cmd --permanent --zone=public \ + --add-rich-rule="rule family='ipv4' \ + source address='{{ h }}' \ + port port='{{ p.split('/')[0].replace(':', '-') }}' \ + protocol='{{ p.split('/')[1] }}' \ + accept" + {% endfor %} + {% for h in ipv6s %} + firewall-cmd --permanent --zone=public \ + --add-rich-rule="rule family='ipv6' \ + source address='{{ h }}' \ + port port='{{ p.split('/')[0].replace(':', '-') }}' \ + protocol='{{ p.split('/')[1] }}' \ + accept" + {% endfor %} + {% else %} + firewall-cmd --permanent --zone=public \ + --add-rich-rule="rule family='ipv4' \ + port port='{{ p.split('/')[0].replace(':', '-') }}' \ + protocol='{{ p.split('/')[1] }}' \ + accept" + firewall-cmd --permanent --zone=public \ + --add-rich-rule="rule family='ipv6' \ + port port='{{ p.split('/')[0].replace(':', '-') }}' \ + protocol='{{ p.split('/')[1] }}' \ + accept" + {% endif %} + + {% endfor %} + + {% endif %} + + {% endfor %} + + AFTER="$(firewall-cmd --permanent --zone=public --list-all | sort | cksum)" + + if [[ "$AFTER" == "$BEFORE" ]]; then + exit 0 + else + firewall-cmd --reload + exit 78 # EREMCHG + fi + ufw: + cmd: | + set -o errexit -o pipefail + + BEFORE="$(ufw show added | sort | cksum)" + + {% for v in _fw_dict.values() %} + + {%- set local = v.groups.local | map('extract', groups) + | map('d') + | flatten + | select -%} + {%- set peers = v.groups.peers | map('extract', groups) + | map('d') + | flatten + | select + | reject('==', inventory_hostname) -%} + {%- set ipv4s = peers | map('extract', hostvars, ['ansible_facts', + 'default_ipv4', + 'address']) + | map('d') + | flatten + | select -%} + {%- set ipv6s = peers | map('extract', hostvars, ['ansible_facts', + 'default_ipv6', + 'address']) + | map('d') + | flatten + | select -%} + + {% if inventory_hostname in local %} + + {% for p in v.ports %} + + {% if v.groups.peers | count > 0 %} + {% for h in ipv4s + ipv6s %} + ufw allow from '{{ h }}' \ + to any \ + port '{{ p.split('/')[0].replace('-', ':') }}' \ + proto '{{ p.split('/')[1] }}' + {% endfor %} + {% else %} + ufw allow from any \ + to any \ + port '{{ p.split('/')[0].replace('-', ':') }}' \ + proto '{{ p.split('/')[1] }}' + {% endif %} + + {% endfor %} + + {% endif %} + + {% endfor %} + + AFTER="$(ufw show added | sort | cksum)" + + if [[ "$AFTER" == "$BEFORE" ]]; then + exit 0 + else + exit 78 # EREMCHG + fi + register: shell_open_fw_ports + changed_when: + - shell_open_fw_ports.rc in [78] + failed_when: + - shell_open_fw_ports.rc not in [0, 78] From 29193e1b1639c9a3212682273f3e173729e6103c Mon Sep 17 00:00:00 2001 From: Michal Opala Date: Tue, 8 Sep 2026 16:30:53 +0200 Subject: [PATCH 2/3] M #-: Un-hardcode group names (fix) Signed-off-by: Michal Opala --- roles/helper/fw/defaults/main.yml | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/roles/helper/fw/defaults/main.yml b/roles/helper/fw/defaults/main.yml index 43eea920..70538ac7 100644 --- a/roles/helper/fw/defaults/main.yml +++ b/roles/helper/fw/defaults/main.yml @@ -5,113 +5,113 @@ fw_default_ports: - name: OpenSSH ports: [22/tcp] groups: - local: [frontend, node] + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] peers: [] - name: OneFlow ports: [2474/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: FireEdge ports: [2616/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: OneXmlRpc ports: [2633/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: Grafana ports: [3000/tcp] groups: - local: [grafana] + local: ["{{ grafana_group | d('grafana') }}"] peers: [] - name: OneMonitor ports: [4124/tcp, 4124/udp] groups: - local: [frontend] - peers: [node] + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ node_group | d('node') }}"] - name: OneGate ports: [5030/tcp] groups: - local: [frontend] - peers: [node] + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ node_group | d('node') }}"] - name: VncPorts ports: [5900-65535/tcp] groups: - local: [node] - peers: [frontend] + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: Prometheus ports: [9090/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: Alertmanager ports: [9093/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: NodeExporter ports: [9100/tcp] groups: - local: [frontend, node] - peers: [frontend] + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: DbExporter ports: [9104/tcp] groups: - local: [frontend] - peers: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: OvsExporter ports: [9475/tcp] groups: - local: [frontend, node] - peers: [frontend] + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: SmartExporter ports: [9633/tcp] groups: - local: [frontend, node] - peers: [frontend] + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: LvmExporter ports: [9845/tcp] groups: - local: [node] - peers: [frontend] + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: OneExporter ports: [9925/tcp] groups: - local: [frontend] - peers: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: LibvirtExporter ports: [9926/tcp] groups: - local: [node] - peers: [frontend] + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] - name: NoVncProxy ports: [29876/tcp] groups: - local: [frontend] + local: ["{{ frontend_group | d('frontend') }}"] peers: [] - name: LiveMigrations ports: [49152-49215/tcp] groups: - local: [node] - peers: [node] + local: ["{{ node_group | d('node') }}"] + peers: ["{{ node_group | d('node') }}"] From b8ec936f34467a62dcccc9ce9f16600eb7921e5a Mon Sep 17 00:00:00 2001 From: Michal Opala Date: Wed, 9 Sep 2026 12:22:10 +0200 Subject: [PATCH 3/3] M #-: Add 'skip' option to improve UX (fix) Signed-off-by: Michal Opala --- roles/helper/fw/README.md | 14 ++++++-------- roles/helper/fw/tasks/main.yml | 4 +++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/roles/helper/fw/README.md b/roles/helper/fw/README.md index 07ea6782..9330fe7e 100644 --- a/roles/helper/fw/README.md +++ b/roles/helper/fw/README.md @@ -14,10 +14,11 @@ Role Variables | Name | Type | Default | Description | |---------------------------|--------|-------------------|--------------------------------------| | `fw_ports` | `list` | `null` (disabled) | FW configuration to apply. | -| `fw_ports[].name` | `str` | undefined | Unique name of the rule (required). | -| `fw_ports[].ports` | `list` | undefined | List of ports to open. | -| `fw_ports[].groups.local` | `list` | undefined | List of groups to open FW ports on. | -| `fw_ports[].groups.peers` | `list` | undefined | List of groups to open FW ports for. | +| `fw_ports[].skip` | `bool` | `false` | Decide to skip the rule. | +| `fw_ports[].name` | `str` | | Unique name of the rule. | +| `fw_ports[].ports` | `list` | | List of ports to open. | +| `fw_ports[].groups.local` | `list` | | List of groups to open FW ports on. | +| `fw_ports[].groups.peers` | `list` | | List of groups to open FW ports for. | Dependencies ------------ @@ -40,10 +41,7 @@ Example Playbook vars: fw_ports: - name: OneXmlRpc - ports: [2633/tcp] - groups: - local: [frontend] - peers: null # `null` skips the rule + skip: true roles: - role: opennebula.deploy.helper.facts - role: opennebula.deploy.helper.fw diff --git a/roles/helper/fw/tasks/main.yml b/roles/helper/fw/tasks/main.yml index 6967fc77..2d8a4ea5 100644 --- a/roles/helper/fw/tasks/main.yml +++ b/roles/helper/fw/tasks/main.yml @@ -3,6 +3,8 @@ - fw_ports is defined - fw_ports is sequence vars: + # NOTE: Amazingly 'sameas' does not raise error when LHS is undefined. + # Also, it seems that the 'bool' filter is not needed in this particular case. _fw_dict: >- {{ {} | combine( @@ -16,7 +18,7 @@ recursive=true ) | dict2items - | rejectattr('value.groups.peers', 'none') + | rejectattr('value.skip', 'sameas', true) | items2dict }}