Authentication
Nexus GSLB uses named user accounts with session-based authentication. Users log in with email and password, receive a session token, and carry that token (via cookie in the browser, or Authorization: Bearer header in the CLI and API clients). Sessions are stored in NATS KV and are cluster-wide — a session created on one node is valid on all nodes.
The legacy per-tenant API key system remains fully supported for programmatic and CI/CD access.
Environment variables
Two environment variables must be set before starting the daemon when auth is enabled:
| Variable | Purpose |
|---|---|
GSLB_API_KEY |
System-level admin key. Any Bearer request carrying this exact value is granted full access bypassing all RBAC checks. Also serves as a break-glass credential if the database is unavailable. If unset, the daemon runs in dev mode — all requests are treated as system admin with no login required. |
GSLB_SECRET_KEY |
32-byte hex key used to encrypt TOTP secrets at rest (AES-256-GCM). Required at startup if any user has TOTP enabled. Safe to omit until you enroll the first TOTP device. |
Generate values:
openssl rand -hex 32 # for GSLB_API_KEY
openssl rand -hex 32 # for GSLB_SECRET_KEYStore them in an environment file, not in the config file:
# /etc/gslb/env (chmod 600, owned by the gslb service user)
GSLB_API_KEY=<32-byte hex>
GSLB_SECRET_KEY=<32-byte hex>Reference it from the systemd unit:
[Service]
EnvironmentFile=/etc/gslb/envConfiguration
Auth behaviour is controlled by the auth: section in config.yaml. All fields have sensible defaults and can be omitted for a standard deployment.
auth:
sessionTTL: "24h" # How long a session lives without activity
sessionExtend: true # Reset the TTL on every authenticated request (sliding window)
bcryptCost: 12 # bcrypt work factor — higher = slower but more resistant to brute force (10–14 is sensible)
setupEndpoint: true # Allow POST /api/v1/auth/setup; set false after initial bootstrapping if desiredFirst-run setup
On a fresh installation with no users in the database, you have two ways to create the first admin account.
Option A — interactive CLI (on the server)
gslbd --config /etc/gslb/config.yaml --create-adminThe daemon prompts for email and password, creates a tenant_admin user in the default tenant, and exits. Start the daemon normally afterwards.
This command exits immediately if any users already exist.
Option B — setup endpoint (infrastructure-as-code, Docker, Ansible)
While the database is empty, POST /api/v1/auth/setup is available without authentication:
curl -s -X POST https://<host>/api/v1/auth/setup \
-H 'Content-Type: application/json' \
-d '{"email": "admin@example.com", "password": "your-password"}'The endpoint responds with the created user object and is permanently disabled (returns 404) once the first user exists. It can also be disabled explicitly in config:
auth:
setupEndpoint: falseLogging in
Web UI
Navigate to https://<host>/ui/login. Enter email and password. If TOTP is enrolled on the account, a second prompt appears after the password is accepted.
On successful login the browser is redirected to the dashboard. The session is stored in an HttpOnly cookie (gslb_session) — no token is visible or stored in localStorage.
API / CLI
curl -s -X POST https://<host>/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email": "admin@example.com", "password": "your-password"}'Response:
{
"token": "4272f488...",
"userId": "cc543538...",
"role": "tenant_admin",
"expiresAt": 1778587389
}Use the token as a Bearer header for subsequent requests:
curl -H "Authorization: Bearer 4272f488..." https://<host>/api/v1/poolsThe same token also works via cookie — the login endpoint sets gslb_session in the response regardless of the client.
With TOTP
curl -s -X POST https://<host>/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email": "admin@example.com", "password": "your-password", "totpCode": "123456"}'If the account has TOTP enabled and totpCode is omitted, the response is:
{"error": "TOTP code required", "mfaRequired": true}System API key (programmatic / CI)
The GSLB_API_KEY value can be used directly as a Bearer token and bypasses the session system entirely. This is suitable for CI pipelines and automated tooling:
curl -H "Authorization: Bearer $GSLB_API_KEY" https://<host>/api/v1/poolsRoles and permissions
Each user has one of three built-in roles:
| Role | What they can do |
|---|---|
tenant_admin |
Full access within their tenant: manage pools, members, services, health checks, geo rules, and other users. Cannot manage tenants or cross-tenant resources. |
operator |
Read and write pools, members, services, health checks, and geo rules. Can read users. Cannot create, modify, or delete users. |
viewer |
Read-only access to all resources within the tenant. No write operations. |
The system API key (GSLB_API_KEY) is supra-tenant and bypasses all RBAC checks.
Managing users
List users
curl -H "Authorization: Bearer <token>" https://<host>/api/v1/usersCreate a user
Requires tenant_admin role:
curl -s -X POST https://<host>/api/v1/users \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"email": "operator@example.com",
"displayName": "Jane Doe",
"password": "secure-password",
"role": "operator"
}'Update a user (role or password)
curl -s -X PUT https://<host>/api/v1/users/<userId> \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"displayName": "Jane Doe",
"role": "tenant_admin",
"password": "new-password"
}'Omit password to leave it unchanged.
Delete a user
curl -s -X DELETE -H "Authorization: Bearer <token>" \
https://<host>/api/v1/users/<userId>Users cannot delete their own account.
Setting up TOTP (MFA)
TOTP requires GSLB_SECRET_KEY to be set (the secret is encrypted at rest).
- Initiate enrollment — returns a secret and a QR code PNG:
curl -s -H "Authorization: Bearer <token>" \
-X POST https://<host>/api/v1/auth/totp/setup \
-o /tmp/qr.pngOpen /tmp/qr.png in any image viewer and scan it with an authenticator app (Google Authenticator, Authy, 1Password, etc.). The response body also contains the secret as base32 text for manual entry.
- Verify enrollment — submit a live code from the authenticator app to activate TOTP:
curl -s -X POST https://<host>/api/v1/auth/totp/verify \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{"code": "123456"}'Once verified, all future logins for this account require a valid TOTP code.
- Disable TOTP — a
tenant_admincan clear it by updating the user withPUT /api/v1/users/<id>(this clears the stored secret). Currently requires direct DB access if the admin has also lost their TOTP device (see Troubleshooting).
Session management
Logout
curl -s -X POST -H "Authorization: Bearer <token>" \
https://<host>/api/v1/auth/logoutThis immediately deletes the session from NATS KV — the token is invalid across all cluster nodes within milliseconds.
Session TTL
Sessions expire after auth.sessionTTL (default 24 hours). With auth.sessionExtend: true (default), each authenticated request resets the clock — the session only expires if the user is inactive for the full TTL period.
Revoking a session without the token
Delete the session directly from NATS KV. The KV key is the SHA-256 hex hash of the plaintext token:
# On any node with NATS CLI access
nats kv del NEXUS_SESSIONS <sha256-of-token>If NATS is unavailable, sessions fall back to a per-node in-memory store and are lost on daemon restart.
Backwards compatibility
Deployments using the per-tenant API key (Authorization: Bearer <tenant-api-key>) continue to work without changes. The auth middleware checks:
- Cookie session
- Bearer — system API key
- Bearer — user session token
- Bearer — tenant API key (existing behaviour)
In that order. No config changes are required for existing programmatic clients.
SSO via OIDC
Nexus GSLB supports Single Sign-On through any OpenID Connect (OIDC) provider — Google, Okta, Keycloak, Azure AD, GitHub, and others. When enabled, a Sign in with SSO button appears on the login page. Accounts are provisioned automatically on first login (JIT provisioning).
Password-based login remains available alongside SSO; the two methods coexist.
Configuration
Add an oidc: block under auth: in config.yaml:
auth:
oidc:
enabled: true
discoveryUrl: "https://accounts.google.com" # provider OIDC discovery endpoint
clientId: "1234567890-abc.apps.googleusercontent.com"
clientSecret: "GOCSPX-..."
redirectUri: "https://nexus.example.com/api/v1/auth/oidc/callback"
scopes: ["openid", "profile", "email"] # default; usually no need to change
roleClaim: "groups" # which claim to inspect for role mapping
roleMapping:
"nexus-admins": "tenant_admin"
"nexus-operators": "operator"
defaultRole: "viewer" # role assigned if no mapping matches
allowedDomains: ["example.com"] # optional — reject emails from other domains| Field | Type | Required | Description |
|---|---|---|---|
enabled |
bool | — | Set true to activate OIDC. |
discoveryUrl |
string | Yes | OIDC issuer URL. The provider's /.well-known/openid-configuration is fetched from this base. Must be https://. |
clientId |
string | Yes | OAuth2 client ID from the provider. |
clientSecret |
string | Yes | OAuth2 client secret from the provider. |
redirectUri |
string | Yes | Full callback URL registered with the provider. Must end with /api/v1/auth/oidc/callback. |
scopes |
[]string | — | OAuth2 scopes to request. Default: [openid, profile, email]. |
roleClaim |
string | — | Claim in the userinfo response whose value is inspected for role mapping (e.g., groups). Dot-separated paths walk nested objects — Keycloak's default layout works as realm_access.roles with no custom protocol mapper. If omitted, defaultRole is used for all users. |
roleMapping |
map | — | Maps claim values to Nexus role names (tenant_admin, operator, viewer). The claim may be a string or a list of strings; the first matching entry wins. The mapped role is re-applied on every login, so role changes (and revocations) in the provider take effect at the user's next sign-in. |
defaultRole |
string | — | Role assigned when no roleMapping entry matches. Default: viewer. |
allowedDomains |
[]string | — | When non-empty, only emails whose domain matches one of these values are permitted. Others receive a 403. |
Provider setup
Register a new OAuth2 / OIDC application with your provider and copy the client ID and secret. Set the redirect URI to exactly:
https://<your-nexus-host>/api/v1/auth/oidc/callback
Most providers call this the "Authorized redirect URI" or "Callback URL".
How it works
- User clicks Sign in with SSO on the login page.
- Browser is redirected to
GET /api/v1/auth/oidc/authorize, which generates a single-use CSRF state nonce (10-minute expiry) and redirects to the provider's authorization endpoint. When NATS is configured the nonce is stored in NATS KV, so in a cluster the authorize and callback requests may be served by different nodes (multi-replica Kubernetes Services and round-robin admin hostnames work). Without NATS the nonce is process-local: both requests must reach the same node, and a daemon restart drops in-flight logins. - User authenticates with the provider and approves the application.
- Provider redirects back to
/api/v1/auth/oidc/callback?code=…&state=…. - The daemon validates the state nonce (CSRF protection), exchanges the code for tokens, and fetches the userinfo endpoint.
- The email claim is extracted and the domain allowlist is checked (if configured).
- The role is resolved:
roleClaimvalue →roleMappinglookup →defaultRolefallback. - The user account is looked up by email. If it doesn't exist, it is created automatically (
auth_provider: oidc) with the resolved role. - A session cookie (
gslb_session) is set and the browser is redirected to/.
OIDC-provisioned users have no password set and cannot log in via the password form. To grant or change a role for an OIDC user, update the user record via PUT /api/v1/users/{id}.
Common provider configurations
Google Workspace
auth:
oidc:
enabled: true
discoveryUrl: "https://accounts.google.com"
clientId: "<google-client-id>"
clientSecret: "<google-client-secret>"
redirectUri: "https://nexus.example.com/api/v1/auth/oidc/callback"
allowedDomains: ["example.com"]
defaultRole: "viewer"Okta
auth:
oidc:
enabled: true
discoveryUrl: "https://<your-okta-domain>/oauth2/default"
clientId: "<okta-client-id>"
clientSecret: "<okta-client-secret>"
redirectUri: "https://nexus.example.com/api/v1/auth/oidc/callback"
roleClaim: "groups"
roleMapping:
"Nexus Admins": "tenant_admin"
"Nexus Operators": "operator"
defaultRole: "viewer"Keycloak
auth:
oidc:
enabled: true
discoveryUrl: "https://keycloak.example.com/realms/<realm>"
clientId: "nexus-gslb"
clientSecret: "<keycloak-secret>"
redirectUri: "https://nexus.example.com/api/v1/auth/oidc/callback"
# Keycloak returns realm roles nested under realm_access.roles in the
# userinfo response; the dotted path reads them directly — no custom
# protocol mapper needed. Create matching realm roles and assign them
# to users/groups in Keycloak.
roleClaim: "realm_access.roles"
roleMapping:
"nexus-admin": "tenant_admin"
"nexus-operator": "operator"
defaultRole: "viewer"Authentication endpoint reference
| Method | Path | Auth required | Description |
|---|---|---|---|
POST |
/api/v1/auth/setup |
No | Create first admin user. Disabled after first user exists. |
POST |
/api/v1/auth/login |
No | Authenticate with email + password. Returns token + sets cookie. |
POST |
/api/v1/auth/logout |
Session | Revoke current session. |
GET |
/api/v1/auth/me |
Session | Return current user details and permissions. |
POST |
/api/v1/auth/totp/setup |
Session | Begin TOTP enrollment; returns QR PNG + secret. |
POST |
/api/v1/auth/totp/verify |
Session | Confirm TOTP code to activate MFA. |
GET |
/api/v1/auth/config |
No | Returns {"oidcEnabled": true/false}. Used by the login page to show/hide the SSO button. |
GET |
/api/v1/auth/oidc/authorize |
No | Initiates OIDC flow; redirects to provider. |
GET |
/api/v1/auth/oidc/callback |
No | Handles provider redirect; sets session cookie. |
GET |
/api/v1/users |
operator+ |
List users in tenant. |
POST |
/api/v1/users |
tenant_admin |
Create user. |
GET |
/api/v1/users/{id} |
operator+ |
Get user details. |
PUT |
/api/v1/users/{id} |
tenant_admin |
Update display name, role, or password. |
DELETE |
/api/v1/users/{id} |
tenant_admin |
Delete user. |