Skip to content

Commit 2cdeace

Browse files
Add LDAP GSSAPI support
Add LDAP GSSAPI support and port DNS discovery tests Add enable_gssapi() method to LDAP role for GSSAPI/SASL authentication testing. Added `LDAP.enable_gssapi(kdc)` method to configure Directory Server for GSSAPI authentication. **Usage**: ```python ldap.enable_gssapi(kdc) client.sssd.domain["ldap_sasl_mech"] = "GSSAPI" Signed-off-by: Madhuri Upadhye <mupadhye@redhat.com>
1 parent 39fd0f1 commit 2cdeace

5 files changed

Lines changed: 431 additions & 2 deletions

File tree

docs/guides/testing-ldap-krb5.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ That is enough for LDAP GSSAPI (``ldap_sasl_mech = gssapi``) without each test
2626
running ``ktadd``/upload itself. Tests still call
2727
``client.sssd.common.krb5_auth(kdc)`` and configure the SSSD domain as usual.
2828

29+
Reusable client utilities
30+
-------------------------
31+
32+
LDAP/Kerberos system tests can share the following helpers (no per-test ``named``
33+
or ``getent`` boilerplate):
34+
35+
* :meth:`~sssd_test_framework.utils.tools.GetentUtils.resolve_ipv4` —
36+
``client.tools.getent.resolve_ipv4(hostname, host=role.host)`` (or
37+
``client.tools.resolve_ipv4``) uses topology ``host.ip`` when set, otherwise
38+
``getent ahostsv4``.
39+
40+
* :meth:`~sssd_test_framework.utils.network.NetworkUtils.dig` —
41+
``client.net.dig(name, server)`` for A/PTR checks (prefer over shell ``dig``).
42+
43+
* :meth:`~sssd_test_framework.utils.network.NetworkUtils.setup_sasl_canonicalize_bogus_ptr` —
44+
local ``named`` + ``/etc/hosts`` setup for BZ 732935 (bogus PTR for the LDAP
45+
server IP, forward A for the LDAP FQDN, ``resolv.conf`` → ``127.0.0.1``).
46+
Files are backed up via ``client.fs`` and restored after the test.
47+
48+
* :func:`~sssd_test_framework.misc.ip_to_ptr` — reverse zone name for an IPv4
49+
address (also used inside the bogus-PTR helper).
50+
51+
Kerberos templates from :meth:`~sssd_test_framework.roles.kdc.KDC.config` include
52+
``rdns = false`` in ``[libdefaults]`` so tests that call
53+
``client.sssd.common.krb5_auth(kdc)`` do not need to edit ``/etc/krb5.conf`` for
54+
that option.
55+
2956
.. seealso::
3057

3158
* :class:`sssd_test_framework.roles.kdc.KDC`

sssd_test_framework/roles/kdc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def config(self) -> str:
131131
ticket_lifetime = 24h
132132
renew_lifetime = 7d
133133
forwardable = yes
134+
rdns = false
134135
135136
[realms]
136137
{self.host.realm} = {{

sssd_test_framework/roles/ldap.py

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from __future__ import annotations
44

55
import base64
6+
import tempfile
7+
import time
68
from datetime import datetime
79
from enum import Enum
8-
from typing import Any, Generic, TypeVar
10+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
911

1012
import ldap
1113
import ldap.ldapobject
@@ -17,6 +19,9 @@
1719
from .generic import GenericNetgroupMember, GenericPasswordPolicy, ProtocolName
1820
from .nfs import NFSExport
1921

22+
if TYPE_CHECKING:
23+
from .kdc import KDC
24+
2025
__all__ = [
2126
"LDAPRoleType",
2227
"LDAPPasswordPolicy",
@@ -239,6 +244,197 @@ def setup(self) -> None:
239244
except ldap.TYPE_OR_VALUE_EXISTS:
240245
pass
241246

247+
def enable_gssapi(self, kdc: KDC) -> None:
248+
"""
249+
Configure Directory Server for GSSAPI/SASL authentication.
250+
251+
This method sets up the LDAP server to accept GSSAPI (Kerberos) authentication
252+
by creating a service principal, exporting the keytab, and configuring Directory Server.
253+
254+
.. code-block:: python
255+
:caption: Example usage
256+
257+
@pytest.mark.topology(KnownTopology.LDAP_KRB5)
258+
def test_ldap_gssapi(client: Client, ldap: LDAP, kdc: KDC):
259+
# Enable GSSAPI on LDAP server
260+
ldap.enable_gssapi(kdc)
261+
262+
ldap.user('testuser').add()
263+
kdc.principal('testuser').add()
264+
265+
# Configure SSSD to use GSSAPI
266+
client.sssd.domain["ldap_sasl_mech"] = "GSSAPI"
267+
client.sssd.start()
268+
269+
result = client.tools.id('testuser')
270+
assert result is not None
271+
272+
:param kdc: KDC role object to create service principal
273+
:type kdc: KDC
274+
"""
275+
276+
# 1. Install required packages
277+
self.host.conn.run(
278+
"dnf install -y cyrus-sasl-gssapi krb5-workstation || "
279+
"yum install -y cyrus-sasl-gssapi krb5-workstation",
280+
)
281+
self.host.conn.run("rpm -q cyrus-sasl-gssapi")
282+
283+
# 2. Create LDAP service principal
284+
ldap_principal = f"ldap/{self.host.hostname}"
285+
kdc.principal(ldap_principal).add(password=None)
286+
287+
# 3. Export keytab to LDAP server (same transfer pattern as LDAPKRB5TopologyController)
288+
keytab_path = "/etc/dirsrv/ds.keytab"
289+
keytab_staging = "/tmp/sssd-test-framework-ds.keytab"
290+
qualified_principal = kdc.qualify(ldap_principal)
291+
kdc.host.conn.run(f"rm -f {keytab_staging}", raise_on_error=False)
292+
kdc.host.conn.run(f"kadmin.local -q 'ktadd -k {keytab_staging} -norandkey \"{qualified_principal}\"'")
293+
with tempfile.NamedTemporaryFile() as tmp:
294+
kdc.host.fs.download(keytab_staging, tmp.name)
295+
self.host.fs.upload(tmp.name, keytab_path)
296+
kdc.host.conn.run(f"rm -f {keytab_staging}", raise_on_error=False)
297+
self.host.conn.run(f"chown dirsrv:dirsrv {keytab_path}")
298+
self.host.conn.run(f"chmod 600 {keytab_path}")
299+
300+
# 4. Copy krb5.conf from KDC to LDAP server
301+
krb5_conf = kdc.config()
302+
self.host.conn.run(f"cat > /etc/krb5.conf << 'EOFKRB5'\n{krb5_conf}\nEOFKRB5")
303+
304+
# Add default_keytab_name to krb5.conf as fallback
305+
self.host.conn.run(f"sed -i '/\\[libdefaults\\]/a\\ default_keytab_name = {keytab_path}' /etc/krb5.conf")
306+
307+
# 5. Configure Cyrus SASL to use the keytab
308+
# This is critical - without this, the SASL GSSAPI plugin won't know where to find the keytab
309+
self.host.conn.run(
310+
f"mkdir -p /etc/sasl2 && "
311+
f"cat > /etc/sasl2/slapd.conf << 'EOFSASL'\n"
312+
f"mech_list: GSSAPI EXTERNAL PLAIN LOGIN\n"
313+
f"keytab: {keytab_path}\n"
314+
f"EOFSASL"
315+
)
316+
317+
# Also create for other possible SASL application names
318+
self.host.conn.run("cp /etc/sasl2/slapd.conf /etc/sasl2/ns-slapd.conf")
319+
self.host.conn.run("cp /etc/sasl2/slapd.conf /etc/sasl2/ldap.conf")
320+
321+
# 6. Set KRB5_KTNAME environment variable for Directory Server via sysconfig
322+
# Note: systemd Environment= directives don't work reliably in containers
323+
# Use EnvironmentFile instead
324+
self.host.conn.run(f"echo 'KRB5_KTNAME={keytab_path}' > /etc/sysconfig/dirsrv-localhost")
325+
326+
# 7. Configure SASL identity mapping in Directory Server
327+
# Align default Kerberos maps with the data suffix (sssd-qe krb_credential_cache) and
328+
# add a high-priority map for host/ldap service principals used by SSSD GSSAPI binds.
329+
realm = kdc.realm
330+
base_dn = self.naming_context
331+
binddn = self.host.binddn
332+
bindpw = self.host.bindpw
333+
334+
sasl_ldif = ""
335+
for cn in (
336+
"Kerberos uid mapping",
337+
"rfc 2829 dn syntax",
338+
"rfc 2829 u syntax",
339+
"uid mapping",
340+
):
341+
sasl_ldif += (
342+
f"dn: cn={cn},cn=mapping,cn=sasl,cn=config\n"
343+
"changetype: modify\n"
344+
"replace: nsSaslMapBaseDNTemplate\n"
345+
f"nsSaslMapBaseDNTemplate: {base_dn}\n"
346+
"\n"
347+
)
348+
self.host.conn.run(
349+
f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost",
350+
input=sasl_ldif,
351+
)
352+
for cn in (
353+
"SSSD service principals",
354+
"SSSD service principals no realm",
355+
):
356+
self.host.conn.run(
357+
f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost",
358+
input=(f"dn: cn={cn},cn=mapping,cn=sasl,cn=config\n" "changetype: delete\n"),
359+
raise_on_error=False,
360+
)
361+
362+
# cn=Directory Manager is a bind identity, not a searchable LDAP entry (BASE
363+
# search returns No such object). Map GSSAPI clients to a real entry under the
364+
# data suffix, per 389-ds server-to-server SASL examples (full target DN +
365+
# (objectclass=*)).
366+
people_ou = f"ou=People,{base_dn}"
367+
gssapi_proxy_dn = f"uid=sssd-gssapi,{people_ou}"
368+
bootstrap_ldif = (
369+
f"dn: {people_ou}\n"
370+
"changetype: add\n"
371+
"objectClass: organizationalUnit\n"
372+
"ou: People\n"
373+
"\n"
374+
f"dn: {gssapi_proxy_dn}\n"
375+
"changetype: add\n"
376+
"objectClass: top\n"
377+
"objectClass: person\n"
378+
"objectClass: organizationalPerson\n"
379+
"objectClass: inetOrgPerson\n"
380+
"cn: SSSD GSSAPI proxy\n"
381+
"sn: proxy\n"
382+
"uid: sssd-gssapi\n"
383+
)
384+
self.host.conn.run(
385+
f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost",
386+
input=bootstrap_ldif,
387+
raise_on_error=False,
388+
)
389+
service_map_ldif = (
390+
"dn: cn=SSSD service principals,cn=mapping,cn=sasl,cn=config\n"
391+
"changetype: add\n"
392+
"objectClass: top\n"
393+
"objectClass: nsSaslMapping\n"
394+
"cn: SSSD service principals\n"
395+
f"nsSaslMapRegexString: ^(host|ldap)/.*@{realm}$\n"
396+
f"nsSaslMapBaseDNTemplate: {gssapi_proxy_dn}\n"
397+
"nsSaslMapFilterTemplate: (objectclass=*)\n"
398+
"nsSaslMapPriority: 10\n"
399+
"\n"
400+
"dn: cn=SSSD service principals no realm,cn=mapping,cn=sasl,cn=config\n"
401+
"changetype: add\n"
402+
"objectClass: top\n"
403+
"objectClass: nsSaslMapping\n"
404+
"cn: SSSD service principals no realm\n"
405+
"nsSaslMapRegexString: ^(host|ldap)/.*$\n"
406+
f"nsSaslMapBaseDNTemplate: {gssapi_proxy_dn}\n"
407+
"nsSaslMapFilterTemplate: (objectclass=*)\n"
408+
"nsSaslMapPriority: 11\n"
409+
)
410+
self.host.conn.run(
411+
f"ldapmodify -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost",
412+
input=service_map_ldif,
413+
)
414+
verify = self.host.conn.run(
415+
f"ldapsearch -x -D '{binddn}' -w '{bindpw}' -H ldap://localhost "
416+
f"-b '{gssapi_proxy_dn}' -s base '(objectclass=*)' dn",
417+
raise_on_error=False,
418+
)
419+
if verify.rc != 0 or "dn:" not in (verify.stdout or ""):
420+
raise RuntimeError(
421+
f"SASL GSSAPI proxy entry {gssapi_proxy_dn} is not searchable: " f"{verify.stdout or verify.stderr}"
422+
)
423+
424+
# 8. Reload systemd and restart Directory Server
425+
self.host.conn.run("systemctl daemon-reload")
426+
self.host.conn.run("systemctl restart dirsrv@localhost")
427+
428+
# Wait for Directory Server to fully start with GSSAPI support
429+
time.sleep(3)
430+
431+
klist = self.host.conn.run(f"klist -kt {keytab_path}", raise_on_error=False)
432+
if qualified_principal not in (klist.stdout or ""):
433+
raise RuntimeError(
434+
f"LDAP GSSAPI keytab {keytab_path} does not contain {qualified_principal}: "
435+
f"{klist.stdout or klist.stderr}"
436+
)
437+
242438
def fqn(self, name: str) -> str:
243439
"""
244440
Return fully qualified name in form name@domain.

0 commit comments

Comments
 (0)