APIToken¶
A non-interactive credential for the REST API: a bearer secret with an explicit scope list, used by scripts and CI instead of an admin session cookie.
| Field | Type | Required | Notes |
|---|---|---|---|
scopes |
[]string | yes | What this token may do (below). At least one. |
expiresAt |
RFC3339 time | no | Token stops authenticating after this instant. Unset never expires. |
tokenHash |
string | server-owned | Lowercase SHA-256 hex digest of the secret. Written by the server; a client-supplied value is discarded, and it is never returned by any endpoint (json:"-"): a digest is offline-crackable. It exists only in the YAML at rest. |
disabled |
bool | no | Keep the token in config without it authenticating. |
The secret is never stored. It is generated server-side (gpm_ + 32 random
bytes, base64url) and returned exactly once, as the token field in the
response to the PUT that created it. Only its digest is committed, in a plain
string field rather than a Secret: a digest is not a value to resolve from the
environment, and the store refuses literal Secret values outright.
PUT /api/api-tokens/ci # create; response carries "token" once
PUT /api/api-tokens/ci # ordinary edit; digest carried forward, no new secret
PUT /api/api-tokens/ci?rotate=1 # rotate; new secret returned once, old one dies
Use it as a bearer credential:
curl -H 'Authorization: Bearer gpm_...' https://gpm.example.com/api/proxy-hosts
Scopes¶
A scope is <subject>:read, <subject>:write, *:read, *:write, or admin.
- write implies read on the same subject; read never implies write.
*matches any subject, but a concrete subject never satisfies a*requirement: a whole-config read (/api/config,/api/history,/api/logs,/api/upstream-health) genuinely needs*:read.adminsatisfies everything, and is the only scope that reaches:/api/api-tokens(a token that could mint tokens could widen itself),PUT /api/settings(see below),GET /api/backup: the archive is the raw on-disk YAML, so unlike the JSON reads it carries the api-tokens' stored digests,POST /api/restore,POST /api/revert,POST /api/sso/revoke,/debug/pprof/*when profiling is enabled: a heap dump and the process command line contain resolved backend credentials in cleartext, and every token principal is admin-role by construction, so the role gate alone is not a boundary there.
GET /metrics is gated the same way but on its own, narrower scope
(metrics:read) rather than admin: an exposition is not a credential dump.
settings:write does not reach PUT /api/settings. Writing settings is
admin-equivalent and takes the admin scope: a settings write can point
dnsSync.pihole.url or a webhook at an attacker-controlled URL while supplying
${ENV:SOME_TOKEN} as its credential, and the write itself triggers the
reconcile/dispatch that resolves that env var and sends it offsite, and it can
rewrite adminAuth outright. settings:read still grants GET /api/settings
(reading resolves nothing). settings:write remains a valid scope string for
forward compatibility but grants nothing beyond settings:read today; the UI
greys the box out rather than offering a grant that does nothing.
Reverting an APIToken is refused: scoped (POST
/api/api-tokens/{name}/revert) and whole-config (POST /api/revert, which
preserves the api-tokens directory across the restore). Restoring an older
token file would restore an older tokenHash and silently revive a secret the
operator rotated away, so rotation would stop meaning revocation. Create a
replacement token instead.
Valid subjects are the REST resource plurals (proxy-hosts, redirect-hosts,
stream-hosts, parked-hosts, certificates, client-cas, dns-providers,
identity-providers, upstream-groups, access-lists, middlewares,
api-tokens), plus five pseudo-resources for non-CRUD endpoint groups:
settings, dns-sync, ingress-discovery, docker-discovery and metrics. An unknown subject or verb is rejected at write time.
metrics:read is what a Prometheus scrape credential needs for
GET /metrics (mounted only with GPM_METRICS=1, see
Prometheus metrics). It is its own subject rather
than *:read so a token that lives in a monitoring config forever buys exactly
one thing: the exposition names hosts and certificates but carries no field
values, so it is not a config read.
GET /api/capabilities and GET /api/me need no scope: any authenticated caller
may ask what the instance supports.
Scopes constrain API tokens only. An admin session keeps full access exactly as before.
name: ci-deploy
scopes:
- proxy-hosts:write
- certificates:read
expiresAt: 2027-01-01T00:00:00Z
Auth mechanics. A request carrying Authorization: Bearer gpm_... is
resolved as a token before the cookie path and never falls through to it: a
presented-but-invalid token is a 401, not an invitation to try a session cookie
riding along on the same request. Any other bearer scheme is left alone and the
cookie path runs as usual. Token principals are CSRF-exempt: the
double-submit check defends against a browser attaching ambient credentials, and
a bearer token is never attached automatically. Successful and failed token
authentications are logged (token name on success; never any secret material).
Last-use is tracked in memory only and surfaced as lastUsed on
GET /api/api-tokens; the config store is git-backed, so persisting a timestamp
per request would be a commit flood. It resets on restart.