From 8553e52acd79dc8457747def955a749eabf0cb96 Mon Sep 17 00:00:00 2001 From: k-s-dean Date: Tue, 28 Jun 2022 12:18:35 +0100 Subject: [PATCH] adds firewalld configuration based on enabled services This change introduces automated configuration of firewalld and adds a new filter for extracting services from the project_services dict. the filter selects any enabled services and their haproxy element and returns them so they can be iterated over. This commit also enables automated configuration of firewalld from enabled openstack services and adds them to the defined zone and reloads the system firewall. Change-Id: Iea3680142711873984efff2b701347b6a56dd355 --- ansible/group_vars/all.yml | 6 + .../roles/haproxy-config/handlers/main.yml | 6 + ansible/roles/haproxy-config/tasks/main.yml | 18 + ansible/roles/loadbalancer/tasks/precheck.yml | 20 + .../bootstrap-servers.rst | 19 +- doc/source/user/security.rst | 81 ++++ etc/kolla/globals.yml | 9 + kolla_ansible/filters.py | 23 + kolla_ansible/tests/unit/test_filters.py | 405 ++++++++++++++++++ ...-on-enabled-services-96dd418219953a05.yaml | 10 + 10 files changed, 589 insertions(+), 8 deletions(-) create mode 100644 ansible/roles/haproxy-config/handlers/main.yml create mode 100644 releasenotes/notes/add-firewalld-rules-based-on-enabled-services-96dd418219953a05.yaml diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 09360bc87a..e639537f23 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -528,6 +528,12 @@ internal_protocol: "{{ 'https' if kolla_enable_tls_internal | bool else 'http' } # TODO(yoctozepto): Remove after Zed. Kept for compatibility only. admin_protocol: "{{ internal_protocol }}" +################## +# Firewall options +################## +enable_external_api_firewalld: "false" +external_api_firewalld_zone: "public" + #################### # OpenStack options #################### diff --git a/ansible/roles/haproxy-config/handlers/main.yml b/ansible/roles/haproxy-config/handlers/main.yml new file mode 100644 index 0000000000..0f57dca449 --- /dev/null +++ b/ansible/roles/haproxy-config/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Reload firewalld + become: True + service: + name: "firewalld" + state: reloaded diff --git a/ansible/roles/haproxy-config/tasks/main.yml b/ansible/roles/haproxy-config/tasks/main.yml index 19920d463a..ee1b2ef376 100644 --- a/ansible/roles/haproxy-config/tasks/main.yml +++ b/ansible/roles/haproxy-config/tasks/main.yml @@ -21,3 +21,21 @@ with_dict: "{{ project_services }}" notify: - Restart haproxy container + +- name: "Configuring firewall for {{ project_name }}" + firewalld: + offline: "yes" + permanent: "yes" + port: "{{ item.value.port }}/tcp" + state: "enabled" + zone: "{{ external_api_firewalld_zone }}" + become: true + when: + - enable_haproxy | bool + - item.value.enabled | bool + - item.value.port is defined + - item.value.external | default('false') | bool + - enable_external_api_firewalld | bool + with_dict: "{{ project_services | extract_haproxy_services }}" + notify: + - "Reload firewalld" diff --git a/ansible/roles/loadbalancer/tasks/precheck.yml b/ansible/roles/loadbalancer/tasks/precheck.yml index b81dcf28e2..8aa85ff6e8 100644 --- a/ansible/roles/loadbalancer/tasks/precheck.yml +++ b/ansible/roles/loadbalancer/tasks/precheck.yml @@ -838,3 +838,23 @@ - inventory_hostname in groups['loadbalancer'] - haproxy_stat.find('vitrage_api') == -1 - haproxy_vip_prechecks + +- name: Firewalld checks + block: + - name: Check if firewalld is running # noqa command-instead-of-module + become: true + command: + cmd: "systemctl is-active firewalld" + register: firewalld_is_active + changed_when: false + failed_when: false + + - name: Fail if firewalld is not running + fail: + msg: >- + firewalld is not running. + Please install and configure firewalld. + when: + - firewalld_is_active.rc != 0 + when: + - enable_external_api_firewalld | bool diff --git a/doc/source/reference/deployment-and-bootstrapping/bootstrap-servers.rst b/doc/source/reference/deployment-and-bootstrapping/bootstrap-servers.rst index 9a396a3274..d433bdaab2 100644 --- a/doc/source/reference/deployment-and-bootstrapping/bootstrap-servers.rst +++ b/doc/source/reference/deployment-and-bootstrapping/bootstrap-servers.rst @@ -196,18 +196,21 @@ file. Example: } -Disabling firewalls -~~~~~~~~~~~~~~~~~~~ +Enabling/Disabling firewalls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Kolla Ansible does not support configuration of host firewalls, and instead -attempts to disable them. +Kolla Ansible supports configuration of host firewalls. -On Debian family systems where the UFW firewall is enabled, a default policy -will be added to allow all traffic. +Currently only Firewalld is supported. -On Red Hat family systems where firewalld is installed, it will be disabled. +On Debian family systems Firewalld will need to be installed beforehand. -This behaviour can be avoided by setting ``disable_firewall`` to ``false``. +On Red Hat family systems firewalld should be installed by default. + +To enable configuration of the system firewall set ``disable_firewall`` +to ``false`` and set ``enable_external_api_firewalld`` to ``true``. + +For further information. See :doc:`../../user/security` Creation of Python virtual environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/user/security.rst b/doc/source/user/security.rst index e61b2410ba..d0e185bce5 100644 --- a/doc/source/user/security.rst +++ b/doc/source/user/security.rst @@ -76,3 +76,84 @@ via SSH) is default configuration owner and group in target nodes. From Rocky release, Kolla support connection using any user which has passwordless sudo capability. For setting custom owner user and group, user can set ``config_owner_user`` and ``config_owner_group`` in ``globals.yml``. + +FirewallD +~~~~~~~~~ +Prior to Zed, Kolla Ansible would disable any system firewall leaving +configuration up to the end users. Firewalld is now supported and will +configure external api ports for each enabled OpenStack service. + +The following variables should be configured in Kolla Ansible's +``globals.yml`` + +* external_api_firewalld_zone + * The default zone to configure ports on for external API Access + * String - defaults to the public zone +* enable_external_api_firewalld + * Setting to true will enable external API ports configuration + * Bool - set to true or false +* disable_firewall + * Setting to false will stop Kolla Ansible + from disabling the systems firewall + * Bool - set to true or false + + +Prerequsites +============ +Firewalld needs to be installed beforehand. + +Kayobe can be used to automate the installation and configuration of firewalld +before running Kolla Ansible. If you do not use Kayobe you must ensure that +that firewalld has been installed and setup correctly. + +You can check the current active zones by running the command below. +If the output of the command is blank then no zones are configured as active. + +.. code-block:: console + + sudo firewall-cmd --get-active-zones + +You should ensure that the system is reachable via SSH to avoid lockout, +to add ssh to a particular zone run the following command. + +.. code-block:: console + + sudo firewall-cmd --permanent --zone= --add-service=ssh + +You should also set the required interface on a particular zone by running the +below command. This will mark the zone as active on the specified interface. + +.. code-block:: console + + sudo firewall-cmd --permanent --zone= --change-interface= + +if more than one interface is required on a specific zone this can be achieved +by running + +.. code-block:: console + + sudo firewall-cmd --permanent --zone=public --add-interface= + +Any other ports that need to be opened on the system should be done +before hand. The following command will add additional ports to a zone + +.. code-block:: console + + sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent + +Dependent on your infrastructure security policy you may wish to add a policy +of drop on the public zone this can be achieved by running the following +command. + +.. code-block:: console + + sudo firewall-cmd --permanent --set-target=DROP --zone=public + +To apply changes to the system firewall run + +.. code-block:: console + + sudo firewalld-cmd --reload + +For additional information and configuration please see: +https://firewalld.org/documentation/man-pages/firewall-cmd.html diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml index f80dc13c2f..c92548711b 100644 --- a/etc/kolla/globals.yml +++ b/etc/kolla/globals.yml @@ -206,6 +206,15 @@ workaround_ansible_issue_8743: yes #default_container_healthcheck_retries: 3 #default_container_healthcheck_start_period: 5 +################## +# Firewall options +################## +# Configures firewalld on both ubuntu and centos systems +# for enabled services. +# firewalld should be installed beforehand. +# disable_firewall: "true" +# enable_external_api_firewalld: "false" +# external_api_firewalld_zone: "public" ############# # TLS options diff --git a/kolla_ansible/filters.py b/kolla_ansible/filters.py index 7eb826b7c0..88dcff5e9f 100644 --- a/kolla_ansible/filters.py +++ b/kolla_ansible/filters.py @@ -34,6 +34,28 @@ def service_enabled(context, service): return _call_bool_filter(context, enabled) +@jinja2.pass_context +def extract_haproxy_services(context, services): + """Return a Dict of haproxy services + + :param context: Jinja2 Context object. + :param service: Services definition, dict. + :returns: A Dict. + """ + haproxy = {} + for key in services: + service = services.get(key) + if service_enabled(context, service): + service_haproxy = service.get('haproxy') + if service_haproxy: + if not set(haproxy).isdisjoint(set(service_haproxy)): + raise exception.FilterError( + "haproxy service names should be unique") + haproxy.update(service_haproxy) + + return haproxy + + @jinja2.pass_context def service_mapped_to_host(context, service): """Return whether a service is mapped to this host. @@ -89,6 +111,7 @@ def select_services_enabled_and_mapped_to_host(context, services): def get_filters(): return { + "extract_haproxy_services": extract_haproxy_services, "service_enabled": service_enabled, "service_mapped_to_host": service_mapped_to_host, "service_enabled_and_mapped_to_host": ( diff --git a/kolla_ansible/tests/unit/test_filters.py b/kolla_ansible/tests/unit/test_filters.py index 859cfd7aae..c6c6adf75a 100644 --- a/kolla_ansible/tests/unit/test_filters.py +++ b/kolla_ansible/tests/unit/test_filters.py @@ -110,6 +110,411 @@ class TestFilters(unittest.TestCase): filters.service_mapped_to_host, self.context, service) + def test_extract_haproxy_services_empty_dict(self): + example_service = {} + actual = filters.extract_haproxy_services( + self.context, example_service) + # No change + self.assertDictEqual({}, actual) + + def test_extract_haproxy_services_no_haproxy_dict(self): + example_service = { + "keystone-ssh": { + "container_name": "keystone_ssh", + "dimensions": {}, + "enabled": True, + "group": "keystone", + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_listen sshd 8023" + ], + "timeout": "30" + }, + "image": "keystone-ssh:latest", + "volumes": [ + "/etc/kolla/keystone-ssh/:/var/lib/kolla/config_files/:ro", + "/etc/localtime:/etc/localtime:ro", + "", + "kolla_logs:/var/log/kolla/", + "keystone_fernet_tokens:/etc/keystone/fernet-keys" + ] + } + } + actual = filters.extract_haproxy_services(self.context, + example_service) + self.assertDictEqual({}, actual) + + def test_extract_haproxy_services_haproxy_dict(self): + example_service = { + "keystone": { + "container_name": "keystone", + "dimensions": {}, + "enabled": True, + "group": "keystone", + "haproxy": { + "keystone_admin": { + "enabled": True, + "external": False, + "listen_port": "35357", + "mode": "http", + "port": "35357", + "tls_backend": True + }, + "keystone_external": { + "backend_http_extra": [], + "enabled": True, + "external": True, + "listen_port": "5000", + "mode": "http", + "port": "5000", + "tls_backend": True + }, + "keystone_internal": { + "backend_http_extra": [], + "enabled": True, + "external": False, + "listen_port": "5000", + "mode": "http", + "port": "5000", + "tls_backend": True + } + }, + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_curl https://1.2.3.4:5000" + ], + "timeout": "30" + }, + "image": "keystone:latest", + "volumes": [ + "/etc/kolla/keystone/:/var/lib/kolla/config_files/:ro", + "/etc/localtime:/etc/localtime:ro", + "", + "", + "kolla_logs:/var/log/kolla/", + "keystone_fernet_tokens:/etc/keystone/fernet-keys" + ] + } + } + expected = { + 'keystone_admin': { + 'enabled': True, + 'external': False, + 'listen_port': '35357', + 'mode': 'http', + 'port': '35357', + 'tls_backend': True + }, + 'keystone_external': { + 'backend_http_extra': [], + 'enabled': True, + 'external': True, + 'listen_port': '5000', + 'mode': 'http', + 'port': '5000', + 'tls_backend': True + }, + 'keystone_internal': { + 'backend_http_extra': [], + 'enabled': True, + 'external': False, + 'listen_port': '5000', + 'mode': 'http', + 'port': '5000', + 'tls_backend': True + } + } + actual = filters.extract_haproxy_services(self.context, + example_service) + self.assertDictEqual(expected, actual) + + def test_extract_two_services_with_haproxy_dict(self): + example_service = { + "glance-api": { + "container_name": "glance_api", + "dimensions": {}, + "enabled": True, + "environment": { + "http_proxy": "", + "https_proxy": "", + "no_proxy": "127.0.0.1,localhost,1.2.3.4,1.2.3.4" + }, + "group": "glance-api", + "haproxy": { + "glance_api": { + "backend_http_extra": [ + "timeout server 6h" + ], + "custom_member_list": [ + "server someserver 1.2.3.4:9292 " + "check inter 2000 rise 2 fall 5", + "" + ], + "enabled": False, + "external": False, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292" + }, + "glance_api_external": { + "backend_http_extra": [ + "timeout server 6h" + ], + "custom_member_list": [ + "server someserver 1.2.3.4:9292 " + "check inter 2000 rise 2 fall 5", + "" + ], + "enabled": False, + "external": True, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292" + } + }, + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_curl http://localhost:9292" + ], + "timeout": "30" + }, + "host_in_groups": True, + "image": "centos-source-glance-api:latest", + "privileged": False, + "volumes": [ + "/etc/localtime:/etc/localtime:ro", + "", + "glance:/var/lib/glance/", + "", + "kolla_logs:/var/log/kolla/", + "", + "" + ] + }, + "glance-tls-proxy": { + "container_name": "glance_tls_proxy", + "dimensions": {}, + "enabled": True, + "group": "glance-api", + "haproxy": { + "glance_tls_proxy": { + "backend_http_extra": [ + "timeout server 6h" + ], + "custom_member_list": [ + "server someserver 1.2.3.4:9292 " + "check inter 2000 rise 2 fall 5 ssl verify " + "required ca-file ca-bundle.trust.crt", + "" + ], + "enabled": True, + "external": False, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292", + "tls_backend": "yes" + }, + "glance_tls_proxy_external": { + "backend_http_extra": [ + "timeout server 6h" + ], + "custom_member_list": [ + "server someserver 1.2.3.4:9292 " + "check inter 2000 rise 2 fall 5 ssl verify " + "required ca-file ca-bundle.trust.crt", + "" + ], + "enabled": True, + "external": True, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292", + "tls_backend": "yes" + } + }, + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_curl -u openstack:asdf 1.2.3.4:9293" + ], + "timeout": "30" + }, + "host_in_groups": True, + "image": "centos-source-haproxy:latest", + "volumes": [ + "/etc/localtime:/etc/localtime:ro", + "", + "kolla_logs:/var/log/kolla/" + ] + } + } + expected = { + "glance_api": { + "backend_http_extra": [ + "timeout server 6h" + ], + 'custom_member_list': ['server someserver ' + '1.2.3.4:9292 check inter 2000 ' + 'rise 2 fall 5', + ''], + "enabled": False, + "external": False, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292" + }, + "glance_api_external": { + "backend_http_extra": [ + "timeout server 6h" + ], + 'custom_member_list': ['server someserver ' + '1.2.3.4:9292 check inter 2000 ' + 'rise 2 fall 5', + ''], + "enabled": False, + "external": True, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292" + }, + "glance_tls_proxy": { + "backend_http_extra": [ + "timeout server 6h" + ], + 'custom_member_list': ['server someserver 1.2.3.4:9292 ' + 'check inter 2000 rise 2 fall 5 ' + 'ssl verify required ca-file ' + 'ca-bundle.trust.crt', + ''], + "enabled": True, + "external": False, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292", + "tls_backend": "yes" + }, + "glance_tls_proxy_external": { + "backend_http_extra": [ + "timeout server 6h" + ], + 'custom_member_list': ['server someserver 1.2.3.4:9292 ' + 'check inter 2000 rise 2 fall 5 ' + 'ssl verify required ca-file ' + 'ca-bundle.trust.crt', + ''], + + "enabled": True, + "external": True, + "frontend_http_extra": [ + "timeout client 6h" + ], + "mode": "http", + "port": "9292", + "tls_backend": "yes" + } + } + actual = filters.extract_haproxy_services(self.context, + example_service) + self.assertDictEqual(expected, actual) + + def test_extract_haproxy_services_haproxy_dict_duplicate(self): + example_service = { + "keystone": { + "container_name": "keystone", + "dimensions": {}, + "enabled": True, + "group": "keystone", + "haproxy": { + "keystone_admin": { + "enabled": True, + "external": False, + "listen_port": "35357", + "mode": "http", + "port": "35357", + "tls_backend": True + }, + }, + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_curl https://1.2.3.4:5000" + ], + "timeout": "30" + }, + "image": "keystone:latest", + "volumes": [ + "/etc/kolla/keystone/:/var/lib/kolla/config_files/:ro", + "kolla_logs:/var/log/kolla/" + ] + }, + "keystone-ssh": { + "container_name": "keystone_ssh", + "dimensions": {}, + "enabled": True, + "group": "keystone", + "haproxy": { + "keystone_admin": { + "enabled": True, + "external": False, + "listen_port": "35357", + "mode": "http", + "port": "35357", + "tls_backend": True + }, + }, + "healthcheck": { + "interval": "30", + "retries": "3", + "start_period": "5", + "test": [ + "CMD-SHELL", + "healthcheck_listen sshd 8023" + ], + "timeout": "30" + }, + "image": "keystone-ssh:latest", + "volumes": [ + "/etc/kolla/keystone-ssh/:/var/lib/kolla/config_files/:ro", + "kolla_logs:/var/log/kolla/" + ] + } + } + self.assertRaises(exception.FilterError, + filters.extract_haproxy_services, + self.context, example_service) + @mock.patch.object(filters, 'service_enabled') @mock.patch.object(filters, 'service_mapped_to_host') def test_service_enabled_and_mapped_to_host(self, mock_mapped, diff --git a/releasenotes/notes/add-firewalld-rules-based-on-enabled-services-96dd418219953a05.yaml b/releasenotes/notes/add-firewalld-rules-based-on-enabled-services-96dd418219953a05.yaml new file mode 100644 index 0000000000..5123b5f1a8 --- /dev/null +++ b/releasenotes/notes/add-firewalld-rules-based-on-enabled-services-96dd418219953a05.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Enables configuring firewalld for external API services. + Extracts the required services and checks the external port, + then adds the ports to a firewalld zone. + Assumes that firewalld has been installed and configured beforehand. + The variable disable_firewall, is disabled by default to preserve + backwards compatibility. + But its good practice to have the system firewall configured.