read-secret-yields-identity

Reading a secret that is credentials for an identity yields the ability to authenticate as that identity.

derived any cloud emits CanImpersonate

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 typeIdentity
source?principal
target?identity
conditionsiam_permission resource_policy key_permission
state logicinherit_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}.'
move · open · esc close