Skip to content

0008 — Credentials only where we govern membership

Status: Accepted; supersedes 0002 in part; refined by 0014; applied to Slack by 0025 Date: 2026-09-26

Context

ADR 0002 draws a boundary: the issuer mints tokens that carry groups, and reconciles membership lists from the same table. Minting credentials for another system is out of scope. Yet the issuer already mints GitHub App installation tokens — it holds a GitHub App private key and hands out short-lived installation tokens decided by policy. This is a credential for another system. The boundary as written is not honest about what ships.

A new pressure on the same boundary is incoming: a CI system needs short-lived, prefix-scoped object-storage credentials for a cache on a provider with no OIDC federation. The request asks whether the issuer should mint them. The same tension will recur for each new system that cannot read an identity token but can do something useful with a short-lived credential keyed to a policy group.

Decision

The issuer mints a credential for another system only where that system is one sluis already governs by membership — that is, a system it reconciles, for which it holds that system's app identity and a catalogue of policy-driven grants.

Today that system is GitHub alone. The issuer holds a GitHub App's private key; when a caller asks for an installation token, the issuer decides the grant from groups — using the same catalogue and policy evaluation every other feature uses — and mints a credential scoped to that grant and no wider.

Everywhere else, the pattern is an external broker that is an ordinary relying party: sluis mints a token for the broker's own audience, gated by requires with groups decided by policy. The broker verifies the token against the issuer's JWKS and alone holds the upstream secret it mints from. Example: a broker service that turns an identity token into short-lived, prefix-scoped object-storage credentials for an S3-compatible store without OIDC federation. The issuer knows the broker exists and what audience it has; it does not know or hold the store's secret.

Keeping the upstream secret in the broker is also a containment line: a compromised issuer must not yield every downstream secret, and a compromised broker must not yield identity. Each holds only its own.

Consequences

This supersedes ADR 0002 in part: its "Out of scope" paragraph now reads through this record. A minter for any other system inside the issuer needs a membership reconciler for that same system first. Everything else in ADR 0002 stands.

An installation that wants to mint object-storage credentials for a cache on a provider without OIDC federation gets a broker and a policy example: CI job identity (with ci:cache:reader and ci:cache:writer groups decided by workflow event, branch, or pull-request permission), a client or resource row for the broker's audience, gated by requires.

For CI in particular: write access is only granted for event_name == push on the default branch, never for pull_request_target or workflow_run, which run in the base branch's context. A CI system that runs third-party code must not be able to overwrite another system's cache entries.

Alternatives considered

A generic, pluggable minter interface inside the issuer, callable by any broker. Rejected: it grows the issuer into a secret store for every upstream system, with every store's policies, every store's key rotation, every store's breach. That is exactly the thing ADR 0002 exists to prevent. A minter specific to GitHub is defensible because the issuer already reconciles GitHub membership; a generic minter for every provider is not.

Forbid the GitHub minter too, for consistency and purity. Rejected: it removes a working, policy-scoped capability that already ships. GitHub is already governed here; the issuer's GitHub App private key is already kept. Removing the feature serves purity over practicality, when GitHub is a different case than an external provider.