Skip to content

CI setup and tool coverage

There are two ways to send a package manager’s traffic through Upwarden’s registry proxy:

  1. The setup-upwarden GitHub 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.
  2. 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.

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.

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 token
steps:
- 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, authenticated

Every 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: write
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: npm
mode: static
tenant-vk: ${{ secrets.UPWARDEN_TENANT_VK }}
- run: npm ci

Pinning. @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.

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.

EcosystemToolStatus
npm registrynpmNative
npm registrypnpmNative
npm registryyarn (Berry v2–4)Native
npm registryyarn-classic (v1)Native
PyPIpipNative
PyPIuvNative
PyPIpoetryNative
MavenmavenNative
MavengradleNative
crates.iocargoNative
Go modulesgoNative
NuGetnugetNative
RubyGemsbundlerNative

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.

Everything below has a Manual / any CI route that needs nothing GitHub-specific. The recipe is always the same:

  1. 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 from UPWARDEN_TOKEN.
  2. 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.io on the Upwarden-hosted instance.
  3. 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.


Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: npm
- run: npm ci

Manual / 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=true

Then npm install as usual.

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: pnpm
- run: pnpm install --frozen-lockfile

Manual / 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=true

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: yarn
- run: yarn install --immutable

Manual / 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.)

Terminal window
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)

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: yarn-classic
- run: yarn install --frozen-lockfile

Manual / any CI — Yarn v1 reads the same .npmrc as npm and needs always-auth:

.npmrc
registry=https://npm.pkg.upwarden.io/
//npm.pkg.upwarden.io/:_authToken=${UPWARDEN_TOKEN}
always-auth=true

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: pip
- run: pip install -r requirements.txt

Manual / 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 /, +, =:

Terminal window
# 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 # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: uv
- run: uv sync

Manual / any CI — pure environment (uv uses Basic auth; pass the token as the password):

Terminal window
export UV_DEFAULT_INDEX="upwarden=https://pypi.pkg.upwarden.io/simple/"
export UV_INDEX_UPWARDEN_USERNAME="__token__"
export UV_INDEX_UPWARDEN_PASSWORD="$UPWARDEN_TOKEN"

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: poetry
- run: poetry install

Manual / any CI — declare the source in pyproject.toml (non-secret), supply the credential via environment, and turn the keyring off in headless CI:

pyproject.toml
[[tool.poetry.source]]
name = "upwarden"
url = "https://pypi.pkg.upwarden.io/simple/"
priority = "primary"
Terminal window
export POETRY_HTTP_BASIC_UPWARDEN_USERNAME="__token__"
export POETRY_HTTP_BASIC_UPWARDEN_PASSWORD="$UPWARDEN_TOKEN"
export POETRY_KEYRING_ENABLED=false

Poetry authenticates over HTTP Basic (POETRY_HTTP_BASIC_*), consistent with the rest of the PyPI tooling.


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 # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: maven
- run: mvn -B verify

Manual / 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):

~/.m2/settings.xml
<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.

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: gradle
- run: ./gradlew build

Manual / 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 build
allprojects {
repositories {
maven {
url "https://maven.pkg.upwarden.io/maven2"
credentials(PasswordCredentials) {
username = "token"
password = System.getenv("UPWARDEN_TOKEN")
}
}
}
}

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 # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: cargo
- run: cargo build

Manual / any CI — the custom-registry form is pure environment:

Terminal window
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 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 # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: go
- run: go mod download

Manual / any CI — Go ≥ 1.24 can auth without a file via GOAUTH:

Terminal window
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_TOKEN
export 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.


nuget is Native — validated keyless this session.

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: nuget
- run: dotnet restore

Manual / 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>
Terminal window
# 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.


bundler is Native — validated keyless this session, and the v2.1.1 Action aligned its manual username to token.

Preferred — setup-upwarden Action (GitHub Actions):

permissions:
contents: read
id-token: write # keyless
steps:
- uses: actions/checkout@v4
- uses: upwarden-io/setup-upwarden@v2
with:
tool: bundler
- run: bundle install

Manual / any CI — declare the source in the Gemfile, supply the credential via BUNDLE_<HOST>:

# Gemfile
source "https://rubygems.pkg.upwarden.io"
Terminal window
# Host -> env-var name: '.' becomes '__', '-' becomes '___', uppercased.
# rubygems.pkg.upwarden.io -> RUBYGEMS__PKG__UPWARDEN__IO
export BUNDLE_RUBYGEMS__PKG__UPWARDEN__IO="token:$UPWARDEN_TOKEN"