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.

Read enforcement is not wired yet. anonymous_read is recorded on every index and honored by the decision function, but no serving path consults it for a read, so reads are served openly today. The read challenge (Basic for PyPI, a Bearer token realm for OCI so docker login validates honestly) is the next step; the keys below let you declare the read policy ahead of that gate. External identity sources (LDAP, OIDC), token revocation, and per-mirror upstream credential refresh are also out of this release.

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

signing_key and token_ttl_secs configure the token realm that the forthcoming OCI Bearer flow will mint from. peryx reads the key at startup so a deployment can stage it now; nothing mints a token from it in this release. Set at most one of signing_key and signing_key_file.

default_anonymous_read = false makes every index private by default, so a fully private server is one knob rather than one flag per index. 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.

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