-
Notifications
You must be signed in to change notification settings - Fork 15
Setting up LDAP with 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.
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.
- 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
ldapextension enabled βextension=ldapinphp.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.
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-adStart it:
docker compose up -d # first run pulls ~1 GB
docker ps # all three should be UpSamba 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.
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| 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.
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.ldifNow 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.ldifNote 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.
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 mailSystem β 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.
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.
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 phpldapadminOnly 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.
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 whatINSECURELDAPdisables 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.
- LDAP & Active Directory β configuring and running it
- LDAP β Developer Guide β how it's built and the traps
- Setting up SSO with Keycloak (Docker) β the OIDC equivalent
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)