Administration

LDAP and Active Directory Setup

LDAP and Active Directory Setup

This page goes from an empty form to a working directory sign-in, and from a failed sign-in to the reason it failed. It assumes you know your own directory and nothing at all about docu.nexus.

There are two worked examples below, one for Active Directory and one for OpenLDAP. Follow the one that matches your directory. Their defaults differ in ways that decide whether anyone can sign in at all.

What a connection is, and what it is not

A directory connection belongs to the instance. It is not attached to any nexus, and it puts nobody into one. It does three things:

  • It signs people in, while its Use for sign-in switch is on.
  • It creates their accounts and keeps them in step with the directory: names and email addresses, and switching off the accounts it owns once the directory stops listing them.
  • One connection, chosen separately, may let its groups decide who is an instance administrator. See Directory groups and instance administrators.

What somebody can reach once they are signed in is decided exactly as it is for everybody else, by an admin placing them. See Placing directory people.

Only an instance administrator sees or changes connections. The first account made on the setup screen is one; Admin, then Instance Administrators says who else is. Somebody who administers a nexus but not the instance sees the Sign-in methods tab of Authentication read only, the Single sign-on tab, and no LDAP tab at all.

How a directory sign-in actually works

Every sign-in runs the same four steps against a connection.

  1. Connect to the host, encrypted the way the connection’s Transport says.
  2. Bind as the service account. This happens only when both a Bind DN and a bind password are stored. With no Bind DN the search runs anonymously. A Bind DN with no password also searches anonymously, which is almost never what anybody meant, so Test connection reports it as Incomplete and never as green.
  3. Search for the person, using your search base, scope and filter, with {{username}} replaced by whatever was typed in the sign-in box.
  4. Bind a second time as that person, using the DN the search returned in step 3 and the password they typed.

If step 4 succeeds they are authenticated. docu.nexus then looks for the account this connection already owns, first by the directory username and then by email address, and creates one if Create an account on first successful sign-in is on.

With more than one connection used for sign-in, the four steps run against each connection in the order the connections were added, until one of them accepts the password. A connection that cannot be used is skipped and the next one is tried: it does not answer, TLS fails, or steps 2 or 3 are refused. The reason goes to the server log, not to the sign-in page. Only when no connection could be used does the sign-in page say the directory could not be reached. That also means a person’s password is offered to each directory in turn until one accepts it, so switch on only directories you trust with your people’s passwords.

When step 4 is refused, the sign-in page says only Authentication failed, whatever the directory said. Active Directory reports a locked, disabled or expired account only when the password was right, so naming the reason would tell whoever is at the sign-in page that they had the password. The directory’s words go to the server log.

Two consequences are worth holding on to, because most broken configurations come from missing them.

  • Step 3 decides who step 4 tries. The search takes the first entry that matched. A filter that never mentions the username is not a restriction, it is a query that ignores who is signing in.
  • The typed password is only ever used for the bind in step 4. It is not stored, and directory accounts get no local password.

Before you start

Collect these from whoever runs the directory.

You needExampleUsed for
Host and port, and whether it is encrypteddc01.corp.example, 636, LDAPSTransport, Host and Port
A service account that can read user entriessvc-docunexus@corp.exampleBind DN and Bind password
The container that holds peopleOU=Staff,DC=corp,DC=exampleSearch base DN
The attribute people type as their usernamesAMAccountName or uidUsername attribute
For LDAPS or StartTLS against an internal CA, that CA’s certificate in PEM formthe file your CA publishesTLS CA certificate
Optionally, a group whose members should be instance administratorsCN=Docs Admins,OU=Groups,DC=corp,DC=exampleAdmin, then Instance Administrators

The service account only ever reads. Nothing in docu.nexus writes to your directory.

Where the settings live

Open Admin, then Authentication, which sits under People & Access in the panel’s navigation, and pick the LDAP tab.

The tab is one list, Directory connections, with one row per connection in the order sign-in tries them. Each row shows:

  • the connection’s name, and its host and port;
  • how it is encrypted: LDAPS, StartTLS, or No encryption;
  • its status, from the same check Test connection runs (see Test before you save);
  • Grants instance administrator, on the one connection whose groups may make somebody an instance administrator;
  • how many accounts the connection owns;
  • and at the end, its own Use for sign-in switch.

Click anywhere on a row to open that connection’s form below the list. Add connection opens a blank one, and the arrow at the top of a form goes back to the list.

The status checks itself when the tab opens only while there is a single connection. With several, each row waits, reading Not checked (or Off for a connection not used for sign-in), until you press Check all, so opening the tab does not bind to every directory at once: each check is a bind as the service account, and in Active Directory a failed one counts towards that account’s lock-out.

A sentence appears above the list only when no connection signs anybody in, and it says which of the two switches below is the reason.

The same connections also appear under Admin, then Connections, one row each, with the scope Instance.

Two switches decide whether anyone signs in

  • LDAP / Active Directory on the Sign-in methods tab turns directory sign-in on or off for the whole instance, without touching any connection. While it is off, Sync users now is refused as well.
  • Use for sign-in on each connection, on its row and on its form. A new connection is saved with it on.

The sign-in page grows a second tab beside the email and password form once both are on for at least one connection. The tab is labelled with that connection’s Name while exactly one connection is used for sign-in, and LDAP / Active Directory when there are several, because one form serves them all.

Worked example: Active Directory

Every value below is a placeholder. Substitute your own.

Connection, at the top of the form

FieldValue
NameCorp sign in
TransportLDAPS (636)
Hostdc01.corp.example
Portleave it empty, which means 636
Bind DNsvc-docunexus@corp.example
Bind passwordthe service account password
Search base DNOU=Staff,DC=corp,DC=example

Under the port the form shows the address it will connect to, here ldaps://dc01.corp.example. If you paste a whole URL such as ldaps://dc01.corp.example:636 into Host, the form reads it and fills in Transport, Host and Port for you.

Active Directory accepts either the user principal name shown above or a full DN such as CN=svc-docunexus,OU=Services,DC=corp,DC=example as the Bind DN. Whatever you type is handed to the directory unchanged.

Advanced

FieldValue
TLS CA certificatepaste your issuing CA in PEM form if the domain controller’s certificate comes from an internal CA
Verify the directory’s certificateon
Search filterleave it alone
Search scopeSubtree (default)
Active Directory bind domainleave it empty
Username attributesAMAccountName
Email attributemail
Display name attributedisplayName
Group membership attributememberOf
First name attributegivenName
Last name attributesn
Create an account on first successful sign-inon
Refresh name, email and groups from the directory on every sign-inon
Adopt local accountsoff

Then:

  1. Press Test connection. It should come back green, saying it bound with the service account and how many people the sign-in filter matches. Read Test before you save before you trust that result.
  2. Press Save connection. The save binds with the new password before it stores it; see Saving a bind password.
  3. On the Sign-in methods tab, switch LDAP / Active Directory on if it is not on already.
  4. Sign out, or open a private window, and sign in on the new tab as a real person from the directory.
  5. Place that person where they will work: see Placing directory people.

Because you left the filter alone, it follows your attribute mapping and matches either the short username or the email address. The uid in the default is replaced with sAMAccountName for you.

Worked example: OpenLDAP

Connection, at the top of the form

FieldValue
NameDirectory
TransportStartTLS (389)
Hostldap01.corp.example
Portleave it empty, which means 389
Bind DNcn=svc-docunexus,ou=Services,dc=corp,dc=example
Bind passwordthe service account password
Search base DNou=People,dc=corp,dc=example

If your server offers LDAPS instead, choose LDAPS (636). The transport is one choice, so there is no way to ask for both.

Advanced

FieldValue
TLS CA certificatepaste your CA in PEM form if it is not in the system trust store
Verify the directory’s certificateon
Search filterleave it alone
Search scopeSubtree (default)
Active Directory bind domainleave it empty, it is an Active Directory feature
Attribute mappingthe stock defaults already suit OpenLDAP: uid, mail, cn, memberOf, givenName, sn. Change nothing unless your schema differs
Provisioningas for Active Directory above

Then test, save, switch on, sign in as a real person, and place them, exactly as above.

The search filter, and the one mistake that matters

The filter is the single field most likely to lock everyone out or let everyone in. It sits under Advanced, in Directory search, and it behaves in two distinct modes.

Left at a built-in default, it is rebuilt from your attribute mapping every time it is used, so it matches either the username or the email address.

Two strings count as a default, and they behave identically. A new connection shows this one in the box, unless the deployment prefills another:

(|(uid={{username}})(mail={{username}}))

A connection saved by an older release may show the shorter one, which is recognised just the same:

(uid={{username}})

Leave either alone and the filter that actually runs is built from your mapping. Map the username attribute to sAMAccountName and it becomes this, with no editing on your part:

(|(sAMAccountName={{username}})(mail={{username}}))

So a box reading (uid={{username}}) does not mean sign-in matches the username only. That is an untouched default, and it accepts the email address too.

Changed to anything else, it is used exactly as you typed it. Nothing is added to it. This is deliberate: quietly appending an alternative to your filter would widen a restriction you meant to impose, such as a group clause.

That leads to the mistake. Some products take a username attribute and a separate user filter and combine the two for you. This one does not. If you paste in a bare group filter:

(memberOf=CN=Docs Users,OU=Groups,DC=corp,DC=example)

then the search never mentions the person signing in. It returns the group’s members, the first one is taken, and the password is checked against that entry. The group clause has become the whole query rather than an extra condition. Test connection cannot catch this for you: it runs the filter with any username in place of {{username}}, and a filter that ignores the username finds the group’s members and reads green.

A custom filter must locate the user itself. Combine your conditions with & and keep the username clause inside:

(&(objectClass=user)(sAMAccountName={{username}})(memberOf=CN=Docs Users,OU=Groups,DC=corp,DC=example))

The placeholder is {{username}}, spelled exactly like that, in lower case, with two braces on each side. Any other spelling is sent to the directory as literal text and matches nothing.

Special characters in what someone types are escaped before substitution, so a username containing *, (, ) or \ cannot turn into a wildcard.

Nested groups in Active Directory

memberOf in Active Directory carries direct membership only. Someone in a group that is itself a member of your target group will not match a plain memberOf clause, and Test connection says Finds nobody when that leaves the filter matching no one. Ask the directory to walk the chain with the matching rule OID:

(&(sAMAccountName={{username}})(memberOf:1.2.840.113556.1.4.1941:=CN=Docs Users,OU=Groups,DC=corp,DC=example))

This works for the filter, because the directory evaluates it. It does not help a group listed for instance administrators, which is compared against the group values the directory returned on the entry. Those are direct memberships, so add people to that group directly.

The filter is also the sync filter

Sync users now reuses the same filter with every {{username}} replaced by *. The default becomes a query for every entry that has a username or an email, and a group filter like the one above becomes a query for that group’s members. A filter with no placeholder at all is used unchanged, so it imports exactly what it says.

Attribute defaults are OpenLDAP defaults

The stock mapping suits OpenLDAP. Active Directory needs two of the six changed. The mapping sits under Advanced, in Attribute mapping.

FieldDefaultOpenLDAPActive Directory
Username attributeuiduidsAMAccountName
Email attributemailmailmail
Display name attributecncndisplayName
Group membership attributememberOfmemberOfmemberOf
First name attributegivenNamegivenNamegivenName
Last name attributesnsnsn

Only the attributes named in this table are requested from the directory, so nothing else on the entry is read. If you map a name the entry does not carry, that field arrives empty.

An account’s name is the entry’s display name when it has one; otherwise its first and last names, joined by a space; otherwise the username.

An entry with no email value gets a placeholder address of the form username@ldap.local so the account can be created at all. If you see those in the People list, your email attribute is mapped to something the entries do not carry.

Choosing the transport

Transport is one choice of three, and the address under the port is composed from it, the host and the port.

What you wantTransportUsual port
Encrypted from the first byte, the usual choiceLDAPS636
A plain connection upgraded to TLS before anything is sentStartTLS389
No encryption at all, only ever on a trusted segment or a test rigNone389

None sends everything in plain text, the bind password and every person’s password included.

An empty port means the transport’s usual one, which the field shows as its placeholder. Some sites really do run LDAPS on 3269 or StartTLS on 3268, so any port is accepted. A port that looks like the other transport’s default, 389 with LDAPS or 636 with the other two, earns a warning with an (i) under the field, never a refusal.

LDAPS and StartTLS together have never connected: StartTLS cannot run inside a connection that LDAPS has already encrypted. The form offers no way to ask for both, and a save that asks for both is refused. A connection saved that way by an older release has never connected and still does not: its row reads LDAPS and StartTLS in red, its form opens with no transport chosen and says so, and Save stays disabled until you pick one. Nothing picks for you, because the choice changes who can sign in.

The certificate

Under Advanced, Certificate verification applies to LDAPS and StartTLS only. With the transport at None the card says there is no certificate to verify.

  • Verify the directory’s certificate is on by default. Turning it off accepts any certificate at all, and a warning stays on the form for as long as it is off. Treat it as a temporary diagnostic.
  • TLS CA certificate is where you paste the issuing CA, in PEM form from -----BEGIN CERTIFICATE----- to -----END CERTIFICATE-----, when the directory’s certificate comes from a CA the container does not trust. Leave it empty and the system trust store is used. A paste that does not read as a certificate is refused when you save.

Leave verification on. If the certificate comes from an internal CA, paste that CA rather than turning verification off.

Once a CA is saved it is not shown again as text. The form shows Stored, and under it one line per certificate: whose it is, who issued it, and when it expires. One that has expired, or that expires within 30 days, is flagged there. Its SHA-256 fingerprint is behind the (i), for comparing against what your CA publishes.

  • Replace opens the field to paste a new one.
  • Remove marks it Removed on save, and the next save goes back to the system trust store. Undo takes that back before you save.

A CA stored on a connection whose transport is now None is still shown, so that Remove stays within reach.

The bind password

Until a password is stored, Bind password is an ordinary field. Once one is stored, the form shows Stored in its place, with Replace and Remove, and no field at all. That is deliberate: a browser that fills a saved password into anything that looks like a sign-in form has nothing to fill.

  • Replace opens an empty field for the new password, with Cancel to keep the stored one.
  • Remove marks it Removed on save. Undo takes that back. Saving it leaves a Bind DN with no password, which searches anonymously, and the status then reads No bind password.

Saving a bind password

Three checks run before a new password is stored.

It goes with a Bind DN. The password is only ever sent in a bind beside the Bind DN, so a new password saved with the Bind DN empty is refused. For an anonymous bind, clear the Bind DN and Remove the password.

It is not your own password. A new bind password that is the password you sign in to docu.nexus with is refused: Not saved: that is the password you sign in to docu.nexus with, not the directory service account password, and a browser may have filled it in. This check needs no directory, so it holds even when the directory cannot be reached.

The directory is asked. A save that changes where or how the service account binds (the Bind DN, the password, the transport, host or port, certificate verification or the CA), and has a password to bind with, binds first. A save that changes only the name, the filter or the mapping does not bind at all, because every bind counts in Active Directory.

What the directory saysWhat the save does
The credentials are wrong: invalid credentials (result 49), on Active Directory data 52e (wrong password) or data 525 (no such account), or a Bind DN that does not exist (result 32)Refused. Nothing is saved, and the sentence at the top of the app names the field to fix.
The account is right but cannot bind now. On Active Directory: 530 or 531 (logon hours or workstation restriction), 532 (password expired), 533 (account disabled), 701 (account expired), 773 (must change password), 775 (locked out)Saved, with a warning at the top of the app. Active Directory reports these only for the correct password, so refusing would refuse exactly the password you need to store while the account is fixed.
Nothing about the password: the directory could not be reached, or TLS failedSaved, with a warning.
It bindsSaved, with a green check beside the button.

There is no override, and none is needed: everything that is not refused is stored.

A saved password is only ever sent to the connection it was saved for: the address shown under the port is the one it was saved with, StartTLS and certificate verification have not been switched off, and the CA is the same. A save that moves the connection away from any of those without a new password is refused, and nothing is saved: the password would otherwise go wherever the connection now points, at every sign-in. Press Replace and type the password, which is then checked against the new values before it is stored, or Remove it.

Test before you save

Test connection sits under the search base. It runs the values on the form, including anything you have not saved, so you can try a change before committing it and prove a fix without saving a broken configuration over a working one. The result stays on screen until you change a field.

It runs one check, the same one behind the status on each row and the one the server runs at every start:

  1. Connect with the transport you chose.
  2. Bind as the service account, or anonymously when there is no Bind DN.
  3. Run the sign-in search under the search base, with every {{username}} replaced by *, and count who it finds, up to 50.
ResultColourMeaning
ConnectedgreenIt bound, and the sign-in filter matches somebody: “The sign-in filter matches 12 people under OU=Staff,DC=corp,DC=example.” Only this is green.
Finds nobody, or a reason such as Search base not foundamberIt bound, but the search found no one or was refused. Every sign-in would be refused, so this is the filter or the search base to fix, not the network.
Incomplete, No bind password, No search baseamberSomething the check needs is missing.
Not testedamberSee the stored password rule below.
A short reason such as CA not trusted, Wrong bind DN or password, Host not foundredIt failed. The sentence beside it says which field to change, and the directory’s own words sit behind the (i).

On a saved connection, a password or CA you have not replaced is used as stored, so a connection that pins an internal CA tests exactly the way it signs people in. The stored password is sent only while the form still describes the connection it was saved for, by the rule above. Change the address under the port or the CA, or switch StartTLS or verification off, and Test answers Not tested without dialling anything. Replace the password to test the new values.

On a new connection that has never been saved, nothing is stored, so Test uses only what is on the form.

What a green result does not tell you is that the filter picks out the right person. A filter that ignores the username matches people and reads green; see The search filter. The only end-to-end test is a real sign-in by a real person.

Placing directory people

A directory places nobody into a nexus. The first time somebody signs in through a connection, or a sync reaches them, they get an account and the private nexus every account gets, and nothing else: no shared nexus and no role. That is the same starting point as an account made any other way, and they are placed the same way.

  1. Open Admin, then People & Access, and press + Person.
  2. Choose Existing account. In Who, type their exact username or their exact email address. Somebody who shares none of your nexuses, which is every new directory person, is found only by that exact username or email, never by part of a name.
  3. Pick the Nexus and the Role, and add them.

An instance administrator can also search by part of a name, and sees every account on the instance in the People list, including directory accounts that belong to no nexus yet. The Nexus list in + Person offers only nexuses you administer, so an instance administrator joins a nexus before placing people in it.

To have people placed before their first sign-in, run Sync users now first. It creates an account for everybody the filter finds, and those accounts can be placed straight away. A connection can be synced while its Use for sign-in is off, so this works before anybody can sign in through it.

Nothing the directory says changes a nexus role afterwards. Joining or leaving a directory group promotes, demotes and removes nobody.

Two things the directory does still decide about the account itself:

  • An account the directory stops listing is switched off at the next sync, if this connection owns it.
  • An account the directory lists again is switched back on at the next sync. That includes an account you deactivated by hand, because nothing records why an account was switched off. To keep a directory person out for good, remove them from the directory, or from what the filter matches, as well.

Directory groups and instance administrators

A directory’s groups decide one thing: who is an instance administrator, and only through the one connection chosen for it. They never decide a nexus role.

Set it up under Admin, then Instance Administrators, in Automatic grants.

  • Under Where the group comes from, choose Directory group, and add each group’s full DN. Press Enter after each one, or paste several at once separated by new lines or semicolons. A comma is part of a DN, never a separator between DNs, so paste each DN whole, from its first CN= to its last DC=.
  • Under Directory evidence comes from, choose the one connection whose group memberships may grant it. Memberships reported through every other connection are ignored, and with no connection chosen no directory grants anybody.

The chosen connection carries Grants instance administrator in the LDAP list. Choosing it does not let it sign anybody in, which is its Use for sign-in switch, and switching on Use for sign-in does not let a connection grant anything. Deleting the chosen connection means no directory grants instance administrator until you choose another.

A group is compared with the values the connection’s Group membership attribute returned on the person’s entry: the whole DN, ignoring case and any spaces around the commas and equals signs. Those are direct memberships only. The grant is checked at every directory sign-in and sync, and withdrawn when the membership goes away.

The deployment can pin both answers instead, with INSTANCE_ADMIN_GROUPS and INSTANCE_ADMIN_LDAP_CONNECTION. While either is set it outranks the panel. See the Configuration Reference.

Field reference

Connection

FieldWhat it isLeave it blank and
NameWhat this connection is called in the list, and the label of the sign-in page’s tab while it is the only connection used for sign-in. Required: it is how people tell connections apart.you cannot save.
TransportLDAPS, StartTLS or None. See Choosing the transport.not applicable, one is always chosen.
HostThe directory’s host name. Paste a whole ldap:// or ldaps:// URL here and it is read into Transport, Host and Port.you cannot save or test.
PortThe port to connect to.the transport’s usual port: 636 for LDAPS, 389 otherwise.
Bind DNThe service account that performs the user search. A DN, or a user principal name on Active Directory.the search runs anonymously, which most directories restrict or refuse.
Bind passwordThe service account’s password. Shown as Stored once saved, with Replace and Remove, and held in the database in plaintext, as noted under Before you start.with a Bind DN set, the search runs anonymously and the status reads No bind password.
Search base DNWhere to look for people.you cannot save.

Use for sign-in

FieldWhat it isEffect when off
Sign people in through this connectionThe connection’s one sign-in switch, the same one as on its row. On for a new connection.nobody signs in through it. It stays saved, and it can still be synced: it creates, refreshes and switches off the accounts it owns, but it takes over no account it does not already own.

To turn directory sign-in off for the whole instance, use Sign-in methods instead.

Advanced: certificate verification

FieldWhat it isLeave it blank and
TLS CA certificateA PEM certificate to trust, for an internal CA. Shown as Stored once saved, with a summary, Replace and Remove.the system trust store is used.
Verify the directory’s certificateChecks the directory’s certificate. On by default.not applicable, it is a switch. Off accepts any certificate.
FieldWhat it isLeave it blank and
Search filterHow to find one person from what they typed. {{username}} is the placeholder.the built-in default applies, matching the username attribute or the email attribute.
Search scopeSubtree searches the base and everything under it. One level searches only its direct children. Base examines the base entry itself.Subtree, the default and the right answer for nearly everyone.
Active Directory bind domainActive Directory only. When set, the second bind uses username@domain instead of the DN found by the search.the DN from the search is used, which works on Active Directory too and accepts both username and email sign-ins. Recommended.

Advanced: attribute mapping

Covered in the table above. Each field names one directory attribute to read from the entry.

Do not clear one. An empty name is saved as an empty name, and the default search filter built from it comes out malformed. The LDAP client refuses to send a filter it cannot parse, so the sign-in fails before the directory is ever asked anything.

Advanced: provisioning

FieldWhat it isEffect when off
Create an account on first successful sign-inCreates an account the first time someone signs in successfully, with their private nexus and nothing else.a person with no account is refused with “User not found and auto-provisioning is disabled”, even though their password was correct. Accounts must then come from Sync users now or be created by hand.
Refresh name, email and groups from the directory on every sign-inRefreshes name, address and directory identifiers at each sign-in.local edits stick, and a directory rename only arrives when you run Sync users now.
Adopt local accountsLets this directory take over an existing account that somebody signs into with a password, when the directory lists the same email address. The password on that account then stops being a way in. It asks before it turns on, and a warning stays on the form while it is on. An account holding instance administrator is never taken over, whatever this says.a directory person whose address belongs to a password account is not signed in to it, and Sync users now lists them under Skipped with the reason.

Turn Adopt local accounts on for a migration from password sign-in to the directory, and off again when the migration is done.

Actions

Save connection on a new connection, Save changes on a saved one. A success earns a green check beside the button. A refusal, and a warning from checking the bind password, is a sentence at the top of the app.

Sync users now creates or refreshes an account for everybody the filter matches, takes over the accounts it is allowed to, switches off the accounts it owns that the directory no longer lists, and switches back on the ones it lists again. It places nobody into a nexus. When it finishes, a sentence at the top of the app counts what it did, and anything it skipped is listed with its reason under Skipped in the last sync. It is refused while directory sign-in is off on Sign-in methods, and disabled when no licensed seats remain, because every account it would create would be refused.

Delete connection asks first, and the question names what the delete takes away: that nobody signs in through it any more, how many accounts it owns (they become owned by no connection), and, for the connection that grants instance administrator, that no directory will grant it any more. It cannot be undone. To pause a connection instead, switch off Use for sign-in.

You cannot take away the last way in. If email and password sign-in is off and this connection is what everyone signs in with, switching off its Use for sign-in and deleting it are both refused with That would leave no way to sign in to this instance, and nothing changes. Switch another sign-in method on first.

Every change to a connection, every flip of its sign-in switch, every deletion, sync and account takeover is recorded in the instance’s audit log. The two secrets are recorded only as kept, cleared or replaced, never their values.

Environment variables

The LDAP_* variables in .env fill in the form for a new connection, and nothing else. The form says so when they did. Once a connection is saved, what is saved is what sign-in uses, and editing a variable afterwards changes no saved connection. They do not switch anything on either: directory sign-in needs a saved connection with Use for sign-in on, and the LDAP / Active Directory switch on Sign-in methods.

Three variables used to do more and now do nothing. The server says so in its log at every start while one is set:

VariableWhat happened to it
LDAP_BIND_CREDENTIALSIt filled in the bind password of the next new connection, whatever host that connection named. Type the password into the form instead, where the save checks it.
LDAP_ADMIN_GROUPSIt made directory group members admins of a nexus. A directory no longer makes anybody a nexus admin; for instance administrators, see Directory groups and instance administrators.
LDAP_DEFAULT_ROLEIt was the role a directory placed people at. A directory places nobody now; an admin of each nexus picks the role when they place a person.

LDAP_SIGNIN_CONNECTIONS is on its way out. While it is set it can only narrow sign-in: a connection it does not name signs nobody in, whatever its switch says, and its row reads Not in LDAP_SIGNIN_CONNECTIONS. It never switches a connection on. Keep it exactly as it is until the release that removes it. The version before this one read it before anything stored in the database, and rolling back restores the database but not .env, so a changed value would change who signs in after a rollback; see Rolling back. Decide who signs in with Use for sign-in instead.

Two names people bring from other products’ documentation do not exist here and are read by nothing:

Wrong, and silently ignoredInstead
LDAP_BIND_PASSWORDType the bind password into the form.
LDAP_USER_FILTERLDAP_SEARCH_FILTER

The complete list of names that are read is in the Configuration Reference. There is no environment variable for the CA certificate or the connection name; set those in the form.

Troubleshooting by symptom

Where to look first

The status on each row, and Test connection on the form, say in words what is wrong and which field to change. The server also checks every connection used for sign-in when it starts, and writes one line for each that is not well. From your install directory:

docker compose logs app | grep -i ldap

A sign-in whose search matched nobody is logged as LDAP login: no entry matched under <base> for connection <id>, without the username. A connection a sign-in could not use is logged as LDAP login: connection <id> could not be used: followed by the reason in words.

The status is green but nobody can sign in

Green means the service account bound and the filter matches somebody. Work down this list.

  • No tab on the sign-in page. Directory sign-in is off on Sign-in methods, or no connection has Use for sign-in on. The sentence above the list says which.
  • The row reads Not in LDAP_SIGNIN_CONNECTIONS. Add the connection’s id to that variable in .env and apply the change with docker compose up -d. Keep a copy of the value it had before: rolling back to the previous version needs that value, not this one.
  • The filter ignores the username. See the next symptom.
  • The Active Directory bind domain is set, and people sign in with an email address. See the warning under the field reference.
  • They type something the filter does not match, such as an email address against a custom filter that names only the username attribute.

Then try a real sign-in and read the error. “User not found” means the search matched nothing for what was typed. Anything mentioning invalid credentials means the search found somebody and the second bind was refused.

Everyone can sign in, whatever group they are in

Almost always a custom filter with no {{username}} in it, most often a bare memberOf clause pasted in to restrict access. Custom filters are used verbatim, so that filter returns the group’s members and the first one is taken. The password is then checked against that entry rather than against the person signing in, and the session is attached to that entry’s identity. If the Active Directory bind domain is also set, the bind is whatever-they-typed@domain and the found entry is not even consulted.

Put the username clause back inside the filter and re-test with two real sign-ins, one that should be allowed and one that should not.

Test says Finds nobody

The service account bound, and the sign-in filter matches no one under the search base. That is the filter or the base, not the network. In Active Directory, check first whether a memberOf clause needs the nested form described in Nested groups in Active Directory. Search base not found means the base DN does not exist: check it against the directory, character for character.

Test says Not tested

The form no longer describes the connection the password was saved for: the address under the port changed, StartTLS or verification was switched off, or the CA was replaced or removed. The stored password is only sent where it was saved for. Press Replace, type the password, and test again. Saving those values without doing so is refused for the same reason.

Only some people can sign in

Work down this list.

  • The search base is too deep. People in a sibling OU are invisible. Move the base up.
  • The search scope is One level while entries live in child containers. Use Subtree.
  • Their entry lacks the attribute you mapped as the username attribute, so the filter cannot match them.
  • A custom filter carries a clause they do not satisfy, such as an objectClass or a group.
  • The filter matches more than one entry, for example two accounts sharing a mail value. The first entry the directory returns wins, and it may not be theirs.
  • Account creation is off and they have no account yet.
  • Their address belongs to an account somebody signs into with a password, and Adopt local accounts is off. Sync users now names them under Skipped.
  • Their account was switched off, by a sync after the directory stopped listing them, or by hand. A deactivated account is refused however good the password.
  • The seat limit is reached. Existing accounts keep working; only new ones are refused, with a message naming the licence tier and the seat count.
  • They have two-factor authentication on their account. The code prompt after the directory accepts them is expected, not a failure.

Someone signs in but sees nothing

That is how a directory person starts. Signing in creates the account and its private nexus, and nothing else; a directory places nobody into a shared nexus. Place them as described in Placing directory people.

It worked and then it stopped

  • The service account password was rotated or expired. Every sign-in now fails at the search step, and the row’s status says so. Press Replace on the bind password, type the new one, and save; the save checks it.
  • The service account was locked out. The status reads Account locked. Unlock it in Active Directory before testing again, because every failed bind counts towards the lock-out.
  • The certificate expired or was reissued under a different CA. Affects LDAPS and StartTLS only, and only with verification on, which is where it should stay. The CA summary on the form flags a pinned CA within 30 days of expiry. Replace the CA rather than turning verification off.
  • The filter was edited. Any change away from either built-in default switches it to verbatim mode, including a change that looks cosmetic.
  • The connection was switched off or deleted. With a single connection used for sign-in, the tab disappears from the sign-in page, which is the giveaway. With more than one it stays, and only the people who relied on that connection are turned away.
  • The seat limit was reached. Only new people are affected.

Sign-in works with the username but not the email address, or the other way round

The built-in default matches both. A custom filter matches only what it says, so (sAMAccountName={{username}}) accepts the short name only.

If email sign-in specifically is broken while usernames work, check the Active Directory bind domain. When it is set, an email address becomes user@corp.example@corp.example at the bind and is refused.

The email tab says “This account uses external authentication”

The right message on the wrong tab. Directory accounts have no local password, so they cannot use the email and password form. Sign in on the directory tab instead.

What the status and Test say, and what to do

The sentence beside the result says what to change. The short labels, and the codes behind them:

LabelBehind itWhat to do
Wrong bind DN or passwordresult 49; Active Directory data 52eCheck the Bind DN, then Replace the password.
Bind DN not foundActive Directory data 525, or result 32 at the bindThe Bind DN names nobody. Copy it out of the directory.
Password expired, Account disabled, Account expired, Password must be changed, Account locked, Account restrictedActive Directory data 532, 533, 701, 773, 775, 530 or 531Fix the service account in Active Directory. The password on the form is correct.
Bind refusedany other Active Directory data code, or a bind the directory does not allowRead the directory’s words behind the (i). data 80090346 means the domain controller enforces LDAP channel binding, which is not supported.
Encryption requiredresult 8 (strong authentication required)The directory refuses binds in plain text. Choose LDAPS or StartTLS.
StartTLS failedthe directory refused or could not start TLSOften a missing or expired certificate on the directory. If the port speaks LDAPS, choose LDAPS.
TLS handshake failed, Connection closedthe socket closed or the handshake failedThe port may speak plain LDAP (choose StartTLS or None), or LDAPS (choose LDAPS).
CA not trustedthe certificate chain does not lead to a trusted CAPaste the issuing CA under TLS CA certificate.
Certificate expireda certificate in the chain is out of dateRenew it on the directory, or pin the CA that issued its replacement.
Certificate name mismatchthe certificate is for another nameConnect by a host name the certificate lists.
CA unreadablethe stored CA is not a certificateReplace it with the PEM text of the CA.
Host not found, Connection refused, Unreachable, Timed outname resolution, nothing listening, no route, or no answerWrong host, wrong port, or a firewall between this server and the directory.
Search base not found, Malformed DN, No read access, Filter does not parseresults 32, 34 and 50 during the search, or a filter the client cannot parseFix the search base, the service account’s rights, or the filter’s parentheses.
Directory unavailableresults 51 or 52The directory is busy. Try again shortly.