Skip to content

feat(delegate): a service can act for a person, and the token says so (AUDIT-6) - #109

Merged
braghettos merged 2 commits into
mainfrom
feat/delegate
Nov 8, 2026
Merged

braghettos merged 2 commits into
mainfrom
feat/delegate

Conversation

@braghettos

Copy link
Copy Markdown
Collaborator

Implements the accepted AUDIT-6 design (docs/design/agent-delegation.md, #107; installer#412).

What it adds: POST /delegate, shaped like RFC 8693 token exchange.

  • Request:

    • Authorization: Bearer <the service's projected SA token, audience authn>, authenticated exactly as on /serviceaccount/login, through serviceaccount.Authenticate, now shared.
    • A form body with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:ietf:params:oauth:token-type:jwt and subject_token=<a person's authn JWT>.
  • Opt-in: the service's ServiceAccount mapping must set spec.delegation.allowed: true. maxDepth defaults to 3 (range 1–8) and caps the actor chain.

  • Subject: it must be authn's own token, under the current or a previous key, and not expired.

  • Answer: {access_token, issued_token_type, token_type: "Bearer", expires_in}, with Cache-Control: no-store. The issued JWT:

    • keeps the person's sub, username and groups, without krateo:loa:*
    • adds act: {sub: <service>}, nested for a chain
    • lives min(15m, subject exp)

    No kubeconfig is minted.

  • New causes, all documented in docs/api.md: UnsupportedGrantType, UnsupportedSubjectToken and MissingSubjectToken (400), SubjectTokenInvalid (401), DelegationNotAllowed and DelegationTooDeep (403), plus IssuanceFailed. Form errors are answered before the apiserver is called.

Dependencies: plumbing v1.14.0 → v1.30.0, which adds jwtutil.Actor (plumbing#98). It requires Go 1.26, so go.mod goes 1.25.6 → 1.26.0 and the Dockerfile builder golang:1.25-trixie → golang:1.26-trixie. The rest of the suite passes on v1.30.0 unchanged.

CRD: ServiceAccount.spec.delegation, regenerated, and mirrored into the authn-crds chart. There's no RBAC change: TokenReview and the mapping list were already granted.

Tests: internal/routes/delegate/delegate_test.go covers:

  • issuing, and dropping the step-up group
  • the TTL cap at the subject's expiry
  • chain nesting
  • a previous-key subject
  • 9 refusals with their causes, checking that no token is issued and that malformed requests never reach the apiserver

TestPublicSurface now pins POST /delegate and probes it.

Docs: api.md (route, CRD and failures table), behavior.md, log.md, and the design doc's status set to implemented with its deltas.

Out of scope, per Diego: the apiserver-level audit. Snowplow still dials as the person.

Intended release: 0.37.0. Draft #91 moves to the next minor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TLFnUeufsniBTiPtg82fDb

… (AUDIT-6)

POST /delegate exchanges a person's authn JWT, with the calling service's
own projected ServiceAccount token, for a short-lived JWT that keeps the
person as sub, username and groups (minus krateo:loa:) and names the
service in RFC 8693's act claim, nested for a chain. Shaped like RFC 8693
token exchange. Snowplow derives user, on_behalf_of and delegation for its
AuditEvent from this verified token instead of client-set baggage.

- ServiceAccount mappings opt in with spec.delegation.allowed; maxDepth
  (default 3, 1-8) caps the chain.
- The subject token must be authn's own, under its current or a previous
  key; the issued token lives min(15m, the subject's expiry).
- Service authentication is shared with /serviceaccount/login
  (serviceaccount.Authenticate), so its causes apply unchanged.
- plumbing v1.30.0 carries the act claim; it requires Go 1.26, so go.mod
  and the builder image move to 1.26.

Design: docs/design/agent-delegation.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUeufsniBTiPtg82fDb
/delegate read the subject token's exp with jwt.ParseUnverified after
validating it, which CodeQL flags (go/missing-jwt-signature-check).
plumbing v1.31.0's ValidateClaimsWithKeySource returns the verified
claims, expiry included, in one parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TLFnUeufsniBTiPtg82fDb
@braghettos
braghettos merged commit d2923bd into main Nov 8, 2026
17 checks passed
@braghettos
braghettos deleted the feat/delegate branch November 8, 2026 15:17
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.

2 participants