Repository navigation
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.
-
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.
Collect these values from the directory administrator:
- An LDAP server DNS name covered by the server certificate.
-
ldap://host:389with mandatory StartTLS orldaps://host:636. - The user search base DN.
- A read-only bind DN and password.
- A PEM CA bundle containing the issuer chain needed by WebSSH.
- A user filter containing exactly one literal
{username}placeholder. - A stable unique-ID attribute: usually
entryUUIDfor OpenLDAP orobjectGUIDfor Active Directory. - 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.
Edit docker-compose.ldap.yml and fill every empty directory value. Use the
base and LDAP files from the same WebSSH release or commit.
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: objectGUIDThe 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.
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: entryUUIDRemove or replace the pwdAccountLockedTime clause if that operational
attribute is not available.
| 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.
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-passwordInstall 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.pemPowerShell:
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 --stdinCheck 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 statusThe 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.
-
Confirm every empty overlay value is filled.
-
Populate the bind password and CA.
-
Inspect the effective configuration:
docker compose \ -f docker-compose.yml \ -f docker-compose.ldap.yml \ config
-
Start or recreate WebSSH:
docker compose \ -f docker-compose.yml \ -f docker-compose.ldap.yml \ up -d
-
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 -dWith LDAP_AUTO_PROVISION=false (recommended for controlled rollouts):
- Sign in with the local break-glass administrator.
- Open Admin → Settings → LDAP directory.
- Run Check connection. The browser receives only readiness category, transport, and provider ID, never secrets.
- Create the target standard WebSSH account if it does not exist.
- On the Users tab, select Link LDAP.
- Complete the administrator Step-up prompt, then enter the directory username and exact target WebSSH username.
- 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 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.
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.
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-recreateFor a production deployment, retain the production overlay:
docker compose \
-f docker-compose.yml \
-f docker-compose.production.yml \
up -d --force-recreateThis 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.
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.
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 removeDo 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.
Run the standalone status helper, then read the exact configuration error in
the WebSSH logs.
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.
Test the search base and filter with the directory administrator. WebSSH never selects one entry from an ambiguous result.
Check disabled, locked, expired, and logon-restricted state plus the domain controller's accepted username flow.
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.
Restore the original LDAP_PROVIDER_ID. It is part of every mapping and must
remain stable for the lifetime of that directory integration.
The integration lab validates generic LDAP behavior without an AD domain:
docker compose \
-f tests/integration/ldap/docker-compose.yml \
up --buildIt 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.
WebSSH documentation - Project - Issues - Discussions - Security
This Wiki is published from the versioned docs/wiki source. Please propose documentation changes through a pull request.
WebSSH Wiki
Install and operate
- Installation from Source
- Docker and Docker Compose
- Production Deployment
- Reverse Proxy and Subfolder Deployment
- Upgrading, Rollback, and FAQ
Identity and access
- Users and Account Management
- Authentication Overview
- GitHub Authentication
- LDAP and Active Directory
- OpenID Connect
- Passkeys and Recovery Codes
SSH and files
- SSH Connections and Host Keys
- Profiles, Jump Hosts, and Commands
- Terminal and Persistent tmux Sessions
- SFTP File Workspace and Transfers
- Tailscale SSH
Reference and administration