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..9330fe7e --- /dev/null +++ b/roles/helper/fw/README.md @@ -0,0 +1,83 @@ +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[].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 +------------ + +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 + skip: true + 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..70538ac7 --- /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_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: [] + + - name: OneFlow + ports: [2474/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: FireEdge + ports: [2616/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: OneXmlRpc + ports: [2633/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: Grafana + ports: [3000/tcp] + groups: + local: ["{{ grafana_group | d('grafana') }}"] + peers: [] + + - name: OneMonitor + ports: [4124/tcp, 4124/udp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ node_group | d('node') }}"] + + - name: OneGate + ports: [5030/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ node_group | d('node') }}"] + + - name: VncPorts + ports: [5900-65535/tcp] + groups: + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: Prometheus + ports: [9090/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: Alertmanager + ports: [9093/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: NodeExporter + ports: [9100/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: DbExporter + ports: [9104/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: OvsExporter + ports: [9475/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: SmartExporter + ports: [9633/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}", "{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: LvmExporter + ports: [9845/tcp] + groups: + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: OneExporter + ports: [9925/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: LibvirtExporter + ports: [9926/tcp] + groups: + local: ["{{ node_group | d('node') }}"] + peers: ["{{ frontend_group | d('frontend') }}"] + + - name: NoVncProxy + ports: [29876/tcp] + groups: + local: ["{{ frontend_group | d('frontend') }}"] + peers: [] + + - name: LiveMigrations + ports: [49152-49215/tcp] + groups: + local: ["{{ node_group | d('node') }}"] + peers: ["{{ node_group | d('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..2d8a4ea5 --- /dev/null +++ b/roles/helper/fw/tasks/main.yml @@ -0,0 +1,206 @@ +--- +- when: + - 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( + 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.skip', 'sameas', true) + | 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]