SSO, RBAC & Audit Log
Team-tier deployments add single sign-on (SAML 2.0 and OIDC), a four-role RBAC model (viewer < member < admin < owner), and an append-only auth audit trail. All three ship inside the web container's auth layer — self-hosted, no external identity service of ours in the path. This guide covers the environment-variable surface that turns them on and the security invariants that hold once they are.
What ships, and where
SSO, RBAC, and the audit log live in the ghost-web container — the same control-plane process that serves the dashboard and the /v1/* API. There is no separate auth service to deploy and no callback to a Gostly-hosted endpoint: the SAML SP and the OIDC client run in your own stack and talk only to your identity provider. Multi-user role management and the team activity surface are gated behind the multi_user_workspace and team_activity features (Team tier).
SSO
SAML 2.0 and OIDC auth-code flow, selected per deployment via GOSTLY_AUTH_MODE. Password auth stays available unless you disable it.
RBAC
Four ranked roles enforced on every mutating endpoint. IdP group claims map to roles; owner is never granted by a group.
Audit log
An append-only auth_audit_events table records every login, logout, and user-management action — successes and failures alike.
Single-tenant deployment
default and one license key gates one stack. Per-tenant Postgres row-level-security policies are defined as a defense-in-depth layer, but the default configuration is not engine-enforced multi-tenancy — isolation comes from the single-tenant deployment model with RLS as a defined policy layer beneath it. See the security model for the full posture.Selecting auth methods
The set of enabled login methods is a comma-separated list in GOSTLY_AUTH_MODE. The default is password, which preserves the local-admin login. Adding saml or oidc enables that backend alongside the password form; listing only an SSO method hides the password form entirely.
# Password only (default) GOSTLY_AUTH_MODE=password # Password form + an SSO button on the login page GOSTLY_AUTH_MODE=password,saml # SSO only — the password form is hidden in the dashboard GOSTLY_AUTH_MODE=oidc
The dashboard's login page renders itself from GET /v1/auth/methods, which returns the enabled backends (and their display names) with no secrets. A first-boot deployment always has at least password auth enabled, with an admin user bootstrapped from GOSTLY_ADMIN_EMAIL + GOSTLY_ADMIN_PASSWORD.
Sessions, not bearer tokens for the dashboard
gostly_session cookie (HttpOnly, Secure, SameSite=Lax), and the server stores only its SHA-256 hash. A database leak yields no usable session credentials. Session TTL is GOSTLY_AUTH_SESSION_TTL_HOURS (default 12). The data-plane control endpoints (/mocks, /v1/mode, …) are gated separately by the GHOST_API_KEY, not by the login session.SAML 2.0
The SAML backend is a standard SP-initiated flow. Configure the SP and point it at your IdP's metadata; the SP exposes its own metadata XML for you to import on the IdP side.
GOSTLY_AUTH_MODE=password,saml # IdP — supply either a metadata URL or inline metadata XML GOSTLY_SAML_IDP_METADATA_URL=https://idp.example.com/app/metadata # (or) GOSTLY_SAML_IDP_METADATA_XML=<...> GOSTLY_SAML_IDP_ENTITY_ID=https://idp.example.com/saml # SP — this deployment GOSTLY_SAML_SP_ENTITY_ID=https://gostly.your-co.internal/saml GOSTLY_SAML_SP_ACS_URL=https://gostly.your-co.internal/v1/auth/saml/acs # Assertion attribute names (defaults shown) GOSTLY_SAML_EMAIL_ATTRIBUTE=email GOSTLY_SAML_NAME_ATTRIBUTE=displayName GOSTLY_SAML_GROUP_ATTRIBUTE=memberOf
The SAML surface is three endpoints:
GET /v1/auth/saml/metadataRenders the SP metadata XML. Import this on the IdP side to register Gostly as a service provider.
GET /v1/auth/saml/loginInitiates the AuthnRequest and 303-redirects the browser to the IdP. Accepts a relay_state query param for post-login return.
POST /v1/auth/saml/acsThe Assertion Consumer Service callback. Validates the signed SAMLResponse, resolves the user, mints a session, and returns to a same-origin RelayState path (open-redirects are rejected).
The IdP's group attribute (memberOf by default) feeds the group-to-role mapping described under RBAC.
OIDC
The OIDC backend runs the authorization-code flow with provider discovery and id_token validation. Point it at your issuer and supply the client credentials registered with your IdP.
GOSTLY_AUTH_MODE=oidc GOSTLY_OIDC_ISSUER=https://idp.example.com GOSTLY_OIDC_CLIENT_ID=gostly GOSTLY_OIDC_CLIENT_SECRET=<client-secret> GOSTLY_OIDC_REDIRECT_URI=https://gostly.your-co.internal/v1/auth/oidc/callback GOSTLY_OIDC_SCOPES="openid email profile groups" # Claim names (defaults shown) GOSTLY_OIDC_EMAIL_CLAIM=email GOSTLY_OIDC_NAME_CLAIM=name GOSTLY_OIDC_GROUP_CLAIM=groups
Two endpoints carry the flow:
GET /v1/auth/oidc/loginBuilds the authorization URL, sets a short-lived gostly_oidc_state cookie (HttpOnly, Secure, 10-minute Max-Age) for CSRF protection, and 303-redirects to the IdP.
GET /v1/auth/oidc/callbackExchanges the authorization code, validates the id_token against the discovered JWKS and the round-tripped state, resolves the user, mints a session, and clears the state cookie.
The groups claim feeds the same group-to-role mapping SAML uses — one mapping config covers both backends.
Just-in-time provisioning
SAML and OIDC share one provisioning path. When GOSTLY_AUTH_AUTO_PROVISION=true(the default), a first-time SSO login JIT-creates the local user; with it off, an admin must pre-create the user before SSO login is allowed. On every login the user's name, group memberships, and derived role are refreshed from the assertion — IdP-side group changes propagate to Gostly on the next login, with no manual re-sync.
Users are matched first by the IdP's stable subject identifier (so a user surviving an email change keeps their account), then by email. An optional email-domain allowlist gates provisioning entirely:
GOSTLY_AUTH_AUTO_PROVISION=true GOSTLY_AUTH_ALLOWED_DOMAINS=your-co.com,your-co.internal
An assertion whose email domain is outside the allowlist is rejected before any user row is touched, and the rejection is recorded in the audit log.
RBAC — the four roles
Roles are ranked, not flat. A check is a floor comparison — "at least admin" rather than "exactly admin" — so a higher role always satisfies a lower requirement. Every mutating endpoint declares its required floor and returns a 403 for any user below the threshold.
viewer
rank 0Read-only across mocks, services, recordings, and dashboards. The default role for an SSO user whose groups map to nothing.
member
rank 1Read + write on services, mocks, and recordings. Cannot manage users.
admin
rank 2Manage users, scrub config, license, and the audit log. The floor for every user-management mutation.
owner
rank 3Full control plus owner-only actions. Bootstrap role; cannot be demoted while last in the tenant, and only an existing owner can grant or revoke owner.
Roles are mapped from IdP group claims per deployment via GOSTLY_AUTH_GROUP_TO_ROLE_MAP (JSON). A user gets the highest role any of their groups map to; a user with no matching group falls back to GOSTLY_AUTH_DEFAULT_ROLE (default viewer).
GOSTLY_AUTH_GROUP_TO_ROLE_MAP={"gostly-admins":"admin","engineering":"member"}
GOSTLY_AUTH_DEFAULT_ROLE=viewerOwner cannot be granted by a group
owner — a group literally named owner is silently demoted to admin. Owner is only ever granted by an existing owner through the dashboard. This closes the "add me to the owner group at the IdP" privilege-escalation path. A role change also revokes the user's active sessions, so an open browser tab can't keep an escalated role.User-management endpoints (Team tier, behind multi_user_workspace):
GET /v1/usersList users in the tenant. Admin+.
POST /v1/usersCreate a local-password user. Admin+.
PATCH /v1/users/{id}/roleChange a user's role. Admin+; owner-only for any change touching owner; cannot demote the last owner (409).
PATCH /v1/users/{id}/statusSuspend or reactivate. Admin+; cannot suspend the last owner. Suspending revokes the user's sessions.
DELETE /v1/users/{id}Soft-delete a user. Admin+; cannot delete the last owner.
POST /v1/users/{id}/sessions/revokeKick a user — revoke all of their active sessions. Admin+.
Free and Pro deployments run single-user: the bootstrapped admin can log in on every tier, but the multi-user mutations above are gated to Team. Pair the role floor with a feature gate so an unlicensed tenant gets a license-shaped 403, not an auth-shaped one.
The append-only audit log
Every authentication and user-management action writes a row to the auth_audit_events table. The trail is append-only — the auth layer only ever inserts; it never updates or deletes a recorded event. Both successes and failures are captured, so failed-login telemetry is available for rate-limit and intrusion-detection use. Each row records the actor (user id + email), the event type, the IdP kind, client IP and user-agent (best-effort), a success flag, an error message on failures, a JSON metadata blob, and the timestamp.
Representative event types written today:
Login / logout
login.password.success, login.saml.success, login.oidc.success, the matching .fail variants, and logout.
User management
user.created, user.role.changed (metadata carries the new role), user.suspended, user.deleted, and session.revoked.admin.
The event-type set is enforced by a database CHECKconstraint, so a typo'd or unrecognised event type fails the insert rather than landing silently. Writing the audit row is best-effort and isolated: an audit-write failure rolls back its own transaction and never blocks or fails the login it was recording.
What is and isn't here
activity feed (the activity_log table that backs GET /activity), which records mock-library operations rather than auth events. Retention and export of the auth trail are operator-owned: it's your database, so back it up and ship it to a SIEM with your existing Postgres tooling.Production hardening checklist
- Keep
GOSTLY_AUTH_COOKIE_SECURE=true(the default). Only set it false for local-dev HTTP on localhost — never in a deployment reachable over the network. - Serve the dashboard over HTTPS. SAML and OIDC redirect URLs are derived from the request scheme, and the session cookie is
Secure. - Scope provisioning with
GOSTLY_AUTH_ALLOWED_DOMAINSso only your organisation's domains can JIT-create accounts, and considerGOSTLY_AUTH_AUTO_PROVISION=falsefor tightly-controlled tenants. - Map IdP groups deliberately. Default everyone to
viewerand grantmember/adminby group — never widen the default role. - Keep more than one
ownerso the last-owner protection never locks you out of an owner-only action. - Tune
GOSTLY_AUTH_SESSION_TTL_HOURSto your policy; sessions are revocable centrally (admin kick, role change, suspend, delete).