Workload identity federation — call FlexGalaxy.AI APIs from CI without long-lived secrets¶
⚠️ Stale — describes a design that was not adopted in neo; pending rewrite.
The GitHub Actions / GitLab CI federation flow documented below is not the mechanism currently wired in the running deployment. The live federation trust-root accepts OIDC tokens from FlexGalaxy.AI’s own in-cluster workloads, not from public GitHub or GitLab issuers. If you need to call FlexGalaxy.AI APIs from CI without long-lived secrets, contact FlexGalaxy.AI support for the current integration path before relying on the steps below.
If your CI/CD pipeline calls FlexGalaxy.AI APIs or otherwise acts as a non-human principal, do not ship a long-lived API key in GITHUB_SECRETS or GitLab CI variables. Use workload identity federation: your workload presents its IdP-issued OIDC token to DotID, DotID validates it, and DotID returns a short-lived (15 min) access token scoped to one FlexGalaxy.AI service.
This page collects everything you need at the call site.
TL;DR. Your GitHub Actions or GitLab CI job requests an OIDC token with audience
dotid-token-exchange, posts it tohttps://iam.flexgalaxy.ai/oauth2/tokenas an RFC 8693 token exchange, receives a 15-minute access token, and presents that to the target FlexGalaxy.AI service asAuthorization: Bearer <token>. No refresh tokens. No long-lived secrets in your CI.
Prerequisites — what your FlexGalaxy.AI account admin must set up first¶
This flow requires two pieces of one-time setup inside your FlexGalaxy.AI account. Forward this list to your Account root or an authorized account admin:
Create a workload identity in the account that owns the target service. The account admin opens
{{ dotid_console_prod_account }}→ Workload identities → New and gives it a descriptive name (e.g.bazaar-publish-syrius).Bind the federated subject to that workload identity. For GitHub Actions, the bound subject is the
(issuer, sub)tuple from the GitHub OIDC token — typicallyrepo:<owner>/<repo>:ref:refs/heads/<branch>(or:environment:<env>,:pull_request, etc.) under issuerhttps://token.actions.githubusercontent.com. For GitLab CI, the bound subject isproject_path:<group>/<project>:ref_type:branch:ref:<branch>under issuerhttps://gitlab.com(or your self-hosted GitLab URL). The account admin sends aPOST /identity/v1/accounts/{accountId}/workload-identities/federatedwith:{ "workload_identity_id": "<id from step 1>", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:syrius/bazaar:ref:refs/heads/main" }
Author the IAM policy that allows the workload identity to exchange for the target service. The action is
iam:ExchangeToken; the resource isarn:fg:dotid::service/<target-service>:{ "Effect": "Allow", "Action": "iam:ExchangeToken", "Resource": "arn:fg:dotid::service/bazaar-publisher" }
Attach to the workload identity (or to a group it belongs to) through the standard IAM policy UI.
Once those three steps land, you can run the rest of this guide from your CI without further account-admin involvement.
GitHub Actions — sample workflow¶
name: publish-to-bazaar
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC token request
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Request OIDC token from GitHub
id: oidc
run: |
OIDC_TOKEN=$(curl -sLS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=dotid-token-exchange" \
| jq -r '.value')
echo "oidc_token=$OIDC_TOKEN" >> "$GITHUB_OUTPUT"
- name: Exchange for a DotID access token
id: exchange
run: |
RESPONSE=$(curl -sS -X POST https://iam.flexgalaxy.ai/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "subject_token=${{ steps.oidc.outputs.oidc_token }}" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
--data-urlencode "audience=bazaar-publisher")
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
echo "::add-mask::$ACCESS_TOKEN"
echo "access_token=$ACCESS_TOKEN" >> "$GITHUB_OUTPUT"
- name: Call the target FlexGalaxy.AI service
env:
SERVICE_TOKEN: ${{ steps.exchange.outputs.access_token }}
run: |
# The target path and payload are defined by whichever FlexGalaxy.AI
# service you call. Substitute <your-service> and the real route.
curl -sS -X POST "https://api.flexgalaxy.ai/<your-service>/v1/..." \
-H "Authorization: Bearer $SERVICE_TOKEN" \
-F "artifact=@./build/my-artifact-1.0.0.tar.gz"
Critical points:
permissions: id-token: write— without this,ACTIONS_ID_TOKEN_REQUEST_TOKENis not exported to the job and step 1 fails silently.&audience=dotid-token-exchange— must be this literal string. This is the anti-confused-deputy audience pin. A token with any other audience is rejected.::add-mask::on the access token — prevents accidental log leakage. The token is short-lived, but masking is cheap insurance.The exchanged token’s
audisbazaar-publisher(the value of the requestaudienceparameter). Each downstream service validates its own audience.
GitLab CI — sample job¶
publish-to-bazaar:
image: alpine:3.20
before_script:
- apk add --no-cache curl jq
id_tokens:
DOTID_TOKEN:
aud: dotid-token-exchange # MUST be this literal string
script:
- |
RESPONSE=$(curl -sS -X POST https://iam.flexgalaxy.ai/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "subject_token=$DOTID_TOKEN" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
--data-urlencode "audience=bazaar-publisher")
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
- |
# Substitute <your-service> and the real route for your target service.
curl -sS -X POST "https://api.flexgalaxy.ai/<your-service>/v1/..." \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-F "artifact=@./build/my-artifact-1.0.0.tar.gz"
rules:
- if: $CI_COMMIT_BRANCH == "main"
id_tokens is the GitLab CI keyword that requests an OIDC token from GitLab’s built-in issuer (https://gitlab.com for hosted GitLab, the self-hosted instance URL otherwise). The aud field must be the literal string dotid-token-exchange.
The exchange call (raw)¶
If you are wiring this from a CI system other than GitHub Actions or GitLab CI, the underlying HTTP exchange is the same:
POST /oauth2/token HTTP/1.1
Host: iam.flexgalaxy.ai
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<your federated OIDC JWT>
&subject_token_type=urn:ietf:params:oauth:token-type:jwt
&audience=<target-service-id>
Required parameters¶
Parameter |
Value |
|---|---|
|
|
|
Your federated OIDC JWT |
|
|
|
Target FlexGalaxy.AI service identifier (no default; you must supply one). Examples: |
Optional / ignored parameters¶
Parameter |
What DotID does |
|---|---|
|
Defaults to |
|
Ignored — scope is computed from your IAM policy grants. Sending |
|
Ignored — duplicates |
|
Ignored — delegation chains are not currently supported. |
Successful response (200)¶
{
"access_token": "eyJraWQiOiJ...<DotID-signed JWT>",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 900,
"scope": "bazaar:Artifact:Publish"
}
expires_inis always 900 (15 minutes). There is no longer TTL available.There is no
refresh_tokenfield. Re-exchange on each CI run by requesting a fresh OIDC token from your IdP. This is intentional — workload tokens are deliberately short-lived.scopeis the PDP-computed grant set. Treat it as informational; the actual authorization happens at the target service when it validates the JWT.
Error decoder — when invalid_grant comes back¶
DotID intentionally collapses every subject-token rejection into 401 invalid_grant with a generic message. This is by design (it prevents probing for valid issuers, subjects, or claim shapes), but it means you have to disambiguate from your side.
Below is the canonical decoder. Walk it top-to-bottom on every invalid_grant.
Symptom |
Most likely cause |
What to check / fix |
|---|---|---|
|
Audience mismatch on the subject token |
Your CI is requesting OIDC with the wrong audience. In GitHub Actions, check that the URL passed to |
|
Federated subject changed |
GitHub OIDC subjects encode the branch ( |
|
Issuer not in trust root |
The federation trust root is a fixed allowlist of OIDC issuers. An issuer that is not on the allowlist is rejected. Adding an issuer requires a configuration change on the FlexGalaxy.AI side — contact FlexGalaxy.AI support with your issuer URL. |
|
Stale JWKS cache on DotID |
DotID refreshes its cached copy of your IdP’s JWKS once on a signature failure. If you’re hitting this within seconds of a rotation, retry with a fresh OIDC token after 30 seconds. Persistent failures across multiple minutes indicate a real issue — contact FlexGalaxy.AI support and reference your federated-subject |
|
Subject token expired |
Federated OIDC tokens are minutes-scale. Request a fresh OIDC token immediately before each exchange call; do not cache them across steps. |
|
Subject token is valid but IAM policy denies the exchange |
Your subject token validated successfully. Either the IAM policy is missing, names the wrong resource, or carries a |
Other status codes¶
Status |
RFC 8693 |
What happened |
|---|---|---|
|
|
A required parameter is missing or malformed (e.g. |
|
|
You sent a |
|
|
PDP denied |
|
(no body) |
DotID-side issue. Retry with backoff; if persistent, status page + support. |
What’s NOT supported¶
These are deliberate omissions, not bugs. If you need one of them, contact FlexGalaxy.AI support rather than filing an issue.
Refresh tokens. Workload tokens are intentionally short-lived. Re-exchange on each CI run. Per RFC 8693 §2.2.1, DotID is permitted to omit
refresh_tokenand does.actor_tokendelegation chains. Not supported.resourceparameter (RFC 8707). Accepted but ignored —audienceis the only routing input.Token introspection endpoint (RFC 7662). Consumers validate the JWT signature locally against the DotID JWKS (see
api-auth-jwt-validation.md).AWS STS / Azure AD / corporate-SSO federation. Not supported.
Federation against a custom OIDC IdP your team operates. The trust-root issuer set is fixed; contact FlexGalaxy.AI support if you need an issuer added.
Treating the exchanged token¶
The token DotID returns is a normal DotID-issued JWT. To consume it at the target service, follow the standard pattern in api-auth-jwt-validation.md:
issis the DotID realm URL.audmatches theaudienceparameter you sent (bazaar-publisher, etc.).subis your workload identity ID.scopeis the PDP-computed grant set.The custom claim
federated_subjectcarries{issuer, sub}from the input OIDC token — your service may use it for audit correlation but must not use it for authorization decisions (theaud/sub/scopeclaims are the authorization surface).
Validate the JWT signature against the realm JWKS. Cache JWKS responses per their Cache-Control headers. Reject expired tokens by exp.
Re-exchange cadence¶
A naïve “exchange once at job start” pattern leaves a 15-minute access token sitting in your job environment for the rest of the job. Two refinements:
One exchange per call site if your job calls several FlexGalaxy.AI services. Exchange separately for each
audience(one token per target service), and let each token expire on its own.Re-exchange on
401from the target service. The target service returns401when the token expires; your client catches it, re-requests a fresh OIDC token from the IdP, re-exchanges, and retries the original call. Bound retries to 1.
For most CI jobs (< 15 min total), exchange-once is fine.
Local testing without GitHub / GitLab¶
You cannot generate a valid OIDC token from a trusted issuer outside of a real workflow — that’s the whole point of the trust root. If you need help exercising the FlexGalaxy.AI side of your integration before wiring it into a live pipeline, contact FlexGalaxy.AI support.
See also¶
oidc-client-setup.md— the different thing where you register an OIDC client (a browser/app that accepts user logins). Workload federation does NOT register a client; it binds an external OIDC subject to a DotID workload identity.api-auth-jwt-validation.md— how to validate any DotID-issued JWT at the target service.api-overview.md— the FlexGalaxy.AI API surface.frn-format.md— FRN (resource ARN) format. Theiam:ExchangeTokenresource pattern isarn:fg:dotid::service/<target>.