Skip to content

feat: service accounts as FGA subjects#665

Open
lakhansamani wants to merge 2 commits into
mainfrom
feat/service-account-fga-subjects
Open

feat: service accounts as FGA subjects#665
lakhansamani wants to merge 2 commits into
mainfrom
feat/service-account-fga-subjects

Conversation

@lakhansamani

Copy link
Copy Markdown
Contributor

What

Registered kind=service_account clients can now be first-class FGA subjects: an autonomous client_credentials caller's check_permissions/list_permissions resolve to service_account:<client_id> instead of user:<sub>. Design follows the adversarially-verified research brief (GCP-IAM-style resource grants for autonomous machines; industry two-layer pattern preserved).

Guarantees (each enforced in code and tested)

  • Only service accounts: subject flips only for tokens whose login_method=service_account — stamped exclusively by the client_credentials mint (token.go:703). Lookup re-verifies kind=service_account; fail-closed otherwise.
  • Delegation untouched (security-critical): RFC 8693 delegated tokens carry no login_method and structurally cannot classify as machine — user stays the effective FGA subject; the act chain semantics are unchanged.
  • Opt-in by modeling: if the authorization model lacks the service_account type, machine checks fail closed (engine deny) — never fall back to a user subject.
  • Scopes unchanged: allowed_scopes remains the issuance ceiling; a machine request must pass BOTH the scope check and the FGA check.
  • Public identifier: subject keys on client_id (per fix: expose public client_id on Client API type #664), never the internal surrogate id — proven by a surrogate-keyed decoy-tuple test.
  • Machine callers may self-pin their own subject but cannot probe another subject (super-admin only).

Security review

Full security-engineer pass: no blockers. Both LOW hardening suggestions applied: deactivated (is_active=false) service accounts are denied at FGA-check time even with a live token, and the resolved subject id is rejected if it ever contains tuple syntax (defense in depth — it's a server UUID today).

Scope

One production file (internal/service/fga.go, +~100 lines) + 380-line integration test suite. No schema/proto changes; gRPC gets parity for free via the shared service layer + authctx.Principal.LoginMethod. enforceRequiredRelations (session required_relations) intentionally still user-only — flagged as follow-up if machine callers ever need it.

Testing

  • New TestFGAServiceAccount* suites: allow/deny per tuples, fail-closed without model type, deactivation denial, surrogate-id decoy, self-pin vs probe, user-token isolation, scope-ceiling independence — all minting real tokens through /oauth/token, not hand-crafted claims
  • Full internal/integration_tests suite passes (68s); build/vet/gofmt/golangci-lint clean

…ects

- A caller presenting a machine (client_credentials) access token now
  resolves to service_account:<client_id> in check_permissions and
  list_permissions, instead of a user subject.
- Classification keys only on the login_method claim; delegated (RFC 8693,
  act chain) and user tokens keep resolving to user:<sub>. The delegation
  guard is therefore structural, not a runtime check.
- Reuse the public client_id (not the internal surrogate id) as the FGA
  subject id so admin tuples and the client registry share one key.
- Fail closed: unresolvable/non-service_account client denies; a model
  without the service_account type denies (engine errors on the unknown
  subject type) and never falls back to a user subject.
- allowed_scopes stays the coarse ceiling at token issuance; FGA is
  additive below it. Admin tuple writes are already free-form, so no change
  is needed to reference service_account subjects.
Security-review LOWs: FGA is an authorization decision surface, so a
deactivated service account is denied at check time even while its
token remains valid elsewhere (delete already failed closed). The
resolved client_id is also rejected if it ever contains tuple syntax
(:#@/whitespace) — defense in depth; it is a server UUID today.
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