Plugin Store · Identity provider
Directory username + password → a budgeted key.
Overview
The LDAP / Active Directory method is for shops with no cloud identity provider: developers get a busbar key by signing in with the directory username and password they already have. busbar hosts token-exchange itself: a dev ends up with their own static, self-scoped key, budget auto-provisioned from the team’s child_default, the secret shown on sign-in.
LDAP is a credential flow: busbar shows a username/password form on the login page, and the plugin does the work directly: no browser redirect to an external provider, no token. It opens its own LDAP/LDAPS connection to your directory, performs a BIND with the user’s DN + password (that bind is the credential check), and reads the user’s group memberships. There is no client secret: the plugin never brokers a third-party app credential, it just binds to your directory.
On a successful bind the module reads the group-membership attribute (memberOf by default) off the user entry and turns each group DN into a role: by default the group’s CN (so CN=engineers,OU=… → engineers), or the full lowercased DN with role_from: dn. The principal is ldap:<username> with those group roles, which role_bindings.ldap maps to a busbar team, its budget and pools.
Define the provider under identity-providers: with module: ldap (the map key is the provider instance name, which is what role_bindings keys off). A browser_login: {} block, present but empty, is what puts the sign-in form on GET /auth/token; it carries no client_secret, because a credential method has no third-party app secret to hold. It’s one key per person, not a key list: re-login just shows a dev their same key again, next to a Refresh button that rotates it (the old key dies, the new one takes its place). How long an issued key stays valid is auth.key_ttl (default 90d): an admin-set config value, never a per-dev choice. See How token-exchange works for the shared token system behind this.
Two bind shapes are supported; see the table below.
Bind modes
| Mode | Setting | When to use |
|---|---|---|
| Direct bind | bind_dn_template with a {username} placeholder: uid={username},ou=people,dc=corp,dc=example, or the AD UPN form {username}@corp.example | The login name maps straight to a DN. |
| Search-then-bind | user_search_filter, e.g. (sAMAccountName={username}), plus a service account | The login name isn’t the DN (common on AD), so busbar binds the service account, finds the user’s entry, then binds as the DN it found. |
Install it
The LDAP provider ships as a signed kind: auth plugin. Let busbar auto-download it on boot: add it to plugins.fetch (verified by sha256 + ed25519 signature + allowlist before load), set your public_url, and define the provider:
public_url: "https://busbar.example.com" # busbar builds /auth/token itself
plugins:
enabled: true
fetch:
- github: GetBusbar/[email protected] # auto-downloaded, version-pinned
identity-providers:
ldap: # map key == module name: must be "ldap"
module: ldap
settings:
url: "ldaps://dc.corp.example:636" # LDAPS (implicit TLS)
bind_dn_template: "{username}@corp.example" # {username} is required in the template
base_dn: "dc=corp,dc=example"
group_attr: "memberOf" # default; each value is a group DN
role_from: dn # key role_bindings on full group DNs
ca_cert_pem: { file: /etc/busbar/corp-ca.pem } # private AD CA (optional)
browser_login: {} # present-but-empty = show the login form; NO client_secret
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys] # devs' issued keys authenticate here
key_ttl: 90d # admin-set; default 90d, no dev choice
role_bindings:
ldap:
"CN=engineers,OU=Groups,DC=corp,DC=example": { group: engineering } New in busbar 1.5.2 (token-exchange). The plugin is independently versioned from busbar itself; pin both explicitly in production (the @1.0.0 in fetch is the pin).
Prefer to install by hand? No plugins.fetch config needed: download the signed tarball (links above), drop it in your plugins.dir, and busbar loads it at boot; signature and trust checks still apply. You can also push it live over the admin API. See the plugin install docs.
Settings
| Setting | Required | Default | Description |
|---|---|---|---|
url | Yes | — | Directory URL: ldaps://ad.corp.example:636 (LDAPS), or ldap://ad.corp.example:389 (plaintext, or STARTTLS when start_tls is set). |
bind_dn_template | Yes | — | Template that turns a username into the bind DN; must contain {username} (validated at boot). RFC4519: uid={username},ou=people,dc=corp,dc=example; AD UPN: {username}@corp.example. With user_search_filter set, search-then-bind is used instead. |
base_dn | Yes | — | Search base for the group read (and for the search-then-bind user lookup), e.g. dc=corp,dc=example. |
group_attr | No | memberOf | The attribute on the user entry that lists group memberships; each value is a group DN. |
role_from | No | cn | How a group DN becomes a role string: cn uses the first CN= RDN (CN=engineers,OU=… → engineers); dn uses the full lowercased DN verbatim. This is the shape of the keys under role_bindings.ldap. |
user_search_filter | No | — | Optional search-then-bind filter with {username}, e.g. (sAMAccountName={username}). When set, busbar binds the service account, finds the user entry, then binds as the found DN. Requires bind_service_dn. |
bind_service_dn | No | — | Service-account DN used to bind for the search (only when user_search_filter is set). |
bind_service_password | No | — | Service-account password for search-then-bind. Note: there is no secret-resolution seam for a plugin-opened connection, so this is a raw string in the module settings today (documented ABI gap). |
ca_cert_pem | No | — | PEM CA bundle to trust for LDAPS / STARTTLS (a private AD CA), passed through as an opaque module setting. |
start_tls | No | false | Use STARTTLS over an ldap:// connection instead of implicit LDAPS. |
timeout_secs | No | 10 | Connect / operation timeout in seconds. |
browser_login | No | — | Present-but-empty ({}) puts the username/password form on GET /auth/token. A credential method carries no client_secret inside it (there is no third-party app secret to hold). |
Configuration scenarios
When to use: The common on-prem case: developers authenticate against Active Directory over LDAPS, and AD group membership (via memberOf) decides which busbar team (and budget) they land on.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/[email protected]
identity-providers:
ldap:
module: ldap
settings:
url: "ldaps://dc.corp.example:636" # LDAPS, implicit TLS on 636
bind_dn_template: "{username}@corp.example" # AD UPN bind
base_dn: "dc=corp,dc=example"
group_attr: "memberOf" # default; each value is a group DN
role_from: dn # key role_bindings on full group DNs
browser_login: {} # show the login form; no client_secret
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys]
key_ttl: 90d
role_bindings:
ldap:
"CN=engineers,OU=Groups,DC=corp,DC=example": { group: engineering }
"CN=platform,OU=Groups,DC=corp,DC=example": { group: platform } A dev opens GET /auth/token, enters their AD username + password, and the plugin binds to the domain controller as <username>@corp.example. On success it reads memberOf and, with role_from: dn, uses the full lowercased group DN as the role, so the role_bindings.ldap keys are the group DNs shown. Identity is ldap:<username>; the dev gets their own budgeted key, re-shown on every later sign-in with a Refresh button.
When to use: A non-AD directory (OpenLDAP, 389 Directory Server, …) where the login name maps straight to a DN. Use a bind-DN template and the default CN-based role mapping; no service account needed.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/[email protected]
identity-providers:
ldap:
module: ldap
settings:
url: "ldap://ldap.corp.example:389"
start_tls: true # upgrade the plaintext connection to TLS
bind_dn_template: "uid={username},ou=people,dc=corp,dc=example"
base_dn: "dc=corp,dc=example"
group_attr: "memberOf"
# role_from defaults to "cn" → CN=engineers,ou=… becomes the role "engineers"
browser_login: {}
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys]
key_ttl: 90d
role_bindings:
ldap:
"engineers": { group: engineering } # default role_from: cn → short CN keys The dev’s username is substituted into bind_dn_template ({username} is required and validated at boot) and the plugin binds directly as that DN, with no service account. With the default role_from: cn, each group DN collapses to its first CN value, so role_bindings.ldap keys are short names like engineers. When the login name isn’t the DN, set user_search_filter (e.g. (uid={username})) plus bind_service_dn for search-then-bind instead.
When to use: Your directory’s LDAPS certificate chains to a private/internal CA that isn’t in the public trust store. Trust that CA explicitly so the TLS bind succeeds.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/[email protected]
identity-providers:
ldap:
module: ldap
settings:
url: "ldaps://dc.corp.example:636" # implicit LDAPS on 636
bind_dn_template: "{username}@corp.example"
base_dn: "dc=corp,dc=example"
group_attr: "memberOf"
role_from: dn
ca_cert_pem: { file: /etc/busbar/corp-root-ca.pem } # private AD/LDAPS CA, PEM
browser_login: {}
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys]
key_ttl: 90d
role_bindings:
ldap:
"CN=engineers,OU=Groups,DC=corp,DC=example": { group: engineering } ca_cert_pem layers your private CA onto the trust store so the LDAPS bind validates against a certificate the public roots don’t chain to. For a directory that speaks STARTTLS on the plaintext port instead of implicit LDAPS, use url: ldap://…:389 with start_tls: true and the same ca_cert_pem. This is the direct-bind shape (login name maps to a DN); combine it with search-then-bind (next tab) if your login name isn’t the DN.
When to use: The login name (e.g. sAMAccountName) isn’t the DN, so you can’t template a bind DN directly. A service account locates the user’s entry first, then the plugin binds as the DN it found: the common AD pattern.
public_url: "https://busbar.example.com"
plugins:
enabled: true
fetch:
- github: GetBusbar/[email protected]
identity-providers:
ldap:
module: ldap
settings:
url: "ldaps://dc.corp.example:636"
bind_dn_template: "{username}@corp.example" # still required (must contain {username})
base_dn: "dc=corp,dc=example" # search base for the user lookup
user_search_filter: "(sAMAccountName={username})" # set → search-then-bind
bind_service_dn: "CN=busbar-svc,OU=Service,DC=corp,DC=example"
bind_service_password: { env: LDAP_SVC_PASSWORD }
role_from: dn
browser_login: {}
auth:
signing_key: { file: /run/secrets/busbar-signing.key }
chain: [keys]
key_ttl: 90d
role_bindings:
ldap:
"CN=engineers,OU=Groups,DC=corp,DC=example": { group: engineering } Setting user_search_filter switches on search-then-bind: busbar binds as bind_service_dn, searches base_dn for the user entry, then binds as the DN it found with the dev’s password (that bind is still the credential check). Both user_search_filter and bind_dn_template must contain {username}, and bind_service_dn is required when the filter is set; all validated at boot. Note: bind_service_password is a raw module setting: there’s no core secret-resolution seam for a plugin-opened socket, so a { env: } reference here resolves at the busbar-config layer, not inside the plugin (documented ABI gap).