Cloud authentication from a pipeline
- Read the issuer address and the claim vocabulary
- Fetch the signing keys
- Register Gitmoap as an identity provider in AWS
- Write a trust policy only your repository can satisfy
- Point a pipeline step at the role
- Move a connection to a different role
- Give each environment its own role
- Check that a run really federated
- Move a pipeline off stored access keys
- Take a role away 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:CreateOpenIDConnectProviderandiam: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:manageto create the connection andprovider_connection:readto inspect it,pipeline:manageto change a pipeline andpipeline:triggerto run one,pipeline:readto 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
400when 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
issuervalue 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 onlyTwo 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
| Claim | Example | What it is |
|---|---|---|
iss | https://oidc.gitmoap.com | The issuer, byte-identical to the discovery document's issuer |
sub | repo:9f1c2d3e-…-a1b2c3d4e5f6:ref:refs/heads/main | The repository's permanent id and the branch — the only claim an IAM policy can test |
aud | sts.amazonaws.com | AWS's token-exchange audience |
iat, nbf, exp | epoch seconds | Issued, valid from, expires. nbf is backdated 60 seconds so clock skew never rejects a fresh token |
jti | <run id>:0 | The 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 |
repository | acme/platform/checkout | The repository's full path, which changes if it is renamed |
repository_id | a uuid | The repository's permanent id |
namespace, namespace_id | acme, a uuid | The root group's path and permanent id |
pipeline_id, pipeline_run_id | uuids | Which definition, and which run. pipeline_id is omitted if the definition has since been deleted |
ref | refs/heads/main | The branch the run is for |
sha | the commit id | What the run is building |
event | push | One of push, merge_request, tag, schedule, manual |
actor | a uuid | The id of the user or service account that triggered the run — an identifier, not a username |
visibility | private | The repository's visibility when the run started |
environment, environment_id | acme/platform:production, a uuid | Present only on a token minted for a step that deploys to an environment, and absent, never empty, otherwise |
gitmoap_claim_version | 2 | The 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.
gitmoap oidc oidc-discoverycurl -X GET https://api.gitmoap.example/.well-known/openid-configurationFetch 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.
gitmoap oidc oidc-jwkscurl -X GET https://api.gitmoap.example/.well-known/jwks.jsonRegister Gitmoap as an identity provider in AWS
Once per AWS account, not once per repository.
Copy the
issuervalue from the discovery document exactly — no trailing slash.Create the provider with
sts.amazonaws.comas its audience.Note the provider's ARN. Your trust policies name it.
aws iam create-open-id-connect-provider \ --url https://oidc.gitmoap.com \ --client-id-list sts.amazonaws.comWrite 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:
{ "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 allow | Operator | Value |
|---|---|---|
| One branch of one repository | StringEquals | repo:<repository id>:ref:refs/heads/main |
| A few named branches | StringEquals | a list of exact subjects, one per branch |
| Any branch of one repository | StringLike | repo:<repository id>:ref:refs/heads/* |
| Every run of one repository, whatever triggered it | StringLike | repo:<repository id>:ref:* |
| Two repositories sharing one role | StringLike | a 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
Create the role and its trust policy in your account, pinned to the branch the pipeline deploys from.
Create a second connection, in federation mode, under a different name, naming that role. Both connections can coexist.
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.
Run the pipeline and confirm from the step log that it took the web-identity path.
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.
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).
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
kidheader 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).
- algstringSigning algorithm — always
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,environmentand the rest are not condition keys, and a statement using one matches nothing - What to do
- Rewrite every condition against
oidc.gitmoap.com:subandoidc.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
issuervalue 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
audcondition, does not containsts.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
AssumeRoleWithWebIdentityis 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-Controllifetime and AWS re-fetches on its own. Confirm the key set answers200and 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_NAMEorAWS_WEB_IDENTITY_TOKEN_FILEas 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.
Related
- 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