Token authentication
peryx implements the distribution token authentication
scheme so docker login validates a credential and a token can be scoped to some repositories. The access model behind
it (principals, actions, project-glob grants) is ecosystem-neutral and lives under
authentication and access control; this page is the OCI wire surface that sits on top. For
the concept, read why the token realm exists.
Enabling the realm
The realm needs a signing key. Set signing_key (or signing_key_file) under [auth]; without it GET /v2/ never
challenges, GET /v2/token answers 405, and resource routes accept only Basic auth.
# peryx.toml
[auth]
signing_key_file = "/run/secrets/peryx-signing-key"
token_ttl_secs = 300 # how long a minted token lives; default 300
default_anonymous_read = true # per-index anonymous_read default; default true
The key signs an HS256 JWT. Keep it secret and stable: rotating it invalidates every token minted under the old key, and sharing it across replicas lets any replica verify a token the primary minted.
Version check
GET /v2/ (with or without the trailing slash) answers one of two ways.
200withDocker-Distribution-API-Version: registry/2.0when no OCI index restricts access, or when the request carries a credential the realm accepts (a bearer it signed, or a Basic password one of its indexes issued). This is the frictionless default and thedocker loginsuccess signal.401withWWW-Authenticate: Bearer realm="<base>/v2/token",service="peryx"when an OCI index restricts access. An index restricts when itsanonymous_readisfalseor it carries any named credential.
<base> is the origin peryx is reached at, read from the request's forwarded host. service is always peryx.
The token endpoint
GET /v2/token mints a token. Query parameters:
| Parameter | Meaning |
|---|---|
service | The realm's service name; a client echoes the service from the challenge (peryx). |
scope | An access request, repository:<name>:<actions>. Repeatable, or space-separated in one. |
account | The username the client logged in as. Recorded for audit; not an input to authorization. |
Authentication:
- No
Authorizationheader: the request is anonymous. Basiccredentials: peryx checks the password against every OCI index's tokens. A password that authenticates nowhere gets401; this is what makesdocker loginreject a wrong password. A password that authenticates names its subject.
The response is always 200 on a recognized (or absent) credential, carrying a JWT:
{
"token": "<jwt>",
"access_token": "<jwt>",
"expires_in": 300
}
The token's granted access is the intersection of each requested scope with what the principal may do on the index the
<name> resolves to. An empty intersection is a valid token with no access, not an error: an anonymous request for a
public repository still gets a pull token, and one for a private repository gets a token that carries nothing.
Scope grammar
A scope is repository:<name>:<actions>. <name> is the full /v2/ repository name, index route prefix included
(team/app, dockerhub/library/alpine). <actions> is a comma-separated list; peryx maps each verb to a neutral
action:
| Scope verb | Neutral action | Granted for |
|---|---|---|
pull | read | GET/HEAD on a resource |
push | write | PUT/POST/PATCH |
delete | delete | DELETE |
* | read, write, delete | any of the above |
An unknown verb requests nothing. A scope whose resource type is not repository, whose name is empty, or that resolves
to no configured index is dropped from the request.
Resource routes
Every /v2/<name>/… route authorizes the request against the index the name resolves to before its handler runs. It
accepts a Bearer JWT the realm signed, a Basic token (so an existing docker login -u _ -p <token> push keeps working),
or no credential (an anonymous read, when the index allows it). The HTTP method picks the action: GET/HEAD read,
PUT/POST/PATCH write, DELETE delete.
A refusal answers 401 with a scoped challenge:
WWW-Authenticate: Bearer realm="<base>/v2/token",service="peryx",scope="repository:<name>:pull,push",error="insufficient_scope"
The error follows RFC 6750:
error | Meaning | Client action |
|---|---|---|
| (none) | No credential was presented on a route that needs one. | Request a token, then retry. |
invalid_token | The bearer failed verification: wrong signature, or expired. | Request a fresh token, then retry. |
insufficient_scope | The credential is valid but grants nothing for this action. | Do not retry; the grant is missing. |
The scope names what the request needed, so a client can request the right token and retry. When no signing key is
configured, resource routes fall back to the Basic challenge (WWW-Authenticate: Basic realm="peryx") instead.
See also
- Authentication and access control: the neutral model these routes enforce.
- Client auth versus upstream credentials: why a cached index never forwards a client's token to its upstream.
- HTTP endpoints: the full
/v2/route table.