Skip to content

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/.

Key Type Default
ldap.enabled bool false
ldap.url string empty
ldap.bind_dn string empty
ldap.bind_password string empty
ldap.base_dn string empty
ldap.user_filter string empty
ldap.fields map
ldap.fields.<name>.attribute string empty
ldap.fields.<name>.searches list empty
ldap.fields.<name>.searches[].name string empty
ldap.fields.<name>.searches[].base_dn string empty
ldap.fields.<name>.searches[].filter string empty
ldap.fields.<name>.searches[].value string empty
ldap.schema string empty
ldap.id_attribute string empty
ldap.group_name_attribute string empty
ldap.authoritative_role_groups bool false
ldap.sync.interval duration 15m
ldap.sync.disable_after duration 45m
ldap.sync.reenable bool true
ldap.sync.extra_groups list empty
ldap.limits.max_values_per_attribute int 1000
ldap.limits.max_entries_per_search int 1000
ldap.limits.max_attributes_bytes int 65536
ldap.timeout duration 5s
ldap.start_tls bool false
ldap.tls_ca string empty
ldap.tls_insecure_skip_verify bool false
ldap.logging.*
ldap.logging.level string empty
ldap.logging.add_source bool false
ldap.logging.log_json bool false

bool, default false

Turns on the login-time lookup and the background sync.

ldap:
enabled: false

string, default empty

The directory server to connect to, e.g. ldaps://ldap.example.net.

ldap:
url: ""

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: ""

string, default empty

The password for bind_dn.

ldap:
bind_password: ""

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: ""

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: ""

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>.

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: memberOf

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.

string, default empty

Labels the search in logs and errors.

ldap:
fields:
<name>:
searches:
- name: linked-accounts

string, default empty

The search base. Empty inherits ldap.base_dn.

ldap:
fields:
<name>:
searches:
- base_dn: ou=People,dc=example,dc=net

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}})

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: uid

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: rfc2307bis

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: entryUUID

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: ""

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: false

Caps what one directory can push into the database.

int, default 1000

Caps a single multi-valued attribute.

ldap:
limits:
max_values_per_attribute: 1000

int, default 1000

Caps the matched entries one field search contributes.

ldap:
limits:
max_entries_per_search: 1000

int, default 65536

Caps the serialized user_ldap.attributes JSON.

ldap:
limits:
max_attributes_bytes: 65536

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: 5s

bool, default false

Upgrades a plain ldap:// connection to TLS. Irrelevant for ldaps:// URLs, which are TLS from the start.

ldap:
start_tls: false

string, default empty

A PEM bundle path to verify the directory’s certificate against. Empty uses the system roots.

ldap:
tls_ca: ""

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