Summary
Rotating a service key replaces the human attribution on every token it subsequently mints. act.sub becomes the literal string system:key_rotation instead of a user id, so "which human initiated this action" is permanently lost for the rotated credential and every agent identity delegated from it.
Evidence
Before rotation, an api-key-grant token carries the human who created the key — which is correct and by design (internal/service/oauth.go:1398, ActingUserID: sk.CreatedBy):
After POST /agents/registry/{id}/rotate-key on the same identity, a fresh exchange of the new key yields:
HUMAN act.sub on orchestrator token : system:key_rotation
ORCHESTRATOR sub : spiffe://highflame.dev/<acct>/<proj>/agent/hf-portal-concierge
SUBAGENT act.sub (who delegated) : spiffe://highflame.dev/<acct>/<proj>/agent/hf-portal-concierge
delegation_depth : 1
The agent→agent link survives rotation intact. Only the human→agent link is severed, because the rotated key's CreatedBy is the rotation subsystem rather than a user.
Impact
This was found while validating a customer requirement stated as:
"preserve a traceable chain showing which human, workflow, orchestrator, and sub-agent initiated each action"
Every other link in that chain holds up well. This one breaks on a routine operational action — and key rotation is something we actively encourage. The failure is silent: nothing in the token or the guard response indicates that the human attribution was lost rather than never present, so an audit months later cannot distinguish "rotated key" from "no human involved".
It is also unrecoverable for existing credentials: created_by/owner_user_id is fixed at registration and cannot be updated.
Fix options
- Preserve the original creator across rotation (recommended). Rotation changes the secret, not who owns or created the identity. Carry the prior key's
CreatedBy onto the new key so act.sub continues to name the human.
- Or record the rotation actor. If rotation is performed by a human via Studio/API, that human is the better value than
system:key_rotation. Only truly automated rotation should fall back to a system principal.
- If a system principal is unavoidable, keep the human separately. Emit the identity's
owner_user_id as its own claim so the human is still recoverable even when act.sub is a subsystem — and so consumers can tell "rotated" apart from "no human".
- Make it visible. Whatever the resolution, a token whose human attribution is a system principal should be distinguishable in Studio's audit view, not just in the raw claim.
Summary
Rotating a service key replaces the human attribution on every token it subsequently mints.
act.subbecomes the literal stringsystem:key_rotationinstead of a user id, so "which human initiated this action" is permanently lost for the rotated credential and every agent identity delegated from it.Evidence
Before rotation, an api-key-grant token carries the human who created the key — which is correct and by design (
internal/service/oauth.go:1398,ActingUserID: sk.CreatedBy):After
POST /agents/registry/{id}/rotate-keyon the same identity, a fresh exchange of the new key yields:The agent→agent link survives rotation intact. Only the human→agent link is severed, because the rotated key's
CreatedByis the rotation subsystem rather than a user.Impact
This was found while validating a customer requirement stated as:
Every other link in that chain holds up well. This one breaks on a routine operational action — and key rotation is something we actively encourage. The failure is silent: nothing in the token or the guard response indicates that the human attribution was lost rather than never present, so an audit months later cannot distinguish "rotated key" from "no human involved".
It is also unrecoverable for existing credentials:
created_by/owner_user_idis fixed at registration and cannot be updated.Fix options
CreatedByonto the new key soact.subcontinues to name the human.system:key_rotation. Only truly automated rotation should fall back to a system principal.owner_user_idas its own claim so the human is still recoverable even whenact.subis a subsystem — and so consumers can tell "rotated" apart from "no human".