Skip to content
GitmoapdocsBeta
Guide· How do I…?

Cloud authentication from a pipeline

Overview

Deploy from a pipeline into your own AWS account with no access key stored anywhere. Gitmoap signs a short-lived token for every run, naming the repository, the branch, the commit and — where a step deploys to one — the environment; your account trusts Gitmoap as an identity provider, and the role's trust policy decides which runs may assume it. What a step receives is a credential that expires with the run and a role you can narrow to a single branch of a single repository — and there is nothing left to rotate, because there is no secret.

This replaces the pattern where a long-lived access key sits in a connection and grants whatever it grants until somebody remembers to change it. Setup is three things: register the issuer once in your account, write a trust policy, and point a connection at the role.

Before you start

  • Permission in your own cloud accountto register an identity provider and create a role — iam:CreateOpenIDConnectProvider and iam:CreateRole. This half is not done in Gitmoap at all.
  • Owner access on the group or repository that holds the connection.A pipeline step reaches the role through a provider connection, and creating or changing one is Owner-only. Reading which connections exist needs only Reporter.
  • Maintainer access on the repository to change a pipeline, since naming a connection on a step means editing and publishing the pipeline. Running one by hand afterwards needs Developer, and reading the step log that proves it federated needs Reporter.
  • A token with the right scopes, if you are working from the API or the CLI rather than clicking: provider_connection:manage to create the connection and provider_connection:read to inspect it, pipeline:manage to change a pipeline and pipeline:trigger to run one, pipeline:read to read the run and its step logs. The two issuer documents on this page need no token and no sign-in at all — they are public, and your cloud fetches them itself.
  • A provider that advertises federation.The AWS, Terraform, Shell and Go providers do. A provider that accepts stored credentials only refuses a federation connection with 400 when you try to create it, and its page lists the modes it accepts.
  • A published pipeline with a step that calls AWS.A token is signed at trigger time only when some step in the run resolves a federation connection; a run without one is handed no token and nothing is staged.
  • The issuer address, read rather than assumed.Fetch the discovery document and use its issuer value verbatim. Your registration and your trust policy both compare it byte for byte, and a trailing slash is a mismatch.

How it works

The exchange, end to end

once, by you register the issuer in your AWS account
create a role whose trust policy names the subjects you allow
create a federation connection naming that role
every run Gitmoap signs a token for the run and stages it into the step
the step calls sts:AssumeRoleWithWebIdentity with it
AWS fetches Gitmoap's keys, verifies the signature,
and tests your trust policy against the token's claims
the step gets temporary credentials, good for that step only

Two properties are worth stating plainly. The token grants nothing on Gitmoap — its only audience is AWS's token-exchange endpoint, and it cannot read a repository, a run or a work item. And it outlives nothing: it is signed when the run starts, held encrypted until the steps that need it are dispatched, handed to each of them as a file the AWS SDK reads, and swept away once the run reaches a terminal state. The private signing key never leaves the key-management service, so even the platform cannot export it.

The step receives four environment values it does not have to be told about — AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_WEB_IDENTITY_TOKEN_FILE and AWS_REGION — and the AWS SDK's default credential chain does the exchange with no code. Those four names are reserved: writing one as a pipeline variable or an environment variable is refused with 400, so nothing a pipeline declares can point a Gitmoap-signed token at a role you did not choose.

What the token says

ClaimExampleWhat it is
isshttps://oidc.gitmoap.comThe issuer, byte-identical to the discovery document's issuer
subrepo:9f1c2d3e-…-a1b2c3d4e5f6:ref:refs/heads/mainThe repository's permanent id and the branch — the only claim an IAM policy can test
audsts.amazonaws.comAWS's token-exchange audience
iat, nbf, expepoch secondsIssued, valid from, expires. nbf is backdated 60 seconds so clock skew never rejects a fresh token
jti<run id>:0The run's id and the number of the token within it. 0 is the environment-less token; each distinct environment in the run takes the next number
repositoryacme/platform/checkoutThe repository's full path, which changes if it is renamed
repository_ida uuidThe repository's permanent id
namespace, namespace_idacme, a uuidThe root group's path and permanent id
pipeline_id, pipeline_run_iduuidsWhich definition, and which run. pipeline_id is omitted if the definition has since been deleted
refrefs/heads/mainThe branch the run is for
shathe commit idWhat the run is building
eventpushOne of push, merge_request, tag, schedule, manual
actora uuidThe id of the user or service account that triggered the run — an identifier, not a username
visibilityprivateThe repository's visibility when the run started
environment, environment_idacme/platform:production, a uuidPresent only on a token minted for a step that deploys to an environment, and absent, never empty, otherwise
gitmoap_claim_version2The version of this contract

Only the subject can be pinned in an IAM policy

This is the one thing that costs people an afternoon. AWS exposes rich condition keys for an OIDC provider it has written a built-in mapping for; for any other issuer it offers a fixed handful — aud, sub, amr, email and oaud — and a Gitmoap token carries only the first two. A condition written against oidc.gitmoap.com:repository_id or oidc.gitmoap.com:environment is false for every token ever issued — the statement simply never matches, and AWS answers AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity without naming the key that failed.

Everything you can enforce therefore lives in the subject, which is why the subject carries two things and no more:

repo:<repository id>:ref:<git ref>

The repository's id, not its path, because a policy has to keep matching after a rename or a transfer, and because a path that is freed can later be taken by an unrelated repository. The ref, because pinning a branch is the difference between "this role deploys production" and "this role deploys whatever anyone pushed". The other claims are still in the token, and a relying party that reads tokens directly can use them — they just cannot appear in an IAM condition.

The ref in the subject is always a branch. A push or a merge-request run names the branch it is building — for a merge request, that is the source branch. A tag run and a manual run with no branch given both fall back to the repository's default branch. A pattern like refs/tags/v* matches nothing.

How long the token lives

The token expires five minutes after the run's own timeout, so it outlives every step: the AWS SDK re-reads the token file and re-assumes the role each time credentials refresh, and a token shorter than the run would break a long deploy partway through. A run that names no timeout gets the default of two hours, so its token lives two hours and five minutes. Re-triggering or retrying mints a fresh token; the previous one stays valid until it expires on its own.

One token per environment

environment is a property of a step, not of a run, so a run with a staging step and a production step is minted two tokens — one carrying each environment's claims — plus one carrying no environment claim if some federated step declares none. Each step is handed the token for the environment it resolved to.

That claim comes from the platform's own resolution — walking up from the repository to find the environment, checking the environment's allowed scopes and refs, and waiting for approval where the environment requires it — never from text in the pipeline. A step that declared no environment receives a token with no environment claim, so it cannot present itself as the production step that ran beside it.

Signing keys and rotation

The key set publishes the issuer's signing key as an RSA key for signature verification, with a kid that matches the kid in the token header — which is how a relying party picks the right key out of the set. Each response carries a five-minute Cache-Control lifetime, and your cloud re-fetches on a schedule of its own on top of that.

Rotation is a platform operation and there is nothing for you to do for it: you register the issuer, not a key, so a new signing key needs no change on your side. Nor is there a certificate thumbprint to maintain — AWS validates the issuer's certificate against its own trusted authorities, so that field on an identity provider is a formality.

How to

Read the issuer address and the claim vocabulary

Do this first. The issuer value is what you register and what your trust-policy condition keys are prefixed with, and claims_supported is the complete list of claim names a token can carry — useful when you are deciding what a relying party of your own should read.

Open https://oidc.gitmoap.com/.well-known/openid-configuration in any browser — the document is public and needs no sign-in. The AWS console reads it for you as well: in IAM → Identity providers → Add provider → OpenID Connect, paste the issuer address into Provider URL and the form fills itself in from the same document.

Terminalbash
gitmoap oidc oidc-discovery
curlbash
curl -X GET https://api.gitmoap.example/.well-known/openid-configuration

Fetch the signing keys

Your cloud fetches these on its own, so reading them by hand is a diagnostic rather than a setup step. It answers one question well: which key ids are live right now, which is what you want to know if verification started failing around a rotation.

Open https://oidc.gitmoap.com/.well-known/jwks.json. Every entry is an RSA key published for signature verification, and its kid is what a token's header names.

Terminalbash
gitmoap oidc oidc-jwks
curlbash
curl -X GET https://api.gitmoap.example/.well-known/jwks.json

Register Gitmoap as an identity provider in AWS

Once per AWS account, not once per repository.

  1. Copy the issuer value from the discovery document exactly — no trailing slash.

  2. Create the provider with sts.amazonaws.com as its audience.

  3. Note the provider's ARN. Your trust policies name it.

shellscript
aws iam create-open-id-connect-provider \
--url https://oidc.gitmoap.com \
--client-id-list sts.amazonaws.com

Write a trust policy only your repository can satisfy

The role's trust policy is where you decide which runs are allowed in. Two conditions, both against claims AWS actually exposes:

json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.gitmoap.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.gitmoap.com:aud": "sts.amazonaws.com",
"oidc.gitmoap.com:sub": "repo:9f1c2d3e-4b5a-6c7d-8e9f-a1b2c3d4e5f6:ref:refs/heads/main"
}
}
}
]
}

The condition-key prefix is the issuer's host with no scheme. The repository id is the repository_id the repositories API returns — a uuid, and the same value that appears in the token's repository_id claim.

What you want to allowOperatorValue
One branch of one repositoryStringEqualsrepo:<repository id>:ref:refs/heads/main
A few named branchesStringEqualsa list of exact subjects, one per branch
Any branch of one repositoryStringLikerepo:<repository id>:ref:refs/heads/*
Every run of one repository, whatever triggered itStringLikerepo:<repository id>:ref:*
Two repositories sharing one roleStringLikea list with one entry per repository

Warning

refs/heads/* allows more than it looks like. A merge-request run's subject names the source branch, so a wildcard over branches lets a run started by any proposed change assume the role — and whoever can open a merge request can then run a pipeline with your production credentials. For any role that can change something you care about, pin exact branch names with StringEquals and leave the wildcard for read-only roles.

Never write a subject pattern that starts with repo:*. It grants every repository on the platform, including ones you do not own.

Point a pipeline step at the role

Create a provider connection whose authentication mode is federation, with the role to assume and, unless your calls go to eu-west-1, the region. There is no credentials block, and no secret is stored.

A connection made on a group is inherited by every repository under it. A repository's own connection displaces an inherited one only when the two share a name — the most specific scope wins for that name. Two differently-named connections for the same provider do not shadow each other: they are both candidates, and a step that names neither leaves the run unable to choose, so it refuses to start rather than guessing. Name the connection on the step whenever more than one is in scope.

The full walkthrough of the connection form and its refusals is in Using providers; the part that matters here is that the authentication mode is fixed when the connection is created, so moving from stored keys to federation means creating the new connection and deleting the old one.

Move a connection to a different role

The mode is fixed, but the target is not. Update the federation connection with a new role ARN — or a new region — and the next run picks it up; runs already in flight keep the role they were dispatched with. This is how you promote a role, split one role into two, or move to a different AWS account without touching the pipeline.

Note

Updating a federation connection accepts a federation target and refuses a credentials block; updating a stored-credentials connection does the reverse. Both refusals are 422, and neither changes anything about the connection you sent them to.

Give each environment its own role

Since IAM cannot test the environment claim, separate roles are how you keep staging out of production. Create one role per environment, with its own trust policy and its own permissions, and one connection per role. Then name the right connection on each step — the staging step takes the staging connection, the production step takes the production one.

Pin the branch in each trust policy as well. A production role that trusts only refs/heads/main is protected even if somebody points the wrong connection at the wrong step.

Note

The environment claims are still in the token, and they are worth reading if anything downstream of the step verifies tokens itself. They are simply not available to an IAM Condition, which is a limitation of what AWS exposes for a custom issuer rather than of what Gitmoap signs.

Check that a run really federated

The step's log says which way it authenticated before it makes a single AWS call. A federated step prints a line naming web-identity federation, then the role ARN, the session name, the token file with whether it exists, and the region. A step using stored keys prints Auth: static credentials instead — so if you see that line, your federation connection is not the one the step resolved. If the federation lines are there but AWS_ROLE_ARN prints <unset>, the step resolved no connection at all and nothing pointed it at a role.

To see what the repository resolves before you run anything, read its resolved provider connections — GET /repositories/{repo_id}/resolved-provider-connections — which walks the repository and its parent groups and returns the connection each provider ends up with.

In your own account, the assumed session is named gitmoap-pipeline-<run id>, capped at the 64 characters AWS allows for a session name. That is the string to search CloudTrail for when you want to prove which run made a given API call.

Move a pipeline off stored access keys

  1. Create the role and its trust policy in your account, pinned to the branch the pipeline deploys from.

  2. Create a second connection, in federation mode, under a different name, naming that role. Both connections can coexist.

  3. Name a connection on every step that uses this provider, not just the new ones. Two connections for one provider in scope means no step can auto-resolve any more, and one you forget refuses at the start of the next run.

  4. Run the pipeline and confirm from the step log that it took the web-identity path.

  5. Delete the old connection, then delete the access key in your own account. Deleting the connection stops Gitmoap using the key; only deleting the key in AWS makes it worthless everywhere else.

Take a role away from a pipeline

Deleting the connection stops any future run from being handed the role. It does not reach into a run already in flight, whose credentials live until they expire, and it does not change anything in your account — the role and its trust policy are still there, and anything else satisfying that trust policy still gets in. To cut it off completely, tighten or delete the trust policy in AWS as well.

Reference

Both documents are public: no token, no sign-in, no scope. Each response carries a five-minute Cache-Control lifetime, and your cloud fetches them on its own schedule.

2 endpoints

GET/.well-known/openid-configurationpublic

OIDC Discovery document for the gitmoap issuer.

oidc_discoveryGET /.well-known/openid-configurationOpen in the API reference

Token scope

None — this one is public

Send

Nothing — the address is the whole request.

Refuses

  • 501The OIDC issuer is not configured on this deployment

Receive

  • issuerstring
  • jwks_uristring
GET/.well-known/jwks.jsonpublic

JSON Web Key Set publishing the public half of the OIDC signing key(s).

oidc_jwksGET /.well-known/jwks.jsonOpen in the API reference

Token scope

None — this one is public

Send

Nothing — the address is the whole request.

Refuses

  • 501The OIDC issuer is not configured on this deployment

Receive

  • keys[]object6 fields
    • algstringSigning algorithm — always RS256.
    • estringBase64url (no padding) RSA public exponent (typically AQAB).
    • kidstringThe key's id, equal to the kid header of the tokens it signs, so a relying party picks the right key while keys are being rotated.
    • ktystringKey type — always RSA.
    • nstringBase64url (no padding) RSA modulus.
    • usestringPublic key use — always sig (signature verification).

The endpoints answer 501 when Gitmoap's token issuer is unavailable. That is a service-wide fault rather than anything about your account, and federation is unavailable for everyone until it is fixed.

Troubleshooting

501 NOT_IMPLEMENTEDnaming OIDC_ISSUER_URL / OIDC_KMS_KEY_ID from either document
Cause
Gitmoap's token issuer is unavailable, so no token is ever signed
What to do
Use a connection with stored credentials in the meantime, and contact support@gitmoap.com. Nothing on the AWS side will work until the discovery document answers 200
AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
Cause
The subject in the token does not satisfy the trust policy's condition — usually a wrong repository id, a branch you did not allow, or a tag run whose ref reports the default branch
What to do
Compare your condition value against repo:<repository id>:ref:refs/heads/<branch> character by character. The repository id is a uuid, not a path
The same AccessDenied, and the condition looks right
Cause
The policy conditions on a claim AWS does not expose for a custom issuer — repository, repository_id, environment and the rest are not condition keys, and a statement using one matches nothing
What to do
Rewrite every condition against oidc.gitmoap.com:sub and oidc.gitmoap.com:aud. Split by repository or branch, not by claim
InvalidIdentityToken, or a message about not retrieving a verification key
Cause
No identity provider is registered for exactly this issuer, or the registered URL differs — a trailing slash, http, or a different host
What to do
Register the provider with the issuer value copied verbatim from the discovery document, then retry the run
IncorrectClientId, or a message about the token audience
Cause
The identity provider's audience list, or the policy's aud condition, does not contain sts.amazonaws.com
What to do
Add sts.amazonaws.com. Every token is minted for that audience, whatever audience a connection records
Nothing at all in CloudTrail after a failed exchange
Cause
A rejected AssumeRoleWithWebIdentity is not recorded, so an empty search is not evidence the call was never made
What to do
Read the step's log instead: it prints the role ARN, session name, token-file path and region before the call. Search CloudTrail for the session name only to confirm successes
The step's log says it is using static credentials
Cause
The step resolved a connection that stores keys — often a repository-level connection carrying the same name as the group's federation one, or an older connection nobody deleted
What to do
Read the repository's resolved provider connections, name the federation connection explicitly on the step, and delete the one you no longer want
The step fails with AWS_CREDENTIALS_INCOMPLETE
Cause
A connection carries one half of an access key pair, which is a broken stored-credentials connection, not a federated one
What to do
Supply both halves, or create a federation connection and delete this one. A partial pair is refused rather than quietly falling through to federation
Calls go to eu-west-1 when your resources are elsewhere
Cause
A federated step gets its region from the connection and nowhere else, because there are no stored credentials to carry one; with no region on the connection the AWS provider falls back to eu-west-1
What to do
Set the region on the federation connection and re-run. The step log's AWS_REGION= line tells you which one it actually had
ExpiredTokenException partway through a long run
Cause
Credentials were refreshed after the token expired — it lives for the run's timeout plus five minutes, which is a ceiling nothing inside the run can extend
What to do
Raise the run's timeout to cover the deploy, and do not leave a background process holding credentials after the step that started it has finished
400 BAD_REQUESTsaying the provider does not support federation
Cause
That provider accepts stored credentials only
What to do
Create the connection with stored credentials, or use a provider that advertises federation. The provider's page lists the modes it accepts
400 BAD_REQUESTnaming the role
Cause
The role ARN is not well-formed — a twelve-digit account id and a role/ path are both required
What to do
Copy the ARN from the role in AWS rather than typing it
422 UNPROCESSABLE_ENTITYmentioning credentials and federation
Cause
Credentials were sent to a federation connection, or a federation target to one that stores credentials. The mode is fixed when a connection is created
What to do
Create a connection in the mode you want and delete the other one. Nothing about the existing connection was changed
A policy that used to work stopped matching after a rename
Cause
It pins a path-shaped subject. Subjects carry the repository's permanent id, so a rename cannot break one that is written correctly
What to do
Rewrite the condition as repo:<repository id>:ref:…, and keep the path out of it entirely
A merge-request run assumed a role you meant only for the default branch
Cause
The trust policy allows refs/heads/*, and a merge-request run's subject names its source branch
What to do
Pin exact branch names with StringEquals. Anyone able to open a merge request can otherwise reach that role
Verification fails right after a signing-key rotation
Cause
Your cloud is still holding a copy of the key set from before the rotation and has not re-fetched it
What to do
Retry after a few minutes — the documents carry a five-minute Cache-Control lifetime and AWS re-fetches on its own. Confirm the key set answers 200 and lists a key before escalating
The step log's AWS_ROLE_ARN prints <unset>
Cause
The step resolved no federation connection, so no role was staged and no token was signed for it
What to do
Read the repository's resolved provider connections to see what it actually gets, check the federation connection is on the repository or one of its parent groups, and name it on the step
The run refuses to start, naming a step and listing connection names
Cause
Two or more connections exist for that step's provider in scope and the step names none, so there is nothing to choose between them
What to do
Name one of the listed connections on the step, or delete the one you do not want. Connections displace each other only when they share a name
400 BAD_REQUESTsaying a name is reserved by the platform
Cause
You tried to set AWS_ROLE_ARN, AWS_REGION, AWS_ROLE_SESSION_NAME or AWS_WEB_IDENTITY_TOKEN_FILE as a pipeline variable or an environment variable
What to do
Set the role and the region on the provider connection instead — that is the only place they are authorized, which is what stops a variable redirecting a signed token

FAQ

Can this token read anything in Gitmoap?

No. Its audience is AWS's token-exchange endpoint, and Gitmoap does not accept it as authentication for anything. The credential a step uses to clone the repository is a separate, internal one.

Do I need to maintain a certificate thumbprint?

No. The issuer is served behind a publicly trusted certificate authority, which AWS validates directly.

Can two repositories share one role?

Yes — give the trust policy a list of subject values, one per repository. Consider whether you want that: the point of the subject condition is that a role can be narrowed to exactly the work that should be able to use it.

What happens to a run in flight if I delete the connection?

It finishes with the credentials it already has. Deletion affects the next run.

  • Using providerscreating, inspecting and deleting the connection this page points at a role, and the credential model behind both authentication modes
  • Pipelinesdefining a pipeline, naming a connection on a step, and setting a run's timeout
  • Service catalogdeclaring environments, their protection rules and their approvers, which is what decides whether a step gets an environment-bearing token
  • Access tokensthe personal and service-account tokens that authenticate to Gitmoap, which are a different thing from the token described here
  • Provider policy and administrationinstalling the AWS provider into a group and pinning the version it runs at, one level above the connection