diff --git a/changelogs/fragments/6668-ldap-client-cert.yml b/changelogs/fragments/6668-ldap-client-cert.yml new file mode 100644 index 0000000000..e566b4ae13 --- /dev/null +++ b/changelogs/fragments/6668-ldap-client-cert.yml @@ -0,0 +1,2 @@ +minor_changes: + - ldap_* - add new arguments ``client_cert`` and ``client_key`` to the LDAP modules in order to allow certificate authentication (https://github.com/ansible-collections/community.general/pull/6668). diff --git a/plugins/doc_fragments/ldap.py b/plugins/doc_fragments/ldap.py index 451b3f3e06..e11ab065d8 100644 --- a/plugins/doc_fragments/ldap.py +++ b/plugins/doc_fragments/ldap.py @@ -29,6 +29,18 @@ options: - Set the path to PEM file with CA certs. type: path version_added: "6.5.0" + client_cert: + type: path + description: + - PEM formatted certificate chain file to be used for SSL client authentication. + - Required if O(client_key) is defined. + version_added: "7.1.0" + client_key: + type: path + description: + - PEM formatted file that contains your private key to be used for SSL client authentication. + - Required if O(client_cert) is defined. + version_added: "7.1.0" dn: required: true description: diff --git a/plugins/module_utils/ldap.py b/plugins/module_utils/ldap.py index 6553713210..ef444e9778 100644 --- a/plugins/module_utils/ldap.py +++ b/plugins/module_utils/ldap.py @@ -42,11 +42,17 @@ def gen_specs(**specs): 'validate_certs': dict(default=True, type='bool'), 'sasl_class': dict(choices=['external', 'gssapi'], default='external', type='str'), 'xorder_discovery': dict(choices=['enable', 'auto', 'disable'], default='auto', type='str'), + 'client_cert': dict(default=None, type='path'), + 'client_key': dict(default=None, type='path'), }) return specs +def ldap_required_together(): + return [['client_cert', 'client_key']] + + class LdapGeneric(object): def __init__(self, module): # Shortcuts @@ -60,6 +66,8 @@ class LdapGeneric(object): self.verify_cert = self.module.params['validate_certs'] self.sasl_class = self.module.params['sasl_class'] self.xorder_discovery = self.module.params['xorder_discovery'] + self.client_cert = self.module.params['client_cert'] + self.client_key = self.module.params['client_key'] # Establish connection self.connection = self._connect_to_ldap() @@ -102,6 +110,10 @@ class LdapGeneric(object): if self.ca_path: ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.ca_path) + if self.client_cert and self.client_key: + ldap.set_option(ldap.OPT_X_TLS_CERTFILE, self.client_cert) + ldap.set_option(ldap.OPT_X_TLS_KEYFILE, self.client_key) + connection = ldap.initialize(self.server_uri) if self.referrals_chasing == 'disabled': diff --git a/plugins/modules/ldap_attrs.py b/plugins/modules/ldap_attrs.py index c2cac86444..d93b4672a4 100644 --- a/plugins/modules/ldap_attrs.py +++ b/plugins/modules/ldap_attrs.py @@ -182,7 +182,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together import re @@ -300,6 +300,7 @@ def main(): state=dict(type='str', default='present', choices=['absent', 'exact', 'present']), ), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/plugins/modules/ldap_entry.py b/plugins/modules/ldap_entry.py index 619bbf9279..11fa5278dc 100644 --- a/plugins/modules/ldap_entry.py +++ b/plugins/modules/ldap_entry.py @@ -151,7 +151,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_native, to_bytes -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -255,6 +255,7 @@ def main(): ), required_if=[('state', 'present', ['objectClass'])], supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/plugins/modules/ldap_passwd.py b/plugins/modules/ldap_passwd.py index f47fa330e3..3f9e6ec4ad 100644 --- a/plugins/modules/ldap_passwd.py +++ b/plugins/modules/ldap_passwd.py @@ -72,7 +72,7 @@ modlist: import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -133,6 +133,7 @@ def main(): module = AnsibleModule( argument_spec=gen_specs(passwd=dict(no_log=True)), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/plugins/modules/ldap_search.py b/plugins/modules/ldap_search.py index 672ad51347..c6dbac4bf9 100644 --- a/plugins/modules/ldap_search.py +++ b/plugins/modules/ldap_search.py @@ -113,7 +113,7 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.six import string_types, text_type -from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs +from ansible_collections.community.general.plugins.module_utils.ldap import LdapGeneric, gen_specs, ldap_required_together LDAP_IMP_ERR = None try: @@ -136,6 +136,7 @@ def main(): base64_attributes=dict(type='list', elements='str'), ), supports_check_mode=True, + required_together=ldap_required_together(), ) if not HAS_LDAP: diff --git a/tests/integration/targets/ldap_search/tasks/tests/auth.yml b/tests/integration/targets/ldap_search/tasks/tests/auth.yml new file mode 100644 index 0000000000..a8c7a13ee9 --- /dev/null +++ b/tests/integration/targets/ldap_search/tasks/tests/auth.yml @@ -0,0 +1,47 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- debug: + msg: Running tests/auth.yml + +#################################################################### +## Search ########################################################## +#################################################################### +- name: Test simple search for password authenticated user + ldap_search: + dn: "ou=users,dc=example,dc=com" + scope: "onelevel" + filter: "(uid=ldaptest)" + bind_dn: "uid=ldaptest,ou=users,dc=example,dc=com" + bind_pw: "test1pass!" + ignore_errors: true + register: output + +- name: assert that test LDAP user can read its password + assert: + that: + - output is not failed + - output.results | length == 1 + - output.results.0.userPassword is defined + +- name: Test simple search for cert authenticated user + ldap_search: + dn: "ou=users,dc=example,dc=com" + server_uri: "ldap://localhost/" + start_tls: true + ca_path: /usr/local/share/ca-certificates/ca.crt + scope: "onelevel" + filter: "(uid=ldaptest)" + client_cert: "/root/user.crt" + client_key: "/root/user.key" + ignore_errors: true + register: output + +- name: assert that test LDAP user can read its password + assert: + that: + - output is not failed + - output.results | length == 1 + - output.results.0.userPassword is defined diff --git a/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif b/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif new file mode 100644 index 0000000000..fc97e5f5c3 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif @@ -0,0 +1,15 @@ +dn: cn=config +add: olcTLSCACertificateFile +olcTLSCACertificateFile: /usr/local/share/ca-certificates/ca.crt +- +add: olcTLSCertificateFile +olcTLSCertificateFile: /etc/ldap/localhost.crt +- +add: olcTLSCertificateKeyFile +olcTLSCertificateKeyFile: /etc/ldap/localhost.key +- +add: olcAuthzRegexp +olcAuthzRegexp: {0}"UID=([^,]*)" uid=$1,ou=users,dc=example,dc=com +- +add: olcTLSVerifyClient +olcTLSVerifyClient: allow diff --git a/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif.license b/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif.license new file mode 100644 index 0000000000..edff8c7685 --- /dev/null +++ b/tests/integration/targets/setup_openldap/files/cert_cnconfig.ldif.license @@ -0,0 +1,3 @@ +GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +SPDX-License-Identifier: GPL-3.0-or-later +SPDX-FileCopyrightText: Ansible Project diff --git a/tests/integration/targets/setup_openldap/files/initial_config.ldif b/tests/integration/targets/setup_openldap/files/initial_config.ldif index 8f8c537bd3..6f2c940c15 100644 --- a/tests/integration/targets/setup_openldap/files/initial_config.ldif +++ b/tests/integration/targets/setup_openldap/files/initial_config.ldif @@ -18,5 +18,6 @@ homeDirectory: /home/ldaptest cn: LDAP Test gecos: LDAP Test displayName: LDAP Test +userPassword: test1pass! mail: ldap.test@example.com sn: Test diff --git a/tests/integration/targets/setup_openldap/tasks/main.yml b/tests/integration/targets/setup_openldap/tasks/main.yml index 25077de166..00f8f6a108 100644 --- a/tests/integration/targets/setup_openldap/tasks/main.yml +++ b/tests/integration/targets/setup_openldap/tasks/main.yml @@ -44,6 +44,22 @@ cmd: "export DEBIAN_FRONTEND=noninteractive; cat /root/debconf-slapd.conf | debconf-set-selections; dpkg-reconfigure -f noninteractive slapd" creates: "/root/slapd_configured" + - name: Enable secure ldap + lineinfile: + path: /etc/default/slapd + regexp: "^SLAPD_SERVICES" + line: 'SLAPD_SERVICES="ldap:/// ldaps:/// ldapi:///"' + + - name: Create certificates + shell: | + openssl req -x509 -batch -sha256 -days 1825 -newkey rsa:2048 -nodes -keyout /root/ca.key -out /usr/local/share/ca-certificates/ca.crt + openssl req -batch -sha256 -days 365 -newkey rsa:2048 -subj "/CN=$(hostname)" -addext "subjectAltName = DNS:localhost" -nodes -keyout /etc/ldap/localhost.key -out /etc/ldap/localhost.csr + openssl x509 -req -CA /usr/local/share/ca-certificates/ca.crt -CAkey /root/ca.key -CAcreateserial -in /etc/ldap/localhost.csr -out /etc/ldap/localhost.crt + chgrp openldap /etc/ldap/localhost.key + chmod 0640 /etc/ldap/localhost.key + openssl req -batch -sha256 -days 365 -newkey rsa:2048 -subj "/UID=ldaptest" -nodes -keyout /root/user.key -out /root/user.csr + openssl x509 -req -CA /usr/local/share/ca-certificates/ca.crt -CAkey /root/ca.key -CAcreateserial -in /root/user.csr -out /root/user.crt + - name: Start OpenLDAP service become: true service: @@ -61,10 +77,14 @@ mode: '0644' loop: - rootpw_cnconfig.ldif + - cert_cnconfig.ldif - initial_config.ldif - name: Configure admin password for cn=config - shell: "ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/rootpw_cnconfig.ldif" + shell: "ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/{{ item }}" + loop: + - rootpw_cnconfig.ldif + - cert_cnconfig.ldif - name: Add initial config become: true