GitHub Repo: @ShobanChiddarth/openldap-iam-lab
Introduction
This home lab is a part of my setup for an IAM lab using KeyCloak. In this lab I set up OpenLDAP in a Virtualbox lab.
Pre-Requisites
Setup Process
Let's say the fictional organization I am working in is Acme Inc. The domain name would be acme.internal. I have the VM intercommunication setup that I linked earlier ready. I also setup custom DNS records in the Pi-hole (ldap-server.acme.internal points to 192.168.57.5 and ldap-client.acme.internal points to 192.168.57.6).
Setting up LDAP server
I installed slapd and ldap-utils
sudo apt update
sudo apt install slapd ldap-utils
And set the admin password mid installation, then configured it with
sudo dpkg-reconfigure slapd
It asked me for organization name along with domain, I filled in with the appropriate details. And verified it with
debian@ldap-server:~$ sudo slapcat
dn: dc=acme,dc=internal
objectClass: top
objectClass: dcObject
objectClass: organization
o: Acme
dc: acme
structuralObjectClass: organization
entryUUID: 2938c77a-b601-1040-8564-d9abd6bd9b70
creatorsName: cn=admin,dc=acme,dc=internal
createTimestamp: 20260317035728Z
entryCSN: 20260317035728.729122Z#000000#000#000000
modifiersName: cn=admin,dc=acme,dc=internal
modifyTimestamp: 20260317035728Z
Creating Directory structure
I created structure.ldif to add two organizational units - one for users and one for groups, and loaded it.
debian@ldap-server:~$ nano structure.ldif
debian@ldap-server:~$ cat structure.ldif
dn: ou=users,dc=acme,dc=internal
objectClass: organizationalUnit
ou: users
dn: ou=groups,dc=acme,dc=internal
objectClass: organizationalUnit
ou: groups
debian@ldap-server:~$ ldapadd -x -D "cn=admin,dc=acme,dc=internal" -W -f structure.ldif
Enter LDAP Password:
adding new entry "ou=users,dc=acme,dc=internal"
adding new entry "ou=groups,dc=acme,dc=internal"
Adding users
I will be creating 2 users "John Smith" and "Alice Doe" with passwords Password123 and Password456 respectively. I generated the password hashes using
debian@ldap-server:~$ sudo /usr/sbin/slappasswd -s Password123
{SSHA}2sIS9koZfMFfwEjWoglS4Iu8XpCvamLk
debian@ldap-server:~$ sudo /usr/sbin/slappasswd -s Password456
{SSHA}QlqSwZdrWeINW5PS54vTx4Bpqt+6PLdi
debian@ldap-server:~$
Then created 2 users in users.ldif and loaded them.
debian@ldap-server:~$ nano users.ldif
debian@ldap-server:~$ cat users.ldif
dn: uid=jsmith,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jsmith
sn: Smith
givenName: John
cn: John Smith
displayName: John Smith
uidNumber: 10001
gidNumber: 10001
userPassword: {SSHA}2sIS9koZfMFfwEjWoglS4Iu8XpCvamLk
loginShell: /bin/bash
homeDirectory: /home/jsmith
mail: jsmith@acme.internal
dn: uid=adoe,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: adoe
sn: Doe
givenName: Alice
cn: Alice Doe
displayName: Alice Doe
uidNumber: 10002
gidNumber: 10002
userPassword: {SSHA}QlqSwZdrWeINW5PS54vTx4Bpqt+6PLdi
loginShell: /bin/bash
homeDirectory: /home/adoe
mail: adoe@acme.internal
debian@ldap-server:~$ ldapadd -x -D "cn=admin,dc=acme,dc=internal" -W -f users.ldif
Enter LDAP Password:
adding new entry "uid=jsmith,ou=users,dc=acme,dc=internal"
adding new entry "uid=adoe,ou=users,dc=acme,dc=internal"
Adding groups
I created groups engineering and HR, added jsmith to engineering and Alice to HR, and loaded them.
debian@ldap-server:~$ nano groups.ldif
debian@ldap-server:~$ cat groups.ldif
dn: cn=engineers,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: engineers
gidNumber: 10001
memberUid: jsmith
dn: cn=hr,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: hr
gidNumber: 10002
memberUid: adoe
debian@ldap-server:~$ ldapadd -x -D "cn=admin,dc=acme,dc=internal" -W -f groups.ldif
Enter LDAP Password:
adding new entry "cn=engineers,ou=groups,dc=acme,dc=internal"
adding new entry "cn=hr,ou=groups,dc=acme,dc=internal"
debian@ldap-server:~$
Setting up LDAPS (using mkcert)
I created TLS certificates for ldap-server.acme.internal.
debian@debian:~/acme-certs$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
ERROR: no Firefox and/or Chrome/Chromium security databases found
debian@debian:~/acme-certs$ mkcert -CAROOT
/home/debian/.local/share/mkcert
debian@debian:~/acme-certs$ cp $(mkcert -CAROOT)/rootCA.pem .
debian@debian:~/acme-certs$ ls
rootCA-key.pem rootCA.pem
debian@debian:~/acme-certs$ mkcert ldap-server.acme.internal
Note: the local CA is not installed in the Firefox and/or Chrome/Chromium trust store.
Run "mkcert -install" for certificates to be trusted automatically ⚠️
Created a new certificate valid for the following names 📜
- "ldap-server.acme.internal"
The certificate is at "./ldap-server.acme.internal.pem" and the key at "./ldap-server.acme.internal-key.pem" ✅
It will expire on 17 June 2028 🗓
debian@debian:~/acme-certs$ ls -l
total 16
-rw------- 1 debian debian 1704 Mar 17 09:55 ldap-server.acme.internal-key.pem
-rw-r--r-- 1 debian debian 1472 Mar 17 09:55 ldap-server.acme.internal.pem
-rw-r--r-- 1 debian debian 1619 Mar 17 09:54 rootCA.pem
debian@debian:~/acme-certs$
Copied those 3 files onto the server and set the required permissions.
debian@ldap-server:~/acme-certs$ ls
ldap-server.acme.internal-key.pem ldap-server.acme.internal.pem rootCA.pem
debian@ldap-server:~/acme-certs$ sudo mkdir -p /etc/ldap/tls
debian@ldap-server:~/acme-certs$ sudo cp ldap-server.acme.internal.pem /etc/ldap/tls/ldap-server.crt
debian@ldap-server:~/acme-certs$ sudo cp ldap-server.acme.internal-key.pem /etc/ldap/tls/ldap-server.key
debian@ldap-server:~/acme-certs$ sudo cp rootCA.pem /etc/ldap/tls/rootCA.pem
debian@ldap-server:~/acme-certs$ sudo chown -R openldap:openldap /etc/ldap/tls
debian@ldap-server:~/acme-certs$ sudo chmod 640 /etc/ldap/tls/ldap-server.key
debian@ldap-server:~/acme-certs$ sudo chmod 644 /etc/ldap/tls/ldap-server.crt /etc/ldap/tls/rootCA.pem
debian@ldap-server:~/acme-certs$ ls -l /etc/ldap/tls/
total 12
-rw-r--r-- 1 openldap openldap 1472 Mar 17 10:51 ldap-server.crt
-rw-r----- 1 openldap openldap 1704 Mar 17 10:51 ldap-server.key
-rw-r--r-- 1 openldap openldap 1619 Mar 17 10:51 rootCA.pem
debian@ldap-server:~/acme-certs$ ls -ld /etc/ldap/tls/
drwxr-xr-x 2 openldap openldap 4096 Mar 17 10:51 /etc/ldap/tls/
debian@ldap-server:~/acme-certs$
Then I created tls conf file (tls.ldif) and applied it.
debian@ldap-server:~$ nano tls.ldif
debian@ldap-server:~$ cat tls.ldif
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/tls/rootCA.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/tls/ldap-server.crt
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/tls/ldap-server.key
debian@ldap-server:~$ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f tls.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "cn=config"
debian@ldap-server:~$
Then I set SLAPD_SERVICES in /etc/default/slapd to use LDAPS
SLAPD_SERVICES="ldapi:/// ldaps:///"
And specified LDAP client tools on the server to use the certificate
debian@ldap-server:~$ echo "TLS_CACERT /etc/ldap/tls/rootCA.pem" | sudo tee -a /etc/ldap/ldap.conf
TLS_CACERT /etc/ldap/tls/rootCA.pem
debian@ldap-server:~$
Then I restarted slapd service and checked if LDAPS is working
debian@ldap-server:~$ sudo systemctl restart slapd
debian@ldap-server:~$ ldapsearch -x -H ldaps://ldap-server.acme.internal \
-D "cn=admin,dc=acme,dc=internal" -W \
-b "dc=acme,dc=internal" "(objectClass=*)"
Enter LDAP Password:
# extended LDIF
#
# LDAPv3
# base <dc=acme,dc=internal> with scope subtree
# filter: (objectClass=*)
# requesting: ALL
#
# acme.internal
dn: dc=acme,dc=internal
objectClass: top
objectClass: dcObject
objectClass: organization
o: Acme
dc: acme
# users, acme.internal
dn: ou=users,dc=acme,dc=internal
objectClass: organizationalUnit
ou: users
# groups, acme.internal
dn: ou=groups,dc=acme,dc=internal
objectClass: organizationalUnit
ou: groups
# jsmith, users, acme.internal
dn: uid=jsmith,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jsmith
sn: Smith
givenName: John
cn: John Smith
displayName: John Smith
uidNumber: 10001
gidNumber: 10001
userPassword:: e1NTSEF9MnNJUzlrb1pmTUZmd0VqV29nbFM0SXU4WHBDdmFtTGs=
loginShell: /bin/bash
homeDirectory: /home/jsmith
mail: jsmith@acme.internal
# adoe, users, acme.internal
dn: uid=adoe,ou=users,dc=acme,dc=internal
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: adoe
sn: Doe
givenName: Alice
cn: Alice Doe
displayName: Alice Doe
uidNumber: 10002
gidNumber: 10002
userPassword:: e1NTSEF9UWxxU3daZHJXZUlOVzVQUzU0dlR4NEJwcXQrNlBMZGk=
loginShell: /bin/bash
homeDirectory: /home/adoe
mail: adoe@acme.internal
# engineers, groups, acme.internal
dn: cn=engineers,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: engineers
gidNumber: 10001
memberUid: jsmith
# hr, groups, acme.internal
dn: cn=hr,ou=groups,dc=acme,dc=internal
objectClass: posixGroup
cn: hr
gidNumber: 10002
memberUid: adoe
# search result
search: 2
result: 0 Success
# numResponses: 8
# numEntries: 7
debian@ldap-server:~$
But there was one small problem with this. The userPassword's base64 encoded SSHA has is visible. I am going to fix that using ACLs.
I created acls.ldif and set the contents and loaded it and verified it.
debian@ldap-server:~$ nano acls.ldif
debian@ldap-server:~$ cat acls.ldif
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword
by self write
by anonymous auth
by dn="cn=admin,dc=acme,dc=internal" write
by * none
olcAccess: {1}to attrs=shadowLastChange
by self write
by * read
olcAccess: {2}to *
by self write
by dn="cn=admin,dc=acme,dc=internal" write
by * read
debian@ldap-server:~$ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f acls.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"
debian@ldap-server:~$ ldapsearch -x -H ldaps://ldap-server.acme.internal \
-b "dc=acme,dc=internal" "(uid=jsmith)" userPassword
# extended LDIF
#
# LDAPv3
# base <dc=acme,dc=internal> with scope subtree
# filter: (uid=jsmith)
# requesting: userPassword
#
# jsmith, users, acme.internal
dn: uid=jsmith,ou=users,dc=acme,dc=internal
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
debian@ldap-server:~$
Setting up LDAP client
I copied rootCA.pem to the LDAP client and trusted it.
debian@ldap-client:~/acme-certs$ ls
rootCA.pem
debian@ldap-client:~/acme-certs$ sudo cp rootCA.pem /usr/local/share/ca-certificates/acme-rootCA.crt
[sudo] password for debian:
debian@ldap-client:~/acme-certs$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
debian@ldap-client:~/acme-certs$
Then I installed System Security Services
sudo apt install -y sssd libpam-sss libnss-sss
I now created the configuration for it, set the permissions and enabled the service.
debian@ldap-client:~$ sudo nano /etc/sssd/sssd.conf
debian@ldap-client:~$ sudo cat /etc/sssd/sssd.conf
[sssd]
services = nss, pam
config_file_version = 2
domains = acme.internal
[domain/acme.internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://ldap-server.acme.internal
ldap_search_base = dc=acme,dc=internal
ldap_tls_cacert = /etc/ssl/certs/ca-certificates.crt
ldap_schema = rfc2307
cache_credentials = true
enumerate = true
debian@ldap-client:~$ sudo chmod 600 /etc/sssd/sssd.conf
debian@ldap-client:~$ sudo systemctl enable --now sssd
Synchronizing state of sssd.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
Executing: /usr/lib/systemd/systemd-sysv-install enable sssd
debian@ldap-client:~$
The only thing left is automatic home dir creation.
sudo pam-auth-update --enable mkhomedir
This will make sure a home directory is automatically created when john ssh-es into the ldap client.
LDAP Client Verification
debian@ldap-client:~$ id jsmith
uid=10001(jsmith) gid=10001(engineers) groups=10001(engineers)
debian@ldap-client:~$ getent passwd jsmith
jsmith:*:10001:10001:John Smith:/home/jsmith:/bin/bash
debian@ldap-client:~$ getent group engineers
engineers:*:10001:jsmith
debian@ldap-client:~$
LDAP client can see the users and groups set in the LDAP server.
SSH verification
From another desktop, copied rootCA.pem and trusted it.
debian@debian:~/acme-certs$ ls
rootCA.pem
debian@debian:~/acme-certs$ sudo cp rootCA.pem /usr/local/share/ca-certificates/acme-rootCA.crt
[sudo] password for debian:
debian@debian:~/acme-certs$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt, it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
debian@debian:~/acme-certs$
Now I tried to ssh login
debian@debian:~/acme-certs$ ssh jsmith@ldap-client.acme.internal
The authenticity of host 'ldap-client.acme.internal (192.168.57.6)' can't be established.
ED25519 key fingerprint is SHA256:wo67g9IGEfMUrZBC1KzzKlHS1G41PidIUGXZ5kTGmV0.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'ldap-client.acme.internal' (ED25519) to the list of known hosts.
jsmith@ldap-client.acme.internal's password:
Creating directory '/home/jsmith'.
Linux ldap-client.acme.internal 6.12.73+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.73-1 (2026-02-17) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
jsmith@ldap-client:~$ pwd
/home/jsmith
jsmith@ldap-client:~$
It worked. Also the home dir was automatically created.
Stopping anonymous bind and setup a service account for LDAP client
I need a password hash for the service account first. So I ran
debian@ldap-server:~$ sudo /usr/sbin/slappasswd -s SSSDPass123
[sudo] password for debian:
{SSHA}ASFdTu5vaW4YSytPSFTpTc1StjoC+giz
debian@ldap-server:~$
Then I put it in service accounts conf file and then load it
debian@ldap-server:~$ nano service-accounts.ldif
debian@ldap-server:~$ cat service-accounts.ldif
dn: ou=service-accounts,dc=acme,dc=internal
objectClass: organizationalUnit
ou: service-accounts
dn: cn=sssd,ou=service-accounts,dc=acme,dc=internal
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: sssd
description: Read-only service account for SSSD
userPassword: {SSHA}ASFdTu5vaW4YSytPSFTpTc1StjoC+giz
debian@ldap-server:~$ ldapadd -x -H ldaps://ldap-server.acme.internal \
-D "cn=admin,dc=acme,dc=internal" -W -f service-accounts.ldif
Enter LDAP Password:
adding new entry "ou=service-accounts,dc=acme,dc=internal"
adding new entry "cn=sssd,ou=service-accounts,dc=acme,dc=internal"
debian@ldap-server:~$
And then I create another ACL to make sure I only allow that specific account and disallow anonymous binds
debian@ldap-server:~$ nano acls2.ldif
debian@ldap-server:~$ cat acls2.ldif
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword
by self write
by anonymous auth
by dn="cn=admin,dc=acme,dc=internal" write
by * none
olcAccess: {1}to attrs=shadowLastChange
by self write
by * read
olcAccess: {2}to *
by self write
by dn="cn=admin,dc=acme,dc=internal" write
by dn="cn=sssd,ou=service-accounts,dc=acme,dc=internal" read
by * none
debian@ldap-server:~$ sudo ldapmodify -Y EXTERNAL -H ldapi:/// -f acls2.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"
debian@ldap-server:~$
Now I set the LDAP client to use those accounts and restart the service
debian@ldap-client:~$ sudo cat /etc/sssd/sssd.conf
[sssd]
services = nss, pam
config_file_version = 2
domains = acme.internal
[domain/acme.internal]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldaps://ldap-server.acme.internal
ldap_search_base = dc=acme,dc=internal
ldap_tls_cacert = /etc/ssl/certs/ca-certificates.crt
ldap_schema = rfc2307
cache_credentials = true
enumerate = true
ldap_default_bind_dn = cn=sssd,ou=service-accounts,dc=acme,dc=internal
ldap_default_authtok = SSSDPass123
debian@ldap-client:~$ sudo systemctl restart sssd
Note:
ldap_default_authtokis stored in plaintext, butsssd.confis locked torootonly via thechmod 600set earlier, so it is not readable by other users on the system.Verifying again
LDAP client records fetching:
debian@ldap-client:~$ id jsmith
uid=10001(jsmith) gid=10001(engineers) groups=10001(engineers)
debian@ldap-client:~$ getent passwd adoe
adoe:*:10002:10002:Alice Doe:/home/adoe:/bin/bash
debian@ldap-client:~$
SSH verification:
debian@debian:~/acme-certs$ ssh jsmith@ldap-client.acme.internal
jsmith@ldap-client.acme.internal's password:
Linux ldap-client.acme.internal 6.12.73+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.73-1 (2026-02-17) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Tue Mar 17 11:43:45 2026 from 192.168.57.104
jsmith@ldap-client:~$
It is working. Only the LDAP client can call the LDAP servers and anonymous binds are disabled.
Screenshots
VBoxManager
ALL VMs
Conclusion
OpenLDAP is now running as the centralized identity backend for acme.internal. The directory is secured with LDAPS using the lab's mkcert CA. ACLs enforce least-privilege access - password hashes are not exposed to anyone except the admin, and anonymous binds are disabled entirely. A dedicated read-only service account is the only thing allowed to query the directory, which is what SSSD on ldap-client uses. Users that exist only in LDAP can SSH into ldap-client with their home directories created automatically on first login.
This is the identity foundation for the next part of this series - connecting Keycloak to this LDAP directory as its user backend.


Top comments (0)