Documentation

Private OCI indexes and scoped tokens

An OCI index allows anonymous reads and authenticates writes by default. The examples restrict reads, issue repository-scoped tokens, and disable anonymous reads for the server. See authentication and access control for the shared access model.

Configure token signing

Set a signing key under [auth]. Without it peryx cannot mint tokens, so docker login does not validate and reads stay open.

# peryx.toml
[auth]
signing_key_file = "/run/secrets/peryx-signing-key"

Keep the key in a file instead of inline. signing_key_file accepts a mounted Docker or Kubernetes secret, a systemd credential, or a Vault-rendered file. Follow the shared signing-key guidance for secure generation, the 32-byte minimum, and coordinated rotation.

Restrict index reads

Set anonymous_read = false on the index and give a token a read grant. Now a pull needs a token that covers the repository.

[[index]]
name = "team"
route = "team"
ecosystem = "oci"
hosted = true
anonymous_read = false

[[index.access_token]]
name = "ci"
secret_file = "/run/secrets/ci-token"
projects = ["team/*"]
actions = ["read", "write"]

A client pulls after docker login localhost:4433 --username ci --password <ci-token>. A pull of a repository the token does not cover, or an anonymous pull, gets 401 insufficient_scope.

The gate covers every web UI read; server rendering uses the incoming credential. After hydration, same-origin /+ui and /+search requests apply the same ACL. Put the UI behind an authenticating proxy or send an Authorization header to browse a private index. Search omits inaccessible repositories before calculating totals and pages.

Scope a token

projects is a list of globs. * matches any run of characters, including /, so team/* covers repositories at any depth under team, and a bare * covers the whole index. Grant the verbs the credential needs under actions (read, write, delete).

[[index.access_token]]
name = "reader"
secret_file = "/run/secrets/reader-token"
projects = ["team/public/*"]
actions = ["read"]

[[index.access_token]]
name = "releaser"
secret_file = "/run/secrets/releaser-token"
projects = ["team/*"]
actions = ["read", "write", "delete"]

An index may define multiple [[index.access_token]] tables. One table with actions = ["write", "delete"] and no projects filter is the credential that writes and deletes everywhere on the index.

Set token expiry

Add expires_at, an RFC 3339 timestamp. After it passes, the token stops authenticating and a JWT already minted from it stops verifying at its own expiry.

[[index.access_token]]
name = "ci-2027"
secret_file = "/run/secrets/ci-2027-token"
projects = ["team/*"]
actions = ["read", "write"]
expires_at = "2027-01-01T00:00:00Z"

Allow catalog reads

GET /v2/_catalog spans the configured OCI indexes, so it uses registry:catalog:* instead of a repository pull scope. Give the same named credential an explicit projects = ["*"] read grant on each private OCI index in the catalog. A team/* grant authorizes matching pulls but cannot list the catalog.

Test the scope with these requests.

token=$(curl -sS -u ci:<ci-token> \
  'http://127.0.0.1:4433/v2/token?service=peryx&scope=registry%3Acatalog%3A%2A' | jq -r .token)
curl -sS --oauth2-bearer "$token" http://127.0.0.1:4433/v2/_catalog

peryx names registry:catalog:* in the 401 challenge for a missing credential. It returns 401 insufficient_scope for a valid token without that exact grant.

Disable anonymous reads

Set the server-wide default to make indexes private unless they opt into anonymous reads:

[auth]
signing_key_file = "/run/secrets/peryx-signing-key"
default_anonymous_read = false

Each index inherits this value when it omits anonymous_read. A public index can override it with anonymous_read = true.

Set token lifetime

token_ttl_secs under [auth] sets how long a minted token lives. The OCI token realm accepts 60 through 86400 seconds and defaults to 300. A shorter lifetime makes a revoked ACL take hold sooner; a longer one cuts token-endpoint traffic for a busy CI fleet.

[auth]
signing_key_file = "/run/secrets/peryx-signing-key"
token_ttl_secs = 900

See also

On this page