Skip to content

Setting up LDAP with Docker

Ed Mozley edited this page Jul 16, 2026 · 1 revision

Setting up OpenLDAP & Samba AD in Docker

A complete, copy-paste walkthrough for standing up two throwaway directory servers in Docker and wiring them into FreeITSM's LDAP / Active Directory sign-in. No Windows Server, no touching your real directory.

Development only. Every password on this page is public, TLS is off, and the Samba container runs privileged. It's ideal for evaluating FreeITSM or developing against it β€” never run it anywhere real. See Going to production.

The files are in the repo at docker/ldap-test/ β€” if you have FreeITSM checked out, you can skip the copy-pasting and jump to step 1.

Why two servers?

samba-ad openldap
Speaks Real Active Directory Plain LDAP
Username attribute sAMAccountName uid
Immutable ID objectGUID (binary) entryUUID (text)
Groups group + memberOf, nested groupOfNames, no nesting
Represents What most FreeITSM users actually have A genuinely different flavour

Samba AD is the one that matters β€” it gives you true AD semantics (nested groups, referrals, binary GUIDs, disabled-account handling) without a Windows licence. OpenLDAP is worth keeping alongside it precisely because it's different: code developed against only one directory silently grows that one's assumptions. Nested groups don't exist on OpenLDAP at all, and memberOf isn't even present without an extra overlay.

If you only want to try LDAP sign-in, run Samba AD and ignore OpenLDAP.

Prerequisites

  • Docker installed and running (Docker Desktop on Windows/Mac). Everything below assumes Linux containers.
  • FreeITSM running and reachable in a browser (this guide assumes http://localhost/freeitsm-app/).
  • The PHP ldap extension enabled β€” extension=ldap in php.ini, then restart your web server. Some stacks (WAMP, XAMPP) have two php.ini files, one for the web server and one for the CLI; enable it in both. FreeITSM's Authentication page tells you plainly if it's missing.
  • Ports 3890, 3891, 6361 and 8091 free.

1. Start the directories

Create a folder outside your web root, e.g. C:\Users\<you>\docker\ldap\, and add a docker-compose.yml:

services:
  openldap:
    image: osixia/openldap:1.5.0
    container_name: freeitsm-ldap
    environment:
      LDAP_ORGANISATION: "FreeITSM Test"
      LDAP_DOMAIN: "freeitsm.test"
      LDAP_ADMIN_PASSWORD: "adminpass"
      LDAP_TLS: "false"
    ports:
      - "3890:389"
    command: ["--copy-service"]

  # Samba 4 Active Directory Domain Controller.
  # INSECURELDAP relaxes "ldap server require strong auth" so simple binds work
  # over plain 389 in testing. Real AD usually demands LDAPS.
  samba-ad:
    image: nowsci/samba-domain:latest
    container_name: freeitsm-samba-ad
    hostname: dc1
    privileged: true
    environment:
      DOMAIN: "AD.FREEITSM.TEST"
      DOMAINPASS: "Passw0rd!2026"
      DNSFORWARDER: "1.1.1.1"
      INSECURELDAP: "true"
      NOCOMPLEXITY: "true"
    ports:
      - "3891:389"
      - "6361:636"
    dns:
      - 127.0.0.1

  # Browse either directory in a GUI; pick the server on the login page.
  phpldapadmin:
    image: osixia/phpldapadmin:0.9.0
    container_name: freeitsm-ldap-ui
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: "#PYTHON2BASH:[{'openldap': [{'server': [{'tls': False}]},{'login': [{'bind_id': 'cn=admin,dc=freeitsm,dc=test'}]}]},{'samba-ad': [{'server': [{'tls': False}]},{'login': [{'bind_id': 'Administrator@AD.FREEITSM.TEST'}]}]}]"
      PHPLDAPADMIN_HTTPS: "false"
    ports:
      - "8091:80"
    depends_on:
      - openldap
      - samba-ad

Start it:

docker compose up -d        # first run pulls ~1 GB
docker ps                   # all three should be Up

Samba takes 20–30 seconds to provision the domain on first boot. docker logs freeitsm-samba-ad should end with samba version 4.x started. A few Unable to parse dn lines are the image's optional sshPublicKey schema helper and are harmless.

Note the ports. LDAP's standard port is 389, but these are published on 3890 (OpenLDAP) and 3891 (Samba) so they can coexist and won't collide with anything else. Use those ports in FreeITSM.

2. Seed Samba AD with a company

An empty directory teaches you nothing. This builds a small business β€” "Northwind Trading" β€” including the awkward cases real directories have.

Save as seed-ad.sh and run bash seed-ad.sh:

#!/usr/bin/env bash
set -e
D=freeitsm-samba-ad
BASE="DC=ad,DC=freeitsm,DC=test"
st() { docker exec "$D" samba-tool "$@"; }

st ou create "OU=Northwind,$BASE"
st ou create "OU=Staff,OU=Northwind,$BASE"
st ou create "OU=IT,OU=Staff,OU=Northwind,$BASE"
st ou create "OU=Sales,OU=Staff,OU=Northwind,$BASE"
st ou create "OU=Finance,OU=Staff,OU=Northwind,$BASE"
st ou create "OU=Groups,OU=Northwind,$BASE"
st ou create "OU=Service Accounts,OU=Northwind,$BASE"
st ou create "OU=Leavers,OU=Northwind,$BASE"

# --- the IT team: your FreeITSM analysts ---
st user create a.chen    'Nw!Chen2026'  --userou="OU=IT,OU=Staff,OU=Northwind" \
   --given-name=Amy     --surname=Chen     --mail-address=a.chen@northwind.test    --job-title="IT Manager"
st user create r.patel   'Nw!Patel2026' --userou="OU=IT,OU=Staff,OU=Northwind" \
   --given-name=Raj     --surname=Patel    --mail-address=r.patel@northwind.test   --job-title="Service Desk Analyst"
st user create s.oconnor 'Nw!Conn2026'  --userou="OU=IT,OU=Staff,OU=Northwind" \
   --given-name=Siobhan --surname="O'Connor" --mail-address=s.oconnor@northwind.test --job-title="2nd Line"
st user create j.muller  'Nw!Mull2026'  --userou="OU=IT,OU=Staff,OU=Northwind" \
   --given-name=JΓΌrgen  --surname=MΓΌller  --mail-address=j.muller@northwind.test   --job-title="Infrastructure"

# --- the rest of the business ---
st user create t.brooks  'Nw!Broo2026'  --userou="OU=Sales,OU=Staff,OU=Northwind" \
   --given-name=Tom      --surname=Brooks --mail-address=t.brooks@northwind.test
st user create l.garcia  'Nw!Garc2026'  --userou="OU=Sales,OU=Staff,OU=Northwind" \
   --given-name=LucΓ­a    --surname=GarcΓ­a --mail-address=l.garcia@northwind.test
st user create p.ndlovu  'Nw!Ndlo2026'  --userou="OU=Finance,OU=Staff,OU=Northwind" \
   --given-name=Precious --surname=Ndlovu --mail-address=p.ndlovu@northwind.test

# --- edge cases ---
st user create w.noemail 'Nw!NoMa2026' --userou="OU=Staff,OU=Northwind" \
   --given-name=Wendy --surname=Warehouse            # NO email address at all
st user create x.leaver  'Nw!Leav2026' --userou="OU=Leavers,OU=Northwind" \
   --given-name=Xavier --surname=Leaver --mail-address=x.leaver@northwind.test
st user disable x.leaver                              # a leaver, still in the directory

# --- the service account FreeITSM binds as ---
st user create svc-ldap 'Nw!Svc2026' --userou="OU=Service Accounts,OU=Northwind" \
   --description="FreeITSM read-only directory lookup"
st user setexpiry svc-ldap --noexpiry

# --- groups ---
st group add "NW-IT-Support" --groupou="OU=Groups,OU=Northwind"
st group add "NW-IT-Admins"  --groupou="OU=Groups,OU=Northwind"
st group add "NW-Sales"      --groupou="OU=Groups,OU=Northwind"
st group add "NW-Finance"    --groupou="OU=Groups,OU=Northwind"
st group add "NW-All-Staff"  --groupou="OU=Groups,OU=Northwind"

st group addmembers "NW-IT-Support" r.patel,s.oconnor,j.muller
st group addmembers "NW-IT-Admins"  a.chen
st group addmembers "NW-Sales"      t.brooks,l.garcia
st group addmembers "NW-Finance"    p.ndlovu

# NESTED: All-Staff contains the department GROUPS, not people.
st group addmembers "NW-All-Staff" NW-IT-Support,NW-IT-Admins,NW-Sales,NW-Finance --object-types=group

Who's who, and why

User Password Why they exist
r.patel Nw!Patel2026 Ordinary analyst in NW-IT-Support, nested three OUs deep
a.chen Nw!Chen2026 NW-IT-Admins only β€” an admins group is not automatically your analyst group
s.oconnor Nw!Conn2026 Siobhan O'Connor β€” an apostrophe in the DN
j.muller Nw!Mull2026 JΓΌrgen MΓΌller β€” UTF-8 round-tripping
l.garcia Nw!Garc2026 LucΓ­a GarcΓ­a β€” same
t.brooks Nw!Broo2026 Sales β€” for the self-service user group
p.ndlovu Nw!Ndlo2026 Finance β€” in neither ITSM group, so must be denied despite a correct password
w.noemail Nw!NoMa2026 No email attribute β€” auto-create must refuse cleanly
x.leaver Nw!Leav2026 Disabled β€” must never sign in, even with the right password

NW-All-Staff contains the other groups, not people. Nobody has it in their memberOf; only AD's chain-matching rule finds it. Gate on it to test nested groups.

3. Seed OpenLDAP (optional)

Skip this if you only want Samba. Save as seed.ldif:

dn: ou=people,dc=freeitsm,dc=test
objectClass: organizationalUnit
ou: people

dn: cn=svc-freeitsm,dc=freeitsm,dc=test
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: svc-freeitsm
userPassword: svcpass

dn: uid=alice,ou=people,dc=freeitsm,dc=test
objectClass: inetOrgPerson
uid: alice
cn: Alice Analyst
sn: Analyst
mail: alice@freeitsm.test
userPassword: alicepass
docker cp seed.ldif freeitsm-ldap:/tmp/seed.ldif
docker exec freeitsm-ldap ldapadd -x -H ldap://localhost \
  -D "cn=admin,dc=freeitsm,dc=test" -w adminpass -f /tmp/seed.ldif

Now the bit that catches everyone. OpenLDAP's default ACL ends by * none, so the service account can't read anything β€” and searches come back No such object, exactly as though the user doesn't exist. Grant it read (acl.ldif):

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcAccess: {1}to attrs=userPassword,shadowLastChange by self write by dn="cn=admin,dc=freeitsm,dc=test" write by anonymous auth by * none
olcAccess: {2}to * by self read by dn="cn=admin,dc=freeitsm,dc=test" write by dn="cn=svc-freeitsm,dc=freeitsm,dc=test" read by * none
docker cp acl.ldif freeitsm-ldap:/tmp/acl.ldif
docker exec freeitsm-ldap ldapmodify -Y EXTERNAL -H ldapi:/// -f /tmp/acl.ldif

Note rule {2} grants the service account read, and rule {1} deliberately does not give it access to userPassword β€” a lookup account never needs to see password hashes.

4. Look around

Open http://localhost:8091, pick the server from the dropdown, and log in:

Server Login Password
samba-ad Administrator@AD.FREEITSM.TEST Passw0rd!2026
openldap cn=admin,dc=freeitsm,dc=test adminpass

AD accepts a UPN (user@domain) for a simple bind, which is why its login isn't a DN. Expand DC=ad,DC=freeitsm,DC=test β†’ OU=Northwind.

Or stay on the command line:

docker exec freeitsm-samba-ad samba-tool user list
docker exec freeitsm-samba-ad samba-tool group listmembers "NW-IT-Support"

docker exec freeitsm-samba-ad ldapsearch -x -H ldap://localhost \
  -D "Administrator@AD.FREEITSM.TEST" -w 'Passw0rd!2026' \
  -b "OU=Northwind,DC=ad,DC=freeitsm,DC=test" "(objectClass=user)" dn mail

5. Configure FreeITSM

System β†’ Authentication β†’ + Add, set Type to LDAP / Active Directory, click the Active Directory preset, then:

Field Value
Display name Northwind AD
Server 127.0.0.1
Encryption / Port None (plain LDAP) / 3891
Service account svc-ldap@AD.FREEITSM.TEST
Service account password Nw!Svc2026
Base DN OU=Northwind,DC=ad,DC=freeitsm,DC=test
Analyst group NW-IT-Support
Self-service user group NW-Sales
Auto-create users on first login βœ…
Default module access tickets, knowledge

Everything else the preset filled in β€” the user filter, the attribute names and the group filter β€” is already right for AD.

Hit Test before saving. With the test user blank it proves the service account can connect and read. Put r.patel / Nw!Patel2026 in and it runs a real sign-in, then reports the name, email, groups and the access it worked out.

For OpenLDAP instead: port 3890, service account cn=svc-freeitsm,dc=freeitsm,dc=test / svcpass, base dc=freeitsm,dc=test, and the OpenLDAP preset.

6. Test the login

Sign out and try each of these at the normal FreeITSM login form. The whole point is that all four passwords are correct β€” only groups differ:

Sign in as Expected
r.patel / Nw!Patel2026 βœ… In. An analyst account is created automatically with tickets + knowledge
t.brooks / Nw!Broo2026 β›” "does not have analyst access β€” use the self-service portal"
p.ndlovu / Nw!Ndlo2026 β›” "not a member of a group that grants access"
x.leaver / Nw!Leav2026 β›” Refused β€” disabled in the directory
w.noemail / Nw!NoMa2026 β›” Refused β€” no email to build an account from

Then check System β†’ Analysts: only r.patel exists. The denied users leave no trace, because the group check runs before anything is created.

Try nested groups too: change Analyst group to NW-All-Staff and p.ndlovu gets in β€” she's in NW-Finance, which is inside NW-All-Staff. That only works because the AD preset walks nested groups.

Troubleshooting

No such object, but the user is definitely there. Almost always permissions, not a missing user β€” most directories report a subtree they can't read as though it isn't there. On OpenLDAP, you skipped the ACL step. On AD, check the base DN.

Everything is refused after I set a group. That's the gate working. Check the group name, and use Test β€” it prints the groups it found. Remember NW-IT-Admins is not NW-IT-Support.

Samba won't start / exits immediately. It needs privileged: true. On Docker Desktop, make sure you're in Linux-container mode.

Can't contact LDAP server from FreeITSM. Use 127.0.0.1 and port 3891/3890, not localhost:389. FreeITSM runs on your host, not inside the compose network β€” the container names (samba-ad) only resolve between the containers.

Changing a phpLDAPadmin env var does nothing. The osixia image generates its config on first run into an anonymous volume, so up -d β€” even --force-recreate β€” keeps the old one. Drop the volume:

docker compose rm -fsv phpldapadmin && docker compose up -d phpldapadmin

Only ever do this to phpldapadmin. The directories keep their data in the container, so removing theirs wipes everything you seeded.

Everything vanished after a reboot. The containers have no restart policy and no volumes. docker start freeitsm-ldap freeitsm-samba-ad freeitsm-ldap-ui brings them back with data intact β€” but a docker rm loses it, and you re-seed.

Going to production

Nothing here is production-grade. For a real deployment:

  • Point at your real domain controller, not a container. You need a hostname, a read-only service account, and a base DN β€” nothing else changes.
  • Turn encryption on. With None, passwords cross the network in the clear on every sign-in. Use LDAPS (636) or STARTTLS. Many real AD servers refuse password binds over plain LDAP anyway β€” that's what INSECURELDAP disables here, and you should not replicate it.
  • Give the service account read only. It never needs write access, and never needs to read userPassword.
  • Name your groups. Leaving both group fields blank means everyone in the directory becomes an analyst.
  • Keep one local admin as break-glass β€” see SSO Β§ break-glass.

Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally