decrypt-gates-secret-read

Holding decrypt on a secret's wrapping key upgrades a key-gated (CONDITIONAL) secret read to ACTIVE.

derived any cloud emits CanReadSecret

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'principal': None} CanReadSecret {'secret': None} {'principal': None} CanDecrypt {'key': None}

where

state_of(match[0]) == CONDITIONAL and gating_condition(match[0]) == key_permission wraps(?key, ?secret) node_class(?key) == Secret and node_type(?key) == EncryptionKey

emit

source?principal
target?secret
state logicUPGRADE: re-emit the CanReadSecret with state ACTIVE (the key_permission gate is now satisfied by the matched CanDecrypt). If the CanDecrypt input is itself CONDITIONAL (e.g. a KMS key policy / grant condition unmet) then the upgraded read is only as strong as that decrypt — CONDITIONAL(key_permission) persists; if CanDecrypt is BLOCKED (explicit key-policy/SCP deny on kms:Decrypt) the read stays BLOCKED for the plaintext. Monotonic re-emit updates the existing edge in place (RULE-FORMAT: re-derivation updates state).

Narrative

{principal.name} can read {secret.name} outright: its secret-store read was gated on the wrapping key {key.name}, and {principal.name} also holds decrypt on {key.name} ({perm:CanDecrypt}), satisfying the key-permission gate.

Raw rule rules/derived/credential-chains.yaml

id: decrypt-gates-secret-read
emits: CanReadSecret
description: Holding decrypt on a secret's wrapping key upgrades a key-gated (CONDITIONAL) secret read
  to ACTIVE.
applies_to:
- '*'
match:
- - principal: null
  - CanReadSecret
  - secret: null
- - principal: null
  - CanDecrypt
  - key: null
where:
- state_of(match[0]) == CONDITIONAL and gating_condition(match[0]) == key_permission
- wraps(?key, ?secret)
- node_class(?key) == Secret and node_type(?key) == EncryptionKey
emit:
  source: ?principal
  target: ?secret
  permissions: []
  conditions: []
  state_logic: "UPGRADE: re-emit the CanReadSecret with state ACTIVE (the key_permission gate is now satisfied\
    \ by the matched CanDecrypt). If the CanDecrypt input is itself CONDITIONAL (e.g. a KMS key policy\
    \ / grant condition unmet) then the upgraded read is only as strong as that decrypt \u2014 CONDITIONAL(key_permission)\
    \ persists; if CanDecrypt is BLOCKED (explicit key-policy/SCP deny on kms:Decrypt) the read stays\
    \ BLOCKED for the plaintext. Monotonic re-emit updates the existing edge in place (RULE-FORMAT: re-derivation\
    \ updates state)."
  confidence: min(contributing_confidences)
  derived_from:
  - ?principal CanReadSecret ?secret (CONDITIONAL key_permission)
  - ?principal CanDecrypt ?key
  - wraps(?key, ?secret)
  false_positive_note: "Do NOT upgrade unless the decrypt is on the EXACT key that wraps the secret; a\
    \ decrypt grant on a different key is irrelevant. Some stores decrypt secret material with a service-managed\
    \ key transparently (no caller decrypt needed) \u2014 in that case the adapter should have emitted\
    \ the read as ACTIVE, not CONDITIONAL(key_permission), and this rule is a no-op. Honor key-policy\
    \ / grant conditions (e.g. AWS kms:ViaService, EncryptionContext, GCP CMEK binding, Azure Key Vault\
    \ key-permission): if the CanDecrypt is conditional or denied, do not force the read to ACTIVE. Also\
    \ applies when the decrypt is held by an identity the principal CanExecuteAs \u2014 resolve the effective\
    \ decryptor."
  narrative: '{principal.name} can read {secret.name} outright: its secret-store read was gated on the
    wrapping key {key.name}, and {principal.name} also holds decrypt on {key.name} ({perm:CanDecrypt}),
    satisfying the key-permission gate.'
move · open · esc close