> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0278 — API keys: Stackra issues them, with a scanner prefix, checksum, rotation overlap, expiry and revoke

> Accepted · 2026-10-21

> [!NOTE]
>
> Amends [0207](/adr/0207-authentication-assurance-and-machine-identities) decision 2: API keys for
> programmatic access are issued by Stackra and mapped to a ZITADEL machine identity. Restates and
> extends the token rules of [0087](/adr/0087-identity-and-iam-are-driver-packages) decision 5
> (cross-reference only; 0087 stays superseded by 0206). Amends
> [0217](/adr/0217-api-surfaces-mcp-first-plus-rest-sdks-and-rate-limits) (API keys used on the REST
> and MCP surfaces).

## Context

[0207](/adr/0207-authentication-assurance-and-machine-identities) decision 2 sends every machine identity through ZITADEL, and
[0087](/adr/0087-identity-and-iam-are-driver-packages) decision 5 said tokens are shown once, stored hashed, scoped and revocable. Neither says
who issues an API key, how it is rotated, recognised when leaked, or seen in use. The owner's answers of 2026-10-21 (screenshot gap review,
item 9, and the issuer decision that followed the PR review) set the issuer and the whole lifecycle.

## Decision

1. **Stackra issues API keys.** `@figentra/service-identity` issues API keys for programmatic access by developers and integrations.
2. **Each key maps to a ZITADEL machine identity.** The gateway ([0237](/adr/0237-generated-api-gateway-in-four-layers)) validates the key and
   maps it to a ZITADEL machine identity. The machine identity is the principal; its permissions are its IAM grants
   ([0208](/adr/0208-iam-model-scope-tree-roles-conditions-deny-delegation-and-permission-names)), decided by the policy decision point
   ([0209](/adr/0209-iam-enforcement-pipeline-gate-sql-predicate-embedded-pdp-and-service-calls)). ZITADEL stays the issuer for sign-in, OIDC
   and machine-to-machine JWTs.
3. **Shown once, stored hashed.** The secret is shown once at creation and stored only as a hash.
4. **Prefix registered with GitHub secret scanning.** Every key starts with a recognisable prefix registered with GitHub secret scanning.
5. **Checksum suffix.** The key ends with a CRC32 checksum (as GitHub's suffix does). It catches typos and malformed keys, so a scanner and the
   gateway reject them without a database lookup. It does not prove a key is authentic; only the hash lookup does.
6. **Tenant tokens are bound to one tenant.** A tenant service token works only in the tenant it was issued for.
7. **Scopes.** A key carries scopes, and the gateway checks each call against them.
8. **Last used.** The key list shows each key's last-used time and IP address.
9. **Rotation with overlap.** Rotating a key issues a new one; old and new both work during an overlap window, then the old one stops.
10. **Expiry.** A key has an expiry.
11. **Revoke, and auto-revoke on a scanning report.** A key can be revoked at once. A key reported by GitHub secret scanning is revoked
    automatically and its owner is notified.
12. **Example usage.** Each key's page shows an example usage snippet for that key's scopes.

## Consequences

* A leaked key is revoked when GitHub reports it, not found by the customer weeks later.
* The key is a credential, not a second permission model: the machine identity is the principal, its permissions are its IAM grants (0208),
  and the policy decision point (0209) decides each call.
* service-identity owns key issuance, hashing, the checksum format and the secret-scanning endpoint.
* Rotation does not need a deploy at the exact moment the old key dies, because of the overlap window.
* Last-used data is written on calls, so its write path must stay cheap (sampled or batched).

## Enforcement

planned — a storage test asserts no plaintext key is persisted; a gateway test rejects a bad checksum before lookup, refuses a tenant token in
another tenant, accepts both keys inside the overlap window and only the new one after it, and refuses expired and revoked keys; a test of the
secret-scanning endpoint asserts a reported key is revoked.

## Open details

The owner did not give these; the conservative default below applies until an ADR changes it.

1. **Prefix.** `<product>_<env>_` followed by the secret and the checksum, for example `stk_live_` and `stk_test_`; the exact strings are
   fixed when the GitHub secret-scanning partnership is filed.
2. **Checksum.** CRC32 of the secret, base62-encoded, as the last 6 characters.
3. **Overlap window.** 24 hours by default, settable up to 7 days.
4. **Expiry.** Required at creation, 90 days by default, at most 1 year.
5. **Last-used IP retention.** Follows the access-log retention of
   [0212](/adr/0212-audit-evidence-access-logs-hash-chain-and-retention).

## Supersedes and sources

Supersedes: none. Amends: [0207](/adr/0207-authentication-assurance-and-machine-identities) decision 2,
[0217](/adr/0217-api-surfaces-mcp-first-plus-rest-sdks-and-rate-limits). Cross-references:
[0087](/adr/0087-identity-and-iam-are-driver-packages) decision 5.
Sources: owner answers in chat, 2026-10-21 (screenshot gap review, item 9; issuer decision after the PR review).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.