azure-storage-listkeys-exposes-credential
listKeys/action returns storage account keys, bypassing all Azure RBAC and network controls for all sub-services (when SharedKey auth is enabled).
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
ObjectStorage
node_type(?account) matched provider_type == 'Microsoft.Storage/storageAccounts'
?principal has EFFECTIVE Microsoft.Storage/storageAccounts/listKeys/action at ?account scope (Storage Account Key Operator Service Role 81a9662b-bebf-436f-a333-f67b29880f12, Contributor, Owner, or custom role with listKeys)
emit
| source type | Identity |
|---|---|
| target type | AccessKey |
| source | ?principal |
| target | <primary/secondary AccessKey node(s) of ?account> |
| permissions | Microsoft.Storage/storageAccounts/listKeys/action |
| conditions | iam_permission |
| state logic | if ?account.properties.allowSharedKeyAccess == false: CONDITIONAL(service_state). Keys are retrieved by the ARM call (succeeds) but the storage service rejects SharedKey authentication — keys exist but are inert for data-plane access. Emit with confidence ~0.20 (keys present but non-functional). Elif a deny assignment or deny Azure Policy explicitly covers listKeys/action at the effective scope: BLOCKED; else ACTIVE. CRITICAL: the storage account firewall (networkAcls / publicNetworkAccess) gates the DATA-PLANE endpoint, NOT the ARM listKeys call — listKeys succeeds over management.azure.com from any network regardless of networkAcls.defaultAction. Do NOT apply network_reachability here. |
Narrative
{principal.name} can call listKeys on {account.name} (Microsoft.Storage/storageAccounts/listKeys/action), retrieving the primary and secondary account keys{if allowSharedKeyAccess==false: ' (however, SharedKey authentication is disabled on the account, rendering the keys non-functional for data-plane access)'}. These keys{if allowSharedKeyAccess!=false: ' bypass all Azure RBAC role assignments and network controls, authenticate as the storage account itself across all sub-services (blob, file, queue, table), and allow generating arbitrary SAS tokens - a complete credential exposure'else: ' are present but inert because allowSharedKeyAccess=false'}.
Raw rule rules/derived/azure/storage.yaml
id: azure-storage-listkeys-exposes-credential
emits: CanReadCredential
description: listKeys/action returns storage account keys, bypassing all Azure RBAC and network controls
for all sub-services (when SharedKey auth is enabled).
match:
- - principal: null
- HasPermission
- account: null
where:
- node_class(?account) == Storage and node_type(?account) == ObjectStorage
- node_type(?account) matched provider_type == 'Microsoft.Storage/storageAccounts'
- ?principal has EFFECTIVE Microsoft.Storage/storageAccounts/listKeys/action at ?account scope (Storage
Account Key Operator Service Role 81a9662b-bebf-436f-a333-f67b29880f12, Contributor, Owner, or custom
role with listKeys)
emit:
source_type: Identity
target_type: AccessKey
source: ?principal
target: <primary/secondary AccessKey node(s) of ?account>
permissions:
- Microsoft.Storage/storageAccounts/listKeys/action
conditions:
- iam_permission
state_logic: "if ?account.properties.allowSharedKeyAccess == false: CONDITIONAL(service_state). Keys\
\ are retrieved by the ARM call (succeeds) but the storage service rejects SharedKey authentication\
\ \u2014 keys exist but are inert for data-plane access. Emit with confidence ~0.20 (keys present\
\ but non-functional). Elif a deny assignment or deny Azure Policy explicitly covers listKeys/action\
\ at the effective scope: BLOCKED; else ACTIVE. CRITICAL: the storage account firewall (networkAcls\
\ / publicNetworkAccess) gates the DATA-PLANE endpoint, NOT the ARM listKeys call \u2014 listKeys\
\ succeeds over management.azure.com from any network regardless of networkAcls.defaultAction. Do\
\ NOT apply network_reachability here."
confidence: 'if allowSharedKeyAccess == false: 0.20 (keys exist but auth disabled); else 0.97'
derived_from:
- rule_id: azure-storage-listkeys-permission
binding: ?principal HasPermission ?account
false_positive_note: "listKeys is a control-plane ARM action callable regardless of the storage account\
\ firewall. Do NOT apply a network_reachability condition. Storage Account Contributor (17d1049b)\
\ does NOT include listKeys \u2014 verify effective permissions by resolving role definition actions,\
\ not by role name. CRITICAL: if allowSharedKeyAccess=false, the keys are retrieved (ARM call succeeds)\
\ but storage service rejects all SharedKey authentication \u2014 emit CONDITIONAL(service_state)\
\ with note that keys are structurally present but operationally inert. Emit BLOCKED only when a deny\
\ assignment or deny Azure Policy deny effect covers listKeys/action at the effective scope. The account\
\ keys are a ContainsCredential structural fact on the account; this rule derives the ExposesCredential\
\ capability edge. Also applies to ADLS Gen2 (HNS) accounts \u2014 the listKeys action is identical;\
\ ADLS-specific rules in adls.yaml are additive, not exclusive."
narrative: "{principal.name} can call listKeys on {account.name} (Microsoft.Storage/storageAccounts/listKeys/action),\
\ retrieving the primary and secondary account keys{if allowSharedKeyAccess==false: ' (however, SharedKey\
\ authentication is disabled on the account, rendering the keys non-functional for data-plane access)'}.\
\ These keys{if allowSharedKeyAccess!=false: ' bypass all Azure RBAC role assignments and network\
\ controls, authenticate as the storage account itself across all sub-services (blob, file, queue,\
\ table), and allow generating arbitrary SAS tokens \u2014 a complete credential exposure'else: '\
\ are present but inert because allowSharedKeyAccess=false'}."