The Authentik LDAP outpost is the second authentication path Authentik
exposes to applications that cannot speak OIDC (or can but are easier to
wire as classic LDAP). It is deployed alongside the Authentik web stack
on VM-SL-00 and serves the directory at dc=ldap,dc=blackreset,dc=com
on host ports 389 (cleartext) and 636 (TLS).
This page is the runbook: provider config, application binding, outpost
container, ports, gotchas, and recovery.
| Field | Value |
|---|---|
| Host | VM-SL-00 |
| Outpost name | blackreset-ldap-outpost |
| Outpost pk | 7aaded43-faf7-44be-ad85-562a3b425f92 |
| Outpost type | ldap |
| Container name | authentik-ldap-outpost |
| Compose root | /opt/authentik/ (docker-compose.override.yml) |
| Token file | .secrets/.ldap-outpost-token |
| Provider name | svc-ldap-outpost |
| Provider pk | 16 |
| Application name | svc-ldap |
| Bind base DN | dc=ldap,dc=blackreset,dc=com |
| Bind DN | cn=svc-ldap-bind,ou=users,dc=ldap,dc=blackreset,dc=com |
| Bind password file | .secrets/.ldap-bind-password (BindLdapAuthentik2026) |
| Bind service-account user | svc-ldap-bind (Authentik user pk=42, type=internal) |
| Listening (host) | 0.0.0.0:389 (clear), 0.0.0.0:636 (TLS) |
| Listening (container) | :3389 (clear), :6636 (TLS) — unprivileged |
+---------------------+ +-------------------+
| NC user_ldap (s02) | 389/636 -> 3389/6636 | authentik-ldap- |
| on vm-sl-03 | ---------------------> | outpost |
+---------------------+ | container on |
| vm-sl-00 |
+---------------------+ +-------------------+
| any other LDAP- | |
| consuming app | | HTTPS API
+---------------------+ v
+-------------------+
| authentik-server |
| (provider svc- |
| ldap-outpost) |
+-------------------+
Each LDAP query becomes an authenticated API call from the outpost
container to the Authentik server. The outpost is stateless; all
identity data lives in the Authentik DB.
svc-ldap-outpost (pk=16) is created via the Authentik admin API or UI
with the following fields:
| Field | Value |
|---|---|
name |
svc-ldap-outpost |
base_dn |
dc=ldap,dc=blackreset,dc=com |
bind_mode |
direct |
search_mode |
direct |
gid_start_number |
4000 |
uid_start_number |
2000 |
tls_server_name |
(empty) |
mfa_support |
false — see gotcha 2 below |
authorization_flow |
default-authentication-flow (e533b7d5-9e66-4b7c-897f-219ec08de118) — see gotcha 1 below |
certificate |
self-signed Authentik cert (re-used from the OIDC providers) |
The bound application is svc-ldap, with the provider attached via the
provider: field (single primary provider), not via
backchannel_providers: — the LDAP outpost ignores backchannel
providers and the app's bind would silently 403 otherwise.
The outpost runs alongside Authentik on VM-SL-00. The compose override
adds it to /opt/authentik/docker-compose.override.yml:
services:
authentik-ldap-outpost:
image: ghcr.io/goauthentik/ldap:2025.8.4
restart: unless-stopped
networks:
- default
ports:
- "389:3389" # clear (host:container)
- "636:6636" # TLS (host:container)
environment:
AUTHENTIK_HOST: https://authentik-server:9443
AUTHENTIK_INSECURE: "true" # internal compose net, self-signed
AUTHENTIK_TOKEN_FILE: /run/secrets/ldap_outpost_token
secrets:
- ldap_outpost_token
secrets:
ldap_outpost_token:
file: ../.secrets/.ldap-outpost-token
Container internal ports are 3389/6636 — non-privileged inside the
container — and Docker forwards the standard 389/636 from the host.
This avoids running the container as root.
The token in .secrets/.ldap-outpost-token is the per-outpost API token
generated by Authentik when the outpost was created. Rotate by deleting
the outpost token in Authentik admin and regenerating.
The bind user is not a regular human Authentik user; it is created
as type=internal so policies that target normal users do not apply:
| Field | Value |
|---|---|
| Username | svc-ldap-bind |
| Authentik user pk | 42 |
| Type | internal |
| Password | stored in .secrets/.ldap-bind-password |
| DN | cn=svc-ldap-bind,ou=users,dc=ldap,dc=blackreset,dc=com |
Applications use this DN for the LDAP bind that precedes any user search.
The user passwords in subsequent user-binds are checked against the
real Authentik passwords — that's the actual delegation.
authorization_flow MUST be the default authentication flowThe Authentik docs list authorization_flow as a generic field on every
provider type and present default-provider-authorization-implicit-consent
(an OAuth-style implicit-consent flow) as the obvious default. Setting
that flow on the LDAP provider breaks every bind: the outpost calls
the flow synchronously on each LDAP request and the implicit-consent
flow returns no usable authorization for an LDAP bind.
The correct value is the default authentication flow:
authorization_flow = default-authentication-flow
= e533b7d5-9e66-4b7c-897f-219ec08de118
Symptom of getting it wrong: every LDAP bind returns
LDAP Result Code 49 - Invalid Credentials, the outpost log shows
flow returned no auth, and the Authentik server log shows the
authorization flow being executed but never granting a token.
mfa_support must be false for the bind pathBy default LDAP providers carry mfa_support = true. In combination
with authorization_flow = default-authentication-flow, every bind ends
up in the MFA stage of the authentication flow. The outpost cannot
satisfy a TOTP / WebAuthn challenge synchronously, the flow loops
forever, and the bind times out with
LDAP Result Code 1 - Operations error.
Set mfa_support = false on the LDAP provider. MFA stays enforced on
the OIDC web sign-in path; only the LDAP bind path skips it.
uid attribute is hex, not the dashed UUIDThe outpost emits User.uid as a 64-character hex string, not the
dashed UUID format that the OIDC sub claim uses. NC's
ldapExpertUUIDUserAttr=uid then stores that hex value into
oc_ldap_user_mapping.directory_uuid. To make NC's LDAP-mapped users
land on the same oc_users.uid as the OIDC-mapped users, the
oc_ldap_user_mapping must be pre-populated with both forms (dashed in
owncloud_name, hex in directory_uuid) — see the
Nextcloud migration page.
members_by_username=lowercased returns HTTP 400Some LDAP clients (NC user_ldap with group sync enabled) issue a
member-resolution query of the form
members_by_username=<lowercased-cn> against the outpost. Authentik
2025.8.4 responds with HTTP 400. Workaround: leave ldapBaseGroups
empty in NC user_ldap (group sync off). Groups remain owned by
Authentik; clients use OIDC group claims for role mapping where they
need them.
provider:, not backchannel_providers:The LDAP outpost only reads the provider field of its application.
Binding via backchannel_providers produces a working application UI
but the outpost answers every bind with HTTP 403. There is no error in
the Authentik UI hinting at this — the test path is to actually bind
with ldapsearch from the consuming host.
From VM-SL-03:
LDAPTLS_REQCERT=never ldapsearch -x \
-H ldap://10.200.0.200:389 \
-D "cn=svc-ldap-bind,ou=users,dc=ldap,dc=blackreset,dc=com" \
-w "$(cat .secrets/.ldap-bind-password)" \
-b "ou=users,dc=ldap,dc=blackreset,dc=com" \
"(cn=A.Korff)" cn mail uid
A successful response shows one user entry with cn, mail, and a
64-char hex uid. If the bind fails with code 49, see gotcha 1.
If it hangs, see gotcha 2.
| Scope | Action |
|---|---|
| Stop the LDAP path entirely | docker compose -f /opt/authentik/docker-compose.yml -f /opt/authentik/docker-compose.override.yml stop authentik-ldap-outpost — clients fall back to whatever else they have (NC: re-enable user_ldap config s01 against AD, or stay on user_oidc only). |
| Re-issue the token | Delete the token in Authentik admin -> Outposts -> blackreset-ldap-outpost -> Tokens, generate a new one, write to .secrets/.ldap-outpost-token, recreate the container. |
| Drop the outpost entirely | Stop the container, remove the authentik-ldap-outpost service block from docker-compose.override.yml, delete the outpost in Authentik admin. The provider/application can stay or be deleted depending on whether the path will be revived later. |
If a service account was excluded from ldap-xio-bio source via the user_object_filter and you need it back (e.g. Printer on 2026-05-01):
# tools/authentik-restore-ldap-user.py
import urllib.request, json, time
TOKEN = open('.secrets/credentials.env').read().split('AUTHENTIK__API_TOKEN=')[1].split('\n')[0].strip()
URL = 'https://auth.blackreset.com'
SLUG = 'ldap-xio-bio'
SAM = 'Printer' # the sAMAccountName to re-include
# 1) Remove the (!(sAMAccountName=<SAM>)) clause from user_object_filter
req = urllib.request.Request(f'{URL}/api/v3/sources/ldap/{SLUG}/')
req.add_header('Authorization', f'Bearer {TOKEN}')
with urllib.request.urlopen(req) as r:
src = json.loads(r.read())
old = src['user_object_filter']
new = old.replace(f'(!(sAMAccountName={SAM}))', '')
print(f'old: {old}')
print(f'new: {new}')
body = json.dumps({'user_object_filter': new}).encode()
req = urllib.request.Request(f'{URL}/api/v3/sources/ldap/{SLUG}/', data=body, method='PATCH')
req.add_header('Authorization', f'Bearer {TOKEN}')
req.add_header('Content-Type', 'application/json')
urllib.request.urlopen(req)
# 2) Trigger sync by toggling enabled
for v in (False, True):
body = json.dumps({'enabled': v}).encode()
req = urllib.request.Request(f'{URL}/api/v3/sources/ldap/{SLUG}/', data=body, method='PATCH')
req.add_header('Authorization', f'Bearer {TOKEN}')
req.add_header('Content-Type', 'application/json')
urllib.request.urlopen(req)
time.sleep(10)
# 3) Verify
req = urllib.request.Request(f'{URL}/api/v3/core/users/?username={SAM}')
req.add_header('Authorization', f'Bearer {TOKEN}')
with urllib.request.urlopen(req) as r:
d = json.loads(r.read())
print(f'{SAM} count: {d["pagination"]["count"]}')
for u in d.get('results', []):
print(f' pk={u["pk"]} uuid={u["uuid"]} uid={u["uid"]}')
No native sync endpoint: Authentik's POST /api/v3/sources/ldap/<slug>/sync/ returns 405. Toggling enabled: false → true is the documented restart trigger.
NC-side update for an existing LDAP-uid: if the previously-excluded user has data under an old uid (e.g. Printer's home::D698C90D-... with 7 GB preserved), do not re-create or rename the NC uid — just update the existing oc_ldap_user_mapping row to point at the new LDAP DN + Authentik uid hex:
UPDATE oc_ldap_user_mapping
SET ldap_dn = 'cn=Printer,ou=users,dc=ldap,dc=blackreset,dc=com',
directory_uuid = '<Authentik User.uid hex from /api/v3/core/users/?username=Printer>',
ldap_dn_hash = SHA2('cn=Printer,ou=users,dc=ldap,dc=blackreset,dc=com', 256)
WHERE owncloud_name = 'D698C90D-132F-4D57-9BDA-7ADBE7A77615';
The owncloud_name stays the legacy AD objectGUID — it's the join key into the NC data layer (home::<uid>, file shares, calendars, etc.) and changing it would orphan data. This is the deliberate exception to the "Authentik UUID is canonical" rule for service accounts that were never migrated.
LDAP-Outpost migriert von vm-sl-00:389 → vm-rz-svc-prod-01:389/636 am 2026-05-02. Outpost-UUID, Provider-PK und Bind-DN unverändert; nur der Host wechselte. Konsumenten (NC, GitLab, Mailcow, etc.) wurden im selben Wave auf den neuen Endpoint umgehängt.