Plugin Store · Identity provider

LDAP / Active Directory

Docs GitHub
v1.0.2

Directory username + password → a budgeted key.

What it is A directory username + password becomes the way developers self-serve a busbar key.
When to use it On-prem Active Directory / LDAP, no cloud OIDC identity provider.
CategoryIdentity provider StatusAvailable

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

ModeSettingWhen to use
Direct bindbind_dn_template with a {username} placeholder: uid={username},ou=people,dc=corp,dc=example, or the AD UPN form {username}@corp.exampleThe login name maps straight to a DN.
Search-then-binduser_search_filter, e.g. (sAMAccountName={username}), plus a service accountThe 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

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

← Back to the Plugin Store