NetStacksNetStacks

Authentication (LDAP/OIDC)

Enterprise

Configure 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.

Multiple sources can coexist

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.

Controller-only feature

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

  1. User submits username and password to POST /api/auth/ldap.
  2. The Controller performs a service-account bind using the configured bind DN and bind password.
  3. It searches under the base DN using the user filter, where {username} is substituted with the login name (e.g. (&(objectClass=user)(sAMAccountName={username}))).
  4. 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.
  5. On success, user attributes (username, email, display name, DN) are mapped from LDAP attributes using the configured attribute mapping.
  6. If auth.ldap.auto_create_users is enabled and the user does not yet exist, a record is created with auth_source: ldap. The default is disabled.
  7. A JWT token pair is issued, just like local authentication.

OIDC Authentication Flow

  1. 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.
  2. The user authenticates at the identity provider and is redirected back to GET /api/auth/oidc/callback with an authorization code and state.
  3. The Controller validates state, exchanges the code for tokens at the token endpoint, and validates the ID token and nonce.
  4. Claims are mapped to NetStacks user fields using the configured claim mapping.
  5. If auth.oidc.auto_create_users is enabled and the user does not exist, a record is created with auth_source: oidc.
  6. 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.

Zero standing privilege

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

  1. Set auth.ldap.server_url — use LDAPS for encrypted transport, e.g. ldaps://ldap.dc1.example.net:636.
  2. Set auth.ldap.bind_dn and auth.ldap.bind_password for the service account, e.g. cn=netstacks-svc,ou=services,dc=example,dc=net.
  3. Set auth.ldap.base_dn — the search base for users, e.g. ou=netops,dc=example,dc=net.
  4. Set auth.ldap.user_filter with the {username} placeholder, e.g. (&(objectClass=user)(sAMAccountName={username})).
  5. Set auth.ldap.attribute_mapping — fields are username, email, name, and dn. The defaults are Active Directory friendly: sAMAccountName, mail, displayName, distinguishedName.
  6. Optionally set auth.ldap.start_tls to true for STARTTLS on a plain ldap:// connection. Not needed with ldaps://.
  7. Optionally set auth.ldap.connection_timeout_secs.
  8. Optionally set auth.ldap.auto_create_users to true to provision users on first login (default false).
  9. Set auth.ldap.enabled to true.
  10. Verify the service-account bind and search with POST /api/auth/ldap/test (admin only) or with ldapsearch.
Use LDAPS or STARTTLS in production

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

  1. Set auth.oidc.provider_url to the issuer URL, e.g. https://idp.example.net. The Controller discovers /.well-known/openid-configuration from this issuer.
  2. Set auth.oidc.client_id and auth.oidc.client_secret from your provider's application registration.
  3. 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.
  4. Set auth.oidc.scopes (typically openid profile email).
  5. Set auth.oidc.claim_mapping to map claims (sub, email, name, preferred_username) to NetStacks user fields.
  6. Optionally set auth.oidc.auto_create_users to true.
  7. Set auth.oidc.enabled to true.
Supported OIDC providers

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

  1. In the admin UI, open SSH CA from the sidebar (route /ssh-ca, requires the admin permission).
  2. 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.
  3. Mark one CA as the default if you keep more than one.
  4. Copy the CA public key and deploy it to each managed device as a trusted user CA in /etc/ssh/sshd_config:
trust-ca-key.shbash
# 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 sshd

From 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.

Rotation invalidates all certificates

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

list-providers.shbash
curl -s https://netstacks.dc1.example.net/api/auth/providers | jq
providers-response.jsonjson
{
  "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": ...}.

configure-ldap.shbash
# 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

ldap-attribute-mapping.shbash
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

test-ldap-bind.shbash
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 distinguishedName

OIDC Discovery Verification

verify-oidc-discovery.shbash
curl -s https://idp.example.net/.well-known/openid-configuration | jq '{
  issuer,
  authorization_endpoint,
  token_endpoint,
  userinfo_endpoint,
  scopes_supported
}'
oidc-discovery-response.jsonjson
{
  "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

configure-oidc.shbash
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.

ldap-login.shbash
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:

auth-response.jsonjson
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "rt_a1b2c3d4e5f6...",
  "token_type": "Bearer",
  "expires_in": 86400
}
Token lifetimes are configurable

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

ssh-ca-public-key.shbash
# 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 | jq

Questions & Answers

How do I set up LDAP authentication in NetStacks?
Configure the auth.ldap.* settings: server_url (use ldaps://), bind_dn and bind_password for the service account, base_dn, a user_filter containing {username}, and an attribute_mapping. Then set auth.ldap.enabled to true. 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 enabled flag, and GET /api/auth/providers reports the live state of all three. A user's auth_source determines which provider verifies their login.
Does NetStacks auto-create users on first login?
Only if you opt in. Set auth.ldap.auto_create_users or auth.oidc.auto_create_users to true. 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 as TrustedUserCAKeys, 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_url includes 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_verify can 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_id and auth.oidc.client_secret are 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.pub is set in /etc/ssh/sshd_config and that sshd was restarted.
  • If AuthorizedPrincipalsFile is 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 ldapsearch to confirm it returns exactly one entry.
  • Ensure the {username} placeholder is present — the Controller substitutes the login name into it.
  • Check the base_dn scope; users in a different OU than the base will not be found.
  • For Active Directory, use sAMAccountName (not uid) in the filter and attribute mapping.

OIDC Claims Missing or Wrong

  • Include profile and email in auth.oidc.scopes — some providers omit those claims unless the scope is requested.
  • Adjust auth.oidc.claim_mapping; providers differ (e.g. preferred_username vs upn).
  • Inspect the discovery document to see what your provider exposes: curl https://idp.example.net/.well-known/openid-configuration.
  • 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.*, and session.* 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.