Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

LDAPDirectory

Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated

An LDAPDirectory connects an organization to its LDAP directory or Active Directory. A client that opts in lets the directory's users sign in on the hosted sign-in page with their directory username and password. The operator reads the service account's password from a Secret and the CA bundle from a ConfigMap or Secret, and sends both with the spec to the server's gRPC AdminService (UpsertLDAPDirectory). The server stores the password sealed and never returns it.

PropertyValue
API group / versionnauthera.io/v1alpha1
KindLDAPDirectory
Plural / short nameldapdirectories / ldap
ScopeNamespaced
Definitiondeploy/crds/ldapdirectory-crd.yaml in nauthera-server

The server only reads the directory. Passwords are set, reset and expired there, never here, and no directory is offered to a client by default.

Example

Put the service account's password and the directory's CA certificate beside the resource. Use --from-literal or printf '%s', not echo, so the password has no trailing newline:

kubectl -n nauthera create secret generic corp-ldap \
  --from-literal=password=<the service account's password>
kubectl -n nauthera create configmap corp-ca --from-file=ca.crt=corp-ca.pem
apiVersion: nauthera.io/v1alpha1
kind: LDAPDirectory
metadata:
  name: corp
  namespace: nauthera
spec:
  url: ldaps://dc1.corp.example:636       # or ldap://, always upgraded with StartTLS
  caBundleRef:                            # optional; the system roots when unset
    kind: ConfigMap                       # the default; or Secret
    name: corp-ca
    key: ca.crt                           # the default
  bind:                                   # optional; searched anonymously when unset
    dn: CN=svc-nauthera,OU=Service Accounts,DC=corp,DC=example
    passwordSecretRef:
      name: corp-ldap
      key: password                       # the default
  userBaseDn: OU=People,DC=corp,DC=example
  userFilter: (&(objectCategory=person)(objectClass=user))   # optional
  usernameAttribute: sAMAccountName       # uid for OpenLDAP
  deletionPolicy: Retain

A client then offers the directory in its own spec, by slug:

apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
  name: intranet
  namespace: nauthera
spec:
  clientId: intranet
  # …
  ldapDirectories:
    - slug: corp
      allowSignIn: true
      allowSignUp: true

kubectl get ldapdirectories shows the slug, the URL, the phase and the Ready condition's status and reason; -o wide adds the message.

Before you apply one

  • The operator's identity needs the ldap_directories operation, and the organization and namespace, in its server.grpc.tls.allowed_clients entry. The Helm chart's default entry (certManager.grpcTLS.allowedClients) grants it in the main organization; add a tenant's slug for directories there.
  • The server needs network access to the directory's host and port.
  • The service account needs to search userBaseDn and read the attributes sign-in uses: usernameAttribute, mail, objectGUID (Active Directory) or entryUUID (OpenLDAP), and on Active Directory userAccountControl, msDS-User-Account-Control-Computed, accountExpires and pwdLastSet. A read-only account is enough.

The connection

  • No connection binds in cleartext. The scheme of url is the TLS mode: ldaps:// is TLS from the first byte, and ldap:// is upgraded with StartTLS (RFC 4511 §4.14) before anything else is sent. The URL names a host and port only; the base DN is userBaseDn.
  • The directory's certificate must chain to the PEM certificates in caBundleRef, or to the system roots when it is unset, and be issued for the URL's host.
  • The service account in bind is what the server searches as. Without bind it searches anonymously.
  • Read-only. readOnly defaults to true, and true is the only value the server accepts: the server never writes to the directory.

Every five minutes the server connects to each directory as sign-in will and records whether it answered. A change is logged as ldap directory reachability changed, with the directory's issuer, slug and state: reachable, unreachable, tls_failed, service_bind_refused, or password_unavailable (the stored bind password does not open with the server's current keys). The admin gRPC API reports the same in the directory's reachability status. A broken connection shows there before a user meets it.

How sign-in works

When a client lists directories, its sign-in page asks for Username or email instead of an email address, and leaves out Forgot password?. A password sign-in then goes like this:

  1. Each directory the client lists is asked, in order, for an entry under userBaseDn that matches userFilter and whose usernameAttribute is the identifier typed.
  2. The first directory that holds the identifier decides. The server binds as that entry with the password typed. An identifier no listed directory holds signs in against a local account, as on a client without directories.
  3. A bind the directory accepts signs in as the account linked to the entry's objectGUID or entryUUID, wherever the entry has since moved in the tree.
  4. On the entry's first sign-in no account is linked yet. One is created from the entry's mail when the client's allowSignUp and the organization's sign-up (auth.registration.enabled) allow it. The address is not counted as verified.
  5. The rest of the sign-in is the same as for a local password: email verification where the organization requires it, the second factor, consent, then the application.

A directory account is never linked to an existing account by address, since a directory's mail does not prove who controls the mailbox. An account that already holds the address cannot sign in through the directory.

When it is refused

The user sees the same answer as for a wrong password, The email or password you entered is incorrect., whatever the cause, so the page does not reveal which accounts the directory holds. The auth.sign_in.completed audit record says why:

CauseMeaning
password_mismatchThe directory refused the password. It counts toward the lockout like any wrong password, kept apart from a local account of the same name.
directory_account_disabledThe Active Directory account is disabled.
directory_account_lockedThe account is locked out in the directory.
directory_account_expiredThe account has expired.
directory_password_expiredThe password has expired in the directory.
directory_password_change_requiredThe directory requires the user to change their password first.
directory_account_restrictedThe sign-in is outside the account's logon hours, or from a workstation it may not use.
directory_entry_ambiguousMore than one entry matches the identifier. Tighten userFilter or pick a unique usernameAttribute.
directory_entry_unlinkableThe entry has neither objectGUID nor entryUUID, so no account can be linked to it.
upstream_unavailableThe directory did not answer, TLS failed, or it refused the service account's bind. The search stops here, since the identifier may be the unreachable directory's.
sign_up_closedThe entry has no account yet, and the organization takes no new accounts.
identity_provider_not_allowedThe client's allowSignIn or allowSignUp does not admit this sign-in.
account_not_linkedAn account with the entry's address exists and is not linked to the entry.
upstream_email_missingThe entry has no mail to create an account from.

An account state the directory reports is decided by the directory: a user enabled or unlocked there can sign in on the next attempt, with no change here. Only Active Directory reports these states. Other directories report only whether the password was accepted.

Passwords stay in the directory

An account a directory signs in has no local password, and the server will not give it one. The admin API refuses to set its password, send it a reset link, require a password change, or send it an invitation. Each answers 409 with the problem password_held_by_directory, which names the directory. Change, reset or expire the password with the directory's own tools.

GET /v1/admin/orgs/{slug}/users/{id}/identities lists the account's link, under the provider ldap/<slug>.

What the operator does

  • Registers and updates the connection, sending the spec, the bind password and the CA bundle for every generation the server does not hold yet, and again whenever the Secret or ConfigMap changes.
  • Keeps the password in its Secret. The resource never holds it. The server reports only a non-reversible fingerprint, in status.bindPasswordFingerprint, which is empty for a directory searched anonymously. Editing the Secret sends the new password.
  • Waits for its inputs. A Secret, ConfigMap or key that does not exist leaves the resource in Error under SecretNotFound, SecretKeyMissing, ConfigMapNotFound or ConfigMapKeyMissing, naming it. A password that begins or ends with whitespace, such as the newline echo leaves, is Error under SecretPadded. Nothing is sent until it is fixed, and the resource is reconciled when the object changes.
  • Fixes the slug and organization. spec.slug (the resource's name when unset) cannot be changed after creation, since accounts are linked under it. The organization follows from the resource's namespace (see Which organization a resource configures); spec.organization names it where several organizations admit the namespace, and once set cannot be changed.

Creating or replacing a connection is recorded as ldap_directory.applied, and deleting it as ldap_directory.deleted.

Deleting a resource

spec.deletionPolicy decides, and defaults to Retain:

  • Retain leaves the connection registered, so deleting a namespace cannot take a sign-in method away from the organization's users.
  • Delete removes the connection from the server.

A finalizer (nauthera.io/ldapdirectory) holds the resource until that is done. A removal the server does not take keeps the resource in phase Deleting with reason DirectoryNotRemoved, and is retried.

Spec

spec.bind

Type: object | Optional

The service account the server searches the directory as. Left out, it searches anonymously.

FieldTypeRequiredDefaultDescription
dnstringYes—The service account's distinguished name (RFC 4514).
passwordSecretRefobjectYes—The Secret in this namespace holding the service account's password. Editing the Secret sends the new password to the server.

spec.caBundleRef

Type: object | Optional

The ConfigMap or Secret in this namespace holding the PEM certificates the directory's certificate must chain to. Left out, the system roots are trusted. Editing it sends the new bundle to the server.

FieldTypeRequiredDefaultDescription
keystringNo"ca.crt"The key holding the certificates.
kindstringNo"ConfigMap"ConfigMap or Secret. One of: ConfigMap, Secret.
namestringYes—The ConfigMap's or Secret's name.

spec.deletionPolicy

Type: string | Optional

What deleting this resource does to the connection it registered. Retain leaves it registered, so deleting a namespace cannot take a sign-in method away from the organization's users. Delete removes it from the server. A removal that fails keeps the resource, in phase Deleting, until it succeeds.

Defaults to "Retain".

One of: Retain, Delete.

spec.organization

Type: string | Optional

The slug of the organization this directory belongs to. Needed only where more than one organization admits the namespace in its spec.namespaces; otherwise the one that does is taken. The main organization is "default". Immutable.

spec.readOnly

Type: boolean | Optional

Whether the server only reads the directory, so a user's password lives there alone: the server offers no password reset and no password change to an account the directory signs in. True is the only value the server accepts until password write-back exists.

Defaults to true.

spec.slug

Type: string | Optional

The directory's name within its organization: a lowercase letter followed by up to 62 lowercase letters, digits or dashes. Defaults to the resource's name. Immutable.

spec.url

Type: string | Required

The directory's address, host and port only, such as ldaps://dc1.corp.example:636. The scheme is the TLS mode: ldaps:// is TLS from the first byte, and ldap:// is always upgraded with StartTLS (RFC 4511 §4.14) before anything else is sent.

spec.userBaseDn

Type: string | Required

The distinguished name (RFC 4514) under which a signing-in user is searched for, such as ou=people,dc=corp,dc=example.

spec.userFilter

Type: string | Optional

A search filter (RFC 4515) every user entry must also match, such as (objectClass=person). Left out, every entry under userBaseDn matches.

spec.usernameAttribute

Type: string | Required

The attribute a sign-in identifier is matched against, such as uid or sAMAccountName.

Status

The operator writes status from the server's answer. phase is one of Pending, Ready, Error or Deleting. Beside the connection reasons every resource shares (Refused, Unreachable, Unsupported, CredentialUnavailable, TrustAnchorUnavailable) and the namespace reasons (NamespaceNotAdmitted, OrganizationAmbiguous, OrganizationNotReady, OrganizationChanged), both explained in the OIDCClient reference, the Ready condition's reason is one of:

ReasonPhaseMeaning
RegisteredReadyThe server holds the current generation, password and CA bundle.
NamespaceNotCoveredErrorThe resource's namespace is not in the operator's --namespaces. Nothing is sent.
SecretNotFoundErrorThe Secret spec.bind.passwordSecretRef names does not exist in the resource's namespace.
SecretKeyMissingErrorThe Secret holds nothing under the key.
SecretPaddedErrorThe password begins or ends with whitespace.
ConfigMapNotFoundErrorThe ConfigMap spec.caBundleRef names does not exist in the resource's namespace.
ConfigMapKeyMissingErrorThe ConfigMap holds nothing under the key.
DirectoryNotRemovedDeletingUnder deletionPolicy: Delete, the server has not removed the connection yet.

Ready means the server holds the connection, not that the directory answers: see The connection for how reachability is reported.

FieldTypeDescription
bindPasswordFingerprintstringA non-reversible fingerprint of the bind password the server holds, as the server reports it; never the password. Empty for a directory searched anonymously.
conditions[]objectStandard Kubernetes conditions (Ready, ...).
etagstringThe server's revision of the connection as the operator last wrote it.
issuerstringThe issuer the server homed the connection at, after its default was applied.
lastReconciledTimestringWhen the operator last reconciled the resource, whatever the outcome. A stale value means the operator is not running, not the leader, or cannot see the resource.
observedGenerationinteger.metadata.generation last reconciled by the server.
organizationstringThe slug of the organization the server holds the directory in. Once set, a namespace that comes to configure another organization is refused rather than moving the directory.
phasestringHigh-level lifecycle phase.