mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
ldap: Add client certificate support (#6668)
* Set up secure ldap server * ldap: Added client cert options Shamelessly copied from https://github.com/andrewshulgin/ldap_search * Added tests for ldap client authentication * Add changelog fragment * Make sure the openssl commands work on older versions of openssl * Apply suggestions from code review Co-authored-by: Felix Fontein <felix@fontein.de> * Remove aliases for new arguments * Add required_together to ldap module declerations --------- Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
bb2169340d
commit
f3ecf4c7f8
12 changed files with 121 additions and 5 deletions
2
changelogs/fragments/6668-ldap-client-cert.yml
Normal file
2
changelogs/fragments/6668-ldap-client-cert.yml
Normal file
|
@ -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).
|
|
@ -29,6 +29,18 @@ options:
|
||||||
- Set the path to PEM file with CA certs.
|
- Set the path to PEM file with CA certs.
|
||||||
type: path
|
type: path
|
||||||
version_added: "6.5.0"
|
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:
|
dn:
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -42,11 +42,17 @@ def gen_specs(**specs):
|
||||||
'validate_certs': dict(default=True, type='bool'),
|
'validate_certs': dict(default=True, type='bool'),
|
||||||
'sasl_class': dict(choices=['external', 'gssapi'], default='external', type='str'),
|
'sasl_class': dict(choices=['external', 'gssapi'], default='external', type='str'),
|
||||||
'xorder_discovery': dict(choices=['enable', 'auto', 'disable'], default='auto', 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
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
def ldap_required_together():
|
||||||
|
return [['client_cert', 'client_key']]
|
||||||
|
|
||||||
|
|
||||||
class LdapGeneric(object):
|
class LdapGeneric(object):
|
||||||
def __init__(self, module):
|
def __init__(self, module):
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
|
@ -60,6 +66,8 @@ class LdapGeneric(object):
|
||||||
self.verify_cert = self.module.params['validate_certs']
|
self.verify_cert = self.module.params['validate_certs']
|
||||||
self.sasl_class = self.module.params['sasl_class']
|
self.sasl_class = self.module.params['sasl_class']
|
||||||
self.xorder_discovery = self.module.params['xorder_discovery']
|
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
|
# Establish connection
|
||||||
self.connection = self._connect_to_ldap()
|
self.connection = self._connect_to_ldap()
|
||||||
|
@ -102,6 +110,10 @@ class LdapGeneric(object):
|
||||||
if self.ca_path:
|
if self.ca_path:
|
||||||
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, 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)
|
connection = ldap.initialize(self.server_uri)
|
||||||
|
|
||||||
if self.referrals_chasing == 'disabled':
|
if self.referrals_chasing == 'disabled':
|
||||||
|
|
|
@ -182,7 +182,7 @@ import traceback
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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.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
|
import re
|
||||||
|
|
||||||
|
@ -300,6 +300,7 @@ def main():
|
||||||
state=dict(type='str', default='present', choices=['absent', 'exact', 'present']),
|
state=dict(type='str', default='present', choices=['absent', 'exact', 'present']),
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
|
required_together=ldap_required_together(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not HAS_LDAP:
|
if not HAS_LDAP:
|
||||||
|
|
|
@ -151,7 +151,7 @@ import traceback
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||||
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
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
|
LDAP_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
|
@ -255,6 +255,7 @@ def main():
|
||||||
),
|
),
|
||||||
required_if=[('state', 'present', ['objectClass'])],
|
required_if=[('state', 'present', ['objectClass'])],
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
|
required_together=ldap_required_together(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not HAS_LDAP:
|
if not HAS_LDAP:
|
||||||
|
|
|
@ -72,7 +72,7 @@ modlist:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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
|
LDAP_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
|
@ -133,6 +133,7 @@ def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=gen_specs(passwd=dict(no_log=True)),
|
argument_spec=gen_specs(passwd=dict(no_log=True)),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
|
required_together=ldap_required_together(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not HAS_LDAP:
|
if not HAS_LDAP:
|
||||||
|
|
|
@ -113,7 +113,7 @@ import traceback
|
||||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
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.common.text.converters import to_bytes, to_native, to_text
|
||||||
from ansible.module_utils.six import string_types, text_type
|
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
|
LDAP_IMP_ERR = None
|
||||||
try:
|
try:
|
||||||
|
@ -136,6 +136,7 @@ def main():
|
||||||
base64_attributes=dict(type='list', elements='str'),
|
base64_attributes=dict(type='list', elements='str'),
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
|
required_together=ldap_required_together(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not HAS_LDAP:
|
if not HAS_LDAP:
|
||||||
|
|
47
tests/integration/targets/ldap_search/tasks/tests/auth.yml
Normal file
47
tests/integration/targets/ldap_search/tasks/tests/auth.yml
Normal file
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -18,5 +18,6 @@ homeDirectory: /home/ldaptest
|
||||||
cn: LDAP Test
|
cn: LDAP Test
|
||||||
gecos: LDAP Test
|
gecos: LDAP Test
|
||||||
displayName: LDAP Test
|
displayName: LDAP Test
|
||||||
|
userPassword: test1pass!
|
||||||
mail: ldap.test@example.com
|
mail: ldap.test@example.com
|
||||||
sn: Test
|
sn: Test
|
||||||
|
|
|
@ -44,6 +44,22 @@
|
||||||
cmd: "export DEBIAN_FRONTEND=noninteractive; cat /root/debconf-slapd.conf | debconf-set-selections; dpkg-reconfigure -f noninteractive slapd"
|
cmd: "export DEBIAN_FRONTEND=noninteractive; cat /root/debconf-slapd.conf | debconf-set-selections; dpkg-reconfigure -f noninteractive slapd"
|
||||||
creates: "/root/slapd_configured"
|
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
|
- name: Start OpenLDAP service
|
||||||
become: true
|
become: true
|
||||||
service:
|
service:
|
||||||
|
@ -61,10 +77,14 @@
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
loop:
|
loop:
|
||||||
- rootpw_cnconfig.ldif
|
- rootpw_cnconfig.ldif
|
||||||
|
- cert_cnconfig.ldif
|
||||||
- initial_config.ldif
|
- initial_config.ldif
|
||||||
|
|
||||||
- name: Configure admin password for cn=config
|
- 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
|
- name: Add initial config
|
||||||
become: true
|
become: true
|
||||||
|
|
Loading…
Reference in a new issue