Skip to main content

IdP internal LDAP

info

This runbook documents how the QNAP NAS LDAP server is used as an identity provider for the internal ZITADEL instance (auth.reids.net.au), including schema assumptions, ZITADEL console settings, TLS trust path, and troubleshooting steps.

Identity provider series

  1. IdP dual overview
  2. IdP dual architecture
  3. IdP internal deployment
  4. IdP internal console
  5. IdP internal SMTP
  6. IdP internal LDAP - you are here
  7. IdP internal OIDC
  8. IdP internal OAUTH2 proxy
  9. IdP internal backup and restore

High-level design

  • QNAP NAS runs the LDAP server and presents a wildcard TLS certificate for *.reids.net.au on LDAPS.
  • Kubernetes runs ZITADEL #2 in the identity-internal namespace.
  • Cluster-wide TLS trust for the wildcard certificate is provided by trust-manager and Gatekeeper:
    • cert-manager/wildcard-reids-tls (secret managed by Flux + SOPS) → trust-manager Bundlewildcard-reids-bundle ConfigMap in trust=enabled namespaces → Gatekeeper volume and volumeMount → /etc/ssl/certs/wildcard-reids.crt inside Pods.
  • ZITADEL connects to LDAP over LDAPS and uses it as an external identity provider, trusting the NAS certificate via SSL_CERT_FILE=/etc/ssl/certs/wildcard-reids.crt.
  • ZITADEL automatically creates or links users on first login, based on LDAP attributes.
User browser → ZITADEL login → choose "nas" IdP
→ ZITADEL ↔ LDAPS (QNAP)
→ LDAP bind + search + password check
→ ZITADEL account link / auto-provision

QNAP LDAP server

Directory layout

The QNAP LDAP server is configured with:

  • Base DN: dc=reids,dc=net,dc=au
  • People OU: ou=people,dc=reids,dc=net,dc=au
  • Example user:
dn: uid=USERNAME,ou=people,dc=reids,dc=net,dc=au
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: USERNAME
sn: USERNAME
uid: USERNAME
mail: USERNAME@DOMAIN

Bind user

A dedicated bind account is used by ZITADEL:

  • DN: uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au
  • Password: stored and rotated on the NAS; used only for LDAP binds and searches.
  • Permissions: must be able to search ou=people,… and read the attributes listed later.

QNAP logs for LDAP are written to nc.log and rotated copies in /share/CACHEDEV1_DATA/.logs (path may vary). Relevant entries look like:

[LDAP Server] Created user "zitadel-bind".
[LDAP Server] Changed the password of LDAP user "zitadel-bind".
[LDAP Server] Created user "USERNAME".

These confirm user creation and password changes, but do not log failed binds in detail.

Testing LDAP from the workstation

Use ldapsearch to validate connectivity, bind DN and filters before touching ZITADEL.

Plain LDAP

ldapsearch \
-H ldap://nas.reids.net.au:389 \
-D "uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au" \
-W \
-b "dc=reids,dc=net,dc=au" \
-s base \
"(objectClass=*)"

Common result codes:

  • 49 Invalid credentials → DN or password wrong.
  • 0 Success → bind is working.

LDAPS

ldapsearch \
-H ldaps://nas.reids.net.au:636 \
-D "uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au" \
-W \
-b "dc=reids,dc=net,dc=au" \
-s base \
"(objectClass=*)"

To test the exact search ZITADEL will perform for user USERNAME:

ldapsearch \
-H ldaps://nas.reids.net.au:636 \
-D "uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au" \
-W \
-b "ou=people,dc=reids,dc=net,dc=au" \
"(&(objectClass=inetOrgPerson)(uid=USERNAME))"

You should see one entry with all attributes.

ZITADEL Helm configuration (TLS trust)

ZITADEL needs to trust the same wildcard certificate that the NAS presents for LDAPS.

The canonical wildcard certificate/key pair is stored and managed by Flux + SOPS as cert-manager/wildcard-reids-tls. From there, the cluster-wide wildcard TLS trust with trust-manager and Gatekeeper path takes over:

  • trust-manager Bundle wildcard-reids-bundle reads cert-manager/wildcard-reids-tls and writes a ConfigMap wildcard-reids-bundle into every namespace with trust=enabled.
  • Gatekeeper Assign mutations ensure a volume and volumeMount are injected into Pods in trusted namespaces, exposing /etc/ssl/certs/wildcard-reids.crt inside each container.

ZITADEL does not need to mount the wildcard-reids-tls secret directly any more. It only needs to opt in to the shared trust bundle by pointing Go's TLS stack at the injected file.

Key snippet in k8s/prod/50-helm-release-zitadel.yaml:

spec:
values:
# Environment variables for database passwords
env:
- name: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-secret
key: POSTGRES_PASSWORD
- name: ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
valueFrom:
secretKeyRef:
name: zitadel-db-secret
key: POSTGRES_PASSWORD
- name: SSL_CERT_FILE
value: /etc/ssl/certs/wildcard-reids.crt

With this in place:

  • trust-manager and Gatekeeper provide the file /etc/ssl/certs/wildcard-reids.crt.
  • SSL_CERT_FILE tells the ZITADEL process to use that file as its CA bundle, so LDAPS connections to the NAS succeed without relaxing verification.

Refer to the dedicated runbook "Cluster-wide wildcard TLS trust with trust-manager and Gatekeeper" for the full trust path and verification steps.

ZITADEL LDAP identity provider configuration

All configuration below is done in the Default Settings → Identity Providers section for the reids-internal instance.

Connection

  • Name: nas
  • Servers: ldaps://nas.reids.net.au:636
  • BaseDn: dc=reids,dc=net,dc=au
  • BindDn: uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au
  • Bind password: set via the console (matches QNAP LDAP user)

Ensure StartTLS is disabled when using ldaps://…. Using both at once causes errors such as:

LDAP Result Code 200 "Network Error": tls: failed to verify certificate

User binding

  • Userbase: ou=people,dc=reids,dc=net,dc=au

User filters

This is where most early problems appeared.

The rule from ZITADEL is:

"User filters: attributes of the user which are 'or'-joined in the query.
Used value is the input of the login name."

We want "whatever the user types as Login Name" to map directly to the LDAP uid attribute.

Final configuration:

  • User filters:
    • uid

No additional filters are used. Earlier experiments like uid=LoginName or multiple filters produced errors such as:

  • LDAP Result Code 201 "Filter Compile Error": ldap: finished compiling filter with extra at end: ob))
  • user does not exist or too many entries returned

These were caused by ZITADEL building invalid search filters from the extra text.

User Object Classes

  • User Object Classes:
    • inetOrgPerson

This matches the QNAP user entries, which include inetOrgPerson among other object classes.

LDAP attributes mapping

These control how LDAP attributes are projected into ZITADEL user fields.

Final working configuration:

  • ID attribute: uid
  • Displayname attribute: displayName
  • Email attribute: mail
  • Family name attribute: sn
  • Preferred username attribute: uid
  • Other attribute fields left empty.

The critical one is ID attribute = uid. This is used as the stable external identity key.

Provider activation and login policy

  1. Create the provider as above.
  2. In the provider list, enable the availability tick so it is active.
  3. Under Login Behavior and Security, ensure:
    • External Login allowed is enabled.
    • At least one of:
      • Account creation allowed
      • Account linking allowed is enabled.

For the internal instance it is typical to enable account creation for LDAP users so they are auto-provisioned on first login.

Login flow and first-time user behaviour

When everything is configured:

  1. User opens the internal login page (for example via a client app's OIDC flow).
  2. The login screen shows an extra button labelled nas (or whatever Name you chose).
  3. Clicking it redirects to the LDAP login screen.
  4. User enters:
    • Login Name: USERNAME
    • Password: LDAP password for uid=USERNAME,…
  5. If bind and search succeed, ZITADEL looks for an existing linked account.
  • If none exists and account creation is enabled, you see the "External User Not Found" form pre-filled with:
    • Given name
    • Family name
    • Username (derived from uid)
    • Email (from mail)
  • Submitting the form creates a ZITADEL account linked to the LDAP identity.

Troubleshooting notes

Invalid credentials (Result Code 49)

If ZITADEL shows:

LDAP Result Code 49 "Invalid Credentials":

check the following:

  • Re-run ldapsearch with the same BindDn and password from your workstation.
  • Confirm the bind DN uses uid=… and not cn=….
  • Make sure the NAS logs (nc.log) show the password change you expect.

User does not exist or too many entries returned

This error typically appeared when:

  • User filters were misconfigured (uid=LoginName, or multiple filters that match extra entries).
  • BaseDn or Userbase did not match the actual tree.

Fix by:

  • Ensuring Userbase is ou=people,dc=reids,dc=net,dc=au.
  • Using a single user filter of uid.
  • Testing the equivalent filter with ldapsearch:
ldapsearch \
-H ldaps://nas.reids.net.au:636 \
-D "uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au" \
-W \
-b "ou=people,dc=reids,dc=net,dc=au" \
"(&(objectClass=inetOrgPerson)(uid=**USERNAME**))"

TLS and certificate issues

If you see errors like:

LDAP Result Code 200 "Network Error": tls: failed to verify certificate: x509: certificate signed by unknown authority

check:

  • NAS is presenting the wildcard certificate you expect.
  • ZITADEL container sets SSL_CERT_FILE=/etc/ssl/certs/wildcard-reids.crt.
  • The wildcard-reids-bundle ConfigMap exists in the identity-internal namespace and contains a full chain (leaf + intermediate + root as needed).
  • Gatekeeper Assign mutations are present and injecting the wildcard-reids-bundle volume and /etc/ssl/certs/wildcard-reids.crt mount into Pods in identity-internal.

If the file /etc/ssl/certs/wildcard-reids.crt is missing inside a ZITADEL Pod, troubleshoot using the "Cluster-wide wildcard TLS trust with trust-manager and Gatekeeper" runbook.

Verification checklist

Use this checklist if you need to re-build or audit the setup.

  • QNAP LDAP:
    • Base DN is dc=reids,dc=net,dc=au.
    • ou=people exists and contains users with objectClass: inetOrgPerson.
    • uid=zitadel-bind,ou=people,… exists and password is known.
  • TLS:
    • LDAPS works from a workstation using ldapsearch on port 636.
    • wildcard-reids-bundle ConfigMap exists in identity-internal.
    • A ZITADEL Pod in identity-internal has /etc/ssl/certs/wildcard-reids.crt present.
  • ZITADEL Helm:
    • Internal instance deployed in identity-internal namespace.
    • SSL_CERT_FILE=/etc/ssl/certs/wildcard-reids.crt is set in the ZITADEL Deployment (env).
  • ZITADEL LDAP provider:
    • Servers: ldaps://nas.reids.net.au:636.
    • BaseDn: dc=reids,dc=net,dc=au.
    • BindDn: uid=zitadel-bind,ou=people,dc=reids,dc=net,dc=au.
    • Userbase: ou=people,dc=reids,dc=net,dc=au.
    • User filters: uid.
    • User Object Classes: inetOrgPerson.
    • ID attribute: uid.
  • Login policy:
    • External Login allowed.
    • Either account creation or account linking allowed.
  • End-to-end:
    • Logging in as USERNAME via the nas IdP succeeds and either:
      • Links to an existing ZITADEL account, or
      • Shows the "External User Not Found" form with pre-filled details and completes successfully.