read-credential-yields-identity
Reading a credential that is credentials for an identity yields the ability to authenticate as that identity.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
CanReadCredential {'credential': None}
{'credential': None} CredentialsFor {'identity': None}
where
node_class(?identity) == Identity
node_type(?identity) in [
ServiceAccount, MachineIdentity, ManagedIdentity, WorkloadIdentity, ApplicationIdentity] # Azure SP, GCP SA, k8s SA, AWS service account variants
emit
| source type | Identity |
|---|---|
| source | ?principal |
| target | ?identity |
| conditions | iam_permission resource_policy key_permission |
| state logic | inherit_weakest(CanReadCredential, CredentialsFor): if the CanReadCredential input is CONDITIONAL (e.g., key_permission on CMEK-wrapped credential, decrypt not yet proven) then CONDITIONAL carries forward; if either input is BLOCKED then BLOCKED; else ACTIVE. CredentialsFor is a zero-cost link and never downgrades on its own. |
Narrative
{principal.name} can read {credential.name} ({perm:CanReadCredential}), which holds credentials for {identity.name}; reading it lets {principal.name} authenticate and act as {identity.name}.
Raw rule rules/derived/credential-chains.yaml
id: read-credential-yields-identity
emits: CanImpersonate
description: Reading a credential that is credentials for an identity yields the ability to authenticate
as that identity.
applies_to:
- '*'
match:
- - principal: null
- CanReadCredential
- credential: null
- - credential: null
- CredentialsFor
- identity: null
where:
- node_class(?identity) == Identity
- 'node_type(?identity) in [ServiceAccount, MachineIdentity, ManagedIdentity, WorkloadIdentity, ApplicationIdentity] #
Azure SP, GCP SA, k8s SA, AWS service account variants'
emit:
source_type: Identity
source: ?principal
target: ?identity
permissions: []
conditions:
- iam_permission
- resource_policy
- key_permission
state_logic: 'inherit_weakest(CanReadCredential, CredentialsFor): if the CanReadCredential input is
CONDITIONAL (e.g., key_permission on CMEK-wrapped credential, decrypt not yet proven) then CONDITIONAL
carries forward; if either input is BLOCKED then BLOCKED; else ACTIVE. CredentialsFor is a zero-cost
link and never downgrades on its own.'
confidence: min(contributing_confidences)
derived_from:
- ?principal CanReadCredential ?credential
- ?credential CredentialsFor ?identity
false_positive_note: "Only emit when the credential genuinely authenticates the target identity (CredentialsFor\
\ must be an explicit, evidenced link \u2014 e.g. an AD machine account AccessKey, an SSH key, an\
\ API token, or a signing certificate bound to that principal), not merely a credential the identity\
\ happens to own or that sits in its namespace. If the credential is rotated/disabled/expired, the\
\ adapter should not have emitted CredentialsFor -> no impersonation. When the CanReadCredential is\
\ CONDITIONAL(key_permission), do NOT report this as an ACTIVE impersonation path until key_permission\
\ gate is satisfied. For a plain User backed by a password, prefer CredentialValidFor/AuthenticatesTo\
\ \u2014 do not force a CanImpersonate whose target set excludes User."
narrative: '{principal.name} can read {credential.name} ({perm:CanReadCredential}), which holds credentials
for {identity.name}; reading it lets {principal.name} authenticate and act as {identity.name}.'