Lab Metadata
| Attribute | Value |
|---|---|
| Lab ID | IB-SIA-01 |
| Track | Identity Bytes Senior IAM Architect Track (36 lab curriculum) |
| Difficulty | Beginner (no prior IAM knowledge assumed) |
| Core technologies | OpenLDAP 2.4 (osixia/openldap:1.5.0), phpLDAPadmin 0.9.0, Docker Engine 24+, ldap-utils 2.5+, LDIF, TLS/LDAPS |
| Protocols and standards | LDAPv3 (RFC 4511), Distinguished Names (RFC 4514), LDIF (RFC 2849), inetOrgPerson schema (RFC 2798) |
| Feeds into | IB-SIA-02 (Keycloak realm design), IB-SIA-03 (SAML federation), IB-SIA-26 (AD DS and Keycloak brokering) |
Lab Title and Description
Enterprise Directory Foundation: Deploying and Structuring OpenLDAP as a Central Identity Store
Northgate Financial is a fictional UK financial services firm with 5,000 employees, four internal web applications, a VPN, and a Linux server estate. Today, every one of those systems keeps its own list of usernames and passwords. When a new analyst joins, the IT team creates five separate accounts by hand. When someone leaves, at least one account is usually forgotten and stays active. The audit team has flagged this repeatedly, and the security team cannot answer a basic question: who has access to what?
In this lab you take the role of the identity engineer hired to fix the foundation. You will deploy an OpenLDAP directory server, the open source implementation of the Lightweight Directory Access Protocol, and turn it into a single, authoritative store of people, groups, and service accounts. You will design a directory tree that mirrors how enterprises actually organise identity data, load it with users and groups using LDIF files, prove that applications can authenticate people against it, and secure the connection with TLS so credentials never cross the network in plain text.
Everything runs in Docker containers on your own machine, so nothing you do here touches any real system, and the whole environment can be destroyed and rebuilt in minutes. Estimated completion time is 2.5 to 3 hours at a careful, read-everything pace.
Prerequisites
3.1 Prior labs required
None. This is the first lab of the track and assumes zero prior IAM experience. If you can open a terminal and type commands, you are ready.
3.2 System requirements
| Resource | Minimum | Recommended |
|---|---|---|
| Operating system | Ubuntu 22.04, macOS 13, or Windows 11 with WSL2 | Ubuntu 22.04 LTS (all commands verified here) |
| RAM | 4 GB | 8 GB |
| Disk | 10 GB free | 20 GB free |
| CPU | 2 cores | 4 cores |
| Network | Internet access to pull Docker images. Local ports 389, 636, and 8080 must be free. | |
3.3 Required tools and versions
| Tool | Version | Purpose |
|---|---|---|
| Docker Engine | 24.0 or later | Runs the OpenLDAP and phpLDAPadmin containers |
| ldap-utils (client tools) | 2.5.x or later | Provides ldapsearch, ldapadd, ldapwhoami, ldappasswd |
| osixia/openldap image | 1.5.0 (pinned) | Packaged OpenLDAP 2.4 server |
| osixia/phpldapadmin image | 0.9.0 (pinned) | Web based directory browser |
3.4 Installation commands
Ubuntu 22.04 / Debian: install Docker and ldap-utils
# Update the package index so apt knows about current package versions sudo apt-get update # Install Docker Engine from the Ubuntu repository # Purpose: provides the container runtime for the whole track sudo apt-get install -y docker.io # Start Docker now and enable it to start on every boot sudo systemctl enable --now docker # Add your user to the docker group so you can run docker without sudo # SECURITY NOTE: membership of the docker group is equivalent to root on this # machine. Acceptable on a disposable lab VM, a risk on a shared workstation. sudo usermod -aG docker $USER # Apply the new group membership to your current shell session newgrp docker # Install the LDAP command line client tools # Purpose: ldapsearch, ldapadd, ldapwhoami and ldappasswd are the tools an # identity engineer uses daily to query and manage any directory sudo apt-get install -y ldap-utils
macOS 13+: install Docker Desktop and verify client tools
# Install Docker Desktop using Homebrew (https://brew.sh if not installed) brew install --cask docker # Launch Docker Desktop once so its background service starts open -a Docker # macOS ships LDAP client tools by default at /usr/bin # Verify they are present: which ldapsearch ldapwhoami
Expected output for the last command is /usr/bin/ldapsearch and /usr/bin/ldapwhoami. All lab commands work with the built in macOS tools.
Windows 11: install Docker Desktop with WSL2 and run the lab inside Ubuntu
# Run in PowerShell as Administrator # Installs the Windows Subsystem for Linux with an Ubuntu distribution. # WSL2 gives you a real Linux environment inside Windows, which is where # you will run every lab command in this track. wsl --install -d Ubuntu-22.04 # Reboot when prompted, then create your Linux username and password.
Next, download and install Docker Desktop from https://www.docker.com/products/docker-desktop/. During setup, keep the option "Use WSL 2 based engine" ticked. After installation, open Docker Desktop, go to Settings, then Resources, then WSL Integration, and enable integration for Ubuntu-22.04.
# Open the Ubuntu terminal (Start menu, type Ubuntu) and install ldap-utils
sudo apt-get update && sudo apt-get install -y ldap-utils
From this point on, run every command in this lab inside the Ubuntu terminal, not PowerShell.
3.5 Verification of tools
Verify Docker and ldap-utils are working
# Confirm the Docker version. Any 24.x or later value passes.
docker --version
Expected output similar to: Docker version 24.0.7, build afdd53b
# Confirm the Docker daemon is running by launching a test container
docker run --rm hello-world
Expected output includes the line: Hello from Docker!
# Confirm the LDAP client tools respond. -VV prints version information.
ldapsearch -VV
Expected output similar to: ldapsearch: @(#) $OpenLDAP: ldapsearch 2.5.x .... The tool prints this to the error stream and exits; that is normal.
docker run hello-world fails with a permission error on Ubuntu, log out and back in so the docker group membership takes effect, then retry.Real World Problem Statement
This lab solves the single oldest problem in enterprise IT: identity sprawl. Without a central directory, every application, VPN, database, and server maintains its own private list of accounts. Users juggle multiple passwords, IT teams duplicate every joiner and leaver action across systems, and nobody can produce a trustworthy answer to the question auditors always ask first: show me everyone who can access this system.
A directory service is the enterprise answer. It is a purpose built database, optimised for millions of fast reads, that holds people, groups, machines, and service accounts in one hierarchical tree, and exposes them to every other system over a standard protocol, LDAP. Microsoft Active Directory, the identity backbone of roughly 90 percent of large enterprises, is at its core an LDAP directory paired with Kerberos. Master OpenLDAP and you have mastered the mental model behind AD, Entra ID sync, and every HR driven provisioning pipeline you will ever meet.
Why it matters, across four dimensions
Risk
Orphaned accounts on forgotten systems are a top initial access vector. A single authoritative directory means one disable action removes access everywhere, closing the leaver gap that attackers exploit.
Compliance
ISO 27001 (A.5.16, A.5.18), SOC 2, PCI DSS Requirement 8, and UK FCA operational resilience expectations all demand demonstrable control of identity lifecycle. A central directory is the evidence base for every access review.
Productivity
One account per human instead of five. Onboarding drops from days of ticket chasing to a single directory entry, and application teams stop building their own login databases.
Security Posture
Centralised authentication enables centralised control: one password policy, one MFA rollout point, one audit log. Every advanced capability in this track, SSO, federation, Zero Trust, presumes this foundation exists.
Concrete scenario
Northgate Financial, 5,000 employees, has failed an internal audit because 340 active accounts belonged to people who left the company more than 90 days ago, spread across an HR portal, a trading tool, a wiki, and 200 Linux servers. The remediation programme's first deliverable, and your task in this lab, is a central LDAP directory holding every person, structured by organisational unit, with groups that applications can query for authorisation decisions, and a dedicated low privilege service account that applications use to connect. Every later phase of the programme, single sign-on in IB-SIA-02 and federation in IB-SIA-03, builds directly on what you deploy today.
Skills Mapped to Production Solutions
| Skill Learned | Real World Enterprise Application |
|---|---|
| Deploying and operating an LDAP directory server | Running the identity backbone that Active Directory, FreeIPA, Oracle Directory, and every corporate white pages service is built on |
| Designing a directory information tree (base DN, organisational units) | Structuring AD organisational units and Entra ID administrative units during greenfield builds, mergers, and directory consolidation projects |
| Writing and loading LDIF files | Bulk user migrations, HR driven provisioning pipelines, and change controlled directory modifications in regulated environments |
| Creating groups and querying membership with LDAP filters | Application authorisation, RBAC group mapping in SSO platforms, and access certification campaigns |
| Configuring service accounts with least privilege binds | Connecting applications, VPN concentrators, and SSO platforms such as Keycloak or Okta to a corporate directory safely |
Testing authentication with ldapwhoami and diagnosing bind failures | Frontline troubleshooting of login failures, the single most common identity incident ticket in any enterprise |
| Securing directory traffic with TLS (LDAPS) | Meeting the encryption in transit requirements of PCI DSS 4.0 and ISO 27001, and preparing for certificate lifecycle work in the PKI phase of this track |
Architecture Overview
Component breakdown
| Component | Purpose | Technology | Deployment | Ports | Key configuration |
|---|---|---|---|---|---|
| OpenLDAP server | Authoritative identity store: holds users, groups, service accounts; answers authentication (bind) and lookup (search) requests | OpenLDAP 2.4 via osixia/openldap:1.5.0 | Docker container on network ib-lab-net | 389 (LDAP), 636 (LDAPS) | Base DN dc=identitybytes,dc=lab, admin DN cn=admin,dc=identitybytes,dc=lab, TLS enabled with a generated self signed certificate |
| phpLDAPadmin | Web UI to browse and visually verify the directory tree | phpLDAPadmin via osixia/phpldapadmin:0.9.0 | Docker container, same network | 8080 on host, 80 in container | PHPLDAPADMIN_LDAP_HOSTS=ib-openldap, HTTPS disabled for the lab |
| LDAP client tools | The engineer's interface: scripted queries, bulk loads, authentication tests | ldap-utils 2.5+ | Installed on the host | n/a | Connects to ldap://localhost:389 and ldaps://localhost:636 |
| Docker volumes | Persist directory data and server configuration across container restarts | Docker named volumes | ib-ldap-data, ib-ldap-config | n/a | Mounted at /var/lib/ldap and /etc/ldap/slapd.d |
Data flow
- Admin bind: your terminal connects to port 389 and authenticates as
cn=admin,dc=identitybytes,dc=lab. Why: every write to a directory must come from an authenticated, authorised identity, exactly as in production change control. - LDIF load:
ldapaddstreams your LDIF files to the server, which validates each entry against its schema before writing it. Why: schema enforcement is the directory's data quality guarantee; malformed identity data is rejected at the door. - User authentication: a simulated application performs a bind as
uid=asmith,ou=people,...with that user's password. The server verifies the password hash and returns success or failure. Why: this bind operation is precisely how VPNs, wikis, and legacy applications authenticate users against corporate directories today. - Authorisation lookup: the application searches
ou=groupswith a membership filter to decide what the user may do. Why: separating authentication (who you are) from authorisation (what you may do) is the core architectural pattern of the entire IAM discipline. - Encrypted transport: the same operations repeat over LDAPS on port 636, wrapped in TLS. Why: bind requests carry passwords; unencrypted LDAP exposes them to anyone on the network path.
Security considerations
| Control | In this lab |
|---|---|
| Encryption in transit | LDAPS on 636 with a container generated self signed certificate; plain 389 kept only for learning and comparison |
| Authentication | Simple binds with passwords stored as salted SSHA hashes, never plain text, set via ldappasswd |
| Authorisation | Default access controls: users may modify their own password, the admin DN has full write, others read non password attributes |
| Audit | Container stdout logs every connection and operation, viewable with docker logs; production systems ship these to a SIEM |
Step by Step Implementation
Phase 1: Prepare the Environment
Step 1.1: Create the project directory and Docker network
Create a clean workspace for LDIF files and a dedicated Docker network so containers can find each other by name.
This is the first step, so there is nothing to connect to yet. A named network matters because phpLDAPadmin will later reach the LDAP server using the container name ib-openldap instead of a fragile IP address, mirroring how production services discover each other through DNS.
Create the workspace and network
# Create a project directory and move into it. # All LDIF files in this lab will live here. mkdir -p ~/ib-sia-01/ldif && cd ~/ib-sia-01 # Create an isolated Docker bridge network for this lab. # Enterprise context: identity infrastructure always sits in its own # network segment with controlled ingress. This is the lab scale version. docker network create ib-lab-net
The network command prints a long hexadecimal network ID, confirming creation.
docker network ls | grep ib-lab-net. Expected output contains one line with ib-lab-net and driver bridge. If it is missing, re-run the create command and check for typos.Phase 2: Deploy the OpenLDAP Server
Step 2.1: Choose your credentials
Decide the two secrets used throughout this lab before any container exists, so you are never tempted to accept a default.
Wherever you see <YOUR_ADMIN_PASSWORD> below, substitute a password you have chosen, for example Lab-Admin-2026!x. Wherever you see <USER_PASSWORD>, substitute a second, different value, for example Northgate-User-91!. Write both down for the duration of the lab.
Step 2.2: Run the OpenLDAP container
Launch the directory server with a defined organisation, domain, admin password, TLS enabled, and persistent storage.
The image derives your directory's base DN from the domain you supply. A DN, Distinguished Name, is the unique path of an entry in the directory tree, read right to left from the root. Supplying identitybytes.lab produces the base DN dc=identitybytes,dc=lab, where dc means domain component. Every entry you create will sit under this root.
Deploy the OpenLDAP server container
# Deploy OpenLDAP in a Docker container. # Purpose: provides the centralised identity store for the whole track. # Enterprise context: mirrors a corporate directory such as AD or FreeIPA. # # SECURITY NOTE: port 389 is unencrypted LDAP, exposed here for learning. # Production directories require 636 (LDAPS) or StartTLS for all clients. docker run -d \ --name ib-openldap \ --hostname ldap.identitybytes.lab \ --network ib-lab-net \ -p 389:389 \ -p 636:636 \ -e LDAP_ORGANISATION="Identity Bytes Lab" \ -e LDAP_DOMAIN="identitybytes.lab" \ -e LDAP_ADMIN_PASSWORD="<YOUR_ADMIN_PASSWORD>" \ -e LDAP_TLS_VERIFY_CLIENT="never" \ -v ib-ldap-data:/var/lib/ldap \ -v ib-ldap-config:/etc/ldap/slapd.d \ osixia/openldap:1.5.0
Line by line: -d runs the container in the background. --hostname sets the name the generated TLS certificate is issued to. The two -p flags publish LDAP and LDAPS to your machine. The three -e environment variables define the organisation name, the domain that becomes the base DN, and the admin password. LDAP_TLS_VERIFY_CLIENT=never tells the server not to demand certificates from clients, which this image otherwise requests and which would complicate a beginner lab. The two -v flags mount named volumes so your data survives container restarts. The final line pins the exact image version, because unpinned images are a supply chain risk and break reproducibility.
Docker prints a 64 character container ID. First run also downloads the image, which takes a minute or two.
docker ps --filter name=ib-openldap. Expected: one row, STATUS beginning with Up, and ports 0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp. If the container is missing, run docker logs ib-openldap and read the last lines; the most common cause is another service already holding port 389, which sudo ss -tlnp | grep -E '389|636' will reveal.dc=identitybytes,dc=lab, created the administrator entry cn=admin,dc=identitybytes,dc=lab with your chosen password, and generated a self signed TLS certificate for LDAPS. All of that state now lives in the two Docker volumes, not in the container itself.Step 2.3: First authenticated search
Prove the server answers queries and your admin credentials work, using the tool you will use for the rest of your career: ldapsearch.
In LDAP, logging in is called a bind. A simple bind presents a DN and a password. Once bound, a search reads entries beneath a chosen base. This request is the same one every LDAP integrated application performs.
Search the empty directory as admin
# -x use a simple bind (DN + password) rather than SASL # -H the server URL: protocol, host, port # -b base DN: where in the tree to start searching # -D the DN to bind (authenticate) as # -w the bind password (prompted securely with -W instead) ldapsearch -x -H ldap://localhost:389 \ -b "dc=identitybytes,dc=lab" \ -D "cn=admin,dc=identitybytes,dc=lab" \ -w "<YOUR_ADMIN_PASSWORD>"
# extended LDIF # ... dn: dc=identitybytes,dc=lab objectClass: top objectClass: dcObject objectClass: organization o: Identity Bytes Lab dc: identitybytes # numResponses: 2 # numEntries: 1
result: 0 Success and numEntries: 1. If you instead see ldap_bind: Invalid credentials (49), the password does not match what you set in Step 2.2; if you see Can't contact LDAP server (-1), the container is not running or the port is not published, so return to the Step 2.2 verification.Phase 3: Deploy the Web Admin Interface
Step 3.1: Run phpLDAPadmin and log in
Add a visual browser for the directory so you can see the tree you are about to build, not only query it from the terminal.
The UI container joins the same ib-lab-net network and reaches the directory by its container name. Nothing about the directory changes; this is a read and write client like any other.
Deploy phpLDAPadmin
# Web based LDAP browser, pointed at our server by container name. # HTTPS is disabled because this listens only on localhost in a lab. docker run -d \ --name ib-phpldapadmin \ --network ib-lab-net \ -p 8080:80 \ -e PHPLDAPADMIN_LDAP_HOSTS="ib-openldap" \ -e PHPLDAPADMIN_HTTPS="false" \ osixia/phpldapadmin:0.9.0
Open http://localhost:8080 in your browser. Click login on the left. Login DN: cn=admin,dc=identitybytes,dc=lab. Password: your admin password. The left pane shows the tree root dc=identitybytes,dc=lab.
docker logs ib-phpldapadmin and confirm port 8080 is not already in use.Phase 4: Design and Build the Directory Structure
Step 4.1: Create organisational units with LDIF
Create three containers in the tree: people for humans, groups for authorisation groups, and service-accounts for non human identities.
An organisational unit, ou, is a folder in the directory tree. Separating humans from service accounts is a deliberate architectural decision: it lets you apply different password policies, different monitoring, and different access rules to each population, which becomes critical in the PAM and workload identity phases of this track. LDIF, the LDAP Data Interchange Format, is the plain text change format defined in RFC 2849; in regulated environments, LDIF files are the reviewable, version controlled artefact of every directory change.
Create and load 01-structure.ldif
# Write the LDIF file defining three organisational units cat > ~/ib-sia-01/ldif/01-structure.ldif << 'EOF' # Container for human user accounts dn: ou=people,dc=identitybytes,dc=lab objectClass: organizationalUnit ou: people # Container for authorisation groups dn: ou=groups,dc=identitybytes,dc=lab objectClass: organizationalUnit ou: groups # Container for non human application identities dn: ou=service-accounts,dc=identitybytes,dc=lab objectClass: organizationalUnit ou: service-accounts EOF # Load it into the directory as the admin identity ldapadd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" \ -w "<YOUR_ADMIN_PASSWORD>" \ -f ~/ib-sia-01/ldif/01-structure.ldif
Each LDIF stanza declares a dn, the entry's full address, and an objectClass, which tells the schema what type of entry this is and therefore which attributes it must and may carry. Blank lines separate entries; this is mandatory LDIF syntax.
adding new entry "ou=people,dc=identitybytes,dc=lab" adding new entry "ou=groups,dc=identitybytes,dc=lab" adding new entry "ou=service-accounts,dc=identitybytes,dc=lab"
ldapsearch -x -H ldap://localhost:389 -b "dc=identitybytes,dc=lab" -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" "(objectClass=organizationalUnit)" ou. Expected: three entries. If ldapadd returned Already exists (68), the entries were loaded on a previous attempt and you may continue. A syntax error usually means a missing blank line between stanzas.Step 4.2: Create users
Populate ou=people with three Northgate employees and ou=service-accounts with one application bind account.
Human entries use the inetOrgPerson objectClass from RFC 2798, the de facto standard for people in directories, which provides attributes such as uid, cn (common name), sn (surname), mail, and title. The service account reuses the same class for simplicity but lives in its own ou and carries a description stating its owner and purpose, a governance habit that separates professional directories from messy ones.
Create and load 02-users.ldif
cat > ~/ib-sia-01/ldif/02-users.ldif << 'EOF' # Finance analyst dn: uid=asmith,ou=people,dc=identitybytes,dc=lab objectClass: inetOrgPerson uid: asmith givenName: Amina sn: Smith cn: Amina Smith mail: amina.smith@identitybytes.lab title: Finance Analyst departmentNumber: FIN # Platform engineer dn: uid=jpatel,ou=people,dc=identitybytes,dc=lab objectClass: inetOrgPerson uid: jpatel givenName: Jay sn: Patel cn: Jay Patel mail: jay.patel@identitybytes.lab title: Platform Engineer departmentNumber: IT # HR advisor dn: uid=lokafor,ou=people,dc=identitybytes,dc=lab objectClass: inetOrgPerson uid: lokafor givenName: Lara sn: Okafor cn: Lara Okafor mail: lara.okafor@identitybytes.lab title: HR Advisor departmentNumber: HR # Application bind account for the HR web portal dn: uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab objectClass: inetOrgPerson uid: svc-hrportal sn: Service Account cn: svc-hrportal description: Bind account used by the HR portal to authenticate users. Owner: Identity Team. EOF ldapadd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" \ -w "<YOUR_ADMIN_PASSWORD>" \ -f ~/ib-sia-01/ldif/02-users.ldif
Four adding new entry lines, one per account.
ldapsearch -x -H ldap://localhost:389 -b "ou=people,dc=identitybytes,dc=lab" -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" "(objectClass=inetOrgPerson)" uid mail. Expected: three entries showing uid and mail. If an entry failed with Object class violation (65), a mandatory attribute such as sn or cn is missing from that stanza.Step 4.3: Set passwords
Give each account a password, stored by the server as a salted SSHA hash rather than plain text.
We set passwords with ldappasswd rather than embedding them in LDIF, because LDIF files end up in Git repositories and change tickets, and credentials must never travel with them. The server hashes each password on receipt, so even a full database dump does not reveal the original values.
Set passwords for all four accounts
# -s sets the new password for the target DN (the final argument). # The admin DN authorises the change. Repeat for each account. ldappasswd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" \ -s "<USER_PASSWORD>" "uid=asmith,ou=people,dc=identitybytes,dc=lab" ldappasswd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" \ -s "<USER_PASSWORD>" "uid=jpatel,ou=people,dc=identitybytes,dc=lab" ldappasswd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" \ -s "<USER_PASSWORD>" "uid=lokafor,ou=people,dc=identitybytes,dc=lab" ldappasswd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" \ -s "<USER_PASSWORD>" "uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab"
In a real environment every account would receive a unique password; a shared lab value keeps the typing manageable while you learn the mechanics.
Each command returns silently. Silence is success for ldappasswd.
ldapwhoami -x -H ldap://localhost:389 -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" -w "<USER_PASSWORD>". Expected output: dn:uid=asmith,ou=people,dc=identitybytes,dc=lab. That single line means Amina Smith authenticated against your directory. Invalid credentials (49) means the password set and the password tested do not match.ldapwhoami performed a bind as the user and asked the server who it thinks you are. This is the identical operation a VPN or web application performs when someone types a username and password, which makes it your standard tool for answering the most common ticket in identity operations: this user cannot log in, is it the directory or the application?Step 4.4: Create groups
Create two authorisation groups, finance-team and it-admins, and place users in them.
Groups are how directories express authorisation. The groupOfNames objectClass holds one member attribute per member, each containing the member's full DN. Applications then ask a single question, is this DN a member of that group, instead of maintaining their own permission lists. Every RBAC model you build later in this track, in Keycloak, AWS, or OPA, is a descendant of this pattern.
Create and load 03-groups.ldif
cat > ~/ib-sia-01/ldif/03-groups.ldif << 'EOF' # Group granting access to finance applications dn: cn=finance-team,ou=groups,dc=identitybytes,dc=lab objectClass: groupOfNames cn: finance-team description: Members may access Northgate finance applications member: uid=asmith,ou=people,dc=identitybytes,dc=lab # Group granting administrative access to IT platforms dn: cn=it-admins,ou=groups,dc=identitybytes,dc=lab objectClass: groupOfNames cn: it-admins description: Members hold administrative access to IT platforms member: uid=jpatel,ou=people,dc=identitybytes,dc=lab EOF ldapadd -x -H ldap://localhost:389 \ -D "cn=admin,dc=identitybytes,dc=lab" \ -w "<YOUR_ADMIN_PASSWORD>" \ -f ~/ib-sia-01/ldif/03-groups.ldif
Two adding new entry lines.
ldapsearch -x -H ldap://localhost:389 -b "ou=groups,dc=identitybytes,dc=lab" -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" "(member=uid=asmith,ou=people,dc=identitybytes,dc=lab)" cn. Expected: exactly one entry, cn: finance-team.Phase 5: Secure the Transport with LDAPS
Step 5.1: Demonstrate the problem, then connect over TLS
Understand why plain LDAP is unacceptable for credentials, then repeat a real query over the encrypted LDAPS port.
Every bind you have performed so far sent the password across the connection in a form readable by anyone positioned on the network path. On localhost that path is one machine; in an enterprise it crosses switches, firewalls, and sometimes data centres. The osixia image generated a self signed certificate at first boot and is already listening for TLS on port 636, so no server changes are needed.
Query the directory over LDAPS on port 636
# LDAPTLS_REQCERT=never tells the client to accept a certificate it cannot # verify. Required here because the lab certificate is self signed and no # client trusts it yet. See the security warning below before ever using # this outside a lab. LDAPTLS_REQCERT=never ldapsearch -x -H ldaps://localhost:636 \ -b "dc=identitybytes,dc=lab" \ -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" \ -w "<USER_PASSWORD>" \ "(uid=asmith)" cn mail title
dn: uid=asmith,ou=people,dc=identitybytes,dc=lab cn: Amina Smith mail: amina.smith@identitybytes.lab title: Finance Analyst result: 0 Success
ldaps:// on port 636 passes. You can additionally inspect the certificate with echo | openssl s_client -connect localhost:636 2>/dev/null | openssl x509 -noout -subject -dates, which prints a subject containing ldap.identitybytes.lab and the validity window. If the LDAPS query hangs or fails, confirm port 636 appears in docker ps output and check docker logs ib-openldap for TLS errors.LDAPTLS_REQCERT=never disables certificate verification, which removes protection against server impersonation. It is acceptable in this lab only because you built the server seconds ago on your own machine. The correct fix, distributing a trusted CA certificate to clients, is exactly what you will build in the PKI phase of this track (IB-SIA-17 to 23).Testing and Validation
End to end scenario: the HR portal authenticates a user
This test replays, step by step, what a real LDAP integrated application does when Amina Smith logs in. The application first binds with its own service account, searches for the user's entry by username, then attempts a bind as the found DN with the password the user typed, and finally checks group membership. Run all four stages and compare outputs.
Run the four stage application login simulation
# STAGE 1: the application authenticates as itself ldapwhoami -x -H ldaps://localhost:636 \ -D "uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab" \ -w "<USER_PASSWORD>" # Expected: dn:uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab # STAGE 2: the application finds the user who typed "asmith" ldapsearch -x -H ldaps://localhost:636 \ -D "uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab" \ -w "<USER_PASSWORD>" \ -b "ou=people,dc=identitybytes,dc=lab" "(uid=asmith)" dn # Expected: one entry, dn: uid=asmith,ou=people,dc=identitybytes,dc=lab # STAGE 3: the application verifies the user's password by binding as them ldapwhoami -x -H ldaps://localhost:636 \ -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" \ -w "<USER_PASSWORD>" # Expected: dn:uid=asmith,ou=people,dc=identitybytes,dc=lab # STAGE 4: the application checks authorisation via group membership ldapsearch -x -H ldaps://localhost:636 \ -D "uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab" \ -w "<USER_PASSWORD>" \ -b "ou=groups,dc=identitybytes,dc=lab" \ "(member=uid=asmith,ou=people,dc=identitybytes,dc=lab)" cn # Expected: cn: finance-team
Note that stages run over LDAPS with the environment variable omitted only if you have chosen to export LDAPTLS_REQCERT=never for the session; otherwise prefix each command with it as in Phase 5.
Negative tests, because security testing means proving failure works
Run the negative tests
# TEST N1: wrong password must be rejected ldapwhoami -x -H ldap://localhost:389 \ -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" -w "WrongPassword1" # Expected: ldap_bind: Invalid credentials (49) # TEST N2: a non existent user must be rejected identically ldapwhoami -x -H ldap://localhost:389 \ -D "uid=ghost,ou=people,dc=identitybytes,dc=lab" -w "Anything123" # Expected: ldap_bind: Invalid credentials (49) # The identical error for wrong user and wrong password is deliberate: # it prevents attackers enumerating which usernames exist. # TEST N3: a normal user must not be able to change another user's password ldappasswd -x -H ldap://localhost:389 \ -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" -w "<USER_PASSWORD>" \ -s "Hijacked123!" "uid=jpatel,ou=people,dc=identitybytes,dc=lab" # Expected: Result: Insufficient access (50)
Common failure modes and solutions
| Symptom | Likely cause | Solution |
|---|---|---|
Can't contact LDAP server (-1) | Container stopped, or wrong port in the URL | docker ps; restart with docker start ib-openldap; check the -H value |
Invalid credentials (49) | Wrong password, or a typo anywhere in the bind DN | Retype the full DN; test the admin bind first to isolate whether the problem is the account or the server |
No such object (32) | The base DN or target DN does not exist, often a misspelt ou | Search from the root with the admin account and read the tree as it actually is |
Object class violation (65) during ldapadd | An LDIF entry is missing a mandatory attribute for its objectClass | For inetOrgPerson, confirm cn and sn are present in the failing stanza |
Already exists (68) during ldapadd | The LDIF was loaded before | Safe to continue; delete first with ldapdelete if you need a clean reload |
| LDAPS fails but 389 works | Client refusing the self signed certificate | Prefix the command with LDAPTLS_REQCERT=never for lab purposes, and read the Phase 5 warning |
Security Analysis
What makes this implementation secure
- Passwords are stored as salted SSHA hashes, set through
ldappasswd, and never appear in LDIF files or shell history as directory data. - Authentication and authorisation are separated: applications verify identity by user bind and derive permissions from group membership, so no application stores credentials.
- A dedicated service account exists for application binds, in its own ou with a documented owner, so application access can be monitored and revoked independently of any human.
- TLS on port 636 protects credentials and directory data in transit, and the negative tests confirm that wrong passwords, unknown users, and unauthorised writes fail with the correct errors.
- Both container images are version pinned, giving a reproducible and reviewable software supply chain.
What is intentionally simplified for the lab
- The TLS certificate is self signed and clients skip verification; there is no CA hierarchy or certificate lifecycle management yet.
- Plain LDAP on 389 remains enabled for learning and comparison.
- A single directory server: no replication, so no high availability.
- Default access controls and no password policy overlay, so no lockout, complexity, or expiry rules.
- Shared demonstration passwords across accounts, and logs remain local to the container.
Production hardening recommendations
| Recommendation | Why | Covered in |
|---|---|---|
| Issue the server certificate from an internal CA and enforce client verification | Prevents server impersonation and credential interception | IB-SIA-17 to 19 (PKI phase) |
| Disable plain LDAP; require LDAPS or StartTLS for every client | Removes the cleartext credential path entirely, aligned with CIS benchmarks and Microsoft ADV190023 guidance for the AD equivalent | Hardening exercise, this lab's stretch goal |
| Apply the ppolicy overlay: lockout, history, minimum length | Defends against online password guessing, mapped to NIST SP 800-63B | IB-SIA-05 (MFA and policy lab) |
| Add a replica and load balancer | The directory is tier zero infrastructure; its outage is everyone's outage | IB-SIA-06 (HA patterns, applied to Keycloak) |
| Restrict anonymous access and scope the service account to read only on required subtrees | Least privilege for non human identities, a core PAM principle | Reinforced across Phases 6 and 7 of the track |
| Ship directory logs to a SIEM and alert on bind failures | Failed bind spikes are the earliest signal of password spraying | IB-SIA-35 (identity observability) |
Cleanup Instructions
Option A: pause the lab, keep everything
Stop containers without losing data
# Stops both containers. Directory data stays in the Docker volumes. docker stop ib-openldap ib-phpldapadmin # Resume later with: docker start ib-openldap ib-phpldapadmin
Option B: full removal
Remove all lab resources completely
# Remove containers docker rm -f ib-openldap ib-phpldapadmin # Remove the persistent data and configuration volumes. # WARNING: this permanently deletes the directory contents. docker volume rm ib-ldap-data ib-ldap-config # Remove the lab network docker network rm ib-lab-net # Optionally remove the images to reclaim disk space docker rmi osixia/openldap:1.5.0 osixia/phpldapadmin:0.9.0 # Keep ~/ib-sia-01 if you are publishing the portfolio in Section 12; # otherwise remove it too: # rm -rf ~/ib-sia-01
docker ps -a | grep ib-, docker volume ls | grep ib-ldap, and docker network ls | grep ib-lab. All three must return nothing after Option B. Keep the LDIF files: they are portfolio evidence.Recommended Learning Links
- OpenLDAP 2.4 Administrator's Guide (primary vendor documentation)
- RFC 4511: LDAP, The Protocol
- RFC 4514: String Representation of Distinguished Names
- RFC 2849: The LDAP Data Interchange Format (LDIF)
- RFC 2798: The inetOrgPerson Object Class
- LDAP.com reference guides (excellent protocol level explanations)
- osixia/docker-openldap image documentation (all environment variables used in this lab)
- NIST SP 800-63B: Digital Identity Guidelines, Authentication
Portfolio Publishing Guide: Git, GitHub, and LinkedIn
A completed lab that lives only on your laptop advances your skills; a completed lab published well advances your career. This section takes the artefacts you produced, LDIF files, this guide, and your notes, and turns them into public evidence of capability. Assume you have never used Git before.
12.1 What goes in the repository, and what never does
ldappasswd, not embedded. Keep it that way.Recommended structure, one repository for the whole track so it grows visibly over time:
identity-bytes-architect-labs/
├── README.md <- the shop window: track overview + lab index
├── .gitignore
└── lab-01-openldap-directory/
├── README.md <- what, why, architecture image, key commands
├── docs/
│ └── IB-SIA-01-openldap-enterprise-directory.html
├── ldif/
│ ├── 01-structure.ldif
│ ├── 02-users.ldif
│ └── 03-groups.ldif
├── scripts/
│ └── deploy.sh <- optional: the docker commands as a script
└── screenshots/
├── phpldapadmin-tree.png
└── ldapwhoami-success.png
Take the two screenshots now if you have not: the phpLDAPadmin tree showing your three OUs and users, and a terminal showing the successful four stage test from Section 8. Evidence of verification is what separates a portfolio from a tutorial copy.
12.2 Git from zero
Install Git, configure identity, create the local repository
# Install Git (Ubuntu; macOS ships it, or use: brew install git) sudo apt-get install -y git # Tell Git who you are. This name and email appear on every commit. git config --global user.name "Muhammad Rizwan Afzal" git config --global user.email "<YOUR_GITHUB_EMAIL>" git config --global init.defaultBranch main # Create the repository folder and copy your lab artefacts in mkdir -p ~/identity-bytes-architect-labs/lab-01-openldap-directory/{docs,ldif,scripts,screenshots} cp ~/ib-sia-01/ldif/*.ldif ~/identity-bytes-architect-labs/lab-01-openldap-directory/ldif/ # Initialise Git: this turns the folder into a version controlled repository cd ~/identity-bytes-architect-labs git init # Create a .gitignore so secret and junk files can never be committed cat > .gitignore << 'EOF' *.env *secret* *password* .DS_Store EOF
Write the lab README (template provided)
cat > lab-01-openldap-directory/README.md << 'EOF' # Lab 01: Enterprise Directory Foundation with OpenLDAP Part of my Identity Bytes Senior IAM Architect lab series (IB-SIA-01). ## Problem this solves Identity sprawl: five applications, five account stores, orphaned accounts surviving leavers. This lab builds the enterprise answer, a central LDAP directory acting as the single source of truth for people, groups, and service accounts. ## What I built - OpenLDAP 2.4 (Docker, version pinned) with persistent volumes - Directory tree: ou=people, ou=groups, ou=service-accounts under dc=identitybytes,dc=lab - Users and groups loaded via change controlled LDIF (RFC 2849) - Dedicated least privilege service account for application binds - TLS secured transport on LDAPS 636 - Four stage application login simulation plus negative security tests ## Skills demonstrated Directory design (DIT, DNs, objectClasses), LDIF, LDAP search filters, bind based authentication, group based authorisation, TLS for LDAP, Docker operations, verification driven troubleshooting. ## Key verification ldapwhoami as an end user returned the bound DN, group membership queries returned the expected authorisation data, and negative tests confirmed invalid credentials and unauthorised writes are rejected. Full step by step guide: docs/IB-SIA-01-openldap-enterprise-directory.html EOF # Stage everything and make the first commit. # A commit is a snapshot with a message explaining the change. git add . git commit -m "Lab 01: OpenLDAP enterprise directory with LDIF, groups, service account, LDAPS"
12.3 Publish to GitHub
Create the GitHub repository and push
Create a free account at https://github.com if you do not have one, using a professional username. Then click the plus icon, New repository. Name: identity-bytes-architect-labs. Description: Hands-on Senior IAM Architect lab series: directories, SSO, federation, PKI, authorisation, workload identity. Visibility: Public. Do not tick any initialise options, because your local repository already has content. Click Create repository.
# Connect your local repository to GitHub and push (upload) it. # Replace <YOUR_GITHUB_USERNAME> with your actual username. git remote add origin https://github.com/<YOUR_GITHUB_USERNAME>/identity-bytes-architect-labs.git git push -u origin main
When prompted for a password, GitHub requires a Personal Access Token instead. Create one at GitHub, Settings, Developer settings, Personal access tokens, Fine-grained tokens: give it a name, an expiry, select only this repository, grant Contents read and write permission, then paste the generated token at the password prompt. Your terminal will not display it as you paste; press Enter.
https://github.com/<YOUR_GITHUB_USERNAME>/identity-bytes-architect-labs in a browser. The README renders on the repository page and the lab folder is visible. That URL is now a portfolio asset for your CV and LinkedIn profile.main, folder / (root). After a minute your lab guide becomes a live web page at https://<YOUR_GITHUB_USERNAME>.github.io/identity-bytes-architect-labs/lab-01-openldap-directory/docs/IB-SIA-01-openldap-enterprise-directory.html, ideal for sharing with recruiters who will never clone a repository.12.4 Share it on LinkedIn
Publish within a day or two of finishing while the details are fresh. Attach one screenshot, the architecture diagram or the phpLDAPadmin tree, because posts with a single strong image outperform bare text. Place the GitHub link in the first comment rather than the post body. A draft in the Identity Bytes thought leadership style, ready to adapt:
Draft post:
An auditor asks a simple question: show me everyone who can access this system. In most organisations, nobody can answer it in under a week.
The root cause is rarely exotic. It is identity sprawl: every application keeping its own private list of accounts, every leaver surviving in at least one forgotten system, every access review starting from spreadsheets.
This weekend I built the answer from first principles: an OpenLDAP directory serving as a single source of truth. Organisational units separating people from service accounts, groups answering authorisation questions, a least privilege bind account for applications, and TLS protecting every credential in transit. Then I proved it, replaying the exact four step sequence a real application performs at login, and confirming the failures fail correctly too: wrong passwords rejected, unknown users indistinguishable from wrong passwords, unauthorised writes refused.
The lesson that stayed with me: authentication and authorisation are different questions, and directories were separating them decades before Zero Trust made it fashionable. Every modern IAM platform I work with still stands on this pattern.
This is lab one of a thirty six lab series I am publishing as I build towards senior IAM architecture. Full guide, LDIF files, and verification evidence are on my GitHub, link in the comments.
If your organisation had to produce a complete access list for one critical system tomorrow, how long would it honestly take?
Posting mechanics: publish Tuesday to Thursday, between 8 and 10 in the morning UK time, when your professional audience is commuting or settling at their desks. Reply to every comment within the first two hours, because early engagement determines reach. Add three to five hashtags at the end of the post, for example #IAM #IdentityManagement #CyberSecurity #OpenLDAP #InfoSec. Repost the series index monthly as new labs land, so the portfolio compounds instead of scrolling into the void.
12.5 The compounding effect
One lab is a post. Thirty six labs, published consistently under one repository and one hashtag series, become a body of work that interviewers reference before you enter the room. When a hiring panel for a role like the Senior IAM Platform Architect position asks about directory design, you will not claim experience; you will send a link.