decrypt-gates-secret-read
Holding decrypt on a secret's wrapping key upgrades a key-gated (CONDITIONAL) secret read to ACTIVE.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
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 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 — 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.'