CI setup and tool coverage
There are two ways to send a package manager’s traffic through Upwarden’s registry proxy:
- The
setup-upwardenGitHub Action (preferred). One step in your workflow, pick your tool, done. Uses keyless OIDC by default — no registry credential is stored in your repo or written to disk. - Manual per-tool wiring (any CI, or local). The Action is GitHub-Actions-only, and we can’t assume everyone is on GitHub Actions. Every tool can be pointed at the proxy by hand — a few env vars or a small config file — so this works on GitLab CI, CircleCI, Jenkins, Buildkite, a Makefile, or your laptop.
Both routes hit the same proxy and get the same verdict pipeline. Pick per repo.
Prove your org owns its repos — once
Section titled “Prove your org owns its repos — once”Upwarden’s preferred credential story is keyless: the CI job proves who it is with a short-lived OIDC token instead of a stored key. For that to resolve to your tenant, Upwarden needs to know the org owns the repo. You establish that once per org:
- Self-serve, org-admin.
POST /api/v1/admin/orgs/:slug/ownership-proofs— authenticated with an org-admin PAT or a webhook, with an MFA step-up. First use wins. - After that, every repo under that org runs keyless — no per-repo secret, no per-repo setup beyond the two-line drop-in for whichever tool that repo builds with.
That is the whole onboarding story: prove org ownership once, then each repo is a
two-line, per-tool drop-in. Keyless requires this org-ownership proof; without it you fall
back to the static path (below), which works in any CI but means storing a standing vk_
in that CI’s secret store.
The credential story
Section titled “The credential story”Upwarden is built so that no long-lived registry credential is stored on disk.
- Keyless (preferred). The job mints a short-lived OIDC token; Upwarden exchanges it for
a short-lived, tenant-scoped credential that lives only in a masked, job-scoped
environment variable. Today that exchanged credential is a
vke_(roughly 6h); the direction of travel is to use the OIDC token directly (minutes-lived, audience-pinned). - Static (the any-CI mode). Where keyless isn’t available — most non-GitHub-Actions CI
today — you supply a standing
vk_key (long-lived until you revoke it) that you store in your CI’s secret store and use directly as the credential. Every manual snippet below reads it from an environment variable and either keeps it in the environment or writes it into a config file only as an${ENV}reference — never as a literal token committed to a file.
The honest scope: an environment variable is inherited by later steps and child processes, so
the real blast-radius control is the credential’s short lifetime, not merely keeping it
off disk. Prefer keyless; scope and rotate any vk_ you do store.
The setup-upwarden Action — keyless by default
Section titled “The setup-upwarden Action — keyless by default”On GitHub Actions this is the whole integration: grant the job the two least-privilege
permissions an OIDC exchange needs, add one step, pick your tool:, and install as usual.
Keyless is the default — there is no mode: line and nothing to store. It requires
id-token: write on the job (to mint the GitHub OIDC token) plus contents: read (checkout):
permissions: contents: read id-token: write # keyless — lets the job mint its GitHub OIDC tokensteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: npm # swap for your package manager (full list below) - run: npm ci # flows through the Upwarden proxy, authenticatedEvery per-tool “Preferred route” block below is this exact shape — only the tool: value and
the final run: install command change.
Static (any CI / non-keyless). Where the job can’t present a GitHub OIDC identity, switch
to static: add mode: static and a standing vk_ tenant key to with:, and drop
id-token: write from permissions: (no OIDC token is minted — contents: read is all you
need). Store the key as a secrets.* value; never inline it. Shown once — it applies to every
tool:
permissions: contents: read # static needs no id-token: writesteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: npm mode: static tenant-vk: ${{ secrets.UPWARDEN_TENANT_VK }} - run: npm ciPinning. @v2 is the moving major tag — it auto-picks non-breaking fixes and is fine for
most consumers. For supply-chain-strict pipelines, pin an immutable released version
(@v2.0.0) or a full commit SHA; released vX.Y.Z tags are frozen and can’t be silently
re-pointed.
Tool coverage
Section titled “Tool coverage”Upwarden authors wiring for 13 package managers, and all 13 are Native — each ecosystem’s registry protocol is confirmed live on the proxy and validated keyless.
| Ecosystem | Tool | Status |
|---|---|---|
| npm registry | npm | Native |
| npm registry | pnpm | Native |
| npm registry | yarn (Berry v2–4) | Native |
| npm registry | yarn-classic (v1) | Native |
| PyPI | pip | Native |
| PyPI | uv | Native |
| PyPI | poetry | Native |
| Maven | maven | Native |
| Maven | gradle | Native |
| crates.io | cargo | Native |
| Go modules | go | Native |
| NuGet | nuget | Native |
| RubyGems | bundler | Native |
A handful of ecosystems — crates.io, Go, NuGet, RubyGems — have a couple of manual-wiring specifics worth calling out (a transparent-mirror caveat, a Go version split, character escaping); those are documented in their own sections below. The keyless Action route is the same one-step drop-in for every tool.
Not using GitHub Actions?
Section titled “Not using GitHub Actions?”Everything below has a Manual / any CI route that needs nothing GitHub-specific. The recipe is always the same:
- Get a provisioning credential. Mint a
vk_project key (see Create your first key) and put it in your CI’s secret store as an environment variable. On this page the snippets read it fromUPWARDEN_TOKEN. - Point the tool at the proxy. Set the env vars or write the small config file shown for
your tool. The proxy host follows the convention
<eco>.pkg.<your-domain>— e.g.npm.pkg.upwarden.ioon the Upwarden-hosted instance. - Install as normal. Install commands and lockfiles are unchanged; the proxy speaks each registry’s native protocol.
Store UPWARDEN_TOKEN as a masked secret. Do not commit it. Prefer the keyless Action route
on GitHub Actions so there’s nothing to store at all.
JavaScript — npm registry
Section titled “JavaScript — npm registry”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: npm - run: npm ciManual / any CI — a tiny project .npmrc; the secret stays in the environment via
${UPWARDEN_TOKEN}:
# .npmrc (project root)registry=https://npm.pkg.upwarden.io///npm.pkg.upwarden.io/:_authToken=${UPWARDEN_TOKEN}always-auth=trueThen npm install as usual.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: pnpm - run: pnpm install --frozen-lockfileManual / any CI — same as npm, but write it to the user-level ~/.npmrc. pnpm
≥ 11.5.3 no longer expands ${VAR} in a project-level .npmrc:
# ~/.npmrc (user-level — required for pnpm >= 11.5.3)registry=https://npm.pkg.upwarden.io///npm.pkg.upwarden.io/:_authToken=${UPWARDEN_TOKEN}always-auth=trueyarn (Berry, v2–4)
Section titled “yarn (Berry, v2–4)”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: yarn - run: yarn install --immutableManual / any CI — pure environment, no config file. YARN_NPM_ALWAYS_AUTH=true is
required: without it Yarn Berry fetches anonymously and 401s (YN0041). (The equivalent in
a .yarnrc.yml is npmAlwaysAuth: true.)
export YARN_NPM_REGISTRY_SERVER="https://npm.pkg.upwarden.io/"export YARN_NPM_AUTH_TOKEN="$UPWARDEN_TOKEN"export YARN_NPM_ALWAYS_AUTH=true # required — else Berry fetches anonymously and 401s (YN0041)yarn-classic (v1)
Section titled “yarn-classic (v1)”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: yarn-classic - run: yarn install --frozen-lockfileManual / any CI — Yarn v1 reads the same .npmrc as npm and needs always-auth:
registry=https://npm.pkg.upwarden.io///npm.pkg.upwarden.io/:_authToken=${UPWARDEN_TOKEN}always-auth=truePython — PyPI
Section titled “Python — PyPI”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: pip - run: pip install -r requirements.txtManual / any CI — pip has no separate auth variable; the credential rides in the index
URL. Percent-encode the token first — vk_/vke_ tokens can contain /, +, =:
# URL-encode the token (tokens can contain / + =), then:export PIP_INDEX_URL="https://__token__:${UPWARDEN_TOKEN_ENCODED}@pypi.pkg.upwarden.io/simple/"If you print anything, mask the encoded form — masking only catches the exact string it was given.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: uv - run: uv syncManual / any CI — pure environment (uv uses Basic auth; pass the token as the password):
export UV_DEFAULT_INDEX="upwarden=https://pypi.pkg.upwarden.io/simple/"export UV_INDEX_UPWARDEN_USERNAME="__token__"export UV_INDEX_UPWARDEN_PASSWORD="$UPWARDEN_TOKEN"poetry
Section titled “poetry”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: poetry - run: poetry installManual / any CI — declare the source in pyproject.toml (non-secret), supply the
credential via environment, and turn the keyring off in headless CI:
[[tool.poetry.source]]name = "upwarden"url = "https://pypi.pkg.upwarden.io/simple/"priority = "primary"export POETRY_HTTP_BASIC_UPWARDEN_USERNAME="__token__"export POETRY_HTTP_BASIC_UPWARDEN_PASSWORD="$UPWARDEN_TOKEN"export POETRY_KEYRING_ENABLED=falsePoetry authenticates over HTTP Basic (POETRY_HTTP_BASIC_*), consistent with the rest of
the PyPI tooling.
JVM — Maven
Section titled “JVM — Maven”The Maven and Gradle credential files live in $HOME (~/.m2/settings.xml,
~/.gradle/init.gradle), so they are job-global regardless of any working-directory
setting — and can collide with actions/setup-java, which writes the same files. Merge into
an existing file rather than overwriting it.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: maven - run: mvn -B verifyManual / any CI — a settings.xml mirror plus a <server> whose password is an
${env.*} reference (Maven interpolates it — it is not a literal token on disk):
<settings> <mirrors> <mirror> <id>upwarden</id> <url>https://maven.pkg.upwarden.io/maven2</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors> <servers> <server> <id>upwarden</id> <username>token</username> <password>${env.UPWARDEN_TOKEN}</password> </server> </servers></settings>The <server><id> must match the mirror <id>. ${env.X} interpolation inside <server>
is version-fragile — assert it actually resolved (a stale Maven can pass the literal string).
Maven and Gradle authenticate with HTTP Basic (the proxy challenges Basic on a 401); the
credential rides as the <server> password.
gradle
Section titled “gradle”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: gradle - run: ./gradlew buildManual / any CI — Gradle needs a script, not a plain config file. A representative
init.gradle that reads the credential from the environment:
// ~/.gradle/init.gradle — representative shape; verify against a real buildallprojects { repositories { maven { url "https://maven.pkg.upwarden.io/maven2" credentials(PasswordCredentials) { username = "token" password = System.getenv("UPWARDEN_TOKEN") } } }}Rust — crates.io
Section titled “Rust — crates.io”cargo is Native — validated keyless this session, and the v2.1.1 Action added its
credential-provider. One manual-wiring caveat is worth calling out below.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: cargo - run: cargo buildManual / any CI — the custom-registry form is pure environment:
export CARGO_REGISTRIES_UPWARDEN_INDEX="sparse+https://crates.pkg.upwarden.io/index/"export CARGO_REGISTRIES_UPWARDEN_TOKEN="$UPWARDEN_TOKEN"Transparent mirroring of crates.io (so cargo build resolves normal crates.io deps through
Upwarden) needs source-replacement, which env vars can’t express — that requires a
.cargo/config.toml. The custom-registry form above is env-only but only covers crates
published to the Upwarden registry by name.
Go — Go modules
Section titled “Go — Go modules”go is Native — validated keyless this session. The manual path has a Go-version split
worth noting.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: go - run: go mod downloadManual / any CI — Go ≥ 1.24 can auth without a file via GOAUTH:
export GOPROXY="https://go.pkg.upwarden.io,direct"export GONOSUMDB="<your-module-prefix>" # proxy re-serving public modules trips the checksum DB otherwise# Go >= 1.24: a GOAUTH command that emits, for go.pkg.upwarden.io:# Authorization: Bearer $UPWARDEN_TOKENexport GOAUTH="command <script emitting the Authorization header for go.pkg.upwarden.io>"Go < 1.24 cannot do the command form — the credential has to go in a ~/.netrc entry.
.NET — NuGet
Section titled “.NET — NuGet”nuget is Native — validated keyless this session.
Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: nuget - run: dotnet restoreManual / any CI — declare the source in nuget.config, then pass the credential via the
environment (keeps the secret out of the file):
<!-- nuget.config --><configuration> <packageSources> <add key="upwarden" value="https://nuget.pkg.upwarden.io/v3/index.json" protocolVersion="3" /> </packageSources></configuration># The name segment (upwarden) is case-sensitive on Linux and must match the source key.export NuGetPackageSourceCredentials_upwarden="Username=__token__;Password=$UPWARDEN_TOKEN"If the token contains ; or =, the value parses wrong and NuGet silently ignores it —
you get a confusing 401 rather than an error. Guard/escape those characters.
Ruby — RubyGems
Section titled “Ruby — RubyGems”bundler is Native — validated keyless this session, and the v2.1.1 Action aligned its
manual username to token.
bundler
Section titled “bundler”Preferred — setup-upwarden Action (GitHub Actions):
permissions: contents: read id-token: write # keylesssteps: - uses: actions/checkout@v4 - uses: upwarden-io/setup-upwarden@v2 with: tool: bundler - run: bundle installManual / any CI — declare the source in the Gemfile, supply the credential via
BUNDLE_<HOST>:
# Gemfilesource "https://rubygems.pkg.upwarden.io"# Host -> env-var name: '.' becomes '__', '-' becomes '___', uppercased.# rubygems.pkg.upwarden.io -> RUBYGEMS__PKG__UPWARDEN__IOexport BUNDLE_RUBYGEMS__PKG__UPWARDEN__IO="token:$UPWARDEN_TOKEN"Related
Section titled “Related”- Quickstart — the minimal single-tool version of the manual path.
- Create your first key — mint the
vk_provisioning credential the manual route uses. - Tenants, projects, keys — the access model your CI topology maps onto.
- Wire your CI for verified attribution — add OIDC-verified per-workflow audit attribution on top of the proxy wiring.