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 = cc

View on GitHub (pinned to c9def3e214)

Solutions

  1. 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>
  2. Verify subscription_id and resource_group_name match the storage account: az storage account show -n <account> -g <rg> --query id
  3. Switch to explicit access_key or use_azuread_auth = true in the backend block to bypass the ListKeys ARM call entirely
  4. Confirm the credential can list keys directly: az storage account keys list -g <rg> -n <account>
  5. 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

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


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/ea90b0c6b6bbc831. Report an issue: GitHub.