GitLab CI
Your job proves who it is with a token GitLab signs, then runs one step with a credential that lasts fifteen minutes.
Your CI logs in like a person does, with no key to steal. GitLab signs a token that says which project and branch the job ran on. Penv Cloud checks it against a rule you wrote once and hands back a credential that lasts fifteen minutes.
Set up the trust once
Open Machine Identities then Connect a Platform and pick GitLab CI. The form asks for two things, and hides two more behind Advanced.
| Field | What it is | Default |
|---|---|---|
| Project path | The group and project from your project's URL, such as acme/api | none |
| Branch | Only jobs on this branch authenticate | main |
| GitLab host | Self-managed installs use their own URL | https://gitlab.com |
| Ref type | Branch or tag | branch |
From those, the console builds the subject
project_path:acme/api:ref_type:branch:ref:main and stores it. Nobody types that string by hand.
It is compared exactly, with no wildcards, so a typo is a trust that binds and then never matches.
A self-managed GitLab has to be reachable from the internet, because Penv Cloud reads its signing keys from the issuer's own origin.
Run one step with the values
Declare the token in the job, then hand it to penv.
deploy:
id_tokens:
ID_TOKEN:
aud: YOUR_WORKSPACE_ID
script:
- curl -fsSL https://penv.cloud/install | sh
- export PATH="$HOME/.penv/bin:$PATH"
- penv run -- npm run deployaud is your workspace id, which the console shows when it creates the trust.
Ask for this audience and no other. A token requested for two audiences fails every exchange.
Name the variable ID_TOKEN and penv finds it on its own. The binary looks for a credential in
PENV_TOKEN, then the keychain, then an enrolled keypair, then a platform token, and a GitLab
runner has none of the first three. It reads ID_TOKEN or CI_JOB_JWT_V2, exchanges it, and runs
your command with the values in the child process.
If you would rather do the exchange yourself, or your variable is named something else:
deploy:
id_tokens:
PENV_ID_TOKEN:
aud: YOUR_WORKSPACE_ID
script:
- |
C=$(curl -sS "https://penv.cloud/api/v1/auth/oidc" \
-H 'content-type: application/json' \
-d "{\"token\":\"$PENV_ID_TOKEN\"}" | jq -r .credential)
PENV_TOKEN="$C" penv run -- npm run deployPENV_TOKEN takes precedence over everything else, which is how a container hands penv a
credential. A runner has no operating system keychain, so every run is online and nothing is
cached on disk.
For a host that needs a file instead of a process, penv pull writes a plain .env in the build
step.
What the trust does and does not hold
- The trust stores no secret. It is a statement that a token from one issuer, addressed to your workspace, carrying one exact subject, is a given identity.
- The audience is always your workspace id, so a token minted for another workspace cannot authenticate at yours.
- The credential the exchange returns is bound to one project and one environment. A request
naming any other environment is refused with
403 forbidden. - A minted credential never outlives the trust's own expiry, so fifteen minutes is a ceiling rather than a promise.
When it does not work
| What you see | What it means |
|---|---|
401 unauthorized | The subject on the token is not the one the trust names. Check the branch and the project path |
401 expired | The trust's own expiry has passed. Reissue it in the console |
503 unavailable | Penv Cloud could not read your GitLab's signing keys. Retry once |
403 forbidden | The credential is bound to another environment |
A job on a tag rather than a branch carries ref_type:tag, which is a different subject and needs
its own trust. So does a second branch.
Do it in order
- Open Machine Identities then Connect a Platform and pick GitLab CI.
- Fill in the project path and the branch, then read back the subject the console shows.
- Copy the workspace id. It is the audience.
- Add the
id_tokensblock to the job, naming the variableID_TOKEN. - Add the install step, then
penv run -- <your command>. - Push and read the job log. A
401 unauthorizedmeans the subject does not match.