Docs
Guidesince cloud@2026-09-10

Other CI platforms

Thirty-two platforms prove who they are with a token they already mint, and the console builds the trust from what you name.

Your CI logs in like a person does, with no key to steal. Thirty-two platforms are in the connect catalog. On nearly all of them you name the workload and the console builds the rule that recognizes it. GitHub Actions asks for nothing, because the console reads the subject from GitHub itself.

Pick your platform

Open Machine Identities then Connect a Platform. A card asks only for the values that identify the workload, and builds the issuer and the subject from them.

PlatformWhat it asks for
GitHub ActionsNothing. The console reads the subject from GitHub
GitLab CIProject path and branch
CircleCIOrganization ID and project ID
BuildkitePipeline ID
Bitbucket PipelinesWorkspace ID and repository UUID
Kubernetes, GKE, EKS, AKS, OKECluster issuer URL, namespace and service account
JenkinsJenkins URL and the subject the plugin prints
TeamCityServer URL and the build configuration path
GitHub Enterprise ServerServer URL, repository and branch
GitHub Enterprise Cloud with data residencyEnterprise subdomain and the subject
Forgejo ActionsForgejo URL, repository and branch
BitriseApp slug and workflow
HCP TerraformOrganization, workspace and run phase
ScalrWorkspace ID
SPIFFE and SPIREDiscovery provider URL and SPIFFE ID
Teleport Workload IdentityProxy URL and SPIFFE ID
HashiCorp VaultIssuer URL and entity ID
HashiCorp NomadIssuer URL and job ID
VercelTeam slug, project and environment
Deno DeployOrganization slug, app and context
Fly.ioOrganization slug and app name
OnaOrganization ID and project ID
Auth0, Okta, Keycloak, authentikTenant or issuer URL, and the client the machine runs as
DevinThe subject your --subject-keys produces
RWXThe subject the Vault UI prints

Fill the form, and the console creates the identity, stores the trust, and prints a pipeline snippet with your workspace id in it.

Bare VPS sits on the same page and is none of the thirty-two. There is no issuer and no subject to compare, because the host generates a key rather than presenting a token. See a host that proves nothing.

Two rules that decide whether it works

One audience. Ask for this audience and no other. A token requested for two audiences fails every exchange. The audience is always your workspace id, so a token minted for one workspace cannot authenticate at another.

One exact subject. Subjects are compared byte for byte, with no wildcards. That is why the console builds the string rather than asking you to type it. Several platforms put something per-run in sub, so penv pins a different claim instead: CircleCI's project id, Bitbucket's repository UUID, Fly's app_name, Nomad's nomad_job_id, Scalr's scalr_workspace_id and Keycloak's azp. A trust that pinned the raw sub on those would match once and never again.

A self-hosted issuer has to be reachable from the internet. Penv Cloud reads the signing keys from the issuer's own origin, and refuses a discovery document that points anywhere else.

Hand penv the token

Once your platform mints a token, one variable is all penv needs.

PENV_OIDC_TOKEN="$THE_TOKEN" penv run -- npm run deploy

PENV_OIDC_TOKEN is the generic hand-off for any platform. penv exchanges it and runs your command with the values in the child process. If you would rather do the exchange yourself, post the token and use what comes back:

C=$(curl -sS "https://penv.cloud/api/v1/auth/oidc" \
  -H 'content-type: application/json' \
  -d "{\"token\":\"$THE_TOKEN\"}" | jq -r .credential)
PENV_TOKEN="$C" penv run -- npm run deploy

The credential lasts fifteen minutes, capped by the trust's own expiry, and is bound to one project and one environment.

Pods in a cluster

Every managed Kubernetes card wires the same projected volume. Ask the cluster for a token addressed to your workspace, then mount it where the container can read it.

deployment.yaml
volumes:
  - name: penv-token
    projected:
      sources:
        - serviceAccountToken:
            path: token
            audience: YOUR_WORKSPACE_ID
            expirationSeconds: 3600

containers:
  - name: api
    volumeMounts:
      # Without this the volume above exists and no container can read it.
      - name: penv-token
        mountPath: /var/run/secrets/penv
        readOnly: true

The subject is system:serviceaccount:<namespace>:<serviceaccount> on every one of them. Use the projected token rather than the cluster's built-in one: AWS_WEB_IDENTITY_TOKEN_FILE is addressed to sts.amazonaws.com and AZURE_FEDERATED_TOKEN_FILE to Microsoft, and neither audience can be changed.

Nothing in the catalog fits

Two ways out.

Enter the trust by hand. Give the issuer, the audience and the subject on the identity's own page. The console warns you when the issuer's sub names an identity rather than a machine: Google's names the service account, so every instance running as it matches, and Azure's names the managed identity, so every resource assigned it matches.

Prove nothing, and bind a key instead. A plain VPS signs no metadata and has no secure element. See a host that proves nothing.

Do it in order

  1. Open Machine Identities then Connect a Platform and find your platform.
  2. Fill in the fields, then read back the issuer and the subject the console shows.
  3. Copy the workspace id. It is the audience every token must carry.
  4. Make your platform mint a token for that audience and no other.
  5. Put the token in PENV_OIDC_TOKEN, or exchange it yourself and put the answer in PENV_TOKEN.
  6. Run the job. A 401 unauthorized means the subject does not match what the trust names.

An AWS workload