LDAP and Active Directory
Use LDAP when a directory is authoritative but cannot provide OIDC. Jiandu searches for exactly one entry using an escaped username, then verifies the submitted password by binding as that entry. It stores only a provider-scoped immutable subject and local presentation metadata—not the password, search-bind credential, email address, or directory roles.
OIDC is preferable when available because users authenticate at the identity provider and Jiandu does not handle their directory password. LDAP remains a sound compatibility option when the TLS, search, and immutable-attribute boundaries below are explicit.
1. Choose encrypted transport
Use exactly one of these modes:
| Directory endpoint | Jiandu setting | When to choose it |
|---|---|---|
| LDAP over TLS, normally port 636 | "url": "ldaps://directory.example.net:636", "start_tls": false | Simplest dedicated TLS listener |
| StartTLS, normally port 389 | "url": "ldap://directory.example.net:389", "start_tls": true | Existing LDAP listener explicitly upgraded before credentials are sent |
Plain ldap:// without StartTLS is rejected. ldaps:// with start_tls: true is also rejected
because it attempts two incompatible TLS modes. The URL may contain only scheme, host, and optional
port—no embedded credentials, base DN, query, or fragment.
Certificate verification cannot be disabled. Jiandu loads native system trust roots, so install a
private directory CA in the operating system/container trust store and restart the process. The
certificate must cover the hostname in url; using an IP address when only a DNS name is certified
will fail.
2. Choose the search and identity attributes
| Purpose | OpenLDAP-style choice | Active Directory choice | Caveat |
|---|---|---|---|
| Search base | ou=people,dc=example,dc=net | ou=Users,dc=example,dc=net | Search is subtree-scoped; make the base as narrow as practical |
| Username | uid | sAMAccountName or userPrincipalName | Must identify exactly one entry under the base |
| Immutable subject | entryUUID | objectGUID | Never use username, mail, or DN: they can change |
| Display name | displayName or cn | displayName | Optional; falls back to submitted username |
| Admission groups | commonly memberOf | memberOf | Values are compared exactly as returned, usually full DNs |
Jiandu supports Active Directory's binary objectGUID encoding directly. Microsoft documents
objectGUID as a single-valued system attribute that is not normally modified; see the
attribute specification.
The optional additional_user_filter must be one complete parenthesized LDAP filter. Jiandu ANDs it
with the escaped username assertion. For example, input alex with username attribute uid and
(objectClass=person) produces the logical filter:
(&(uid=alex)(objectClass=person))
Submitted LDAP metacharacters are escaped. Search results are limited to two and anything other than exactly one entry fails closed, so duplicate usernames cannot resolve nondeterministically.
3. Decide whether a search bind is needed
If anonymous users may search the narrow base and read only the selected subject, name, and group attributes, omit both bind fields. Otherwise create a read-only service account with no password reset or directory-write privileges, store its password in a protected file, and configure both fields together:
umask 077
install -d -m 0700 /etc/jiandu/secrets
read -r -s ldap_bind_password
printf '%s\n' "$ldap_bind_password" > /etc/jiandu/secrets/ldap-bind-password
unset ldap_bind_password
chmod 0600 /etc/jiandu/secrets/ldap-bind-password
Jiandu requires exact Unix mode 0600, one regular link, and no more than 4 KiB. The search account
binds first; after one entry is found, Jiandu binds as that entry with the person's submitted
password. A successful service bind alone never authenticates a person.
OpenLDAP example
{
"schema_version": 1,
"auth": {
"providers": [
{
"kind": "ldap",
"id": "openldap",
"display_name": "Household directory",
"url": "ldaps://directory.example.net:636",
"start_tls": false,
"bind_dn": "cn=jiandu,ou=services,dc=example,dc=net",
"bind_password_path": "/etc/jiandu/secrets/ldap-bind-password",
"user_base_dn": "ou=people,dc=example,dc=net",
"username_attribute": "uid",
"subject_attribute": "entryUUID",
"display_name_attribute": "displayName",
"additional_user_filter": "(objectClass=inetOrgPerson)",
"admission_policy": "required_group",
"group_attribute": "memberOf",
"admission_group": "cn=jiandu-members,ou=groups,dc=example,dc=net"
}
]
}
}
Many OpenLDAP deployments expose memberOf only when the memberOf overlay is enabled and existing
membership data has been maintained accordingly. If your directory does not return it on the user
entry, either configure a suitable user attribute, use invitation-only admission, or prefer an OIDC
provider that can perform group policy. Jiandu does not perform a second reverse-membership query.
For StartTLS, change only the transport pair:
"url": "ldap://directory.example.net:389",
"start_tls": true
Active Directory example
{
"schema_version": 1,
"auth": {
"providers": [
{
"kind": "ldap",
"id": "active-directory",
"display_name": "Active Directory",
"url": "ldaps://dc01.example.net:636",
"start_tls": false,
"bind_dn": "CN=Jiandu Search,OU=Service Accounts,DC=example,DC=net",
"bind_password_path": "/etc/jiandu/secrets/ad-bind-password",
"user_base_dn": "OU=Users,DC=example,DC=net",
"username_attribute": "sAMAccountName",
"subject_attribute": "objectGUID",
"display_name_attribute": "displayName",
"additional_user_filter": "(&(objectCategory=person)(objectClass=user))",
"admission_policy": "required_group",
"group_attribute": "memberOf",
"admission_group": "CN=Jiandu Members,OU=Security Groups,DC=example,DC=net"
}
]
}
}
Use userPrincipalName instead when members should enter [email protected]; the submitted value is
matched against the configured attribute exactly. Active Directory's memberOf normally lists
direct membership and returns distinguished names. Nested-group expansion is not performed by
Jiandu, so use a directly assigned admission group or an attribute maintained by the directory.
Do not use a Global Catalog port (3268/3269) merely because it is available. A Global Catalog
returns only replicated attributes and has forest-wide search semantics; use it only after verifying
that the chosen immutable/group attributes are present and the search base is intentionally scoped.
Invitation-only variant
For the safest first test, remove group_attribute and admission_group and use:
"admission_policy": "invitation_only"
LDAP does not support open admission. After the identity has consumed a matching invitation, later
sign-ins resolve the same immutable subject without another invitation. If you later switch to
required_group, the exact group check applies to every sign-in, including existing identities.
Validate and diagnose
Test TLS and the service search from the same host/container trust environment as Jiandu before
restart. Keep passwords out of command arguments; interactive ldapsearch -W is preferable:
ldapsearch -x -W \
-H ldaps://directory.example.net:636 \
-D 'cn=jiandu,ou=services,dc=example,dc=net' \
-b 'ou=people,dc=example,dc=net' \
'(&(uid=alex)(objectClass=inetOrgPerson))' \
entryUUID displayName memberOf
jiandu --config /etc/jiandu/jiandu.json --check-config
curl --fail --silent https://documents.example.net/api/v1/auth/methods \
| jq '.ldapProviders'
| Symptom | Check |
|---|---|
| Provider absent after restart | JSON is effective, provider ID is valid, and all configured providers initialized |
| Temporary/unavailable error | DNS, TCP reachability, system CA trust, hostname verification, service bind, or directory timeout |
| Every password is invalid | Search base/filter/username attribute returns exactly one entry and the user DN allows simple bind |
| Admission denied | Exact returned group DN or valid invitation; direct versus nested membership |
| A renamed directory user becomes a new person | subject_attribute was mutable or stopped being returned; restore the immutable attribute mapping before linking anything |
Jiandu bounds the whole directory operation to 20 seconds, connections to 5 seconds, and directory search time to 5 seconds. Local Owner/password recovery remains usable during a directory outage.