Skip to content

LDAP and Active Directory

github-actions[bot] edited this page Sep 2, 2026 · 3 revisions

LDAP and Active Directory

LDAP authentication is optional and disabled by default. The standard Compose deployment creates no LDAP volume, mount, helper, route, or background job. Opt in with docker-compose.ldap.yml.

Security model

  • ldap:// always upgrades with StartTLS before any bind.
  • ldaps:// starts TLS immediately.
  • Plain LDAP authentication and disabled certificate verification are not supported.
  • A least-privilege service account performs read-only user searches.
  • The submitted user password is used only for the final user bind and is never stored.
  • Usernames are escaped as RFC 4515 filter values.
  • Searches are subtree-scoped, limited to two results, and must resolve to exactly one entry.
  • With the safe default LDAP_AUTO_PROVISION=false, an administrator links a stable directory identity to an existing WebSSH account. Explicit opt-in can create a non-admin account only after successful directory authentication; username-only trust is never sufficient.
  • Linked LDAP users cannot be administrators and cannot fall back to a local password or OIDC. After recent directory verification they may enroll local Passkey or TOTP second factors and receive second-factor Recovery Codes.
  • LDAP sessions are periodically revalidated and fail closed.

Keep at least one unlinked local break-glass administrator.

Directory information required

Collect these values from the directory administrator:

  1. An LDAP server DNS name covered by the server certificate.
  2. ldap://host:389 with mandatory StartTLS or ldaps://host:636.
  3. The user search base DN.
  4. A read-only bind DN and password.
  5. A PEM CA bundle containing the issuer chain needed by WebSSH.
  6. A user filter containing exactly one literal {username} placeholder.
  7. A stable unique-ID attribute: usually entryUUID for OpenLDAP or objectGUID for Active Directory.
  8. The directory's real disabled or locked account rule.

DNS and system time must work inside the WebSSH container. Do not use an IP address when the certificate contains only a DNS name.

For example, use ldap://ldap.example.com:389 with mandatory StartTLS or ldaps://ldap.example.com:636 for TLS from connection start.

Configure the Compose overlay

Edit docker-compose.ldap.yml and fill every empty directory value. Use the base and LDAP files from the same WebSSH release or commit.

Active Directory example

services:
  webssh:
    environment:
      LDAP_ENABLED: "true"
      LDAP_PROVIDER_ID: corp-ad
      LDAP_URL: ldaps://dc01.ad.example.com:636
      LDAP_BACKUP_URL: ldaps://dc02.ad.example.com:636
      LDAP_BASE_DN: OU=People,DC=ad,DC=example,DC=com
      LDAP_BIND_DN: CN=svc-webssh,OU=Service Accounts,DC=ad,DC=example,DC=com
      LDAP_USER_FILTER: "(&(objectCategory=person)(objectClass=user)(sAMAccountName={username})(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
      LDAP_UNIQUE_ID_ATTRIBUTE: objectGUID

The final filter clause excludes disabled AD accounts. If access is restricted to a group, use a directory-approved memberOf rule. Nested group semantics vary and must be validated by the AD administrator.

LDAP_BACKUP_URL is optional and must identify another server for the same logical directory. It is tried only after the primary endpoint is unavailable. WebSSH connects to each URL directly, so every endpoint DNS name must match its own TLS certificate and be trusted by LDAP_CA_FILE; a round-robin alias is not needed. Invalid user credentials are not retried against the backup endpoint.

OpenLDAP example

services:
  webssh:
    environment:
      LDAP_ENABLED: "true"
      LDAP_PROVIDER_ID: primary-openldap
      LDAP_URL: ldap://ldap.example.com:389
      # LDAP_BACKUP_URL: ldap://ldap-backup.example.com:389
      LDAP_BASE_DN: ou=people,dc=example,dc=com
      LDAP_BIND_DN: cn=svc-webssh,ou=services,dc=example,dc=com
      LDAP_USER_FILTER: "(&(objectClass=inetOrgPerson)(uid={username})(!(pwdAccountLockedTime=*)))"
      LDAP_UNIQUE_ID_ATTRIBUTE: entryUUID

Remove or replace the pwdAccountLockedTime clause if that operational attribute is not available.

Configuration reference

Variable Default Requirement
LDAP_ENABLED false Enable the subsystem
LDAP_PROVIDER_ID default Stable 1-64 character local provider identifier
LDAP_URL empty Exact ldap:// or ldaps:// server URL
LDAP_BACKUP_URL empty Optional second URL for the same directory, tried after transport failure
LDAP_BASE_DN empty User subtree base
LDAP_BIND_DN empty Least-privilege search account DN
LDAP_BIND_PASSWORD_FILE /run/webssh-auth/ldap_bind_password Absolute private file path
LDAP_CA_FILE /run/webssh-auth/ldap_ca.pem Absolute PEM CA bundle path
LDAP_USER_FILTER empty Exactly one {username} placeholder
LDAP_UNIQUE_ID_ATTRIBUTE empty LDAP attribute name or numeric OID
LDAP_CONNECT_TIMEOUT 5 1-15 seconds
LDAP_OPERATION_TIMEOUT 5 1-30 seconds
LDAP_SESSION_REVALIDATION_SECONDS 300 60-3600 seconds
LDAP_LOGIN_RATE_LIMIT 5 per minute Per-IP login and diagnostic limit

When LDAP is disabled, WebSSH does not read the secret files.

LDAP_ENABLED=true is only the deployment ceiling. After the container starts with a valid directory configuration, sign in with the local break-glass administrator and activate LDAP under Admin → Settings → Authentication features. If Compose leaves LDAP disabled, the Admin toggle is locked and cannot create a provider at runtime.

Populate the secret volume

The overlay creates webssh_auth_secrets. WebSSH mounts it read-only at /run/webssh-auth. The helper service is the only Compose service with write access; it has no network, runs read-only apart from the volume, drops all capabilities, and uses no-new-privileges.

Set the bind password with a hidden interactive prompt:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ldap.yml \
  --profile ldap-tools \
  run --rm ldap-tools set-password

Install and validate the CA bundle on Linux or macOS:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ldap.yml \
  --profile ldap-tools \
  run --rm -T ldap-tools install-ca --stdin < company-ca.pem

PowerShell:

Get-Content -Raw .\company-ca.pem | docker compose `
  -f docker-compose.yml `
  -f docker-compose.ldap.yml `
  --profile ldap-tools `
  run --rm -T ldap-tools install-ca --stdin

Check only file presence; contents are never printed:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ldap.yml \
  --profile ldap-tools \
  run --rm ldap-tools status

The helper validates input, caps file sizes, writes atomically, and applies private permissions. It remains usable when incomplete LDAP configuration stops the Flask app from starting.

Activate LDAP

  1. Confirm every empty overlay value is filled.

  2. Populate the bind password and CA.

  3. Inspect the effective configuration:

    docker compose \
      -f docker-compose.yml \
      -f docker-compose.ldap.yml \
      config
  4. Start or recreate WebSSH:

    docker compose \
      -f docker-compose.yml \
      -f docker-compose.ldap.yml \
      up -d
  5. Review startup logs:

    docker compose \
      -f docker-compose.yml \
      -f docker-compose.ldap.yml \
      logs webssh

Unsafe URL, secret, CA, filter, attribute, and timeout settings stop startup. After readiness succeeds, activate LDAP in the Admin Panel. The login form does not appear until deployment allowance, readiness, and Admin activation are all true.

For production, apply the production overlay last:

export WEBSSH_ORIGIN=https://ssh.example.com
docker compose \
  -f docker-compose.yml \
  -f docker-compose.ldap.yml \
  -f docker-compose.production.yml \
  up -d

Link users

With LDAP_AUTO_PROVISION=false (recommended for controlled rollouts):

  1. Sign in with the local break-glass administrator.
  2. Open Admin → Settings → LDAP directory.
  3. Run Check connection. The browser receives only readiness category, transport, and provider ID, never secrets.
  4. Create the target standard WebSSH account if it does not exist.
  5. On the Users tab, select Link LDAP.
  6. Complete the administrator Step-up prompt, then enter the directory username and exact target WebSSH username.
  7. Sign out and test Sign in with LDAP.

Start with one non-admin pilot.

For larger directories, LDAP_AUTO_PROVISION=true creates a non-admin, LDAP-managed WebSSH account only after the first successful directory password bind. Automatic provisioning never claims an existing local username, never creates an administrator, rejects invalid or overlong names without silently truncating them, and does not automatically delete stored data when a directory account later disappears.

Linking stores the stable provider and subject plus the current DN and directory username. A renamed DN can be updated after successful authentication as long as the stable ID still matches.

Linking destroys the account's dormant local password and removes an incompatible OIDC mapping. Existing local MFA factors remain owned by the same WebSSH account and can protect LDAP primary login. The link operation revokes the target account's active access so the new identity boundary applies on its next login.

LDAP with optional MFA

LDAP proves the primary credential only. If the WebSSH account has not enabled MFA, successful directory verification completes login as before. If the user has enabled MFA, WebSSH creates a short-lived pending transaction and offers the account's active Passkey, TOTP, and Recovery methods. The LDAP password is discarded after the bind and is never stored for the second step.

An LDAP-managed user can enroll a Passkey or authenticator app on Security after a recent successful directory login. Recovery Codes can then recover the second factor, but only after LDAP primary verification succeeds.

Session revalidation

Every linked account is revalidated at the configured interval. WebSSH revokes browser, Socket.IO, SSH, transfer, and pooled-connection access when:

  • LDAP is disabled;
  • the directory is unavailable;
  • certificate validation fails;
  • the user disappears;
  • the stable ID changes;
  • the account no longer matches the filter;
  • the mapping is missing or no longer eligible.

An LDAP outage therefore signs users out by design.

Disable LDAP

Omit only docker-compose.ldap.yml when recreating WebSSH. For a homelab deployment, use the base file:

docker compose \
  -f docker-compose.yml \
  up -d --force-recreate

For a production deployment, retain the production overlay:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.production.yml \
  up -d --force-recreate

This removes LDAP environment and mounts without dropping the production security profile. The named secret volume remains detached, new LDAP logins stop, and linked users do not regain old passwords. Container recreation closes open WebSSH and SSH sessions, so schedule this change as maintenance.

Return one user to local authentication

While LDAP is working, choose Manage LDAP, complete administrator Step-up, provide the exact target username and a new local password, then unlink the identity. The new password establishes a fresh local credential.

Remove LDAP secrets

After all identities are unlinked and LDAP is disabled:

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ldap.yml \
  --profile ldap-tools \
  run --rm ldap-tools remove

Do not delete the normal WebSSH data volume. LDAP mappings live in SQLite and are covered by native backup/restore. The bind password and CA remain in the separate volume and are intentionally excluded.

Troubleshooting

Application refuses to start

Run the standalone status helper, then read the exact configuration error in the WebSSH logs.

Certificate failure

Verify DNS, certificate SAN, system time, the complete issuer chain, and PEM encoding. DER and PKCS#12 files are not CA bundles for this setting.

Zero or multiple search results

Test the search base and filter with the directory administrator. WebSSH never selects one entry from an ambiguous result.

AD user is found but bind fails

Check disabled, locked, expired, and logon-restricted state plus the domain controller's accepted username flow.

LDAP outage signs users out

Restore directory and TLS service. Periodic directory revalidation is intentionally fail-closed and revokes the affected LDAP-managed account when the directory cannot verify it. Do not add a password fallback.

Provider ID changed

Restore the original LDAP_PROVIDER_ID. It is part of every mapping and must remain stable for the lifetime of that directory integration.

Disposable OpenLDAP laboratory

The integration lab validates generic LDAP behavior without an AD domain:

docker compose \
  -f tests/integration/ldap/docker-compose.yml \
  up --build

It exposes WebSSH on http://localhost:5050 and uses test-only passwords. Never deploy it as production infrastructure.

The lab does not replace AD-specific acceptance testing for objectGUID, disabled-account filters, certificate enrollment, or domain-controller policy.

Clone this wiki locally