cloud security

Detecting Azure Storage Account Misconfigurations

Audit Azure Blob and ADLS storage accounts for public access exposure, weak or long-lived SAS tokens, missing encryption at rest, disabled HTTPS-only traffic, and outdated TLS versions using the azure-mgmt-storage Python SDK.

adlsazureazure-mgmt-storageblob-storagecloud-misconfigurationencryptionpublic-accesssas-tokens
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Azure Storage accounts are a frequent target for attackers due to misconfigured public access, long-lived SAS tokens, missing encryption, and outdated TLS versions. This skill uses the azure-mgmt-storage Python SDK with StorageManagementClient to enumerate all storage accounts in a subscription, inspect their security properties, list blob containers for public access settings, and generate a risk-scored audit report identifying critical misconfigurations.

When to Use

  • When investigating security incidents that require detecting azure storage account misconfigurations
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • Python 3.9+ with azure-mgmt-storage, azure-identity
  • Azure service principal with Reader role on target subscription
  • Environment variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_ID

Key Detection Areas

  1. Public blob accessallow_blob_public_access enabled on storage account or individual containers set to Blob/Container access level
  2. HTTPS enforcementenable_https_traffic_only disabled, allowing unencrypted HTTP traffic
  3. Minimum TLS version — accounts accepting TLS 1.0 or TLS 1.1 instead of minimum TLS 1.2
  4. Encryption at rest — storage service encryption not enabled or missing customer-managed keys
  5. Network rules — default action set to Allow instead of Deny, exposing storage to all networks
  6. SAS token risks — account-level SAS with overly broad permissions or excessive lifetime

Output

JSON report with per-account findings, severity ratings (Critical/High/Medium/Low), and remediation recommendations aligned with CIS Azure Benchmark controls.

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md3.3 KB

Azure Storage Account Misconfiguration Detection Reference

SDK Installation

pip install azure-mgmt-storage azure-identity

StorageManagementClient Initialization

from azure.identity import DefaultAzureCredential
from azure.mgmt.storage import StorageManagementClient
 
client = StorageManagementClient(
    credential=DefaultAzureCredential(),
    subscription_id="<subscription-id>"
)

Key Operations

List All Storage Accounts

for account in client.storage_accounts.list():
    print(account.name, account.location, account.kind)

Get Storage Account Properties

account = client.storage_accounts.get_properties(
    resource_group_name="myResourceGroup",
    account_name="mystorageaccount"
)

List Blob Containers

containers = client.blob_containers.list(
    resource_group_name="myResourceGroup",
    account_name="mystorageaccount"
)
for container in containers:
    print(container.name, container.public_access)

Security Properties to Audit

Property Secure Value Risk if Misconfigured
allow_blob_public_access False Critical — data exposed to internet
enable_https_traffic_only True High — credentials sent in cleartext
minimum_tls_version TLS1_2 High — vulnerable to downgrade attacks
encryption.services.blob.enabled True High — data at rest unencrypted
encryption.key_source Microsoft.Keyvault Low — Microsoft-managed keys less controlled
network_rule_set.default_action Deny High — storage open to all networks
encryption.require_infrastructure_encryption True Low — no double encryption

Container Public Access Levels

Level Description Risk
None Private, no public access Safe
Blob Anonymous read for blobs only High
Container Anonymous read for container and blobs Critical

Azure CLI Equivalents

# List storage accounts
az storage account list --query "[].{name:name, publicAccess:allowBlobPublicAccess, httpsOnly:enableHttpsTrafficOnly, minTls:minimumTlsVersion}" -o table
 
# Check specific account
az storage account show -n mystorageaccount -g myResourceGroup
 
# List containers with access level
az storage container list --account-name mystorageaccount --query "[].{name:name, publicAccess:properties.publicAccess}" -o table
 
# Disable public blob access
az storage account update -n mystorageaccount -g myResourceGroup --allow-blob-public-access false
 
# Set minimum TLS
az storage account update -n mystorageaccount -g myResourceGroup --min-tls-version TLS1_2

CIS Azure Benchmark Controls

Control Description
3.1 Ensure 'Secure transfer required' is enabled
3.7 Ensure default network access rule is set to deny
3.8 Ensure 'Trusted Microsoft Services' is enabled
3.10 Ensure storage logging is enabled for Blob service
3.12 Ensure storage account access keys are periodically regenerated

Environment Variables

Variable Description
AZURE_SUBSCRIPTION_ID Target Azure subscription
AZURE_CLIENT_ID Service principal application ID
AZURE_TENANT_ID Azure AD tenant ID
AZURE_CLIENT_SECRET Service principal secret

Scripts 1

agent.py8.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Audit Azure Storage accounts for security misconfigurations using azure-mgmt-storage SDK."""

import argparse
import json
import os
import sys
from datetime import datetime


def get_storage_client():
    """Initialize StorageManagementClient with DefaultAzureCredential."""
    try:
        from azure.identity import DefaultAzureCredential
        from azure.mgmt.storage import StorageManagementClient
    except ImportError:
        print("Install required packages: pip install azure-mgmt-storage azure-identity", file=sys.stderr)
        sys.exit(1)

    subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID")
    if not subscription_id:
        print("Set AZURE_SUBSCRIPTION_ID environment variable", file=sys.stderr)
        sys.exit(1)

    credential = DefaultAzureCredential()
    return StorageManagementClient(credential=credential, subscription_id=subscription_id)


def audit_storage_account(client, account):
    """Audit a single storage account for misconfigurations."""
    findings = []
    resource_group = account.id.split("/")[4]
    account_name = account.name

    # Check public blob access
    if account.allow_blob_public_access is True:
        findings.append({
            "check": "public_blob_access",
            "severity": "Critical",
            "message": f"Storage account '{account_name}' allows public blob access",
            "remediation": "Set allow_blob_public_access to false on the storage account"
        })

    # Check HTTPS-only enforcement
    if account.enable_https_traffic_only is False:
        findings.append({
            "check": "https_enforcement",
            "severity": "High",
            "message": f"Storage account '{account_name}' allows HTTP traffic",
            "remediation": "Enable 'Secure transfer required' in storage account settings"
        })

    # Check minimum TLS version
    min_tls = getattr(account, "minimum_tls_version", None)
    if min_tls and min_tls in ("TLS1_0", "TLS1_1"):
        findings.append({
            "check": "minimum_tls_version",
            "severity": "High",
            "message": f"Storage account '{account_name}' allows {min_tls} (should be TLS1_2)",
            "remediation": "Set minimum TLS version to TLS1_2"
        })

    # Check encryption at rest
    encryption = account.encryption
    if encryption:
        if not getattr(encryption.services, "blob", None) or not encryption.services.blob.enabled:
            findings.append({
                "check": "blob_encryption",
                "severity": "High",
                "message": f"Storage account '{account_name}' does not have blob encryption enabled",
                "remediation": "Enable Azure Storage Service Encryption for blobs"
            })
        if not getattr(encryption.services, "file", None) or not encryption.services.file.enabled:
            findings.append({
                "check": "file_encryption",
                "severity": "Medium",
                "message": f"Storage account '{account_name}' does not have file encryption enabled",
                "remediation": "Enable Azure Storage Service Encryption for files"
            })
    else:
        findings.append({
            "check": "encryption_missing",
            "severity": "Critical",
            "message": f"Storage account '{account_name}' has no encryption configuration",
            "remediation": "Enable Azure Storage Service Encryption"
        })

    # Check network rules - default action
    network_rules = account.network_rule_set
    if network_rules and network_rules.default_action == "Allow":
        findings.append({
            "check": "network_default_allow",
            "severity": "High",
            "message": f"Storage account '{account_name}' allows access from all networks",
            "remediation": "Set network default action to Deny and add specific virtual network/IP rules"
        })

    # Check infrastructure encryption (double encryption)
    if encryption and not getattr(encryption, "require_infrastructure_encryption", False):
        findings.append({
            "check": "infrastructure_encryption",
            "severity": "Low",
            "message": f"Storage account '{account_name}' does not use infrastructure encryption (double encryption)",
            "remediation": "Enable infrastructure encryption for additional protection"
        })

    # Check key source - prefer customer-managed keys
    if encryption and getattr(encryption, "key_source", None) == "Microsoft.Storage":
        findings.append({
            "check": "customer_managed_keys",
            "severity": "Low",
            "message": f"Storage account '{account_name}' uses Microsoft-managed keys instead of customer-managed keys",
            "remediation": "Configure customer-managed keys via Azure Key Vault for enhanced control"
        })

    return {
        "account_name": account_name,
        "resource_group": resource_group,
        "location": account.location,
        "sku": account.sku.name if account.sku else "unknown",
        "kind": account.kind,
        "findings": findings,
        "finding_count": len(findings)
    }


def audit_blob_containers(client, account):
    """Check blob containers for individual public access settings."""
    resource_group = account.id.split("/")[4]
    container_findings = []

    try:
        containers = client.blob_containers.list(
            resource_group_name=resource_group,
            account_name=account.name
        )
        for container in containers:
            public_access = getattr(container, "public_access", None)
            if public_access and public_access != "None":
                container_findings.append({
                    "container_name": container.name,
                    "public_access_level": str(public_access),
                    "severity": "Critical",
                    "message": f"Container '{container.name}' has public access level: {public_access}",
                    "remediation": "Set container public access level to 'None' (private)"
                })
    except Exception as e:
        container_findings.append({
            "error": f"Could not list containers for {account.name}: {str(e)}"
        })

    return container_findings


def run_audit(args):
    """Run the full storage account audit."""
    client = get_storage_client()
    results = {
        "scan_time": datetime.utcnow().isoformat() + "Z",
        "subscription_id": os.environ.get("AZURE_SUBSCRIPTION_ID", ""),
        "accounts": [],
        "summary": {
            "total_accounts": 0,
            "accounts_with_findings": 0,
            "critical": 0,
            "high": 0,
            "medium": 0,
            "low": 0
        }
    }

    accounts = list(client.storage_accounts.list())
    results["summary"]["total_accounts"] = len(accounts)

    for account in accounts:
        account_result = audit_storage_account(client, account)

        if args.check_containers:
            container_findings = audit_blob_containers(client, account)
            account_result["container_findings"] = container_findings

        results["accounts"].append(account_result)

        if account_result["finding_count"] > 0:
            results["summary"]["accounts_with_findings"] += 1

        for finding in account_result["findings"]:
            severity = finding.get("severity", "").lower()
            if severity in results["summary"]:
                results["summary"][severity] += 1

    return results


def main():
    parser = argparse.ArgumentParser(
        description="Audit Azure Storage accounts for security misconfigurations"
    )
    parser.add_argument(
        "--check-containers", action="store_true",
        help="Also check individual blob container public access settings"
    )
    parser.add_argument(
        "--output", "-o", default="-",
        help="Output file path (default: stdout)"
    )
    parser.add_argument(
        "--severity-filter", choices=["critical", "high", "medium", "low"],
        help="Only show findings at or above this severity level"
    )
    args = parser.parse_args()

    results = run_audit(args)

    if args.severity_filter:
        severity_order = {"critical": 4, "high": 3, "medium": 2, "low": 1}
        min_severity = severity_order[args.severity_filter]
        for account in results["accounts"]:
            account["findings"] = [
                f for f in account["findings"]
                if severity_order.get(f.get("severity", "").lower(), 0) >= min_severity
            ]
            account["finding_count"] = len(account["findings"])

    output_json = json.dumps(results, indent=2)

    if args.output == "-":
        print(output_json)
    else:
        with open(args.output, "w") as f:
            f.write(output_json)
        print(f"Audit report written to {args.output}", file=sys.stderr)


if __name__ == "__main__":
    main()
Keep exploring