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:

SettingValue
NoteCI · payments-deploy
Scopesprojects:read, keys:read, keys:write, catalog:read — nothing else
Expiry365 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; } || true

3. 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:

  1. An Admin mints the successor in Settings → API Tokens with the same scopes.
  2. Swap TF_PAT in the CI secret store and run the pipeline once.
  3. 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.