hashicorp/terraform · error
retrieving key for Storage Account %q: %s
Error message
retrieving key for Storage Account %q: %s
What it means
When no explicit access_key, sas_token, or use_azuread_auth is configured, the Azure backend authenticates to the Azure Resource Manager (ARM) plane and calls StorageAccounts.ListKeys to fetch a storage account key, then uses that key to sign data-plane blob requests (getBlobClient default branch at api_client.go:182-184). This error wraps the failure of that ListKeys ARM call and surfaces the underlying ARM error (RBAC denial, account not found, throttling, etc.). It is thrown at the point the backend realizes it cannot obtain a key to talk to the storage data plane.
Source
Thrown at internal/backend/remote-state/azure/api_client.go:184
log.Printf("[DEBUG] Building the Blob Client from an Access Key")
authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, c.accessKey, auth.SharedKey)
if err != nil {
return nil, fmt.Errorf("new shared key authorizer: %v", err)
}
c.configureClient(blobsClient.Client, authorizer)
return blobsClient, nil
case c.azureAdStorageAuth != nil:
log.Printf("[DEBUG] Building the Blob Client from AAD auth")
c.configureClient(blobsClient.Client, c.azureAdStorageAuth)
return blobsClient, nil
default:
// Neither shared access key, sas token, or AAD Auth were specified so we have to call the management plane API to get the key.
log.Printf("[DEBUG] Building the Blob Client from an Access Key (key is listed using client credentials)")
key, err := c.accountDetail.AccountKey(ctx, c.storageAccountsClient)
if err != nil {
return nil, fmt.Errorf("retrieving key for Storage Account %q: %s", c.storageAccountName, err)
}
authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, *key, auth.SharedKey)
if err != nil {
return nil, fmt.Errorf("new shared key authorizer: %v", err)
}
c.configureClient(blobsClient.Client, authorizer)
return blobsClient, nil
}
}
func (c *Client) getContainersClient(ctx context.Context) (cc *containers.Client, err error) {
if c.containersClient != nil {
return c.containersClient, nil
}
defer func() {
if err == nil {
c.containersClient = ccView on GitHub (pinned to c9def3e214)
Solutions
- Grant the authenticated principal the 'Storage Account Contributor' role (or 'Storage Account Key Operator Service' role) scoped to the storage account or its resource group: az role assignment create --role 'Storage Account Key Operator Service' --assignee <principal-id> --scope <storage-account-resource-id>
- Verify subscription_id and resource_group_name match the storage account: az storage account show -n <account> -g <rg> --query id
- Switch to explicit access_key or use_azuread_auth = true in the backend block to bypass the ListKeys ARM call entirely
- Confirm the credential can list keys directly: az storage account keys list -g <rg> -n <account>
- For transient ARM throttling, reduce concurrent Terraform runs or retry after a short backoff
Example fix
// before (no auth method -> relies on ListKeys)
terraform {
backend "azurerm" {
storage_account_name = "mystorage"
container_name = "tfstate"
key = "prod.terraform.tfstate"
# no access_key / sas_token / use_azuread_auth
}
}
// after (avoid ListKeys entirely)
terraform {
backend "azurerm" {
storage_account_name = "mystage"
container_name = "tfstate"
key = "prod.terraform.tfstate"
use_azuread_auth = true
subscription_id = "00000000-0000-0000-0000-000000000000"
resource_group_name = "rg-tfstate"
}
} Defensive patterns
Strategy: validation
Validate before calling
# Before running terraform, confirm the principal can list storage keys ACCOUNT_ID=$(az storage account show -n "$ARM_STORAGE_ACCOUNT_NAME" -g "$ARM_RESOURCE_GROUP_NAME" --query id -o tsv 2>/dev/null) az role assignment list --assignee "$ARM_CLIENT_ID" --scope "$ACCOUNT_ID" --query "[].roleDefinitionName" -o tsv | grep -iE 'Storage Account (Contributor|Key Operator)|Contributor' \ || echo "WARN: principal lacks listKeys permission -> error 140 likely" # sanity: actually try to list keys az storage account keys list -g "$ARM_RESOURCE_GROUP_NAME" -n "$ARM_STORAGE_ACCOUNT_NAME" >/dev/null \ && echo "OK: ListKeys works" || echo "FAIL: ListKeys denied"
Prevention
- Grant the Terraform principal 'Storage Account Key Operator Service' or 'Contributor' scoped to the storage account, not the whole subscription
- Prefer use_azuread_auth = true (data-plane RBAC) to remove the ListKeys dependency entirely
- Pin subscription_id and resource_group_name in the backend block rather than relying on inference
- In CI, assert 'az storage account keys list' succeeds as a pre-deploy gate
When it happens
Trigger: Produced when getBlobClient reaches its default branch (no sasToken, no accessKey, no azureAdStorageAuth) and calls c.accountDetail.AccountKey -> storageaccounts.StorageAccountsClient.ListKeys, which returns an error. This happens on the first blob operation (state read/write) after Configure, because the key is fetched lazily.
Common situations: The Service Principal / Managed Identity / CLI credential used by Terraform lacks the 'Microsoft.Storage/storageAccounts/listKeys/action' permission or a Contributor-equivalent role on the storage account; a wrong subscription_id or storage_account_name typo; conditional-access policies blocking the ARM request; ARM throttling under many concurrent Terraform runs; the storage account is in a region/sovereign cloud the credential cannot reach.
Related errors
- listing Keys for %s: %+v
- retrieving container client: %v
- unable to determine the Write Key for %s
- populating details for %s: `model.Properties` was nil
- populating details for %s: `model.Properties.PrimaryEndpoint
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/ea90b0c6b6bbc831.
Report an issue: GitHub.