Documentation

Issue your first access token

Add a named upload token to a hosted index, scope it with a project glob, and publish through it with twine. The checks use one accepted project and one rejected project. Allow ten minutes after completing getting started.

HTTP Basic auth carries the token with the __token__:<token> convention used by pip, twine, and pypi.org. No separate login step is required.

Scoped upload token

A hosted index that a CI job publishes to. The job's token may write projects named team-* and nothing else, so a mistyped or malicious upload to another name fails at the door instead of landing in your store.

Write the topology

Save this as peryx.toml:

data_dir = "peryx-data"

[[index]]
ecosystem = "pypi"
name = "pypi"

[[index.upstream]]
name = "primary"
url = "https://pypi.org/simple/"

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

[[index.access_token]]
name = "ci"
secret = "ci-secret"
projects = ["team-*"]
actions = ["write"]

[[index]]
ecosystem = "pypi"
name = "root-pypi"
route = "root/pypi"
layers = ["hosted", "pypi"]
write_target = "hosted"

The [[index.access_token]] table names one credential the hosted index accepts. secret is the password a client presents. projects is a list of globs, where * stands for any run of characters; team-* covers every project whose normalized name starts with team-. actions lists what the token may do, from read, write, and delete.

Start peryx:

peryx serve --config peryx.toml

Publish a project the token covers

Build a small package named team-widgets (reuse the steps from getting started, changing the project name), then publish it to the virtual index's route. peryx accepts any username; the token is the password, matching the __token__ convention. Inject ci-secret as TWINE_PASSWORD with TWINE_USERNAME=__token__ through your secret environment before publishing:

twine upload --repository-url http://127.0.0.1:4433/root/pypi/ dist/*

The upload succeeds. peryx matched the password against the ci token, saw the normalized project name team-widgets against the token's team-* glob, and stored the file in the hosted layer.

Reject an out-of-scope project

Now build a package named other-widgets and try the same command:

twine upload --repository-url http://127.0.0.1:4433/root/pypi/ dist/*

This request returns 403 with token does not grant this action. The credential is valid, so peryx does not request authentication again; the token has no grant for a project named other-widgets. Scope is enforced on the name the upload declares, so a token cannot reach past the projects it was issued for.

Inspect the principal rate-limit bucket

Add a one-request listing limit to peryx.toml, then restart peryx:

[rate_limit]
enabled = true

[rate_limit.listing]
requests = 1
window_secs = 60

Send two listing requests with the ci password and different Basic usernames:

curl -o /dev/null -w '%{http_code}\n' -u first:ci-secret http://127.0.0.1:4433/root/pypi/simple/
curl -o /dev/null -w '%{http_code}\n' -u second:ci-secret http://127.0.0.1:4433/root/pypi/simple/

peryx returns 200 for the first request and 429 for the second. Both credentials resolve to the named principal ci, so a Basic username change keeps the bucket. peryx groups a wrong password under the source address. A client cannot gain fresh buckets by rotating invalid Authorization values.

Leave trusted_proxies unset for this local run. Named principals use their verified subject. The proxy list controls the address bucket for anonymous or invalid credentials and which peers may set the public origin. For a proxy deployment, follow the reverse-proxy recipe.

Single-token configuration

If you want a hosted index that a single trusted token may write and delete anywhere, one [[index.access_token]] with no projects filter covers it, granting write and delete over every project:

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

[[index.access_token]]
name = "upload"
secret = "hosted-secret"
actions = ["write", "delete"]

Add a projects filter to that grant when one blanket credential is too much, which is the moment a scoped grant earns its keep.

On this page