Security model¶
Who can reach the admin panel, what a role or a token may do, and what gpm deliberately never stores.
This is a deliberate stance, not a gap: gpm has no local user table and no per-user permission system. Two things authenticate to the admin panel:
- One local break-glass admin. A single username/password pair
(
GPM_LOCAL_ADMIN_USER+ a bcrypt hash: see Environment variables, flags and CLI), always roleadmin, always IdP"local". There is exactly one of these; it is not a config object, cannot be listed, and does not appear in git history. Its purpose is recovery when SSO is unreachable, not day-to-day login: seeadminAuth.ssoOnlyabove. It can require a TOTP code after the password, see Enable TOTP for the local admin. - OIDC group-to-role mapping. Every other admin-panel login is an
IdentityProvider with a
roleMapping: the IdP's group claim resolves to exactly one of two local roles,adminoruser(RoleNone, no access, otherwise). Individual identity is not gpm's to manage: who is inadminGroups/userGroupsis a decision made and audited at the IdP, the same way it already has to be for every other application behind SSO.
Roles¶
There are exactly two roles, and no per-object permission grant.
| Role | Can do | Cannot do |
|---|---|---|
admin |
Everything: every read, every write, settings, backup/restore, revert, API tokens, pprof and metrics | none |
user |
Every read: GET /api/me, GET /api/capabilities, and every GET under /api/ (hosts, certificates, access lists, settings, config, history, logs, discovery status and plans) |
Any POST/PUT/DELETE (403); anything under /api/api-tokens, in either direction; GET /api/backup, /metrics and /debug/pprof/, which need the admin scope |
Internally the user role is the scope grant *:read minus the api-tokens
subject, so it is the same mechanism the API-token scopes use. The
two gates compose: a token presented on a user session can never grant more
than that, whatever scopes the token itself holds.
Why API tokens are excluded. A token is a credential, not configuration:
listing them hands a viewer the name, scopes, expiry and stored digest of every
automation credential on the instance, which is nothing a viewer needs in order
to view the proxy configuration. GET /api/config is narrowed the same way:
a caller without api-tokens:read gets the whole tree with apiTokens omitted,
rather than the rows it was just refused at /api/api-tokens.
GET /api/me reports "readOnly": true for a user session; the admin UI
reads that flag and renders itself read-only (banner, Save controls disabled)
rather than offering controls whose every submission would answer 403.
Assigning the user role. Put the group in roleMapping.userGroups on the
IdentityProvider, or set roleMapping.defaultRole: user to make every
authenticated user a viewer by default:
name: authentik-oidc
type: oidc
roleMapping:
adminGroups: [proxy-admins]
userGroups: [proxy-viewers]
# or, to make everyone who can authenticate a read-only viewer:
# defaultRole: user
The local admin is always admin; the user role is reachable through SSO
only.
Not planned: multi-user local accounts, or per-user permissions finer than the two roles above. Local accounts don't scale past the one break-glass credential they exist for: a second local user would need its own storage, its own rotation story, and its own audit trail, all of which OIDC already provides for free once an IdP is configured. If you need more than one named human with admin access, put an IdentityProvider in front of gpm; if you need per-person restriction to a subset of hosts or actions, that is out of scope for the same reason the two-role model is: gpm's authorization boundary is role (admin = read/write, user = read-only) plus, for automation, API-token scope, not identity.
Delegation is API tokens, not accounts. A script, CI pipeline, or
integration that needs its own credential (distinct from "is logged in as
the admin") gets a scoped APIToken, not a
second local login. Tokens are named, individually revocable, expirable, and
restricted to exactly the resources they touch (proxy-hosts:write and
nothing else, for example). Sharing one local admin account is never the
delegation mechanism.
Audit is git history plus webhooks, not an audit-log table. Every write
through the API or UI (create, update, delete, settings, restore, revert)
is one commit to the config repo (config/), carrying whatever
Author the caller resolved to: the local admin's
username, the SSO subject/email from the session, or the token's own
name (its principal has no email, so the commit's email falls back to
gpm@localhost) for a token-authenticated write. git log
and GET /api/history / GET /api/{plural}/{name}/history are the audit
trail: who changed what, when, and (via git show) exactly what the diff
was, and it is tamper-evident by construction: rewriting it means rewriting
git history, not flipping a row in a database. webhooks above add a
real-time feed of the same events to an external system (SIEM, chat,
ticketing) if you want change notifications outside git log.
What this does not cover: authentication events themselves (successful
and failed logins) are logged to gpm's structured process log
(GPM_LOG_LEVEL/GPM_LOG_CONSOLE), not to git: a failed login attempt
changes nothing in config, so there is no commit for it. Ship the process log
to your log aggregator if you need a durable record of login attempts
alongside the config-change history.