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).

derived azure emits CanReadCredential

match

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

{'principal': None} HasPermission {'account': None}

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 typeIdentity
target typeAccessKey
source?principal
target<primary/secondary AccessKey node(s) of ?account>
permissionsMicrosoft.Storage/storageAccounts/listKeys/action
conditionsiam_permission
state logicif ?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'}."
move · open · esc close