ldap
LDAP is enrichment, never a requirement. If directory data is available a user gets more — extra principals, fresher groups, auto-disable coverage. If it is not, everything still works. Login therefore fails open: an LDAP error during the callback logs to the LDAP destination and proceeds with the OIDC-only identity. There is deliberately no required knob. See https://mnestor.github.io/ssoossh/operations/ldap/.
enabled
Section titled “enabled”bool, default false
Turns on the login-time lookup and the background sync.
ldap: enabled: falsestring, default empty
The directory server to connect to, e.g. ldaps://ldap.example.net.
ldap: url: ""bind_dn
Section titled “bind_dn”string, default empty
The DN to bind as for the search. Together with bind_password this is the implicit “simple” bind mechanism; a keytab-based SASL GSSAPI alternative is tracked separately and slots in beside these keys rather than replacing them.
ldap: bind_dn: ""bind_password
Section titled “bind_password”string, default empty
The password for bind_dn.
ldap: bind_password: ""base_dn
Section titled “base_dn”string, default empty
The search base for the user lookup, and the default base for field searches that do not name their own.
ldap: base_dn: ""user_filter
Section titled “user_filter”string, default empty
A Go template over the OIDC identity, with the same idiom as key ID templates: {{.Username}}, {{.Email}}, {{.Subject}}, and {{.Extra.<name>}}. Values are RFC 4515 escaped during rendering and the operator cannot opt out, because a preferred_username containing * or ) is otherwise filter injection.
The filter must reference at least one of those fields, or the server refuses to start. A filter with none renders the same for every login: either it matches several entries and every lookup fails, or it matches one and links everyone to that entry, handing its accounts and groups to every login. No ldap.schema preset sets this key. For lldap, use (&(objectClass=person)(uid={{.Username}})): the objectClass term matters, because lldap also answers (uid=<name>) with the group entry cn=<name>,ou=groups.
The check is static. (|(objectClass=person)(uid={{.Username}})) passes it and still matches everyone. Login fails closed when more than one entry matches, and ssoosshd ldap probe lists the entries a filter matched.
ldap: user_filter: ""fields
Section titled “fields”map
Maps destinations to the directory sources that populate them, mirroring OAuthFields with attribute names instead of claim names.
The reserved names are other_accounts, service_accounts, groups and name. Any other key is an extra template field, captured into the same contract as OAuthFields.Extra: reachable as {{.Extra.<name>}}, stored empty when absent, and never a reason for login to fail. There is no separate extra sub-map; LDAP enrichment is extra by definition.
The merge rule is per field: a configured LDAP field (any attribute or searches) wins over the OIDC value, and an unconfigured one leaves the OIDC value untouched. Override rather than union, because union makes it impossible to retire a stale principal from only one source. Groups are the exception — both sources persist side by side.
username, email and subject are rejected here: the subject keys the user row, the username is what lookups are keyed by, and the OIDC email claim is the source of truth for users.email. The person’s display name is not in that set — see the name destination above.
A bare string in YAML is shorthand for attribute: <string>.
fields.<name>.attribute
Section titled “fields.<name>.attribute”string, default empty
Reads the value from the person’s own entry — a forward list, the simplest of the linking topologies.
ldap: fields: <name>: attribute: memberOffields.<name>.searches
Section titled “fields.<name>.searches”list, default empty
Resolve linked accounts that are their own directory entries. They run after the primary lookup and are keyed by filter rather than by DN, so they must re-run at sync time: a reverse link can change without the person’s own entry changing, which is much of what the sync exists to catch.
fields.<name>.searches[].name
Section titled “fields.<name>.searches[].name”string, default empty
Labels the search in logs and errors.
ldap: fields: <name>: searches: - name: linked-accountsfields.<name>.searches[].base_dn
Section titled “fields.<name>.searches[].base_dn”string, default empty
The search base. Empty inherits ldap.base_dn.
ldap: fields: <name>: searches: - base_dn: ou=People,dc=example,dc=netfields.<name>.searches[].filter
Section titled “fields.<name>.searches[].filter”string, default empty
A Go template over the OIDC identity plus {{.DN}} and {{.Attr.<name>}} from the primary entry. Values are RFC 4515 escaped during rendering and the operator cannot opt out: a preferred_username containing * or ) is otherwise filter injection.
ldap: fields: <name>: searches: - filter: (manager={{.DN}})fields.<name>.searches[].value
Section titled “fields.<name>.searches[].value”string, default empty
Names the attribute on each matched entry that contributes to the field. It is directory-specific and there is no portable answer: “uid” on OpenLDAP, “sAMAccountName” on Active Directory, which has no uid attribute at all.
Getting it wrong is silent. The search still matches, the attribute is simply absent from the result, and the field resolves empty with nothing logged — because a field search requests only this one attribute, unlike ldapsearch or the probe’s primary lookup, which return the whole entry and so hide the mistake. ssoosshd ldap probe --json shows it as a search with entries but no values; see https://mnestor.github.io/ssoossh/operations/ldap/#reading-the-json.
ldap: fields: <name>: searches: - value: uidschema
Section titled “schema”string, default empty
Selects a preset for the keys whose right value depends on the directory: ldap.id_attribute, nss.user_filter, nss.group_filter, nss.disabled_filter and nss.attributes. One of rfc2307, rfc2307bis, active-directory, lldap or freeipa; any other value fails startup. Empty (the default) applies no preset, and every key keeps its own default. Each preset’s values, and the source each came from, are listed at https://mnestor.github.io/ssoossh/operations/ldap-schema-presets/.
A preset sits between the built-in defaults and the config file: any key the file sets beats the preset, so a preset is a starting point rather than a lock. Setting nss.attributes.shell to an attribute of your own with active-directory selected keeps every other active-directory value.
Every preset sets ldap.id_attribute. On a database that already holds directory ids read from another attribute (or an earlier ldap.id_attribute), set ldap.id_attribute explicitly to the attribute in use, or start from a fresh database: there is no migration tool yet, and the NSS directory treats an entry whose id changed at the same DN as a new entry where the old one used to be.
ldap: schema: rfc2307bisid_attribute
Section titled “id_attribute”string, default empty
Names the attribute holding the entry’s unique, immutable identifier, and is what makes a directory rename survivable. Empty (the default) keeps the DN-then-filter resolution described below. A preset from ldap.schema may set this.
Every directory has one under a different name: entryUUID on OpenLDAP and 389 Directory Server (RFC 4530), objectGUID on Active Directory, ipaUniqueID on FreeIPA. None of them is guessable from the others, so there is no useful default beyond off.
With it set, the sync resolves an entry by ID first, falling back to the stored DN and then to user_filter. That ordering is the point: a DN changes when someone moves between OUs and user_filter stops matching when they are renamed, so without an ID a rename looks exactly like a deletion and walks the account toward the sync.disable_after auto-disable. The ID is read at login, stored on user_ldap.directory_id, and never used for authorization.
Binary-valued attributes (objectGUID) are stored as their hex form behind a 0x marker, and searched with the RFC 4515 escape of each byte.
Changing this on a database that already holds directory ids, by hand or by picking an ldap.schema preset, makes every stored id stop matching: there is no migration tool yet. Login enrichment recovers by DN and logs a warning, but the NSS directory treats an entry whose id changed at the same DN as a new entry. Keep the attribute the ids were read from set explicitly, or start from a fresh database.
ldap: id_attribute: entryUUIDgroup_name_attribute
Section titled “group_name_attribute”string, default empty
Names the attribute to read a group’s name from when a group search is used. When groups arrive as DNs (memberOf, the common case), each DN is reduced to its first RDN value — the CN — and a value that does not parse as a DN is kept as-is. The reduced name is what must match the configured group names.
ldap: group_name_attribute: ""authoritative_role_groups
Section titled “authoritative_role_groups”bool, default false
Lets the directory take a role away between logins. Off by default: roles then come from the OIDC groups claim alone, read at login, and the session lifetime is the revocation window for a role.
With it on, a role group counts for a directory-linked user only while the latest directory read for that user still lists it. The role groups are exactly admin.require_group, admin.soc_group, admin.auditor_group and admin.nss_key_group (fleet key admin). The background sync can therefore remove someone’s admin, SOC, auditor or fleet key admin role between logins, effective on their next request; a fresh login is narrowed the same way, and so is who counts as an admin for the admin email.
It narrows only. The directory never grants a role: a group the directory lists but the OIDC claim does not is never added, so a person needs the group in both places to hold the role.
“Linked” means the server has resolved a directory entry for the user (a user_ldap row with a DN or a directory id). A user with no entry, one never matched, or any user while LDAP is off is never narrowed. “The latest directory read” is the LDAP group rows the last successful login lookup or sync pass wrote. A read that failed (the directory unreachable, a search error) rewrites nothing, so an outage never takes a role away. A user the sync finds missing from the directory keeps the rows of their last successful read; ldap.sync.disable_after is what acts on a missing entry.
Names are compared exactly, the way the role checks compare them. Directory groups are stored reduced to their CN (a memberOf DN becomes its first RDN value), so each role group name must be the same string in the identity provider’s groups claim and as the directory group’s CN. An identity provider that emits full DNs cannot be used with this setting: no stored CN would ever equal the configured DN, and every role would be removed.
Only the role groups are narrowed. Lifetime policy group conditions, cert_options require gates and the groups that manage individual NSS keys are not affected, unless they name a role group itself: removing a role group from the identity removes it for every check that reads the identity’s groups, which can only narrow.
A truncated read counts as a successful one. Keep ldap.limits.max_values_per_attribute and ldap.limits.max_entries_per_search above the largest group count a role holder has, or a role group cut off by the cap is removed.
Requires ldap.enabled and a mapped ldap.fields.groups; startup fails otherwise.
ldap: authoritative_role_groups: falselimits
Section titled “limits”Caps what one directory can push into the database.
limits.max_values_per_attribute
Section titled “limits.max_values_per_attribute”int, default 1000
Caps a single multi-valued attribute.
ldap: limits: max_values_per_attribute: 1000limits.max_entries_per_search
Section titled “limits.max_entries_per_search”int, default 1000
Caps the matched entries one field search contributes.
ldap: limits: max_entries_per_search: 1000limits.max_attributes_bytes
Section titled “limits.max_attributes_bytes”int, default 65536
Caps the serialized user_ldap.attributes JSON.
ldap: limits: max_attributes_bytes: 65536timeout
Section titled “timeout”duration, default 5s
Bounds each directory operation. The login callback is on a user-facing path, so this is the entire latency cost enrichment can add to a login.
ldap: timeout: 5sstart_tls
Section titled “start_tls”bool, default false
Upgrades a plain ldap:// connection to TLS. Irrelevant for ldaps:// URLs, which are TLS from the start.
ldap: start_tls: falsetls_ca
Section titled “tls_ca”string, default empty
A PEM bundle path to verify the directory’s certificate against. Empty uses the system roots.
ldap: tls_ca: ""tls_insecure_skip_verify
Section titled “tls_insecure_skip_verify”bool, default false
Disables certificate verification. A homelab escape hatch, logged loudly at startup; it makes the connection trivially interceptable and has no place in production.
ldap: tls_insecure_skip_verify: false