Skip to content

control-plane-api, agent: capability-token validity is configurable - #3433

Closed
bbartman wants to merge 3 commits into
bmb/3376-stack-8-unmasked-guardsfrom
bmb/3376-stack-9-configurable-validity
Closed

control-plane-api, agent: capability-token validity is configurable#3433
bbartman wants to merge 3 commits into
bmb/3376-stack-8-unmasked-guardsfrom
bmb/3376-stack-9-configurable-validity

Conversation

@bbartman

Copy link
Copy Markdown
Contributor

Follow-up to #3424 (stacked on #3427): the validity window of capability_token mints becomes a deploy-time setting instead of a hardcoded constant.

What this does

  • App gains a capability_token_validity field, fed by a new agent setting --capability-token-validity / CAPABILITY_TOKEN_VALIDITY (humantime syntax, e.g. 1h, 30m), following the existing pattern of the controller duration settings (CONTROLLER_DEQUEUE_INTERVAL, CONTROLLER_HEARTBEAT_TIMEOUT).
  • sign_capability_token takes the validity as a parameter — it stays pure; no env reads at request time.
  • The default is 1h, matching the SQL generate_access_token mint, so an unconfigured deployment (production today, local stacks, test harnesses) mints exactly the window it always has. No deploy changes are required anywhere; overriding is one line in deploy-agent-api.yaml's env_vars or a local stack's agent env file.

Deliberate trade-offs (noted on #3376)

  • An override diverges from the SQL mint's hardcoded interval '1 hour' — claim-set parity between the two mints is unaffected, but the windows differ when configured. The flag docs say so.
  • A longer window widens the exposure of a leaked masked token; the one-hour bound is part of the "one approval = one short-lived token" design, and the flag docs carry that warning.

Tests

test_validity_is_configurable pins a non-default (90s) window at the signing seam; the end-to-end mint test continues to exercise the harness-configured one-hour default.

Task 6 of #3376: close the remaining surfaces a masked bearer must not
reach.

Forbidden::require_unmasked is the one definition of the masked-bearer
refusal - keyed on the capability_mask claim's presence, never its
value - consumed by requirement evaluation at extraction, by the
capability_token mint, and directly by GraphQL resolvers, which an axum
extractor cannot reach.

createRefreshToken refuses masked bearers ahead of its service-account
lookup: a refresh token exchanges for a full-authority access token,
which would escape the mask. Revocation never widens authority and
stays open to masked bearers, with a test pinning both.

/admin/create-data-plane and /admin/update-l2-reporting take
Authority<RequireUnmasked>: their SQL internal.user_roles authorization
cannot bind the capability ceiling, so they fail closed for masked
bearers, byte-identical for unmasked callers. Each gains its first
tests: the structured 403 for a masked bearer, and the handler's own
ops/-admin refusal for an unmasked one.

The RequireUnmasked doc carries the audited inventory of unmasked-only
surfaces and why everything else deliberately stays open; the SQL
functions reachable through PostgREST remain the documented mask-bypass
boundary tracked under #3376 task 8.

The GraphQL schema regen also picks up the userCapability description
from the legacy-decision migration, which had not been regenerated.
revokeApiKey authorizes RevokeApiKey through the mask-filtered grant
walk; it is not an identity-gated revocation, and grouping it with
revokeRefreshToken misstated why it needs no unmasked guard.
The validity window of capability_token mints is an App field fed by
the agent's --capability-token-validity / CAPABILITY_TOKEN_VALIDITY
setting (humantime syntax), following the pattern of the controller
duration settings. Its 1h default matches the SQL generate_access_token
mint, so an unconfigured deployment mints exactly the window it always
has; an override diverges from the SQL mint's fixed hour deliberately,
and a longer window widens the exposure of a leaked masked token — the
flag docs say so. A unit test pins a non-default window at the signing
seam; the HTTP test exercises the harness-configured default.
@bbartman
bbartman force-pushed the bmb/3376-stack-8-unmasked-guards branch from 062c784 to a9c1cb1 Compare August 28, 2026 12:05
@bbartman

Copy link
Copy Markdown
Contributor Author

Folded into #3424 (task 5's PR) rather than landing as a separate stacked PR: the configurable validity is a refinement of the mint itself, so it lives with it. The relocated commit also consolidates the default into a shared DEFAULT_CAPABILITY_TOKEN_VALIDITY constant with a weld test pinning the clap default string to it. #3427 is rebased on the updated base.

@bbartman bbartman closed this Aug 28, 2026
@bbartman
bbartman deleted the bmb/3376-stack-9-configurable-validity branch August 28, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant