Azure Cost Management exports contain highly sensitive financial and organizational data—detailed resource usage, cost allocations, tag information, and subscription hierarchies. A security breach could expose pricing negotiations, project budgets, or confidential business structures. This guide implements defense-in-depth security for cost management data across Azure Storage and Synapse Analytics, ensuring compliance with data protection regulations while maintaining analytical capabilities.
Overview
Securing cost management data requires a multi-layered approach:
- Encryption at rest and in transit: Protect data from unauthorized access
- Network isolation: Use private endpoints to prevent public internet exposure
- Identity-based access control: Implement least-privilege RBAC policies
- Data classification: Apply sensitivity labels and compliance tagging
- Audit logging: Track all access and modifications to billing data
- Key management: Secure encryption keys with Azure Key Vault
- Compliance alignment: Meet GDPR, SOC 2, and industry regulations
This guide covers four security layers:
- Storage Account Security: Encryption, firewall rules, private endpoints
- Synapse Analytics Security: Workspace isolation, SQL security, data masking
- Access Control: RBAC, managed identities, conditional access
- Monitoring & Compliance: Auditing, alerts, compliance posture
Prerequisites
Before you begin, ensure you have:
- Azure subscription with Owner or Security Administrator role
- Existing cost exports configured to storage account
- Azure Storage account with cost export data
- Azure Synapse workspace (if using Synapse for analytics)
- Azure CLI (2.40.0+) OR Azure PowerShell module (7.0.0+)
- Azure Key Vault for customer-managed keys (optional but recommended)
- Understanding of Azure RBAC and networking concepts
Security Layer 1: Storage Account Hardening
Step 1: Enable Infrastructure Encryption
# Create storage account with double encryption
az storage account create \
--name securecoststorage \
--resource-group rg-cost-management \
--location eastus \
--sku Standard_GRS \
--kind StorageV2 \
--require-infrastructure-encryption \
--encryption-services blob file \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--enable-hierarchical-namespace false
# Verify encryption
az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query "{InfraEncryption:encryption.requireInfrastructureEncryption, Services:encryption.services}"
Step 2: Configure Customer-Managed Encryption Keys
# Create Key Vault for encryption keys
az keyvault create \
--name kv-cost-encryption \
--resource-group rg-cost-management \
--location eastus \
--enable-soft-delete true \
--enable-purge-protection true \
--retention-days 90
# Create encryption key
az keyvault key create \
--vault-name kv-cost-encryption \
--name cost-data-encryption-key \
--protection software \
--size 2048 \
--kty RSA
# Grant storage account access to Key Vault
STORAGE_PRINCIPAL_ID=$(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query identity.principalId -o tsv)
az keyvault set-policy \
--name kv-cost-encryption \
--object-id $STORAGE_PRINCIPAL_ID \
--key-permissions get unwrapKey wrapKey
# Enable customer-managed keys
KEY_VAULT_URI=$(az keyvault show \
--name kv-cost-encryption \
--query properties.vaultUri -o tsv)
az storage account update \
--name securecoststorage \
--resource-group rg-cost-management \
--encryption-key-name cost-data-encryption-key \
--encryption-key-source Microsoft.Keyvault \
--encryption-key-vault $KEY_VAULT_URI
Step 3: Implement Network Security
# Disable public network access
az storage account update \
--name securecoststorage \
--resource-group rg-cost-management \
--public-network-access Disabled
# Create virtual network
az network vnet create \
--name vnet-cost-management \
--resource-group rg-cost-management \
--location eastus \
--address-prefix 10.0.0.0/16
# Create subnet for private endpoints
az network vnet subnet create \
--name snet-private-endpoints \
--resource-group rg-cost-management \
--vnet-name vnet-cost-management \
--address-prefix 10.0.1.0/24 \
--disable-private-endpoint-network-policies true
# Create private endpoint for blob storage
az network private-endpoint create \
--name pe-cost-storage-blob \
--resource-group rg-cost-management \
--vnet-name vnet-cost-management \
--subnet snet-private-endpoints \
--private-connection-resource-id $(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query id -o tsv) \
--group-id blob \
--connection-name cost-storage-connection
# Create private DNS zone
az network private-dns zone create \
--name privatelink.blob.core.windows.net \
--resource-group rg-cost-management
# Link DNS zone to VNet
az network private-dns link vnet create \
--name cost-dns-link \
--resource-group rg-cost-management \
--zone-name privatelink.blob.core.windows.net \
--virtual-network vnet-cost-management \
--registration-enabled false
# Create DNS zone group
az network private-endpoint dns-zone-group create \
--name cost-dns-zone-group \
--resource-group rg-cost-management \
--endpoint-name pe-cost-storage-blob \
--private-dns-zone privatelink.blob.core.windows.net \
--zone-name cost-blob-zone
Step 4: Enable Advanced Threat Protection
# Enable Microsoft Defender for Storage
az security atp storage update \
--resource-group rg-cost-management \
--storage-account securecoststorage \
--is-enabled true
# Configure threat protection settings
az storage account update \
--name securecoststorage \
--resource-group rg-cost-management \
--enable-alw true
Step 5: Configure Immutable Storage Policies
# Enable versioning for accidental deletion protection
az storage account blob-service-properties update \
--account-name securecoststorage \
--resource-group rg-cost-management \
--enable-versioning true \
--enable-change-feed true
# Create container with immutability policy
az storage container create \
--name cost-exports-protected \
--account-name securecoststorage \
--public-access off \
--auth-mode login
# Set time-based retention policy (7 years for financial records)
az storage container immutability-policy create \
--account-name securecoststorage \
--container-name cost-exports-protected \
--period 2555 \
--resource-group rg-cost-management
# Lock the policy (irreversible)
az storage container immutability-policy lock \
--account-name securecoststorage \
--container-name cost-exports-protected \
--if-match "*" \
--resource-group rg-cost-management
Security Layer 2: Synapse Analytics Security
Step 1: Create Secure Synapse Workspace
# Create managed virtual network for Synapse
az synapse workspace create \
--name synapse-secure-cost-analytics \
--resource-group rg-cost-management \
--storage-account securecoststorage \
--file-system cost-data \
--sql-admin-login-user sqladmin \
--sql-admin-login-password "$(openssl rand -base64 32)" \
--location eastus \
--enable-managed-virtual-network true \
--prevent-data-exfiltration true \
--allowed-tenant-ids "your-tenant-id"
# Disable public network access
az synapse workspace update \
--name synapse-secure-cost-analytics \
--resource-group rg-cost-management \
--enable-public-network-access false
Step 2: Configure Managed Private Endpoints
# Create managed private endpoint to storage
az synapse managed-private-endpoints create \
--workspace-name synapse-secure-cost-analytics \
--pe-name mpe-cost-storage \
--file-path ./managed-pe-config.json \
--resource-group rg-cost-management
managed-pe-config.json:
{
"name": "mpe-cost-storage",
"properties": {
"privateLinkResourceId": "/subscriptions/{subscription-id}/resourceGroups/rg-cost-management/providers/Microsoft.Storage/storageAccounts/securecoststorage",
"groupId": "blob"
}
}
Step 3: Implement SQL Security (Serverless or Dedicated Pool)
-- Connect to Synapse serverless SQL pool
-- Create database with encryption
CREATE DATABASE CostAnalytics;
GO
USE CostAnalytics;
GO
-- Enable Transparent Data Encryption
ALTER DATABASE CostAnalytics SET ENCRYPTION ON;
GO
-- Create master key
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'Strong_P@ssw0rd_Here';
GO
-- Create database scoped credential using managed identity
CREATE DATABASE SCOPED CREDENTIAL StorageCredential
WITH IDENTITY = 'Managed Identity';
GO
-- Create external data source with managed identity auth
CREATE EXTERNAL DATA SOURCE SecureCostData
WITH (
LOCATION = 'https://securecoststorage.blob.core.windows.net/cost-exports',
CREDENTIAL = StorageCredential
);
GO
-- Create external file format
CREATE EXTERNAL FILE FORMAT SecureCsvFormat
WITH (
FORMAT_TYPE = DELIMITEDTEXT,
FORMAT_OPTIONS (
FIELD_TERMINATOR = ',',
STRING_DELIMITER = '"',
FIRST_ROW = 2,
USE_TYPE_DEFAULT = FALSE
)
);
GO
-- Create external table with row-level security in mind
CREATE EXTERNAL TABLE CostData
(
Date DATE NOT NULL,
SubscriptionId VARCHAR(36) NOT NULL,
SubscriptionName VARCHAR(100),
ResourceGroup VARCHAR(90),
ResourceId VARCHAR(255),
MeterCategory VARCHAR(50),
Cost DECIMAL(18,4),
Currency VARCHAR(3),
Tags VARCHAR(MAX)
)
WITH (
LOCATION = '/costs/*.csv',
DATA_SOURCE = SecureCostData,
FILE_FORMAT = SecureCsvFormat
);
GO
Step 4: Implement Row-Level Security
-- Create security policy to restrict access by subscription
CREATE SCHEMA Security;
GO
-- Create predicate function
CREATE FUNCTION Security.fn_CostSecurityPredicate
(
@SubscriptionId VARCHAR(36)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
SELECT 1 AS result
WHERE
-- Allow admins to see all data
IS_MEMBER('CostAdministrators') = 1
OR
-- Restrict users to their subscriptions
@SubscriptionId IN (
SELECT SubscriptionId
FROM dbo.UserSubscriptionAccess
WHERE UserPrincipal = USER_NAME()
);
GO
-- Create security policy
CREATE SECURITY POLICY CostDataSecurityPolicy
ADD FILTER PREDICATE Security.fn_CostSecurityPredicate(SubscriptionId)
ON dbo.CostData
WITH (STATE = ON);
GO
Step 5: Implement Dynamic Data Masking
-- Mask sensitive cost information for non-finance users
ALTER TABLE CostData
ALTER COLUMN Cost ADD MASKED WITH (FUNCTION = 'default()');
ALTER TABLE CostData
ALTER COLUMN Tags ADD MASKED WITH (FUNCTION = 'default()');
-- Grant unmasking to finance team
GRANT UNMASK TO [FinanceTeam];
Security Layer 3: Access Control
Step 1: Implement Least-Privilege RBAC
# Cost management RBAC script
param(
[Parameter(Mandatory=$true)]
[string]$ResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$StorageAccountName
)
# Define role groups
$roles = @{
# Read-only access for analysts
"CostAnalysts" = @{
Users = @("[email protected]", "[email protected]")
Role = "Storage Blob Data Reader"
Scope = "Container"
Containers = @("cost-exports")
}
# Write access for automation
"CostExportManagers" = @{
ManagedIdentities = @("cost-export-identity")
Role = "Storage Blob Data Contributor"
Scope = "Container"
Containers = @("cost-exports")
}
# Full management for admins
"CostAdministrators" = @{
Users = @("[email protected]")
Role = "Storage Account Contributor"
Scope = "Account"
}
}
# Get storage account
$storageAccount = Get-AzStorageAccount `
-ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName
foreach ($roleName in $roles.Keys) {
$roleConfig = $roles[$roleName]
Write-Host "Configuring role: $roleName" -ForegroundColor Cyan
# Determine scope
$scope = if ($roleConfig.Scope -eq "Account") {
$storageAccount.Id
} else {
# Container scope
foreach ($containerName in $roleConfig.Containers) {
"$($storageAccount.Id)/blobServices/default/containers/$containerName"
}
}
# Assign to users
if ($roleConfig.Users) {
foreach ($user in $roleConfig.Users) {
New-AzRoleAssignment `
-SignInName $user `
-RoleDefinitionName $roleConfig.Role `
-Scope $scope `
-ErrorAction SilentlyContinue
Write-Host " Assigned $($roleConfig.Role) to $user"
}
}
# Assign to managed identities
if ($roleConfig.ManagedIdentities) {
foreach ($identityName in $roleConfig.ManagedIdentities) {
$identity = Get-AzUserAssignedIdentity `
-Name $identityName `
-ResourceGroupName $ResourceGroupName
New-AzRoleAssignment `
-ObjectId $identity.PrincipalId `
-RoleDefinitionName $roleConfig.Role `
-Scope $scope `
-ErrorAction SilentlyContinue
Write-Host " Assigned $($roleConfig.Role) to identity $identityName"
}
}
}
Step 2: Enable Conditional Access Policies
# Require MFA for cost data access
# This requires Azure AD Premium
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
# Create conditional access policy
$conditions = @{
Applications = @{
IncludeApplications = @("All")
}
Users = @{
IncludeGroups = @("CostAnalysts", "CostAdministrators")
}
Locations = @{
IncludeLocations = @("All")
ExcludeLocations = @("AllTrusted")
}
}
$grantControls = @{
Operator = "AND"
BuiltInControls = @("mfa", "compliantDevice")
}
$params = @{
DisplayName = "Require MFA for Cost Data Access"
State = "enabled"
Conditions = $conditions
GrantControls = $grantControls
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Step 3: Configure Service Principal with Certificate Auth
# Create service principal for automated access
$sp = New-AzADServicePrincipal -DisplayName "CostDataAutomation"
# Create self-signed certificate
$cert = New-SelfSignedCertificate `
-Subject "CN=CostDataAutomation" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable `
-KeySpec Signature `
-KeyLength 2048 `
-KeyAlgorithm RSA `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddYears(2)
# Export certificate
$certPath = "C:\Certs\cost-automation.pfx"
$certPassword = ConvertTo-SecureString -String "P@ssw0rd" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath $certPath -Password $certPassword
# Upload certificate to service principal
$keyValue = [System.Convert]::ToBase64String($cert.GetRawCertData())
New-AzADSpCredential -ObjectId $sp.Id -CertValue $keyValue -EndDate $cert.NotAfter
# Grant access to storage
New-AzRoleAssignment `
-ApplicationId $sp.AppId `
-RoleDefinitionName "Storage Blob Data Reader" `
-Scope $storageAccount.Id
Security Layer 4: Monitoring & Compliance
Step 1: Enable Comprehensive Auditing
# Enable diagnostic settings for storage account
az monitor diagnostic-settings create \
--name cost-storage-audit \
--resource $(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query id -o tsv) \
--logs '[
{
"category": "StorageRead",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 365}
},
{
"category": "StorageWrite",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 365}
},
{
"category": "StorageDelete",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 365}
}
]' \
--metrics '[
{
"category": "Transaction",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 90}
}
]' \
--workspace $(az monitor log-analytics workspace show \
--resource-group rg-cost-management \
--workspace-name law-cost-audit \
--query id -o tsv)
# Enable diagnostic settings for Synapse
az monitor diagnostic-settings create \
--name synapse-audit \
--resource $(az synapse workspace show \
--name synapse-secure-cost-analytics \
--resource-group rg-cost-management \
--query id -o tsv) \
--logs '[
{
"category": "SQLSecurityAuditEvents",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 365}
},
{
"category": "SynapseRbacOperations",
"enabled": true,
"retentionPolicy": {"enabled": true, "days": 365}
}
]' \
--workspace $(az monitor log-analytics workspace show \
--resource-group rg-cost-management \
--workspace-name law-cost-audit \
--query id -o tsv)
Step 2: Create Security Alerts
# Alert on unauthorized access attempts
az monitor scheduled-query create \
--name "Unauthorized Cost Data Access" \
--resource-group rg-cost-management \
--scopes $(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query id -o tsv) \
--condition "count > 0" \
--condition-query "StorageBlobLogs
| where StatusCode == 403
| where TimeGenerated > ago(15m)" \
--description "Alert on failed authorization attempts" \
--evaluation-frequency "15m" \
--window-size "15m" \
--severity 2 \
--action-groups "/subscriptions/{sub-id}/resourceGroups/rg-cost-management/providers/microsoft.insights/actionGroups/SecurityAlerts"
# Alert on encryption key access
az monitor scheduled-query create \
--name "Cost Encryption Key Access" \
--resource-group rg-cost-management \
--scopes $(az keyvault show \
--name kv-cost-encryption \
--query id -o tsv) \
--condition "count > 0" \
--condition-query "AzureDiagnostics
| where ResourceProvider == 'MICROSOFT.KEYVAULT'
| where OperationName == 'VaultGet' or OperationName == 'KeyGet'
| where TimeGenerated > ago(5m)" \
--description "Alert on encryption key access" \
--evaluation-frequency "5m" \
--window-size "5m" \
--severity 1 \
--action-groups "/subscriptions/{sub-id}/resourceGroups/rg-cost-management/providers/microsoft.insights/actionGroups/SecurityAlerts"
Step 3: Implement Data Classification
# Apply sensitivity labels to cost data
Import-Module Az.Resources
# Define sensitivity label (requires Azure Information Protection)
$label = @{
DisplayName = "Confidential - Finance"
Description = "Cost management and billing data"
Sensitivity = "Confidential"
}
# Tag storage account
$tags = @{
"DataClassification" = "Confidential"
"ComplianceScope" = "SOC2,GDPR"
"DataOwner" = "Finance"
"RetentionPeriod" = "7years"
}
Update-AzStorageAccount `
-ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName `
-Tag $tags
Step 4: Configure Microsoft Purview for Data Governance
# Create Purview account for data cataloging
az purview account create \
--name purview-cost-governance \
--resource-group rg-cost-management \
--location eastus \
--managed-resource-group-name rg-purview-managed
# Register storage account in Purview
az purview account add-root-collection \
--name purview-cost-governance \
--resource-group rg-cost-management \
--collection-name "Cost Management Data"
# Scan storage account
az purview data-source create \
--name cost-storage-source \
--purview-account purview-cost-governance \
--resource-group rg-cost-management \
--kind "AzureStorage" \
--properties @purview-source-config.json
Best Practices
1. Regular Security Audits
-- Query to audit storage access patterns
SELECT
TimeGenerated,
OperationName,
Identity = CallerIpAddress,
AuthenticationType,
StatusCode,
Uri
FROM StorageBlobLogs
WHERE TimeGenerated > ago(7d)
AND AccountName == 'securecoststorage'
ORDER BY TimeGenerated DESC;
-- Identify suspicious access patterns
SELECT
CallerIpAddress,
COUNT(*) AS AccessCount,
COUNT(DISTINCT OperationName) AS UniqueOperations,
SUM(CASE WHEN StatusCode >= 400 THEN 1 ELSE 0 END) AS FailedAttempts
FROM StorageBlobLogs
WHERE TimeGenerated > ago(24h)
GROUP BY CallerIpAddress
HAVING SUM(CASE WHEN StatusCode >= 400 THEN 1 ELSE 0 END) > 10
ORDER BY FailedAttempts DESC;
2. Automated Key Rotation
# Rotate customer-managed encryption keys annually
$keyVaultName = "kv-cost-encryption"
$keyName = "cost-data-encryption-key"
# Create new key version
$newKey = Add-AzKeyVaultKey `
-VaultName $keyVaultName `
-Name $keyName `
-Destination Software
# Storage account automatically uses latest key version
Write-Host "New key version created: $($newKey.Version)"
3. Backup Encryption Keys
# Backup encryption keys to secure location
$keyBackup = Backup-AzKeyVaultKey `
-VaultName $keyVaultName `
-Name $keyName `
-OutputFile "C:\SecureBackups\cost-encryption-key-backup-$(Get-Date -Format 'yyyyMMdd').blob"
# Store backup in separate subscription/region
4. Implement Zero Trust Principles
- Never trust, always verify every access request
- Use managed identities instead of keys/passwords
- Implement just-in-time (JIT) access for admins
- Require MFA for all human access
- Monitor and alert on all privileged operations
5. Regular Penetration Testing
# Security Testing Checklist
## Quarterly Tests
- [ ] Attempt access from non-approved IPs
- [ ] Test RBAC boundaries (privilege escalation)
- [ ] Verify MFA enforcement
- [ ] Test private endpoint connectivity
- [ ] Validate encryption at rest
## Annual Tests
- [ ] Full penetration test by third party
- [ ] Social engineering assessment
- [ ] Disaster recovery drill (key loss scenario)
- [ ] Compliance audit (SOC 2, ISO 27001)
Troubleshooting
Issue: Cannot access storage after enabling private endpoint
Symptoms: Applications can't reach storage account
Resolution:
# Verify DNS resolution
nslookup securecoststorage.blob.core.windows.net
# Should resolve to private IP (10.x.x.x)
# If resolves to public IP, DNS not configured correctly
# Check private endpoint connection
az network private-endpoint show \
--name pe-cost-storage-blob \
--resource-group rg-cost-management \
--query "customDnsConfigs[].{FQDN:fqdn, IP:ipAddresses}"
Issue: Synapse can't access storage with managed identity
Symptoms: "Authorization failed" when querying external tables
Resolution:
# Verify Synapse managed identity has storage permissions
SYNAPSE_IDENTITY=$(az synapse workspace show \
--name synapse-secure-cost-analytics \
--resource-group rg-cost-management \
--query identity.principalId -o tsv)
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee-object-id $SYNAPSE_IDENTITY \
--scope $(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query id -o tsv)
Issue: Row-level security not filtering data
Symptoms: Users seeing data they shouldn't
Resolution:
-- Verify security policy is enabled
SELECT name, is_enabled
FROM sys.security_policies
WHERE name = 'CostDataSecurityPolicy';
-- Check if user has UNMASK privilege
SELECT pr.name, pe.permission_name
FROM sys.database_principals pr
INNER JOIN sys.database_permissions pe ON pr.principal_id = pe.grantee_principal_id
WHERE pe.permission_name = 'UNMASK';
-- Test predicate function
SELECT * FROM Security.fn_CostSecurityPredicate('test-subscription-id');
Issue: Key Vault access denied for encryption
Symptoms: Storage account can't access encryption keys
Resolution:
# Ensure storage account has system-assigned identity
az storage account update \
--name securecoststorage \
--resource-group rg-cost-management \
--assign-identity
# Grant Key Vault permissions
STORAGE_IDENTITY=$(az storage account show \
--name securecoststorage \
--resource-group rg-cost-management \
--query identity.principalId -o tsv)
az keyvault set-policy \
--name kv-cost-encryption \
--object-id $STORAGE_IDENTITY \
--key-permissions get wrapKey unwrapKey
Compliance Certifications
SOC 2 Compliance
**Required Controls:**
- Encryption: Customer-managed keys ✓
- Access Control: RBAC with least privilege ✓
- Network Security: Private endpoints ✓
- Audit Logging: 365-day retention ✓
- Monitoring: Real-time alerts ✓
GDPR Compliance
**Required Controls:**
- Data Encryption: TLS 1.2+ in transit, AES-256 at rest ✓
- Access Rights: Row-level security for data isolation ✓
- Data Retention: Immutable storage policies ✓
- Breach Notification: Automated security alerts ✓
- Data Minimization: Dynamic data masking ✓
Next Steps
Once security is implemented:
- Document security architecture: Create diagrams and runbooks
- Train team members: Ensure understanding of security controls
- Schedule regular reviews: Quarterly security posture assessments
- Implement cost alerts: See "How to Set Up Cost Alerts and Budgets in Azure"
- Monitor export health: Use "How to Monitor Cost Export Status and Data Freshness in Azure"