Single-JAR, in-memory LDAP server wrapping Apache Directory Server 2.0.0.AM27 — useful for integration testing, SSO simulators, and local development without standing up a real directory. The runtime surface exposes the LDAP protocol (default partition dc=ldap,dc=example) with optional LDAPS, configurable bind address / port, a swappable admin password (uid=admin,ou=system), and one-or-more .ldif files imported at boot via JCommander-driven CLI flags; the delivery surface ships as a self-contained Maven-shaded JAR, a multi-stage non-root Docker image on GHCR (ghcr.io/andriykalashnykov/ldap-server/apacheds-ad) built from @sha256:-digest-pinned base images, toolchain-alignment guards keeping .mise.toml and Dockerfile in lockstep on Java 25 + Maven 3.9.16, a GitHub Actions pipeline gated by dorny/paths-filter, Trivy filesystem + image scans (CRITICAL/HIGH blocking on the image side), a TCP-probe smoke test, an LDAP-bind + search end-to-end gate before push, cosign keyless image signing + SPDX SBOM attestation on tagged releases, OWASP dependency-check (NVD + Sonatype OSS Index; weekly cron + tag pushes + manual dispatch), and Renovate-managed dependencies.
This is a fork of intoolswetrust/ldap-server — every Java change lives upstream; the fork adds the Docker pipeline, Makefile, hardened CI, and Renovate. Java package
com.github.kwart.ldapis intentionally kept aligned with upstream so future syncs stay clean diffs.
C4Context
title ldap-server — in-memory LDAP directory for tests, SSO mocks & dev
Person(client, "App / test / CI client", "Binds and searches over LDAP")
System(srv, "ldap-server", "ApacheDS 2.0.0.AM27, in-memory; LDAP :10389 plus optional LDAPS / StartTLS; ships as one shaded JAR or a non-root Docker image")
System_Ext(ldif, "LDIF seed", "Bundled ldap-example.ldif or a mounted .ldif directory, imported at boot")
Rel(client, srv, "bind + search", "LDAP / LDAPS")
Rel(ldif, srv, "seeds entries at startup")
| Component | Technology |
|---|---|
| Language | Java 25 LTS (source + bytecode target 25; matches eclipse-temurin:25-jre-alpine runtime) |
| LDAP engine | Apache Directory Server 2.0.0.AM27 |
| Build | Maven 3.9.16 + maven-shade-plugin 3.6.2 (single runnable JAR) |
| CLI parser | JCommander 3.0 (org.jcommander coordinate; IUsageFormatter-based) |
| Logging | SLF4J 2.0.18 + slf4j-simple (ServiceLoader binding) |
| Tests | JUnit 5 Jupiter 6.1.0 via junit-bom (9 tests, all passing — incl. StartTLS over TLSv1.3 and anonymous bind) |
| Container | Multi-stage Dockerfile: maven:3.9-eclipse-temurin-25 → eclipse-temurin:25-jre-alpine (both @sha256:-digest-pinned), non-root UID 10001, TCP HEALTHCHECK |
| Version manager | mise (.mise.toml pins Java 25 LTS + Maven 3.9.16) |
| Dep management | Renovate (Maven + GitHub Actions + Dockerfile + .mise.toml) |
| CI | GitHub Actions — paths-filter changes detector + jdx/mise-action + Trivy image scan + TCP smoke test |
make deps # install Java 25 + Maven via mise (one-time, asks you to activate shell)
make ci # lint + test + package -> target/ldap-server.jar
make run-jar # start the server on 0.0.0.0:10389 with bundled LDIF
# Bind URL: ldap://127.0.0.1:10389/dc=ldap,dc=example
# Admin: uid=admin,ou=system / secret
# Test user: uid=jduke,ou=Users,dc=ldap,dc=example / thedukeOverride the bind address / port / LDIF directory via .env (see .env.example):
LDAP_PORT=10399 LDAPS_PORT=10636 BIND_ADDRESS=127.0.0.1 make run-jarFor a quick poke from another terminal:
ldapsearch -x -H ldap://127.0.0.1:10389 -D 'uid=admin,ou=system' -w secret \
-b dc=ldap,dc=example '(objectClass=*)'| Tool | Version | Purpose |
|---|---|---|
| GNU Make | 3.81+ | Build orchestration |
| Git | any | Source control |
| mise | latest | Pins Java + Maven from .mise.toml; make deps installs it on first run |
| JDK (Temurin) | 25 LTS | Auto-installed by mise install |
| Maven | 3.9.16 | Auto-installed by mise install |
| Docker | 20.10+ | Optional — required only for make image-build / make image-smoke-test |
make deps bootstraps mise (no root required, installs to ~/.local/bin), then runs mise install which reads .mise.toml and provisions the pinned Java + Maven. Run make deps-check afterward to verify the toolchain is on PATH.
$ java -jar target/ldap-server.jar --help
The ldap-server is a simple LDAP server implementation based on ApacheDS. It
creates one user partition with root 'dc=ldap,dc=example'.
Usage: java -jar ldap-server.jar [options] [LDIFs to import]
Options:
--admin-password, -ap change password for 'uid=admin,ou=system' (default 'secret')
--allow-anonymous, -a allow anonymous bind (default false)
--bind, -b bind address (default 0.0.0.0)
--port, -p LDAP port (default 10389)
--ssl-port, -sp enable LDAPS on this port (optional)
--ssl-keystore-file, -skf JKS keystore path with the LDAPS private key
--ssl-keystore-password, -skp keystore password
--ssl-enabled-protocol, -sep enable a TLS protocol (repeatable; default TLSv1, TLSv1.1, TLSv1.2)
--ssl-enabled-ciphersuite, -scs enable a cipher suite (repeatable)
--ssl-need-client-auth, -snc enable SSL needClientAuth (default false)
--ssl-want-client-auth, -swc enable SSL wantClientAuth (default false)
--help, -h show this help
LDIFs to import:
- empty -> bundled `ldap-example.ldif` is loaded
- one or more `.ldif` files -> imported in order
- a directory -> every `*.ldif` inside (case-insensitive) imported
Default seed data (src/main/resources/ldap-example.ldif)
dc=ldap,dc=example (root domain)
├── ou=Users,dc=ldap,dc=example
│ └── uid=jduke,ou=Users,dc=ldap,dc=example (Java Duke / theduke)
└── ou=Roles,dc=ldap,dc=example
└── cn=Admin,ou=Roles,dc=ldap,dc=example (member: jduke)
Generate (or import) a JKS keystore with the server private key, then pass --ssl-keystore-file + --ssl-keystore-password alongside --ssl-port:
keytool -validity 365 -genkey -alias myserver -keyalg RSA \
-keystore /tmp/ldaps.keystore -storepass 123456 -keypass 123456 \
-dname cn=myserver.example.com
java -Djavax.net.debug=ssl \
-jar target/ldap-server.jar \
-sp 10636 -skf /tmp/ldaps.keystore -skp 123456StartTLS is also wired (the server registers a
StartTlsHandler) and exercised byStartTlsTest, which negotiates TLSv1.3 +TLS_AES_128_GCM_SHA256against AM27's MINA TLS stack. If no--ssl-keystore-fileis supplied, the server generates a self-signed EC certificate on startup so StartTLS + LDAPS work out of the box for tests and dev.
Pre-built images are published to GHCR on every v* git tag:
docker pull ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latestThe image ships pre-seeded: ldap-example.ldif is baked into /ldap/ldif/ (dc=ldap,dc=example with uid=jduke / theduke + an Admin group), so a bare docker run starts with the example tree:
docker run -it --rm -p 10389:10389 \
ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latest
# bind: uid=jduke,ou=Users,dc=ldap,dc=example / thedukeTo seed your own entries, bind-mount a directory of .ldif files over /ldap/ldif/ — this replaces the baked-in seed; every *.ldif inside is imported (non-.ldif files ignored). For example, mount this repo's src/main/resources/ (same tree), or point at your own directory:
docker run -it --rm -p 10389:10389 \
-v "$PWD/src/main/resources:/ldap/ldif:ro" \
ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latest(Mounting an empty directory shadows the baked-in seed and starts the server with no entries.)
Connecting. The directory's built-in administrator exists regardless of how data is seeded:
| Value | |
|---|---|
| Admin bind DN | uid=admin,ou=system |
| Admin password | secret — override with --admin-password <new> |
| Default partition / search base | dc=ldap,dc=example |
With the example data above there is also a regular user uid=jduke,ou=Users,dc=ldap,dc=example (password theduke). Verify a running container:
ldapsearch -x -H ldap://127.0.0.1:10389 -D 'uid=admin,ou=system' -w secret \
-b dc=ldap,dc=example '(objectClass=*)'The admin account is the ApacheDS system administrator and is independent of the loaded LDIF — when you mount your own .ldif, those files define the regular entries (their own DNs and userPassword values) while admin stays uid=admin,ou=system / secret.
Or build locally:
make image-build # multi-stage build from src/, tags as $(IMAGE_REF)
make image-smoke-test # boot the image, wait for HEALTHCHECK = healthy
make image-run # interactive run with $(LDIF_DIR) bind-mountedThe runtime image is eclipse-temurin:25-jre-alpine-based (~26 MB /usr, Trivy-clean at switch time, no Go binaries), runs as a non-root user (UID 10001), and ships a TCP HEALTHCHECK that probes localhost:${APP_INTERNAL_PORT} via busybox nc -z — no curl / bash / wget install needed.
Run make help to see every target with its description.
| Target | Description |
|---|---|
make deps |
Install Java + Maven via mise (reads .mise.toml) |
make deps-check |
Show installed toolchain (java / mvn / docker / mise versions) |
make check-java-alignment |
Verify Java major matches across .mise.toml + Dockerfile |
make check-maven-alignment |
Verify Maven minor matches across .mise.toml + Dockerfile build stage |
make build |
Compile source (no tests) |
make test |
Run JUnit tests |
make package |
Build the shaded runnable JAR at target/ldap-server.jar |
make run-jar |
Run the packaged JAR with the bundled LDIF |
make lint |
Validate pom.xml + lint the Dockerfile (hadolint) + shell-script executable-bit guard + mermaid-lint |
make mermaid-lint |
Validate README Mermaid diagrams via minlag/mermaid-cli (skipped under act) |
make cve-check |
OWASP dependency-check (transitive deps; ~2 GB NVD download on first run) |
make clean |
Remove Maven build artifacts |
| Target | Description |
|---|---|
make image-build |
Multi-stage build from src/, tagged as $(IMAGE_REF) |
make image-run |
Run the image with $(LDIF_DIR) bind-mounted into /ldap/ldif/ |
make image-smoke-test |
Boot the image and wait for its HEALTHCHECK to report healthy |
make e2e |
End-to-end: boot image + verify LDAP bind + search (correct password) AND that a wrong password is rejected (negative case) |
make docker-login |
Log into $(DOCKER_REGISTRY) using DOCKER_LOGIN + $$DOCKER_PWD (stdin-only) |
make image-push |
Push $(IMAGE_REF) to $(DOCKER_REGISTRY) |
| Target | Description |
|---|---|
make ci |
Full local CI pipeline: deps → toolchain-alignment → lint → test → package |
make ci-run |
Run the GitHub Actions workflow locally via act — exercises changes + build + ci-pass only; the tag-only docker + cve-check + release paths need a real GitHub event context |
| Target | Description |
|---|---|
make renovate-validate |
Validate renovate.json against the live Renovate schema (npx --yes renovate@latest --platform=local) |
Every operator-tunable value is sourced from env vars with ?= fallbacks in the Makefile. Copy .env.example to .env and override per host — make picks up overrides automatically.
| Variable | Default | Used by |
|---|---|---|
DOCKER_REGISTRY |
ghcr.io |
docker-login, image-push |
DOCKER_LOGIN |
(unset) | tags ${DOCKER_LOGIN}/${IMAGE_NAME}:${IMAGE_TAG} — set to <owner>/ldap-server for the GHCR repo-namespace path |
DOCKER_PWD |
(unset; gitignored .env only) |
piped to docker login --password-stdin — NEVER on argv. For GHCR locally, use a GitHub PAT with write:packages scope |
IMAGE_NAME |
apacheds-ad |
image-name segment |
IMAGE_TAG |
latest |
image-tag segment |
JAR_PATH |
target/ldap-server.jar |
run-jar |
LDIF_DIR |
target/classes/ |
bind-mounted into /ldap/ldif/ for image-run |
LDAP_PORT |
10389 |
host-side port mapping |
LDAPS_PORT |
(unset) | when set, run-jar enables -sp $LDAPS_PORT |
BIND_ADDRESS |
0.0.0.0 |
run-jar's -b flag |
APP_INTERNAL_PORT |
10389 |
container-internal LDAP port (baked into image via --build-arg) |
GitHub Actions runs on every push to master, every v* git tag, every pull request, plus a weekly cron: '0 6 * * 1' for cve-check and workflow_dispatch for manual reruns. The workflow (build-test-push.yml) is structured as separate jobs with needs: dependencies for fail-fast + parallelism; a single ci-pass aggregator is the only check the branch-protection ruleset needs to gate.
| Job | Triggers | Purpose |
|---|---|---|
changes |
every event | dorny/paths-filter — doc-only PRs skip every job below; also emits a docs output (README) that drives mermaid-lint |
mermaid-lint |
README-only changes | Cheap docker + minlag/mermaid-cli validation of the README C4 hero diagram (no Maven/mise). README is **.md so a README-only edit skips build; this job validates the diagram on those edits. Idle (skipped) on code/tag events, where build's make ci validates it |
build |
code-changing events + every tag | Provisions Java 25 + Maven 3.9.16 via jdx/mise-action, restores ~/.m2 from actions/cache, runs make ci (alignment guards + lint + test + package), Trivy filesystem scan (informational), uploads target/ldap-server.jar as an artifact |
cve-check |
tag pushes + weekly cron + dispatch | OWASP dependency-check via mvn org.owasp:dependency-check-maven:check (NVD + Sonatype OSS Index analyzers); NVD DB cached at ~/.m2/repository/org/owasp/dependency-check-data, keyed on the ISO week so version bumps don't force a cold fetch. NVD_API_KEY strongly recommended (without it the NVD fetch fails on cold cache); OSS_INDEX_USER/OSS_INDEX_TOKEN enable OSS Index (else it's silently disabled) |
release |
push to master OR v* tag |
Downloads the JAR, recreates the latest GitHub Release via softprops/action-gh-release |
docker |
v* tag only |
Build image for scan → Trivy CRITICAL/HIGH image scan → make image-smoke-test → make e2e (LDAP bind + search) → log in to GHCR (${{ github.actor }} + auto-provisioned GITHUB_TOKEN; job has packages: write) → push single-arch linux/amd64 image to ghcr.io/<owner>/ldap-server/apacheds-ad with flavor: latest=true → cosign keyless-sign the pushed digest (OIDC, id-token: write) + attach an SPDX SBOM attestation. Every gate blocks the push |
ci-pass |
always | if: always() && contains(needs.*.result, 'failure') — single aggregator for branch protection |
Every action is SHA-pinned (verified via gh api …/git/refs/tags). A separate cleanup-runs.yml prunes old workflow runs and caches from deleted branches weekly via the native gh CLI.
Every tagged image (since v1.2.2) is signed with cosign keyless OIDC and carries an SPDX SBOM attestation. Verify a pull before trusting it:
cosign verify ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latest \
--certificate-identity-regexp 'https://github.com/AndriyKalashnykov/ldap-server/.github/workflows/build-test-push.yml@refs/tags/v.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
# inspect the SBOM attestation
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp 'https://github.com/AndriyKalashnykov/ldap-server/.github/workflows/build-test-push.yml@refs/tags/v.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latestThe --certificate-identity-regexp binds the signature to this repo's workflow; --certificate-oidc-issuer confirms it was minted by GitHub Actions OIDC (not a leaked key). Both are required.
For reproducible verification, resolve and verify the immutable digest rather than the mutable
:latesttag —latestis re-pointed on every tagged release:DIGEST=$(crane digest ghcr.io/andriykalashnykov/ldap-server/apacheds-ad:latest) cosign verify "ghcr.io/andriykalashnykov/ldap-server/apacheds-ad@${DIGEST}" \ --certificate-identity-regexp 'https://github.com/AndriyKalashnykov/ldap-server/.github/workflows/build-test-push.yml@refs/tags/v.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com
Configure under Settings → Secrets and variables → Actions.
| Name | Type | Used by | How to obtain |
|---|---|---|---|
NVD_API_KEY |
Secret (strongly recommended) | cve-check job — without it, the dep-check 12.2.2 plugin's parallel NVD fetcher hits an upstream NPE on cold cache and the job fails |
Free API key from NIST NVD; routed via ~/.m2/settings.xml, never via argv |
OSS_INDEX_USER + OSS_INDEX_TOKEN |
Secret (recommended) | cve-check job — enables the Sonatype OSS Index analyzer (second vuln source); without them it's silently disabled (warning only) and coverage drops to NVD-only |
Free account at OSS Index — user is the account email, token its API token; routed via ~/.m2/settings.xml (-DossIndexServerId=ossindex), never via argv |
GITHUB_TOKEN |
(auto-provisioned) | docker (GHCR publish, packages: write), release (GitHub Release, contents: write), cleanup-runs |
GitHub injects automatically |
Apache License 2.0. Java code derived from intoolswetrust/ldap-server (Josef Cacek, same license).