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.
| Property | Value |
|---|---|
| API group / version | nauthera.io/v1alpha1 |
| Kind | LDAPDirectory |
| Plural / short name | ldapdirectories / ldap |
| Scope | Namespaced |
| Definition | deploy/ 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.pemapiVersion: 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: RetainA 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: truekubectl 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_directoriesoperation, and the organization and namespace, in itsserver.entry. The Helm chart's default entry (grpc. tls. allowed_ clients certManager.) grants it in the main organization; add a tenant's slug for directories there.grpcTLS. allowedClients - The server needs network access to the directory's host and port.
- The service account needs to search
userBaseDnand read the attributes sign-in uses:usernameAttribute,mail,objectGUID(Active Directory) orentryUUID(OpenLDAP), and on Active DirectoryuserAccountControl,msDS-User-Account-Control-Computed,accountExpiresandpwdLastSet. A read-only account is enough.
The connection
- No connection binds in cleartext. The scheme of
urlis the TLS mode:ldaps://is TLS from the first byte, andldap://is upgraded with StartTLS (RFC 4511 §4.14) before anything else is sent. The URL names a host and port only; the base DN isuserBaseDn. - 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
bindis what the server searches as. Withoutbindit searches anonymously. - Read-only.
readOnlydefaults totrue, andtrueis 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:
- Each directory the client lists is asked, in order, for an entry under
userBaseDnthat matchesuserFilterand whoseusernameAttributeis the identifier typed. - 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.
- A bind the directory accepts signs in as the account linked to the entry's
objectGUIDorentryUUID, wherever the entry has since moved in the tree. - On the entry's first sign-in no account is linked yet. One is created from the entry's
mailwhen the client'sallowSignUpand the organization's sign-up (auth.) allow it. The address is not counted as verified.registration. enabled - 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:
| Cause | Meaning |
|---|---|
password_mismatch | The directory refused the password. It counts toward the lockout like any wrong password, kept apart from a local account of the same name. |
directory_ | The Active Directory account is disabled. |
directory_account_locked | The account is locked out in the directory. |
directory_ | The account has expired. |
directory_ | The password has expired in the directory. |
directory_ | The directory requires the user to change their password first. |
directory_ | The sign-in is outside the account's logon hours, or from a workstation it may not use. |
directory_ | More than one entry matches the identifier. Tighten userFilter or pick a unique usernameAttribute. |
directory_ | The entry has neither objectGUID nor entryUUID, so no account can be linked to it. |
upstream_unavailable | The 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_closed | The entry has no account yet, and the organization takes no new accounts. |
identity_ | The client's allowSignIn or allowSignUp does not admit this sign-in. |
account_not_linked | An account with the entry's address exists and is not linked to the entry. |
upstream_email_missing | The 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_, which names the
directory. Change, reset or expire the password with the directory's own tools.
GET / 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., which is empty for a directory searched anonymously. Editing the Secret sends the new password.bindPasswordFingerprint - Waits for its inputs. A Secret, ConfigMap or key that does not exist leaves the
resource in
ErrorunderSecretNotFound,SecretKeyMissing,ConfigMapNotFoundorConfigMapKeyMissing, naming it. A password that begins or ends with whitespace, such as the newlineecholeaves, isErrorunderSecretPadded. 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.organizationnames 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:
Retainleaves the connection registered, so deleting a namespace cannot take a sign-in method away from the organization's users.Deleteremoves the connection from the server.
A finalizer (nauthera.) 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dn | string | Yes | — | The service account's distinguished name (RFC 4514). |
passwordSecretRef | object | Yes | — | 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
key | string | No | "ca.crt" | The key holding the certificates. |
kind | string | No | "ConfigMap" | ConfigMap or Secret. One of: ConfigMap, Secret. |
name | string | Yes | — | 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:
| Reason | Phase | Meaning |
|---|---|---|
Registered | Ready | The server holds the current generation, password and CA bundle. |
NamespaceNotCovered | Error | The resource's namespace is not in the operator's --namespaces. Nothing is sent. |
SecretNotFound | Error | The Secret spec. names does not exist in the resource's namespace. |
SecretKeyMissing | Error | The Secret holds nothing under the key. |
SecretPadded | Error | The password begins or ends with whitespace. |
ConfigMapNotFound | Error | The ConfigMap spec.caBundleRef names does not exist in the resource's namespace. |
ConfigMapKeyMissing | Error | The ConfigMap holds nothing under the key. |
DirectoryNotRemoved | Deleting | Under 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.
| Field | Type | Description |
|---|---|---|
bindPasswordFingerprint | string | A 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 | []object | Standard Kubernetes conditions (Ready, ...). |
etag | string | The server's revision of the connection as the operator last wrote it. |
issuer | string | The issuer the server homed the connection at, after its default was applied. |
lastReconciledTime | string | When 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. |
observedGeneration | integer | .metadata.generation last reconciled by the server. |
organization | string | The 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. |
phase | string | High-level lifecycle phase. |