Authentik — IdP, LDAP outpost, and the gidNumber convention

Authentik basics, how the LDAP outpost feeds Slurm/PAM and shared filesystem ownership, the gidNumber=pk+4000 convention, and onboarding tenant users with SSH keys.

Try the commands on this page in the command emulator — type help for the full list, or solutions for copy-paste fix recipes.

Authentik is an open-source identity provider that runs as a few pods in Kubernetes. We use it as the single source of truth for tenant users: name, email, group memberships, SSH keys, and POSIX attributes (uidNumber, gidNumber). An LDAP outpost projects all that into an LDAP-compatible service that Slurm/PAM/Linux tools can talk to.

This page is the operator's view: what Authentik actually deploys, how the LDAP outpost works, the gidNumber convention that makes shared-filesystem ownership consistent, and the tenant-user onboarding flow.

Component overview

ComponentRole
authentik-serverWeb UI + REST API + OAuth/OIDC/SAML provider endpoints
authentik-workerBackground tasks (sync, certificate rotation, email)
authentik-postgresDB (Postgres). Stateful.
authentik-redisSession cache.
authentik-ldap-outpostLDAP server pod. Talks to authentik-server's API; exposes :3389 LDAP
authentik-proxy-outpost(optional) Forward-auth proxy for putting SSO in front of arbitrary apps
authentik-radius-outpost(optional) RADIUS server

The LDAP outpost is the one we care about most for HPC integration. Slurm's PAM/nslcd setup, Linux's getent, and many older HPC tools speak LDAP natively.

How the LDAP outpost works

The outpost is a Go binary (ldap-outpost) that:

  1. On startup, registers with authentik-server via a long-lived token.
  2. Pulls the configured LDAP "Provider" definition: base DN, bind DN, search filter, attribute mapping.
  3. Listens on :3389 (or :636 for LDAPS).
  4. For every LDAP query (e.g., getent passwd alice(&(objectClass=posixAccount)(uid=alice))), translates to authentik-server's user/group API and returns synthetic LDAP entries.

The outpost has no local state. Every query hits authentik-server. Cache once via the consumer (nslcd does this) — there's no caching at the outpost.

A typical query path:

[ login pod ] sshd → PAM → nslcd → LDAPS:636 ──> [ ldap-outpost pod ]
                                                  │
                                                  v
                                                  HTTP API ──> [ authentik-server ] ──> postgres

The pod-to-pod path stays inside the cluster. External LDAP exposure (for non-K8s hosts that need to bind) goes via a Service of type LoadBalancer or an external proxy.

Sample provider config

Bind DN: cn=ldap-bind,ou=users,dc=auth,dc=example,dc=internal
Base DN: dc=auth,dc=example,dc=internal
UID attribute: uid
Group object class: posixGroup
Member attribute: memberUid

Search filter for users (queried via UI):

(&(objectClass=user)(memberOf=cn=tenant-foo,ou=groups,dc=auth,dc=example,dc=internal))

Slurm/PAM clients then bind as cn=ldap-bind,... and search with this filter.

The gidNumber convention

POSIX UID/GID numbers must be consistent across every host that touches the shared filesystem. If alice has UID 5042 on the login pod but 5043 on the slurmd pod, files she creates show wrong ownership when the other side reads them.

Authentik computes UIDs and GIDs from the user/group object's primary key (pk in the database) plus an offset. The convention we use:

uidNumber = user.pk + 2000
gidNumber = group.pk + 4000

(Some sites use pk + 1000 for users and pk + 3000 for groups; the exact offset is a choice as long as it stays out of system UID range and is consistent.)

This makes the mapping deterministic: a user with pk=42 always has UID 2042. Recreate the user, get a new pk, get a new UID — files they own become orphaned. Don't recreate users.

The mapping is implemented as a property mapping in Authentik:

# Property mapping: ldap_uidNumber
return user.pk + 2000

# Property mapping: ldap_gidNumber  
return user.group_set.first().pk + 4000   # primary group

The LDAP outpost uses these to build posixAccount and posixGroup entries.

Onboarding a tenant user

The end-to-end recipe:

  1. Create the user in Authentik (UI or API):

    • username, email, full name
    • assign to tenant's group (tenant-foo)
    • upload SSH public key (custom user attribute, ssh_keys)
  2. Verify POSIX attributes are computed — UI shows uidNumber: 2042, gidNumber: 4099.

  3. Sync to login pod's authorized_keys. There's typically a small operator (or a CronJob) that:

    • Lists users in tenant-foo group
    • For each, reads ssh_keys attribute
    • Writes to a Secret in the tenant namespace, one entry per user
  4. Verify on the login pod:

    kubectl -n tenant-foo exec login-0 -- getent passwd alice
    # alice:x:2042:4099:Alice Researcher:/home/alice:/bin/bash
    
    kubectl -n tenant-foo exec login-0 -- ls /etc/ssh/authorized_keys.d/
    # alice    bob
    
    # User can SSH in from outside
    ssh alice@login.tenant-foo.example.internal
    
  5. Add to Slurm accounting (in slurmdbd):

    kubectl -n tenant-foo exec login-0 -- sacctmgr add user alice Account=tenant-foo
    

If any step fails, the next step won't work — sacctmgr requires the user to resolve via getent first; getent requires the LDAP outpost to be reachable; and so on.

Group memberships and propagation

Authentik supports nested groups. A common pattern:

tenant-foo (parent)
├── tenant-foo-admins
│   └── alice  (admin user)
└── tenant-foo-users
    ├── bob
    └── carol

The LDAP outpost flattens nesting by default in memberOf queries. Slurm's account-vs-user mapping uses Slurm's own coarseness (one account per tenant); finer-grained admin distinctions are handled at the SSH/sudo layer inside the login pod.

When you add a user to a group in Authentik, the change is immediately visible to the LDAP outpost (it doesn't cache). nslcd on the login pod, however, does cache (default 600s). If a user shows up but Slurm says "Invalid account", flush the nslcd cache:

kubectl -n tenant-foo exec login-0 -- nscd -i passwd
kubectl -n tenant-foo exec login-0 -- nscd -i group
# or just restart nslcd
kubectl -n tenant-foo exec login-0 -- pkill -HUP nslcd

Authentik for K8s admin SSO

Beyond LDAP for tenants, Authentik provides OIDC for human admins logging into:

  • ArgoCD UI
  • Grafana
  • Internal dashboards
  • (sometimes) kubectl via kubelogin

OIDC providers are configured per-application in the UI. Each gets a client-id/secret and a redirect URI. The application validates tokens against Authentik's well-known endpoint:

https://auth.example.internal/application/o/argocd/.well-known/openid-configuration

Group membership comes through as a claim (groups) in the ID token. Apps can use it for RBAC: "users in platform-admins get cluster-admin, others read-only".

Common operational issues

LDAP outpost pod crashloops:

  • Token mismatch — outpost's auth token doesn't match what authentik-server expects. Regenerate via UI ("Outposts" → outpost → token).
  • Network policy blocking outpost → server. Check kubectl describe pod.

getent passwd alice returns nothing on login pod:

  • nslcd not running or LDAP unreachable from the pod
  • LDAP base DN wrong in /etc/nsswitch.conf and /etc/openldap/ldap.conf
  • User exists but isn't in the tenant's group (and the search filter requires group membership)

File ownership inconsistent across hosts:

  • gidNumber convention mismatch — a host is computing GIDs from a different field or offset. All consumers must read from the same LDAP outpost with the same property mapping.

Authentik server returns 500 on user save:

  • Postgres connection broken (check kubectl -n auth logs authentik-postgres-0)
  • Disk full on Postgres PV
  • Schema migration in progress (after a major version bump)

SSO login redirects in a loop:

  • Cookie domain wrong; clear cookies and check that the SP and IdP share a common cookie scope.
  • ID token signature mismatch — Authentik's signing key changed and the consumer cached the old one.

Operations on Authentik itself

# Create a recovery key (when admin password is lost)
kubectl -n auth exec deploy/authentik-server -- ak create_recovery_key 24h
# Outputs a one-time URL valid for 24 hours

# Re-render flows from blueprints (after a Git apply)
kubectl -n auth rollout restart deploy/authentik-worker

# Tail outpost logs
kubectl -n auth logs -f deploy/authentik-ldap-outpost-default

Backup and recovery

The Postgres database is the only piece of state you need to back up. Standard pg_dump on a schedule:

kubectl -n auth exec authentik-postgres-0 -- \
  pg_dump -U authentik authentik | gzip > authentik-$(date +%F).sql.gz

Restore: scale authentik-server down, pg_restore, scale back up.

See also

External:

  • goauthentik.io
  • LDAP Outpost docs: goauthentik.io/docs/outposts/integrations/ldap
  • POSIX attribute property mappings: goauthentik.io/docs/property-mappings/expression