Skip to main content
Version: Next

Tutorial: OpenLDAP

This is the smallest useful Nauthilus setup in these tutorials. It gives you a running authentication stack with three services:

  • Valkey for cache and operational state
  • OpenLDAP as the user directory
  • Nauthilus as the HTTP-facing authentication service

Use this tutorial first if you want to understand how Nauthilus talks to a directory backend, how the backchannel API is called, and how the current configuration structure is organized.

What You Build​

The stack exposes:

  • http://127.0.0.1:18080 for Nauthilus
  • ldap://127.0.0.1:18389 for OpenLDAP

The demo user is:

  • Username: alice
  • Login request username: alice@workshop.local
  • Password: workshopSecret1!

Start the Stack​

Create the files shown below in one directory, then start the services:

docker compose up -d

When the services are healthy, run the direct authentication check:

docker compose exec nauthilus curl -i -sS \
-u workshop-backchannel:workshop-backchannel-secret \
http://127.0.0.1:8080/api/v1/auth/json \
-H 'Content-Type: application/json' \
-d '{"username":"alice@workshop.local","password":"workshopSecret1!","protocol":"http","service":"workshop"}'

Expected result:

  • HTTP 200
  • Auth-Status: OK
  • a JSON response with "ok": true

Try the failing path as well:

docker compose exec nauthilus curl -i -sS \
-u workshop-backchannel:workshop-backchannel-secret \
http://127.0.0.1:8080/api/v1/auth/json \
-H 'Content-Type: application/json' \
-d '{"username":"alice@workshop.local","password":"definitely-wrong","protocol":"http","service":"workshop"}'

Expected result:

  • HTTP 403
  • Auth-Status: Invalid login or password

Service Definition​

docker-compose.yml starts the three containers and wires Nauthilus to OpenLDAP and Valkey. The important part is that Nauthilus receives its configuration from /etc/nauthilus/nauthilus.yml, while OpenLDAP imports the bootstrap LDIF at startup.

docker-compose.yml
name: workshop-openldap

services:
valkey:
image: valkey/valkey:8-alpine
command:
- valkey-server
- --bind
- 0.0.0.0
- --save
- ""
- --appendonly
- "no"
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10

openldap:
image: chrroessner/openldap:latest
environment:
LDAP_DOMAIN: workshop.local
LDAP_BASE_DN: dc=workshop,dc=local
LDAP_ORGANISATION: Workshop Org
LDAP_ADMIN_PASSWORD: workshopAdmin
LDAP_ENABLE_TLS: "false"
LDAP_ENABLE_LDAPS: "false"
ports:
- "18389:389"
volumes:
- openldap_data:/var/lib/openldap/openldap-data
- openldap_accesslog:/var/lib/openldap/accesslog
- ./openldap/bootstrap:/docker-entrypoint-initdb.d:ro

nauthilus:
image: ghcr.io/croessner/nauthilus:v3.0.0
depends_on:
valkey:
condition: service_healthy
openldap:
condition: service_started
ports:
- "18080:8080"
volumes:
- ./nauthilus/nauthilus.yml:/etc/nauthilus/nauthilus.yml:ro
healthcheck:
test: ["CMD", "/usr/app/healthcheck", "--url", "http://127.0.0.1:8080/healthz"]
interval: 10s
timeout: 5s
retries: 20

volumes:
openldap_data:
openldap_accesslog:

Nauthilus Configuration​

This file is the main learning target. It shows the current root sections:

  • runtime for listener and instance identity
  • observability for logging
  • storage.redis for Valkey
  • auth.backchannel for API access used by the checks
  • auth.backends.ldap for LDAP lookup and password verification
nauthilus/nauthilus.yml
runtime:
instance_name: "workshop-openldap"

servers:
http:
address: "0.0.0.0:8080"

observability:
log:
level: "debug"
json: false
color: true

storage:
redis:
primary:
address: "valkey:6379"
database_number: 0
prefix: "nt:"
pool_size: 16
idle_pool_size: 4
positive_cache_ttl: "1h"
negative_cache_ttl: "5m"
password_nonce: "workshopPasswordNonce01"
encryption_secret: "workshopEncryption01"

auth:
backchannel:
basic_auth:
enabled: true
username: "workshop-backchannel"
password: "workshop-backchannel-secret"

backends:
order:
- "cache"
- "ldap"

ldap:
default:
server_uri:
- "ldap://openldap:389"
bind_dn: "cn=admin,dc=workshop,dc=local"
bind_pw: "workshopAdmin"
lookup_pool_size: 5
lookup_idle_pool_size: 2
auth_pool_size: 5
auth_idle_pool_size: 2

search:
- protocol:
- "http"
cache_name: "ldap"
base_dn: "ou=people,dc=workshop,dc=local"
filter:
user: "(uid=%{username})"
list_accounts: "(uid=*)"
mapping:
account_field: "uid"
display_name_field: "cn"
unique_user_id_field: "uid"
attribute:
- "uid"
- "cn"
- "mail"

Notice the backend order:

auth:
backends:
order:
- "cache"
- "ldap"

Nauthilus checks its cache first. If no reusable result exists, it queries LDAP.

LDAP Bootstrap Data​

The LDIF creates one user and one group. The user has the attributes Nauthilus later returns from LDAP.

openldap/bootstrap/20-workshop-user.ldif
dn: uid=alice,ou=people,dc=workshop,dc=local
changetype: add
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: posixAccount
cn: Alice Workshop
sn: Workshop
givenName: Alice
uid: alice
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/alice
mail: alice@workshop.local
userPassword: workshopSecret1!

dn: cn=mail-users,ou=groups,dc=workshop,dc=local
changetype: add
objectClass: top
objectClass: groupOfNames
cn: mail-users
member: uid=alice,ou=people,dc=workshop,dc=local

The important match is between the request username and the LDAP search filter:

filter:
user: "(uid=%{username})"

The demo request sends alice@workshop.local. Nauthilus normalizes the username for the lookup context, so the LDAP lookup resolves uid=alice.

Good Experiments​

  1. Change observability.log.level from debug to info, restart Nauthilus, and compare the logs.
  2. Add another LDAP attribute to auth.backends.ldap.search[0].attribute.
  3. Change the LDAP user filter and check how authentication fails.
  4. Change positive_cache_ttl to a short value and repeat the same login several times.

What You Should Understand​

After this tutorial, you should be able to explain:

  • why Valkey is present even in a small setup
  • how Nauthilus finds the LDAP server
  • how backchannel basic auth protects the direct API check
  • where LDAP attributes are mapped into the authentication response

Continue with Tutorial: MariaDB + Lua when you want to see the same idea without LDAP.