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 <opensource@till.name>
This commit is contained in:
Jack Adolph 2020-05-17 18:10:50 +10:00 committed by Till Maas
parent f72fc394ea
commit dd4ae77cbf
10 changed files with 287 additions and 57 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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):

View file

@ -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 = [

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
...

View file

@ -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'

View file

@ -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