Documentation

Authentication and access control

peryx answers one access question the same way for every packaging format: may this client take this action on this project in this index. The model that answers it is ecosystem-neutral, so a PyPI upload and an OCI push run through the same rules and differ only in how the client presents its credential. This page is the reference for that model and its configuration keys.

For a walkthrough see issue your first access token; for task recipes see control access to an index; for the reasoning behind the design see client auth versus upstream credentials.

The model

An access decision has four inputs.

A principal is who a request speaks as once its credential was checked. It is either anonymous or a named subject, the name of the token that authenticated it. A credential that matches no token leaves the request anonymous, so an invalid token is exactly as privileged as no token at all.

An action is one of read, write, and delete. Each ecosystem maps its verbs onto these: a pull or an install is a read, a push or an upload is a write, and a removal is a delete.

A grant pairs a set of actions with a set of project globs. A token carries one grant, and a grant lets its actions reach any project one of its globs matches.

An index ACL is what an index declares: whether anonymous reads are allowed, plus the tokens it accepts. Every index has one, so a cached index, a hosted store, and a virtual index all answer the same question.

Where the model is enforced

Write and delete authorization runs through the model in this release. A PyPI upload and a docker push present their token as an HTTP Basic password, peryx resolves it to a principal against the target index's tokens, and the write proceeds only if a grant covers the project and action.

OCI reads also run through the model. The registry challenges clients through its Bearer token realm, mints repository-scoped tokens, and checks manifest, blob, tag, and catalog access. Server-rendered project pages, hydrated UI requests, and search apply the same read ACLs, so they do not disclose inaccessible repositories.

PyPI's Simple API, JSON, metadata, and artifact routes do not consult read ACLs yet. The neutral discovery, status, usage, and metrics endpoints also remain public. Setting anonymous_read = false does not protect those surfaces until their handlers gain access checks. LDAP, token revocation, and per-mirror upstream credential refresh are also out of this release. PyPI publishing can use a configured CI provider's OIDC identity without making OIDC a general login source.

Project globs

A grant's projects are patterns matched against a project or repository name. * stands for any run of characters, including /, and every other character matches itself.

PatternMatchesDoes not match
*every project in the index
team-*team-widgets, team-toolsother-widgets
team/*team/api, team/api/edgeteam, teamwork/api
acme-internalacme-internal onlyacme-public

A PyPI project name is matched after PEP 503 normalization; an OCI repository name is matched as written. Because * crosses /, team/* covers a whole repository subtree however deeply nested.

[auth]

The [auth] table holds the settings every index's access rules share. All keys are optional.

KeyMeaningDefault
signing_keySecret peryx signs its own tokens with(none)
signing_key_filePath to read signing_key from instead of inlining it(none)
token_ttl_secsLifetime of a minted token, in seconds; must be positive300
default_anonymous_readWhat an index's anonymous_read defaults to when the index omits ittrue
oidc_audienceAudience external CI identity tokens must carryperyx

signing_key and token_ttl_secs configure the token realm used by OCI and PyPI trusted publishing. peryx reads the key at startup and uses it to sign repository-scoped tokens. Set at most one of signing_key and signing_key_file.

Each [[auth.trusted_publisher]] binds one CI issuer, subject, required claim set, writable PyPI repository, and project glob list. Peryx adds the exchange routes after an operator configures a binding. See publish from CI identities for the full table and provider examples.

default_anonymous_read = false makes every index's ACL deny anonymous reads by default. It closes the enforced OCI and project-presentation paths; the public paths listed above stay open. An index that should stay open sets anonymous_read = true.

Per-index keys

These keys sit in an [[index]] table and are also listed under configuration.

KeyRoleMeaningDefault
anonymous_readallWhether a request with no credential may read this index[auth].default_anonymous_read
upload_tokenhostedSugar for one token granted write and delete over every project(none)
upload_token_filehostedPath to read upload_token from instead of inlining it(none)

upload_token = "secret" is shorthand for a single token named upload_token whose grant is write and delete over *, which is the whole of peryx's access model before this release. It keeps working unchanged, so an existing config behaves as it did. Set at most one of upload_token and upload_token_file.

[[index.access_token]]

Each [[index.access_token]] table adds one named credential the index accepts, beyond the upload_token shorthand. Put these under the hosted index that stores the writes.

[[index]]
name = "hosted"
hosted = true

[[index.access_token]]
name = "ci"
secret = "ci-secret"
projects = ["team-*"]
actions = ["write", "delete"]
expires_at = "2027-01-01T00:00:00Z"
KeyMeaningDefault
nameSubject a request authenticating with this token speaks as; unique per index(required)
secretPassword a client presents as its Basic password(required)
secret_filePath to read secret from instead of inlining it(none)
projectsProject globs the token may act on["*"]
actionsAny of read, write, delete; at least one(required)
expires_atRFC 3339 time after which it stops workingnever

A token needs exactly one of secret and secret_file. name may not be upload_token, which is reserved for the shorthand. Once expires_at passes, the token authenticates nothing: a request presenting it becomes anonymous, exactly as if the password were wrong.

Secret files

Every secret key (signing_key, upload_token, and a token's secret) has a _file sibling naming a path to read the value from, so no plaintext lives in the config file. peryx reads each file once at startup and trims surrounding whitespace; an empty file is a startup error. The rationale and the tools it composes with are in client auth versus upstream credentials.

Server-user records

The metadata store can hold server users for management and later authentication features. A user receives a random, opaque ID at creation. Renaming changes its display name and canonical lookup key without changing that ID. This follows the NIST subscriber-account model, in which mutable account attributes do not replace the stable subject identifier.

Display names are trimmed for storage. Lookups compare an NFC-normalized lowercase key, so case changes and equivalent composed Unicode spellings identify the same user. Creating or renaming to an existing canonical name fails without changing either account. The original display spelling remains available for presentation.

New users are active. A disabled user remains inspectable by ID, but the next identity lookup no longer resolves it. Reactivation restores lookup. Create, rename, disable, and reactivate operations append actor-neutral lifecycle records in the same transaction as the account change. No operation in this lifecycle stores a password, token, role, or external identity subject.

Opening an existing metadata store creates the user tables in one metadata transaction. Existing index configuration, cached package records, and access policy remain in their current tables. If table initialization fails, the transaction does not leave a partial user schema, and the prior metadata remains available to the existing recovery procedure.

Server users do not yet authorize package requests: mapping an authenticated user to grants waits on the role model. Existing upload_token and [[index.access_token]] credentials keep their current subjects and behavior when a server user is renamed or disabled.

Local password authentication

A server user may hold a local password. Enrollment derives a memory-hard verifier and discards the password, so the plaintext is never written; only the verifier is stored, beside the account and keyed by its stable ID.

Verifiers are Argon2id, the algorithm RFC 9106 standardizes, with the OWASP Password Storage baseline parameters by default: 19 MiB of memory, two iterations, and a single lane over a random 128-bit salt. Each verifier records the salt and parameters it was made with, so raising the policy does not invalidate the verifiers already stored.

Authentication takes a display name and a password and returns the stable user ID on success. An unknown name, a disabled account, an account with no password, and a wrong password all fail the same way: the same result and the same cost. A login without a stored verifier still spends one derivation against a decoy, so a caller cannot tell an absent account from a wrong password by watching how long the answer takes. An identity-store read that itself fails denies the login rather than falling through to success.

A successful login whose verifier no longer matches the current policy re-enrolls it under the same user ID before returning, so tightening the parameters upgrades verifiers as their owners sign in. A re-enrollment that cannot be stored does not deny the login that already succeeded.

Derivation is memory-hard on purpose, so every hash and every check runs on the blocking pool rather than a request worker, and a semaphore caps how many run at once. A burst of logins therefore bounds its own memory and cannot starve the threads that serve packages.

Passwords and verifiers are secrets end to end: neither appears in logs, errors, diagnostics, or any serialized account view, and a verifier's debug rendering is redacted. Enrolling again replaces the verifier; clearing it removes password authentication entirely. Clearing and then enrolling a new password is the recovery path when a local password is lost — this release adds no self-service reset, password-reset email, or browser session.

What this does not do

The model authorizes a client against peryx. It never sends a client's credential to an upstream: peryx reaches an upstream with the stored per-index username, password, or token on the cached index, and a client's identity has no bearing on that fetch. Client auth versus upstream credentials explains why forwarding would be unsafe for a cache.

On this page