From dd4ae77cbfec46453a532c4b61bc404fd1a6fd0a Mon Sep 17 00:00:00 2001 From: Jack Adolph Date: Sun, 17 May 2020 18:10:50 +1000 Subject: [PATCH] Add ca_path option for 802.1x connections Special notes: Hash the cacert in the ca_path as OpenSSL needs symlinks for ca certificates from their symlinks to their filename if specifying a directory. In case ieee802_1x.ca_path is specified but not supported by NetworkManager, fail to ensure the setting is not silently ignored. References: https://stackoverflow.com/questions/25889341/what-is-the-equivalent-of-unix-c-rehash-command-script-on-linux https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/448 Co-authored-by: Till Maas --- README.md | 6 ++ library/network_connections.py | 29 ++++++ .../network_lsr/argument_validator.py | 9 ++ tests/ensure_provider_tests.py | 1 + tests/playbooks/tests_802_1x.yml | 61 +----------- tests/playbooks/tests_802_1x_updated.yml | 13 +++ tests/tasks/setup_802.1x.yml | 11 +++ tests/tasks/test_802.1x_capath.yml | 94 ++++++++++++++++++ tests/tests_802_1x_updated_nm.yml | 21 ++++ tests/unit/test_network_connections.py | 99 +++++++++++++++++++ 10 files changed, 287 insertions(+), 57 deletions(-) create mode 100644 tests/playbooks/tests_802_1x_updated.yml create mode 100644 tests/tasks/setup_802.1x.yml create mode 100644 tests/tasks/test_802.1x_capath.yml create mode 100644 tests/tests_802_1x_updated_nm.yml diff --git a/README.md b/README.md index f753fa9..d826edc 100644 --- a/README.md +++ b/README.md @@ -459,6 +459,12 @@ SSL certificates and keys must be deployed on the host prior to running the role Absolute path to the PEM encoded certificate authority used to verify the EAP server. + * `ca_path` + + Absolute path to directory containing additional pem encoded ca certificates used to + verify the EAP server. Can be used instead of or in addition to ca_cert. Cannot be + used if system_ca_certs is enabled. + * `system_ca_certs` If set to `True`, NetworkManager will use the system's trusted ca certificates to verify the EAP server. diff --git a/library/network_connections.py b/library/network_connections.py index e7ad1d9..3ae7b3b 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1005,6 +1005,11 @@ class NMUtil: Util.path_to_glib_bytes(connection["ieee802_1x"]["ca_cert"]), ) + if connection["ieee802_1x"]["ca_path"]: + s_8021x.set_property( + NM.SETTING_802_1X_CA_PATH, connection["ieee802_1x"]["ca_path"], + ) + s_8021x.set_property( NM.SETTING_802_1X_SYSTEM_CA_CERTS, connection["ieee802_1x"]["system_ca_certs"], @@ -2086,6 +2091,30 @@ class Cmd_nm(Cmd): % (con_cur.get_id(), con_cur.get_uuid()), ) + if ( + self.check_mode == CheckMode.REAL_RUN + and connection["ieee802_1x"] is not None + and connection["ieee802_1x"].get("ca_path") + ): + # It seems that NM on Fedora 31 + # (NetworkManager-1.20.4-1.fc31.x86_64) does need some time so that + # the D-Bus information is actually up-to-date. + time.sleep(0.1) + Util.GMainLoop_iterate_all() + updated_connection = Util.first( + self.nmutil.connection_list( + name=connection["name"], uuid=connection["nm.uuid"] + ) + ) + ca_path = updated_connection.get_setting_802_1x().props.ca_path + if not ca_path: + self.log_fatal( + idx, + "ieee802_1x.ca_path specified but not supported by " + "NetworkManager. Please update NetworkManager or use " + "ieee802_1x.ca_cert.", + ) + seen = set() if con_cur is not None: seen.add(con_cur) diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index f82e740..234351e 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -871,12 +871,21 @@ class ArgValidator_Dict802_1X(ArgValidatorDict): ), ArgValidatorPath("client_cert", required=True), ArgValidatorPath("ca_cert"), + ArgValidatorPath("ca_path"), ArgValidatorBool("system_ca_certs", default_value=False), ArgValidatorStr("domain_suffix_match", required=False), ], default_value=None, ) + def _validate_post(self, value, name, result): + if result["system_ca_certs"] is True and result["ca_path"] is not None: + raise ValidationError( + name, + "ca_path will be ignored by NetworkManager if system_ca_certs is used", + ) + return result + class ArgValidator_DictConnection(ArgValidatorDict): diff --git a/tests/ensure_provider_tests.py b/tests/ensure_provider_tests.py index 04c26f1..8da2ce7 100755 --- a/tests/ensure_provider_tests.py +++ b/tests/ensure_provider_tests.py @@ -61,6 +61,7 @@ NM_ONLY_TESTS = { "playbooks/tests_reapply.yml": {}, "playbooks/tests_states.yml": {}, "playbooks/tests_802_1x.yml": {}, + "playbooks/tests_802_1x_updated.yml": {}, } IGNORE = [ diff --git a/tests/playbooks/tests_802_1x.yml b/tests/playbooks/tests_802_1x.yml index 02fe0a8..ea46751 100644 --- a/tests/playbooks/tests_802_1x.yml +++ b/tests/playbooks/tests_802_1x.yml @@ -7,17 +7,7 @@ - name: "INIT: 802.1x tests" debug: msg: "##################################################" - - include_tasks: tasks/setup_802_1x_server.yml - - name: Copy client certs - copy: - src: "{{ item }}" - dest: "/etc/pki/tls/{{ item }}" - mode: 0644 - with_items: - - client.key - - client.key.nocrypt - - client.pem - - cacert.pem + - include_tasks: tasks/setup_802.1x.yml - block: - name: "TEST: 802.1x profile with private key password and ca cert" debug: @@ -96,52 +86,8 @@ - name: "{{ interface }}" persistent_state: absent state: absent - - name: >- - TEST: 802.1x profile with unencrypted private key and ca_path - debug: - msg: "##################################################" - - name: Create directory for ca_path test - file: - path: "/etc/pki/tls/my_ca_certs" - state: directory - mode: 0755 - - name: Copy cacert to ca_path - copy: - src: "cacert.pem" - dest: "/etc/pki/tls/my_ca_certs/cacert.pem" - mode: 0644 - - name: Hash cacert - command: openssl x509 -hash -noout - -in /etc/pki/tls/my_ca_certs/cacert.pem - register: cacert_hash - - name: Add symlink for cacert - file: - state: link - path: "/etc/pki/tls/my_ca_certs/{{ cacert_hash.stdout }}.0" - src: cacert.pem - - import_role: - name: linux-system-roles.network - vars: - network_connections: - - name: "{{ interface }}" - interface_name: veth2 - state: up - type: ethernet - ip: - address: - - 203.0.113.2/24 - dhcp4: "no" - auto6: "no" - ieee802_1x: - identity: myhost - eap: tls - private_key: /etc/pki/tls/client.key.nocrypt - client_cert: /etc/pki/tls/client.pem - private_key_password_flags: - - not-required - ca_path: /etc/pki/tls/my_ca_certs - - name: "TEST: I can ping the EAP server" - command: ping -c1 203.0.113.1 + + - include_tasks: tasks/test_802.1x_capath.yml always: - block: - import_role: @@ -170,6 +116,7 @@ state: absent path: "{{ item }}" with_items: + - /etc/pki/tls/my_ca_certs - /etc/pki/ca-trust/source/anchors/cacert.pem - name: Update ca trust command: update-ca-trust diff --git a/tests/playbooks/tests_802_1x_updated.yml b/tests/playbooks/tests_802_1x_updated.yml new file mode 100644 index 0000000..82d5734 --- /dev/null +++ b/tests/playbooks/tests_802_1x_updated.yml @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: BSD-3-Clause +--- +- hosts: all + tasks: + - name: Update NetworkManager + package: + name: NetworkManager + state: latest + - name: Restart NetworkManager + service: + name: NetworkManager + state: restarted +- import_playbook: tests_802_1x.yml diff --git a/tests/tasks/setup_802.1x.yml b/tests/tasks/setup_802.1x.yml new file mode 100644 index 0000000..e83e049 --- /dev/null +++ b/tests/tasks/setup_802.1x.yml @@ -0,0 +1,11 @@ +- include_tasks: tasks/setup_802_1x_server.yml +- name: Copy client certs + copy: + src: "{{ item }}" + dest: "/etc/pki/tls/{{ item }}" + mode: 0644 + with_items: + - client.key + - client.key.nocrypt + - client.pem + - cacert.pem diff --git a/tests/tasks/test_802.1x_capath.yml b/tests/tasks/test_802.1x_capath.yml new file mode 100644 index 0000000..f98b1b2 --- /dev/null +++ b/tests/tasks/test_802.1x_capath.yml @@ -0,0 +1,94 @@ +--- +- name: >- + TEST: 802.1x profile with unencrypted private key and ca_path + debug: + msg: "##################################################" +- set_fact: + # Fixed versions/NVRs: + # 1.25.2 + # NetworkManager-1.24.2-1.fc33 + # NetworkManager-1.22.14-1.fc32 + # NetworkManager-1.20.12-1.fc31 + # 1.18.8 + __NM_capath_ignored_NVRs: + - NetworkManager-1.18.0-5.el7.x86_64 + - NetworkManager-1.18.4-3.el7.x86_64 + - NetworkManager-1.20.0-3.el8.x86_64 + - NetworkManager-1.22.8-4.el8.x86_64 + - NetworkManager-1.20.4-1.fc31.x86_64 + - NetworkManager-1.22.10-1.fc32.x86_64 + - NetworkManager-1.22.12-1.fc32.x86_64 +- name: Create directory for ca_path test + file: + path: "/etc/pki/tls/my_ca_certs" + state: directory + mode: 0755 +- name: Copy cacert to ca_path + copy: + src: "cacert.pem" + dest: "/etc/pki/tls/my_ca_certs/cacert.pem" + mode: 0644 +- name: Hash cacert + command: openssl x509 -hash -noout + -in /etc/pki/tls/my_ca_certs/cacert.pem + register: cacert_hash +- name: Add symlink for cacert + file: + state: link + path: "/etc/pki/tls/my_ca_certs/{{ cacert_hash.stdout }}.0" + src: cacert.pem +- name: Get NetworkManager version + command: + cmd: rpm -qa NetworkManager + warn: false + register: __network_NM_NVR +- block: + - import_role: + name: linux-system-roles.network + vars: + network_connections: + - name: "{{ interface | default('802-1x-test') }}" + interface_name: veth2 + state: up + type: ethernet + ip: + address: + - 203.0.113.2/24 + dhcp4: "no" + auto6: "no" + ieee802_1x: + identity: myhost_capath + eap: tls + private_key: /etc/pki/tls/client.key.nocrypt + client_cert: /etc/pki/tls/client.pem + private_key_password_flags: + - not-required + ca_path: /etc/pki/tls/my_ca_certs + - name: "TEST: I can ping the EAP server" + command: ping -c1 203.0.113.1 + - name: trigger failure in case the role did not fail + fail: + msg: after test + rescue: + - debug: + var: "{{ item }}" + with_items: + - ansible_failed_result + - ansible_failed_task + - __network_NM_NVR.stdout + - __NM_capath_ignored_NVRs + + - name: Assert role behavior + assert: + that: __network_connections_result.failed == + (__network_NM_NVR.stdout in __NM_capath_ignored_NVRs) + msg: "Role {{ __network_connections_result.failed and 'failed' + or 'did not fail' }} but was expected + {{ (__network_NM_NVR.stdout in __NM_capath_ignored_NVRs) + and '' or 'not' }} to fail. NM NVR: {{ __network_NM_NVR.stdout }}" + + - name: Assert ping succeeded + assert: + that: + - "not 'cmd' in ansible_failed_result" +... diff --git a/tests/tests_802_1x_updated_nm.yml b/tests/tests_802_1x_updated_nm.yml new file mode 100644 index 0000000..5cbf876 --- /dev/null +++ b/tests/tests_802_1x_updated_nm.yml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: BSD-3-Clause +# This file was generated by ensure_provider_tests.py +--- +# set network provider and gather facts +- hosts: all + name: Run playbook 'playbooks/tests_802_1x_updated.yml' with nm as provider + tasks: + - name: Set network provider to 'nm' + set_fact: + network_provider: nm + + +# workaround for: https://github.com/ansible/ansible/issues/27973 +# There is no way in Ansible to abort a playbook hosts with specific OS +# releases Therefore we include the playbook with the tests only if the hosts +# would support it. +# The test requires or should run with NetworkManager, therefore it cannot run +# on RHEL/CentOS 6 +- import_playbook: playbooks/tests_802_1x_updated.yml + when: + - ansible_distribution_major_version != '6' diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index 3ec9520..d16d9b2 100755 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -1956,6 +1956,7 @@ class TestValidator(unittest.TestCase): "private_key_password_flags": None, "client_cert": "/etc/pki/tls/client.pem", "ca_cert": "/etc/pki/tls/cacert.pem", + "ca_path": None, "system_ca_certs": False, "domain_suffix_match": None, }, @@ -2029,6 +2030,7 @@ class TestValidator(unittest.TestCase): "private_key_password_flags": ["not-required"], "client_cert": "/etc/pki/tls/client.pem", "ca_cert": None, + "ca_path": None, "system_ca_certs": True, "domain_suffix_match": "example.com", }, @@ -2061,6 +2063,79 @@ class TestValidator(unittest.TestCase): ], ) + def test_802_1x_3(self): + """ + Test 802.1x profile with unencrypted private key and ca_path + """ + self.maxDiff = None + self.do_connections_validate( + [ + { + "actions": ["present", "up"], + "autoconnect": True, + "check_iface_exists": True, + "ethernet": ETHERNET_DEFAULTS, + "ethtool": ETHTOOL_DEFAULTS, + "force_state_change": None, + "ignore_errors": None, + "interface_name": "eth0", + "ip": { + "gateway6": None, + "gateway4": None, + "route_metric4": None, + "auto6": True, + "dhcp4": True, + "address": [], + "route_append_only": False, + "rule_append_only": False, + "route": [], + "dns": [], + "dns_search": [], + "route_metric6": None, + "dhcp4_send_hostname": None, + }, + "mac": None, + "master": None, + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "private_key_password": None, + "private_key_password_flags": ["not-required"], + "client_cert": "/etc/pki/tls/client.pem", + "ca_cert": None, + "ca_path": "/etc/pki/tls/my_ca_certs", + "system_ca_certs": False, + "domain_suffix_match": None, + }, + "mtu": None, + "name": "eth0", + "parent": None, + "persistent_state": "present", + "slave_type": None, + "state": "up", + "type": "ethernet", + "wait": None, + "zone": None, + } + ], + [ + { + "name": "eth0", + "state": "up", + "type": "ethernet", + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "client_cert": "/etc/pki/tls/client.pem", + "private_key_password_flags": ["not-required"], + "ca_path": "/etc/pki/tls/my_ca_certs", + }, + } + ], + ) + def test_invalid_cert_path(self): """ should fail if a relative path is used for 802.1x certs/keys @@ -2107,6 +2182,30 @@ class TestValidator(unittest.TestCase): ] ) + def test_802_1x_ca_path_and_system_ca_certs(self): + """ + should fail if ca_path and system_ca_certs are used together + """ + self.maxDiff = None + self.do_connections_check_invalid( + [ + { + "name": "eth0", + "state": "up", + "type": "ethernet", + "ieee802_1x": { + "identity": "myhost", + "eap": "tls", + "private_key": "/etc/pki/tls/client.key", + "client_cert": "/etc/pki/tls/client.pem", + "private_key_password_flags": ["not-required"], + "ca_path": "/etc/pki/my_ca_certs", + "system_ca_certs": True, + }, + } + ] + ) + def test_802_1x_initscripts(self): """ should fail to create ieee802_1x connection with initscripts