read-secret-yields-identity
Reading a secret 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}
CanReadSecret {'secret': None}
{'secret': None} CredentialsFor {'identity': None}
where
node_class(?identity) == Identity
node_type(?identity) in [
ServiceAccount, MachineIdentity, ManagedIdentity, WorkloadIdentity, ApplicationIdentity] # Azure SP is modeled as ApplicationIdentity
emit
| source type | Identity |
|---|---|
| source | ?principal |
| target | ?identity |
| conditions | iam_permission resource_policy key_permission |
| state logic | inherit_weakest(CanReadSecret, CredentialsFor): if the CanReadSecret input is CONDITIONAL(key_permission) (CMEK/KMS-wrapped secret, decrypt not yet proven) then CONDITIONAL(key_permission); 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 {secret.name} ({perm:CanReadSecret}), 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-secret-yields-identity
emits: CanImpersonate
description: Reading a secret that is credentials for an identity yields the ability to authenticate as
that identity.
applies_to:
- '*'
match:
- - principal: null
- CanReadSecret
- secret: null
- - secret: null
- CredentialsFor
- identity: null
where:
- node_class(?identity) == Identity
- 'node_type(?identity) in [ServiceAccount, MachineIdentity, ManagedIdentity, WorkloadIdentity, ApplicationIdentity] #
Azure SP is modeled as ApplicationIdentity'
emit:
source_type: Identity
source: ?principal
target: ?identity
permissions: []
conditions:
- iam_permission
- resource_policy
- key_permission
state_logic: 'inherit_weakest(CanReadSecret, CredentialsFor): if the CanReadSecret input is CONDITIONAL(key_permission)
(CMEK/KMS-wrapped secret, decrypt not yet proven) then CONDITIONAL(key_permission); 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 CanReadSecret ?secret
- ?secret CredentialsFor ?identity
false_positive_note: "Only emit when the secret genuinely authenticates the target identity (CredentialsFor\
\ must be an explicit, evidenced link \u2014 e.g. a stored access key, service-account key JSON, connection\
\ string, or client secret bound to that principal), not merely a secret the identity happens to own\
\ or that sits in its namespace. If the secret is rotated/disabled/expired, the adapter should not\
\ have emitted CredentialsFor -> no impersonation. When the CanReadSecret is CONDITIONAL(key_permission),\
\ do NOT report this as an ACTIVE impersonation path until decrypt-gates-secret-read upgrades it.\
\ 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 {secret.name} ({perm:CanReadSecret}), which holds credentials
for {identity.name}; reading it lets {principal.name} authenticate and act as {identity.name}.'