From 04f056867cbbe7f56c81609609d8f7ce8e9c7331 Mon Sep 17 00:00:00 2001 From: Jakub Jelen Date: Tue, 13 Dec 2022 17:55:13 +0100 Subject: [PATCH] Add support for managing selinux and firewall on RHEL --- README.md | 34 +++++++++ defaults/main.yml | 8 ++ meta/collection-requirements.yml | 3 + tasks/find_ports.yml | 26 +++++++ tasks/firewall.yml | 25 ++++++ tasks/install.yml | 18 +++++ tasks/selinux.yml | 16 ++++ tests/requirements.yml | 1 + tests/tests_firewall_selinux.yml | 127 +++++++++++++++++++++++++++++++ 9 files changed, 258 insertions(+) create mode 100644 meta/collection-requirements.yml create mode 100644 tasks/find_ports.yml create mode 100644 tasks/firewall.yml create mode 100644 tasks/selinux.yml create mode 100644 tests/tests_firewall_selinux.yml diff --git a/README.md b/README.md index 8c24ebe..b081749 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,21 @@ Tested on: It will likely work on other flavours and more direct support via suitable [vars/](vars/) files is welcome. + +### Optional requirements + +If you want to use advanced functionality of this role that can configure +firewall and selinux for you, which is mostly useful when custom port is used, +the role requires additional collections which are specified in +`meta/collection-requirements.yml`. These are not automatically installed. +You must install them like this: +``` +ansible-galaxy install -vv -r meta/collection-requirements.yml +``` + +For more information, see `sshd_manage_firewall` and `sshd_manage_selinux` +options below. These roles are supported only on Red Hat based Linux. + Role variables --------------- @@ -93,6 +108,25 @@ Using these variables, you can use your own custom templates. With the above default templates, the name of the installed ssh service will be provided by the `sshd_service` variable. +* `sshd_manage_firewall` + +If set to *true*, the the SSH port(s) will be opened in firewall. Note, this +works only on Red Hat based OS. The default is *false*. + +NOTE: `sshd_manage_firewall` is limited to *adding* ports. It cannot be used +for *removing* ports. If you want to remove ports, you will need to use the +firewall system role directly. + +* `sshd_manage_selinux` + +If set to *true*, the the selinux will be configured to allow sshd listening +on the given SSH port(s). Note, this works only on Red Hat based OS. +The default is *false*. + +NOTE: `sshd_manage_selinux` is limited to *adding* policy. It cannot be used +for *removing* policy. If you want to remove ports, you will need to use the +selinux system role directly. + * `sshd` A dict containing configuration. e.g. diff --git a/defaults/main.yml b/defaults/main.yml index f35e3b3..de74add 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -73,3 +73,11 @@ sshd_hostkey_mode: "{{ __sshd_hostkey_mode }}" # instead of replacing the whole configuration file, just add a specified # snippet sshd_config_namespace: null + +# If this option is enabled, the role will configure firewall to open the ports +# defined in the configuration. This works only on Red Hat based systems. +sshd_manage_firewall: false + +# If this option is enabled, the role will configure selinux to allow sshd to +# bind the ports defined in the configuration. This works only on Red Hat based systems. +sshd_manage_selinux: false diff --git a/meta/collection-requirements.yml b/meta/collection-requirements.yml new file mode 100644 index 0000000..a9c13c4 --- /dev/null +++ b/meta/collection-requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - name: fedora.linux_system_roles diff --git a/tasks/find_ports.yml b/tasks/find_ports.yml new file mode 100644 index 0000000..413cdfc --- /dev/null +++ b/tasks/find_ports.yml @@ -0,0 +1,26 @@ +--- +- name: Find the port the ssh service is going to use + vars: + # This mimics the macro body_option() in sshd_config.j2 + # The explicit to_json filter is needed for Python 2 compatibility + __sshd_ports_from_config_tmp: >- + {% if sshd_Port is defined %} + {{ sshd_Port | to_json }} + {% elif sshd['Port'] is defined %} + {{ sshd['Port'] | to_json }} + {% elif __sshd_defaults['Port'] is defined and not sshd_skip_defaults %} + {{ __sshd_defaults['Port'] | to_json }} + {% else %} + {{ [22] | to_json }} + {% endif %} + ansible.builtin.set_fact: + __sshd_ports_from_config: >- + {% if __sshd_ports_from_config_tmp | from_json is string or __sshd_ports_from_config_tmp | from_json is number %} + {{ [__sshd_ports_from_config_tmp | from_json] | to_json }} + {% else %} + {{ __sshd_ports_from_config_tmp }} + {% endif %} + when: + - sshd_manage_firewall | bool or sshd_manage_selinux | bool + - ansible_facts['os_family'] == 'RedHat' + - ansible_virtualization_type|default(None) not in __sshd_skip_virt_env diff --git a/tasks/firewall.yml b/tasks/firewall.yml new file mode 100644 index 0000000..1780614 --- /dev/null +++ b/tasks/firewall.yml @@ -0,0 +1,25 @@ +--- +- name: Ensure the ssh service or custom ports are opened in firewall + block: + - name: Enable the ssh service on default port + ansible.builtin.include_role: + name: fedora.linux_system_roles.firewall + vars: + firewall: + - service: ssh + state: enabled + when: + - __sshd_ports_from_config | from_json == [22] + + - name: Enable the non-default port(s) + ansible.builtin.include_role: + name: fedora.linux_system_roles.firewall + vars: + firewall: + - port: "{{ sshd_item }}/tcp" + state: enabled + loop: "{{ __sshd_ports_from_config | from_json | d([]) }}" + loop_control: + loop_var: sshd_item # avoid conflicts with the firewall loops + when: + - __sshd_ports_from_config | from_json != [22] diff --git a/tasks/install.yml b/tasks/install.yml index e02fe94..22da717 100644 --- a/tasks/install.yml +++ b/tasks/install.yml @@ -121,6 +121,24 @@ when: - __sshd_runtime_directory is not none + - name: Find SSHD ports + ansible.builtin.include_tasks: find_ports.yml + + - name: Configure firewall + ansible.builtin.include_tasks: firewall.yml + when: + - sshd_manage_firewall | bool + - ansible_facts['os_family'] == 'RedHat' + - ansible_facts['distribution_version'] is version('7', '>=') + - ansible_virtualization_type|default(None) not in __sshd_skip_virt_env + + - name: Configure selinux + ansible.builtin.include_tasks: selinux.yml + when: + - sshd_manage_selinux | bool + - ansible_facts['os_family'] == 'RedHat' + - ansible_virtualization_type|default(None) not in __sshd_skip_virt_env + - name: Create the complete configuration file ansible.builtin.include_tasks: install_config.yml when: sshd_config_namespace is none diff --git a/tasks/selinux.yml b/tasks/selinux.yml new file mode 100644 index 0000000..029a4ac --- /dev/null +++ b/tasks/selinux.yml @@ -0,0 +1,16 @@ +--- +- name: Ensure the custom ports are configured in selinux + ansible.builtin.include_role: + name: fedora.linux_system_roles.selinux + vars: + selinux_ports: + - ports: "{{ sshd_item }}" + proto: tcp + setype: ssh_port_t + state: present + local: true + loop: "{{ __sshd_ports_from_config | from_json | d([]) }}" + loop_control: + loop_var: sshd_item # avoid conflicts with the selinux loops + when: + - __sshd_ports_from_config | from_json != [22] diff --git a/tests/requirements.yml b/tests/requirements.yml index a0cd255..d767353 100644 --- a/tests/requirements.yml +++ b/tests/requirements.yml @@ -1,3 +1,4 @@ --- collections: - name: ansible.posix + - name: fedora.linux_system_roles diff --git a/tests/tests_firewall_selinux.yml b/tests/tests_firewall_selinux.yml new file mode 100644 index 0000000..10c92ce --- /dev/null +++ b/tests/tests_firewall_selinux.yml @@ -0,0 +1,127 @@ +--- +- hosts: all + vars: + __sshd_test_backup_files: + - /etc/ssh/sshd_config + - /etc/ssh/sshd_config.d/00-ansible_system_role.conf + tasks: + - name: "Backup configuration files" + ansible.builtin.include_tasks: tasks/backup.yml + + ########## + # First test: default port + ########## + - name: Configure the role on default port and let it handle firewall settings + ansible.builtin.include_role: + name: ansible-sshd + vars: + sshd_manage_selinux: true + sshd_manage_firewall: true + sshd: + Port: 22 + + - name: Verify the options are correctly set + block: + - name: Flush handlers + ansible.builtin.meta: flush_handlers + + - name: Print current configuration file + ansible.builtin.slurp: + src: "{{ main_sshd_config }}" + register: config + + - name: Check the options are in configuration file + ansible.builtin.assert: + that: + - "'Port 22' in config.content | b64decode" + tags: tests::verify + + ########## + # Second test: non-default port + ########## + # is this going to break some tests running ansible through ssh? + - name: Configure the role on another port and let it handle firewall settings + ansible.builtin.include_role: + name: ansible-sshd + vars: + sshd_manage_firewall: true + sshd_manage_selinux: true + sshd: + Port: 222 + + - name: Verify the options are correctly set + block: + - name: Flush handlers + ansible.builtin.meta: flush_handlers + + - name: Print current configuration file + ansible.builtin.slurp: + src: "{{ main_sshd_config }}" + register: config + + - name: Check the options are in configuration file + ansible.builtin.assert: + that: + - "'Port 222' in config.content | b64decode" + tags: tests::verify + + ########## + # Third test: multiple ports + ########## + - name: Configure the role on several ports and let it handle firewall settings + ansible.builtin.include_role: + name: ansible-sshd + vars: + sshd_manage_firewall: true + sshd_manage_selinux: true + sshd: + Port: + - 22 + - 222 + + - name: Verify the options are correctly set + block: + - name: Flush handlers + ansible.builtin.meta: flush_handlers + + - name: Print current configuration file + ansible.builtin.slurp: + src: "{{ main_sshd_config }}" + register: config + + - name: Check the options are in configuration file + ansible.builtin.assert: + that: + - "'Port 222' in config.content | b64decode" + tags: tests::verify + + ########## + # Cleanup + ########## + - name: "Restore configuration files" + ansible.builtin.include_tasks: tasks/restore.yml + + - name: Remove the modification to the firewall rules + ansible.builtin.include_role: + name: fedora.linux_system_roles.firewall + vars: + firewall: + - port: "222/tcp" + state: disabled + when: + - ansible_facts['os_family'] == 'RedHat' + - ansible_virtualization_type|default(None) not in __sshd_skip_virt_env + + - name: Remove the modification to the selinux policy + ansible.builtin.include_role: + name: fedora.linux_system_roles.firewall + vars: + selinux: + port: 222 + proto: tcp + setype: ssh_port_t + state: absent + local: true + when: + - ansible_facts['os_family'] == 'RedHat' + - ansible_virtualization_type|default(None) not in __sshd_skip_virt_env