Authentication (LDAP/OIDC)
EnterpriseConfigure external authentication for NetStacks Controller: local accounts, LDAP/Active Directory, OIDC/OAuth2 SSO, and SSH certificate access.
Overview
NetStacks Controller supports multiple authentication sources so organizations can integrate with their existing identity infrastructure. The login API exposes the configured providers at GET /api/auth/providers, and the admin UI login page renders an option for each enabled source. Three login providers are available:
- Local Authentication — Username and password stored directly in the Controller database. Passwords are hashed with Argon2id. Ideal for standalone deployments, break-glass accounts, and initial setup.
- LDAP / Active Directory — Authenticate against an LDAP directory using bind operations. Supports LDAPS and STARTTLS, a configurable user filter, attribute mapping, connection timeout, and optional automatic user provisioning on first login.
- OIDC / OAuth2 (SSO) — Authenticate via OpenID Connect with any compliant provider (Okta, Microsoft Entra ID, Google Workspace, Keycloak, Auth0). Uses the authorization code flow with PKCE.
Separately, the Controller ships a built-in SSH Certificate Authority for short-lived, certificate-based access to managed devices. SSH certificates are a device-access mechanism rather than a Controller login provider, but they are configured from the same admin area and are covered below.
You can enable local auth, LDAP, and OIDC at the same time. Each user's auth_source determines which provider verifies their login, and the providers endpoint reflects the live enabled state of each service.
External authentication providers and the SSH Certificate Authority are part of the NetStacks Controller. They are not available in the standalone Terminal application.
How It Works
Local Authentication Flow
The user submits a username and password to POST /api/auth/login. The Controller looks up the user, verifies the password against the stored Argon2id hash, and issues a JWT access token plus a refresh token (stored server-side). Login routes are rate-limited to slow down brute-force attempts.
LDAP Authentication Flow
- User submits username and password to
POST /api/auth/ldap. - The Controller performs a service-account bind using the configured bind DN and bind password.
- It searches under the base DN using the user filter, where
{username}is substituted with the login name (e.g.(&(objectClass=user)(sAMAccountName={username}))). - If a single entry is found, the Controller performs a second bind as the user's DN with the supplied password to verify the credentials.
- On success, user attributes (username, email, display name, DN) are mapped from LDAP attributes using the configured attribute mapping.
- If
auth.ldap.auto_create_usersis enabled and the user does not yet exist, a record is created withauth_source: ldap. The default is disabled. - A JWT token pair is issued, just like local authentication.
OIDC Authentication Flow
- The browser is sent to
GET /api/auth/oidc/authorize, which generates a PKCE challenge, CSRF state, and nonce, then redirects to the provider's authorization endpoint. - The user authenticates at the identity provider and is redirected back to
GET /api/auth/oidc/callbackwith an authorization code and state. - The Controller validates state, exchanges the code for tokens at the token endpoint, and validates the ID token and nonce.
- Claims are mapped to NetStacks user fields using the configured claim mapping.
- If
auth.oidc.auto_create_usersis enabled and the user does not exist, a record is created withauth_source: oidc. - The Controller issues a JWT token pair and redirects back to the application.
SSH Certificate Authority
The Controller can host one or more SSH Certificate Authorities. Each CA is an Ed25519 key pair generated by the Controller. When a user connects to a device, the Controller signs a short-lived certificate for the user's public key, embedding the user identity as a certificate principal. Devices that trust the CA public key accept the certificate without any standing per-user keys.
No permanent per-user SSH keys live on devices — only the CA public key. Signed certificates expire automatically, and rotating (deleting and recreating) the CA invalidates every certificate it issued.
Step-by-Step Guide
LDAP and OIDC are configured through Controller settings keys under the auth.ldap.* and auth.oidc.* namespaces. You can set these from the admin UI settings area or directly via the settings API (see Code Examples). The keys below are the source of truth.
Setting Up LDAP Authentication
- Set
auth.ldap.server_url— use LDAPS for encrypted transport, e.g.ldaps://ldap.dc1.example.net:636. - Set
auth.ldap.bind_dnandauth.ldap.bind_passwordfor the service account, e.g.cn=netstacks-svc,ou=services,dc=example,dc=net. - Set
auth.ldap.base_dn— the search base for users, e.g.ou=netops,dc=example,dc=net. - Set
auth.ldap.user_filterwith the{username}placeholder, e.g.(&(objectClass=user)(sAMAccountName={username})). - Set
auth.ldap.attribute_mapping— fields areusername,email,name, anddn. The defaults are Active Directory friendly:sAMAccountName,mail,displayName,distinguishedName. - Optionally set
auth.ldap.start_tlstotruefor STARTTLS on a plainldap://connection. Not needed withldaps://. - Optionally set
auth.ldap.connection_timeout_secs. - Optionally set
auth.ldap.auto_create_userstotrueto provision users on first login (defaultfalse). - Set
auth.ldap.enabledtotrue. - Verify the service-account bind and search with
POST /api/auth/ldap/test(admin only) or withldapsearch.
Never use unencrypted LDAP (ldap:// without STARTTLS) in production — bind credentials are sent in plaintext. Use ldaps:// (port 636) or enable auth.ldap.start_tls on ldap:// (port 389). The auth.ldap.skip_cert_verify option exists for development only; leave it false in production.
Setting Up OIDC / SSO Authentication
- Set
auth.oidc.provider_urlto the issuer URL, e.g.https://idp.example.net. The Controller discovers/.well-known/openid-configurationfrom this issuer. - Set
auth.oidc.client_idandauth.oidc.client_secretfrom your provider's application registration. - Register the callback URL in your provider. The Controller derives it from its base URL as
<base_url>/api/auth/oidc/callback, e.g.https://netstacks.dc1.example.net/api/auth/oidc/callback. - Set
auth.oidc.scopes(typicallyopenid profile email). - Set
auth.oidc.claim_mappingto map claims (sub,email,name,preferred_username) to NetStacks user fields. - Optionally set
auth.oidc.auto_create_userstotrue. - Set
auth.oidc.enabledtotrue.
Any OpenID Connect compliant provider works, including Okta, Microsoft Entra ID, Google Workspace, Keycloak, Auth0, OneLogin, and Ping Identity. The Controller relies on standard OIDC discovery, so no provider-specific code path is required.
Setting Up the SSH Certificate Authority
- In the admin UI, open SSH CA from the sidebar (route
/ssh-ca, requires theadminpermission). - Click Add SSH CA. Provide a name and optional description. The Controller generates a new Ed25519 key pair — there is no private-key import; the private key is created and stored server-side.
- Mark one CA as the default if you keep more than one.
- Copy the CA public key and deploy it to each managed device as a trusted user CA in
/etc/ssh/sshd_config:
# On each managed device:
# 1. Save the CA public key
echo "ssh-ed25519 AAAA..." > /etc/ssh/user_ca.pub
# 2. Add to /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/user_ca.pub
# 3. Restart sshd
systemctl restart sshdFrom then on, when a user opens a session the Terminal generates a session key pair and the Controller signs a short-lived certificate for it — no standing per-user keys are ever placed on the device.
The CA private key is held by the Controller and is never exported. To revoke access broadly, delete the CA and create a new one, then redeploy the new public key. Every certificate signed by the old CA stops being trusted.
Code Examples
List Configured Providers
curl -s https://netstacks.dc1.example.net/api/auth/providers | jq{
"providers": [
{ "name": "local", "enabled": true },
{ "name": "ldap", "enabled": true },
{ "name": "oidc", "enabled": false }
]
}LDAP Configuration via Settings API
Settings are written with PUT /api/admin/settings/{key} and a JSON body of {"value": ...}.
# Enable LDAP authentication
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.enabled \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": true}'
# LDAP server URL (use ldaps:// in production)
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.server_url \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "ldaps://ldap.dc1.example.net:636"}'
# Service-account bind credentials
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.bind_dn \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "cn=netstacks-svc,ou=services,dc=example,dc=net"}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.bind_password \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "REPLACE_WITH_SERVICE_ACCOUNT_PASSWORD"}'
# User search base and filter
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.base_dn \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "ou=netops,dc=example,dc=net"}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.user_filter \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "(&(objectClass=user)(sAMAccountName={username}))"}'
# Optional: auto-create users on first login (default false)
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.auto_create_users \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": true}'LDAP Attribute Mapping
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.ldap.attribute_mapping \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"value": {
"username": "sAMAccountName",
"email": "mail",
"name": "displayName",
"dn": "distinguishedName"
}
}'Verify the LDAP Bind with ldapsearch
ldapsearch -H ldaps://ldap.dc1.example.net:636 \
-D "cn=netstacks-svc,ou=services,dc=example,dc=net" \
-W \
-b "ou=netops,dc=example,dc=net" \
"(&(objectClass=user)(sAMAccountName=jsmith))" \
sAMAccountName mail displayName distinguishedNameOIDC Discovery Verification
curl -s https://idp.example.net/.well-known/openid-configuration | jq '{
issuer,
authorization_endpoint,
token_endpoint,
userinfo_endpoint,
scopes_supported
}'{
"issuer": "https://idp.example.net",
"authorization_endpoint": "https://idp.example.net/oauth2/authorize",
"token_endpoint": "https://idp.example.net/oauth2/token",
"userinfo_endpoint": "https://idp.example.net/oauth2/userinfo",
"scopes_supported": ["openid", "profile", "email", "groups"]
}OIDC Configuration via Settings API
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.oidc.enabled \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": true}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.oidc.provider_url \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "https://idp.example.net"}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.oidc.client_id \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "netstacks-controller"}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.oidc.client_secret \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": "REPLACE_WITH_CLIENT_SECRET"}'
curl -X PUT https://netstacks.dc1.example.net/api/admin/settings/auth.oidc.scopes \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value": ["openid", "profile", "email"]}'Authenticate via LDAP (Login API)
The login body accepts an optional client_type. "admin_ui" is the default and does not consume a license seat; "terminal" consumes a seat.
curl -X POST https://netstacks.dc1.example.net/api/auth/ldap \
-H "Content-Type: application/json" \
-d '{
"username": "jsmith",
"password": "LdapP@ssw0rd",
"client_type": "admin_ui"
}'Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "rt_a1b2c3d4e5f6...",
"token_type": "Bearer",
"expires_in": 86400
}expires_in is in seconds and follows the access-token lifetime, which defaults to 24 hours (86400). It is controlled by session.access_token_hours; the refresh-token lifetime is controlled by session.refresh_token_days.
Retrieve an SSH CA Public Key
# Default CA public key (deploy to devices as TrustedUserCAKeys)
curl -s https://netstacks.dc1.example.net/api/ssh-ca/public-key
# Admin: list all CAs
curl -s -H "Authorization: Bearer $TOKEN" \
https://netstacks.dc1.example.net/api/admin/ssh-ca | jqQuestions & Answers
- How do I set up LDAP authentication in NetStacks?
- Configure the
auth.ldap.*settings:server_url(useldaps://),bind_dnandbind_passwordfor the service account,base_dn, auser_filtercontaining{username}, and anattribute_mapping. Then setauth.ldap.enabledtotrue. NetStacks binds with the service account, searches for the user, then re-binds as the user's DN to verify the password. - What OIDC providers does NetStacks support?
- Any OpenID Connect compliant provider, including Okta, Microsoft Entra ID, Google Workspace, Keycloak, Auth0, OneLogin, and Ping Identity. The Controller uses standard OIDC discovery (
/.well-known/openid-configuration) plus the authorization code flow with PKCE, so no provider-specific configuration is required. - How are passwords hashed?
- Local account passwords are hashed with Argon2id. The plaintext is never stored. LDAP and OIDC users are verified by the external provider, so NetStacks does not store their passwords at all.
- Can I run local, LDAP, and OIDC at the same time?
- Yes. Each provider has its own
enabledflag, andGET /api/auth/providersreports the live state of all three. A user'sauth_sourcedetermines which provider verifies their login. - Does NetStacks auto-create users on first login?
- Only if you opt in. Set
auth.ldap.auto_create_usersorauth.oidc.auto_create_userstotrue. Both default to false, meaning a matching NetStacks account must already exist. - How does the SSH Certificate Authority differ from a login provider?
- The SSH CA is not a Controller login provider — it signs short-lived Ed25519 SSH certificates so users can reach managed devices without standing keys. You create a CA under SSH CA (
/ssh-ca), distribute its public key to devices asTrustedUserCAKeys, and the Controller signs a certificate per session. - Can I import an existing SSH CA private key?
- No. The Controller generates a fresh Ed25519 key pair when you add a CA and keeps the private key server-side. To rotate, delete the CA and create a new one, then redeploy the new public key to your devices.
Troubleshooting
LDAP Connection Refused or Bind Fails
- Confirm
auth.ldap.server_urlincludes the correct scheme and port, e.g.ldaps://ldap.dc1.example.net:636. - Check reachability from the Controller host:
nc -zv ldap.dc1.example.net 636. - For LDAPS, ensure the server certificate chain is trusted.
auth.ldap.skip_cert_verifycan bypass verification for development only — never in production. - Open port 636 (LDAPS) or 389 (LDAP+STARTTLS) on any firewall between the Controller and the directory.
- Run
POST /api/auth/ldap/test(admin) and review Controller logs for service-account bind errors.
OIDC Redirect or State Errors
- Verify the callback registered at the provider exactly matches
<base_url>/api/auth/oidc/callback. - Confirm
auth.oidc.client_idandauth.oidc.client_secretare current and have not been rotated at the provider. - Ensure the Controller's base URL is the externally reachable URL, since the callback is derived from it.
- Check logs for state or nonce mismatches, which usually indicate a stale or replayed authorization attempt.
SSH Certificate Rejected
- Confirm the CA public key on the device matches the current CA from
GET /api/ssh-ca/public-key. If you rotated the CA, redeploy the public key. - Verify
TrustedUserCAKeys /etc/ssh/user_ca.pubis set in/etc/ssh/sshd_configand that sshd was restarted. - If
AuthorizedPrincipalsFileis configured, ensure it accepts the certificate principal (the user identity). - Synchronize clocks — time skew between the Controller and the device can make a short-lived certificate appear expired or not-yet-valid.
LDAP User Filter Not Matching
- Test the filter directly with
ldapsearchto confirm it returns exactly one entry. - Ensure the
{username}placeholder is present — the Controller substitutes the login name into it. - Check the
base_dnscope; users in a different OU than the base will not be found. - For Active Directory, use
sAMAccountName(notuid) in the filter and attribute mapping.
OIDC Claims Missing or Wrong
- Include
profileandemailinauth.oidc.scopes— some providers omit those claims unless the scope is requested. - Adjust
auth.oidc.claim_mapping; providers differ (e.g.preferred_usernamevsupn). - Inspect the discovery document to see what your provider exposes:
curl https://idp.example.net/.well-known/openid-configuration.
Related Features
- User Management — Create and manage accounts and set each user's authentication source.
- Roles & Permissions — Define the permission sets that decide what authenticated users can access.
- Settings — Where the
auth.ldap.*,auth.oidc.*, andsession.*keys live. - SSH Certificates — Certificate-based device access and CA usage in depth.
- Credential Vault — The encrypted vault for credentials used alongside authentication.
- API Authentication — JWT access and refresh token flows for programmatic access.
- Audit Logs — Review login events, failures, and configuration changes.