Automate from a CI pipeline
Use a scoped personal access token to create and rotate a project's keys from CI.
The job: a build system deploys an application that needs an inference key it did not have yesterday — and the old key must stop working before anyone forgets about it. Nothing about this flow should need a human on the day.
The steps below use payments as the project and $CI_RUN_ID as your
pipeline's unique run id.
1. Mint a token for CI (once, in the portal)
An Admin opens Settings → API Tokens and creates a token:
| Setting | Value |
|---|---|
| Note | CI · payments-deploy |
| Scopes | projects:read, keys:read, keys:write, catalog:read — nothing else |
| Expiry | 365 days |
Store it in the CI system's secret store as TF_PAT. The token appears in
the Admin's token list with its scopes and last use from day one, so a
security review can see exactly what CI can do.
Note
The token acts as the Admin who minted it. If that person leaves or is
demoted, CI breaks with a 401 (token_revoked or invalid_credential)
or a 403 insufficient_scope that says why. Mint it under someone who owns the pipeline long-term.
2. Validate the target
Confirm the project exists and its model allowlist names models the catalog still serves. The catalog lists enabled models only, so anything in the allowlist that is missing from it is a problem. Catching it here is cheaper than catching a failed call in production.
PROJECT_ID=5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08
curl -fsS https://portal.<domain>/api/v1/mgmt/projects/$PROJECT_ID \
-H "Authorization: Bearer $TF_PAT" | jq -r '.guardrails.model_allowlist[]' > allowlist.txt
curl -fsS "https://portal.<domain>/api/v1/mgmt/catalog/models?limit=200" \
-H "Authorization: Bearer $TF_PAT" \
| jq -r '.data[].id' > live.txt # follow next_cursor if has_more is true
comm -23 <(sort allowlist.txt) <(sort live.txt) | grep . && { echo "allowlist names non-live models"; exit 1; } || true3. Create the key
Use the run id in the Idempotency-Key, so a retried step returns the same
key instead of creating a second one. The response carries the secret
once: write it to the application's secret store and nowhere else. Log
the key id, never the value.
RESP=$(curl -fsS -X POST https://portal.<domain>/api/v1/mgmt/projects/$PROJECT_ID/keys \
-H "Authorization: Bearer $TF_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: run-$CI_RUN_ID-create-key" \
-d '{"name": "payments-'"$CI_RUN_ID"'"}')
NEW_KEY_ID=$(echo "$RESP" | jq -r .id)
echo "$RESP" | jq -r .secret | your-secret-cli put payments/TOKENFACTORY_API_KEY
echo "created key $NEW_KEY_ID"Deploy. Once the deploy is healthy, retire the key the new one replaces
($OLD_KEY_ID, the id your pipeline recorded on its previous run):
curl -fsS -X DELETE https://portal.<domain>/api/v1/mgmt/keys/$OLD_KEY_ID \
-H "Authorization: Bearer $TF_PAT" \
-H "Idempotency-Key: run-$CI_RUN_ID-delete-old"4. Or rotate the existing key
If the application keeps one long-lived key, rotate it instead. Size
grace_hours (1–168, default 24) to your rollout: the old secret keeps
working until rotation.grace_expires_at. $KEY_ID is that key's id —
GET /projects/{id}/keys
lists it.
KEY_ID=c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74
RESP=$(curl -fsS -X POST https://portal.<domain>/api/v1/mgmt/keys/$KEY_ID/rotate \
-H "Authorization: Bearer $TF_PAT" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: run-$CI_RUN_ID-rotate" \
-d '{"grace_hours": 2}')
echo "$RESP" | jq -r .secret | your-secret-cli put payments/TOKENFACTORY_API_KEY
echo "old secret valid until $(echo "$RESP" | jq -r .rotation.grace_expires_at)"Deploy. If the deploy fails, cancel the rotation — the new secret is discarded and the original keeps working with no deadline:
curl -fsS -X POST https://portal.<domain>/api/v1/mgmt/keys/$KEY_ID/rotate/cancel \
-H "Authorization: Bearer $TF_PAT" \
-H "Idempotency-Key: run-$CI_RUN_ID-rotate-cancel"5. Prove it
The classic failure is a rotation window that ends with the new secret never
deployed — every call then fails 401. Run a standing check that lists keys
still mid-rotation and alerts when the list is not empty:
curl -fsS https://portal.<domain>/api/v1/mgmt/projects/$PROJECT_ID/keys \
-H "Authorization: Bearer $TF_PAT" \
| jq -e '[.data[] | select(.rotation != null)] | length == 0' \
|| echo "ALERT: keys mid-rotation in payments"Rotate the CI token itself
The token rotates on the same discipline, before its expires_at:
- An Admin mints the successor in Settings → API Tokens with the same scopes.
- Swap
TF_PATin the CI secret store and run the pipeline once. - Revoke the predecessor — in the portal, or from CI with the new token:
DELETE /me/tokens/{old_id}(Tokens).
A scheduled job calling GET /me/tokens
can warn a week before expiry.