Data & Storage

Data & Storage Security Baselines

Data services hold your crown jewels. SQL Database, Storage Accounts, Cosmos DB, and Key Vault each store different types of sensitive data — relational records, unstructured blobs, NoSQL documents, and cryptographic secrets. Each requires specific protection patterns.

Azure SQL Database

Managed relational database with built-in intelligence, automatic tuning, and advanced security features. SQL Database has one of the richest security feature sets in Azure — leverage TDE, Always Encrypted, Dynamic Data Masking, and Defender for SQL for comprehensive protection.

PropertyValue
Defender PlanDefender for SQL (VA + Advanced Threat Protection)
Azure Policy Built-inYes — SQL initiative (auditing, TDE, AAD-only auth, PE)
Diagnostic LogsSQLSecurityAuditEvents, AutomaticTuning, QueryStoreRuntimeStatistics
Key FeatureEntra-only auth eliminates SQL authentication completely

Baseline Controls

NS✓ Supported

Private Endpoint

○ ManualCustomer
IM✓ Supported

Entra-only Auth

○ ManualCustomer
DP✓ Supported

TDE (Encryption at Rest)

● DefaultMicrosoft
DP✓ Supported

TDE with CMK

○ ManualCustomer
DP✓ Supported

Always Encrypted

○ ManualCustomer
LT✓ Supported

Defender for SQL

○ ManualCustomer
LT✓ Supported

Auditing

○ ManualCustomer
BR✓ Supported

Automated Backups

● DefaultMicrosoft

Terraform: Hardened SQL Database

hcl
resource "azurerm_mssql_server" "main" {
  name                         = "sql-prod-001"
  resource_group_name          = azurerm_resource_group.data.name
  location                     = azurerm_resource_group.data.location
  version                      = "12.0"
  minimum_tls_version          = "1.2"

  # IM-1: Entra-only auth — disable SQL auth completely
  azuread_administrator {
    login_username              = "sql-admin-group"
    object_id                   = azurerm_user_assigned_identity.sql_admin.principal_id
    azuread_authentication_only = true   # No SQL logins
  }

  # NS-2: Deny public network access
  public_network_access_enabled = false

  identity {
    type = "SystemAssigned"
  }
}

resource "azurerm_mssql_database" "main" {
  name      = "sqldb-app-prod"
  server_id = azurerm_mssql_server.main.id
  sku_name  = "S1"

  # DP-5: TDE with Customer-Managed Key
  transparent_data_encryption_key_vault_key_id = azurerm_key_vault_key.sql_cmk.id

  # BR-1: Long-term retention
  long_term_retention_policy {
    weekly_retention  = "P4W"
    monthly_retention = "P12M"
    yearly_retention  = "P5Y"
    week_of_year      = 1
  }

  short_term_retention_policy {
    retention_days = 35
  }
}

# LT-1: Defender for SQL
resource "azurerm_mssql_server_security_alert_policy" "main" {
  resource_group_name = azurerm_resource_group.data.name
  server_name         = azurerm_mssql_server.main.name
  state               = "Enabled"
}

# LT-3: Auditing to Log Analytics
resource "azurerm_mssql_server_extended_auditing_policy" "main" {
  server_id              = azurerm_mssql_server.main.id
  log_monitoring_enabled = true
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "sql" {
  name                = "pe-sql-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-sql-prod"
    private_connection_resource_id = azurerm_mssql_server.main.id
    subresource_names              = ["sqlServer"]
    is_manual_connection           = false
  }
}
resource "azurerm_mssql_server" "main" {
  name                         = "sql-prod-001"
  resource_group_name          = azurerm_resource_group.data.name
  location                     = azurerm_resource_group.data.location
  version                      = "12.0"
  minimum_tls_version          = "1.2"

  # IM-1: Entra-only auth — disable SQL auth completely
  azuread_administrator {
    login_username              = "sql-admin-group"
    object_id                   = azurerm_user_assigned_identity.sql_admin.principal_id
    azuread_authentication_only = true   # No SQL logins
  }

  # NS-2: Deny public network access
  public_network_access_enabled = false

  identity {
    type = "SystemAssigned"
  }
}

resource "azurerm_mssql_database" "main" {
  name      = "sqldb-app-prod"
  server_id = azurerm_mssql_server.main.id
  sku_name  = "S1"

  # DP-5: TDE with Customer-Managed Key
  transparent_data_encryption_key_vault_key_id = azurerm_key_vault_key.sql_cmk.id

  # BR-1: Long-term retention
  long_term_retention_policy {
    weekly_retention  = "P4W"
    monthly_retention = "P12M"
    yearly_retention  = "P5Y"
    week_of_year      = 1
  }

  short_term_retention_policy {
    retention_days = 35
  }
}

# LT-1: Defender for SQL
resource "azurerm_mssql_server_security_alert_policy" "main" {
  resource_group_name = azurerm_resource_group.data.name
  server_name         = azurerm_mssql_server.main.name
  state               = "Enabled"
}

# LT-3: Auditing to Log Analytics
resource "azurerm_mssql_server_extended_auditing_policy" "main" {
  server_id              = azurerm_mssql_server.main.id
  log_monitoring_enabled = true
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "sql" {
  name                = "pe-sql-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-sql-prod"
    private_connection_resource_id = azurerm_mssql_server.main.id
    subresource_names              = ["sqlServer"]
    is_manual_connection           = false
  }
}

Azure Portal Instructions

Step-by-step instructions for configuring each SQL Database control through the Azure Portal.

NS-2 — Private Endpoints
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Security, select Networking
  3. Select the Private access tab
  4. Click + Create a private endpoint
  5. Select the VNet, subnet, and configure the private DNS zone (privatelink.database.windows.net)
  6. After creation, go back to the Public access tab
  7. Set "Public network access" to Disable and click Save
IM-1 — Entra ID Authentication
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Settings, select Microsoft Entra ID
  3. Click Set admin and select an Entra ID user or group
  4. Click Save to configure the Entra ID administrator
  5. To enforce Entra-only authentication, check "Support only Microsoft Entra authentication for this server"
  6. Click Save — this disables SQL authentication (password-based logins)
IM-3 — Managed Identities
  1. On the application side (e.g., App Service), enable Managed Identity under Settings > Identity
  2. Navigate to your SQL Database resource in the Azure Portal
  3. Connect to the database using SQL Server Management Studio or Azure Data Studio
  4. Create a contained user for the managed identity: CREATE USER [app-name] FROM EXTERNAL PROVIDER
  5. Grant appropriate roles: ALTER ROLE db_datareader ADD MEMBER [app-name]
  6. Update the application connection string to use Authentication=Active Directory Managed Identity
DP-1 — Data Discovery & Classification
  1. Navigate to your SQL Database resource in the Azure Portal
  2. In the left menu under Security, select Data Discovery & Classification
  3. Click the "We have found X columns with classification recommendations" banner
  4. Review the suggested classifications for each column
  5. Select the columns you want to classify and click Accept selected recommendations
  6. Click Save to apply the classifications
  7. You can also manually add classifications by clicking + Add classification
DP-2 — Advanced Threat Protection
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Security, select Microsoft Defender for SQL
  3. Set Microsoft Defender for SQL to On
  4. Configure the storage account for vulnerability assessment scan results
  5. Set email addresses for alert notifications
  6. Enable "Send scan reports to" and "Also send email notification to admins and subscription owners"
  7. Click Save
DP-3 — Data in Transit Encryption

This feature is enabled by default — TLS 1.2+ is enforced for all connections. To verify, navigate to your SQL Server > Networking and check that Minimum TLS version is set to 1.2.

DP-4 — Transparent Data Encryption

This feature is managed by Microsoft and enabled by default. To verify, navigate to your SQL Database > Security > Transparent data encryption and confirm Data encryption is On.

DP-5 — TDE with CMK
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Security, select Transparent data encryption
  3. Change the key source from Service-managed key to Customer-managed key
  4. Select the Key Vault and encryption key (or enter the Key URI)
  5. Enable auto-rotation if prompted
  6. Click Save to apply the CMK configuration
  7. Ensure the SQL Server managed identity has Get, Wrap Key, and Unwrap Key permissions on the Key Vault
LT-1 — Microsoft Defender for SQL
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Security, select Microsoft Defender for SQL
  3. Set Microsoft Defender for SQL to On
  4. Configure vulnerability assessment storage and notification settings
  5. Click Save
  6. Alerts and vulnerability findings will appear in Microsoft Defender for Cloud
LT-3 — Auditing
  1. Navigate to your SQL Server resource in the Azure Portal
  2. In the left menu under Security, select Auditing
  3. Toggle Auditing to On
  4. Select the audit log destination: Log Analytics workspace (recommended), Storage account, or Event Hub
  5. If using Log Analytics, select your workspace
  6. Click Save to enable auditing
  7. Audit logs will capture failed logins, schema changes, and data access events

Azure Storage Account

Blob, File, Queue, and Table storage under one umbrella. Storage Accounts are among the most commonly misconfigured Azure services — public blob access, shared keys, and missing network restrictions are frequent findings in security assessments.

Baseline Controls

NS✓ Supported

Private Endpoint

○ ManualCustomer
NS✓ Supported

Disable Public Blob Access

● DefaultMicrosoft
IM✓ Supported

Disable Shared Key

○ ManualCustomer
DP✓ Supported

SSE (Platform-Managed)

● DefaultMicrosoft
DP✓ Supported

Infrastructure Encryption

○ ManualCustomer
LT✓ Supported

Defender for Storage

○ ManualCustomer
BR✓ Supported

Soft Delete

● DefaultMicrosoft
BR✓ Supported

Immutable Storage

○ ManualCustomer

Terraform: Hardened Storage Account

hcl
resource "azurerm_storage_account" "main" {
  name                     = "stprod001"
  resource_group_name      = azurerm_resource_group.data.name
  location                 = azurerm_resource_group.data.location
  account_tier             = "Standard"
  account_replication_type = "GRS"

  # NS-8: TLS 1.2 minimum
  min_tls_version = "TLS1_2"

  # NS: HTTPS only
  https_traffic_only_enabled = true

  # NS: Disable public blob access
  allow_nested_items_to_be_public = false

  # IM-1: Disable shared key — Entra ID only
  shared_access_key_enabled = false

  # NS-2: Default deny network rules
  network_rules {
    default_action = "Deny"
    bypass         = ["AzureServices"]
  }

  # DP: Infrastructure encryption (double encryption)
  infrastructure_encryption_enabled = true

  # DP-5: Customer-Managed Key
  identity {
    type = "SystemAssigned"
  }

  customer_managed_key {
    key_vault_key_id          = azurerm_key_vault_key.storage_cmk.id
    user_assigned_identity_id = azurerm_user_assigned_identity.storage.id
  }

  # BR-2: Blob soft delete and versioning
  blob_properties {
    delete_retention_policy {
      days = 90
    }
    container_delete_retention_policy {
      days = 90
    }
    versioning_enabled = true
  }
}
resource "azurerm_storage_account" "main" {
  name                     = "stprod001"
  resource_group_name      = azurerm_resource_group.data.name
  location                 = azurerm_resource_group.data.location
  account_tier             = "Standard"
  account_replication_type = "GRS"

  # NS-8: TLS 1.2 minimum
  min_tls_version = "TLS1_2"

  # NS: HTTPS only
  https_traffic_only_enabled = true

  # NS: Disable public blob access
  allow_nested_items_to_be_public = false

  # IM-1: Disable shared key — Entra ID only
  shared_access_key_enabled = false

  # NS-2: Default deny network rules
  network_rules {
    default_action = "Deny"
    bypass         = ["AzureServices"]
  }

  # DP: Infrastructure encryption (double encryption)
  infrastructure_encryption_enabled = true

  # DP-5: Customer-Managed Key
  identity {
    type = "SystemAssigned"
  }

  customer_managed_key {
    key_vault_key_id          = azurerm_key_vault_key.storage_cmk.id
    user_assigned_identity_id = azurerm_user_assigned_identity.storage.id
  }

  # BR-2: Blob soft delete and versioning
  blob_properties {
    delete_retention_policy {
      days = 90
    }
    container_delete_retention_policy {
      days = 90
    }
    versioning_enabled = true
  }
}

Azure Portal Instructions

Step-by-step instructions for configuring each Storage Account control through the Azure Portal.

NS-2 — Private Endpoints
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Security + networking, select Networking
  3. Select the Private endpoint connections tab
  4. Click + Private endpoint to create a new private endpoint
  5. Select the target sub-resource (blob, file, table, or queue)
  6. Select the VNet, subnet, and configure the private DNS zone
  7. Repeat for each sub-resource that needs private access
NS-2 — Disable Public Network Access
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Security + networking, select Networking
  3. On the Firewalls and virtual networks tab, set Public network access to "Enabled from selected virtual networks and IP addresses" or "Disabled"
  4. If using selected networks, add allowed VNets and IP addresses below
  5. Check "Allow Azure services on the trusted services list to access this storage account" if needed
  6. Click Save
IM-1 — Entra ID Authorization
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Settings, select Configuration
  3. Set "Allow storage account key access" to Disabled (this forces Entra ID auth)
  4. Click Save
  5. Navigate to Access control (IAM) on the storage account
  6. Click + Add role assignment and assign roles like "Storage Blob Data Reader" or "Storage Blob Data Contributor" to users/groups/service principals
DP-3 — Secure Transfer Required
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Settings, select Configuration
  3. Verify that "Secure transfer required" is set to Enabled (this is the default)
  4. Set the Minimum TLS version to Version 1.2
  5. Click Save
DP-4 — Data at Rest Encryption

This feature is managed by Microsoft and enabled by default. To verify, navigate to your Storage Account > Security + networking > Encryption and confirm Encryption type shows Microsoft-managed keys.

DP-5 — Encryption with CMK
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Security + networking, select Encryption
  3. Under Encryption type, select Customer-managed keys
  4. Select the Key Vault and encryption key (or enter the Key URI)
  5. Choose the managed identity to use for accessing Key Vault
  6. To enable infrastructure encryption (double encryption), this must be set at account creation time
  7. Click Save
LT-1 — Defender for Storage
  1. Navigate to Microsoft Defender for Cloud in the Azure Portal
  2. In the left menu, select Environment settings
  3. Select your subscription
  4. Under Cloud Workload Protection (CWP), find the Storage row
  5. Toggle the plan to On
  6. Click Settings to configure malware scanning and sensitive data threat detection options
  7. Click Save
BR-2 — Immutable Storage
  1. Navigate to your Storage Account resource in the Azure Portal
  2. In the left menu under Data management, select Data protection
  3. Enable "Enable versioning for blobs" and "Enable soft delete for blobs" with desired retention
  4. Click Save
  5. To set immutability policies, navigate to a specific container
  6. Click the Access policy or Immutability policy option
  7. Add a time-based retention policy or legal hold as required

Shared Key = Backdoor

Shared key access bypasses all Entra ID RBAC and Conditional Access policies. Set shared_access_key_enabled = false unless you have a specific, documented need. This is one of the highest-impact hardening steps for Storage Accounts.

Azure Cosmos DB

Globally distributed multi-model database. Cosmos DB's security baseline follows similar patterns to SQL Database, but with unique considerations for its key-based auth model and global replication.

Baseline Controls

NS✓ Supported

Private Endpoint

○ ManualCustomer
IM✓ Supported

Entra ID RBAC

○ ManualCustomer
DP✓ Supported

Encryption at Rest

● DefaultMicrosoft
DP✓ Supported

CMK Encryption

○ ManualCustomer
LT✓ Supported

Defender for Cosmos DB

○ ManualCustomer
BR✓ Supported

Continuous Backup

○ ManualCustomer

Terraform: Hardened Cosmos DB

hcl
resource "azurerm_cosmosdb_account" "main" {
  name                = "cosmos-prod-001"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  offer_type          = "Standard"
  kind                = "GlobalDocumentDB"

  # IM-1: Disable key-based auth — Entra RBAC only
  local_authentication_disabled = true

  # NS-2: Disable public access
  public_network_access_enabled = false

  # NS-8: TLS 1.2 minimum (default)
  minimal_tls_version = "Tls12"

  # BR: Continuous backup (PITR)
  backup {
    type                = "Continuous"
    tier                = "Continuous30Days"
  }

  consistency_policy {
    consistency_level = "Session"
  }

  geo_location {
    location          = azurerm_resource_group.data.location
    failover_priority = 0
    zone_redundant    = true
  }

  # DP-5: CMK encryption
  key_vault_key_id = azurerm_key_vault_key.cosmos_cmk.id

  identity {
    type = "SystemAssigned"
  }
}
resource "azurerm_cosmosdb_account" "main" {
  name                = "cosmos-prod-001"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  offer_type          = "Standard"
  kind                = "GlobalDocumentDB"

  # IM-1: Disable key-based auth — Entra RBAC only
  local_authentication_disabled = true

  # NS-2: Disable public access
  public_network_access_enabled = false

  # NS-8: TLS 1.2 minimum (default)
  minimal_tls_version = "Tls12"

  # BR: Continuous backup (PITR)
  backup {
    type                = "Continuous"
    tier                = "Continuous30Days"
  }

  consistency_policy {
    consistency_level = "Session"
  }

  geo_location {
    location          = azurerm_resource_group.data.location
    failover_priority = 0
    zone_redundant    = true
  }

  # DP-5: CMK encryption
  key_vault_key_id = azurerm_key_vault_key.cosmos_cmk.id

  identity {
    type = "SystemAssigned"
  }
}

Azure Portal Instructions

Step-by-step instructions for configuring each Cosmos DB control through the Azure Portal.

NS-2 — Private Endpoints
  1. Navigate to your Cosmos DB account in the Azure Portal
  2. In the left menu under Settings, select Networking
  3. Select the Private access tab
  4. Click + Add private endpoint
  5. Select the VNet, subnet, and configure the private DNS zone (privatelink.documents.azure.com)
  6. After creation, go to the Public access tab
  7. Uncheck "Allow access from Azure Portal" and "Allow access from my IP" if fully private
  8. Click Save
IM-1 — Entra ID Authentication
  1. Navigate to your Cosmos DB account in the Azure Portal
  2. In the left menu under Settings, select Keys
  3. Note: To fully disable local auth, you must currently use ARM/CLI (set disableLocalAuth=true)
  4. For RBAC setup, go to Access control (IAM) on the Cosmos DB account
  5. Click + Add role assignment
  6. Assign the "Cosmos DB Built-in Data Contributor" or "Cosmos DB Built-in Data Reader" role to users, groups, or managed identities
DP-4 — Data at Rest Encryption

This feature is managed by Microsoft and enabled by default. No portal configuration is required — all Cosmos DB data including indexes and backups is encrypted.

DP-5 — Encryption with CMK
  1. CMK must be configured at account creation time — it cannot be added later
  2. When creating a new Cosmos DB account, go to the Encryption tab
  3. Select Customer-managed key
  4. Select your Key Vault and encryption key
  5. Choose or create a managed identity for accessing the Key Vault
  6. Complete the account creation wizard
  7. Note: Existing accounts cannot be migrated to CMK encryption
LT-3 — Diagnostic Logging
  1. Navigate to your Cosmos DB account in the Azure Portal
  2. In the left menu under Monitoring, select Diagnostic settings
  3. Click + Add diagnostic setting
  4. Give the setting a name (e.g., "cosmos-diag-all")
  5. Select the log categories: DataPlaneRequests, QueryRuntimeStatistics, PartitionKeyStatistics, ControlPlaneRequests
  6. Select the destination: Send to Log Analytics workspace
  7. Select your Log Analytics workspace
  8. Click Save
BR-1 — Continuous Backup
  1. Continuous backup mode must be set at account creation or migrated for existing accounts
  2. For new accounts: during creation, on the Backup Policy tab, select Continuous (7-day or 30-day)
  3. For existing accounts: navigate to the Cosmos DB account
  4. In the left menu under Settings, select Backup & Restore
  5. If you are on periodic mode, click "Migrate to continuous backup" (if available)
  6. Select the retention tier (7-day or 30-day)
  7. To restore, go to Backup & Restore > Point in time restore and select the container and timestamp

Azure Key Vault

HSM-backed secrets, keys, and certificate management. Key Vault is the foundation of your entire encryption strategy — it holds the CMK keys that protect all other services. Securing Key Vault is non-negotiable.

Baseline Controls

NS✓ Supported

Private Endpoint

○ ManualCustomer
IM✓ Supported

RBAC Authorization

○ ManualCustomer
DP✓ Supported

Soft Delete

● DefaultMicrosoft
DP✓ Supported

Purge Protection

○ ManualCustomer
DP✓ Supported

HSM-Backed Keys

○ ManualCustomer
DP✓ Supported

Key Rotation

○ ManualCustomer
LT✓ Supported

Defender for Key Vault

○ ManualCustomer
LT✓ Supported

Audit Logging

○ ManualCustomer

Terraform: Hardened Key Vault

hcl
resource "azurerm_key_vault" "main" {
  name                       = "kv-prod-001"
  resource_group_name        = azurerm_resource_group.security.name
  location                   = azurerm_resource_group.security.location
  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "premium"          # HSM-backed keys
  soft_delete_retention_days = 90
  purge_protection_enabled   = true               # Cannot be disabled once set

  # IM: RBAC authorization (preferred over access policies)
  enable_rbac_authorization = true

  # NS-2: Network isolation
  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
  }
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "kv" {
  name                = "pe-kv-prod"
  location            = azurerm_resource_group.security.location
  resource_group_name = azurerm_resource_group.security.name
  subnet_id           = azurerm_subnet.security.id

  private_service_connection {
    name                           = "psc-kv-prod"
    private_connection_resource_id = azurerm_key_vault.main.id
    subresource_names              = ["vault"]
    is_manual_connection           = false
  }
}

# LT-3: Diagnostic Settings — ALL Key Vault operations
resource "azurerm_monitor_diagnostic_setting" "kv" {
  name                       = "diag-kv-prod"
  target_resource_id         = azurerm_key_vault.main.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  enabled_log { category = "AuditEvent" }
  enabled_log { category = "AzurePolicyEvaluationDetails" }

  metric { category = "AllMetrics" }
}
resource "azurerm_key_vault" "main" {
  name                       = "kv-prod-001"
  resource_group_name        = azurerm_resource_group.security.name
  location                   = azurerm_resource_group.security.location
  tenant_id                  = data.azurerm_client_config.current.tenant_id
  sku_name                   = "premium"          # HSM-backed keys
  soft_delete_retention_days = 90
  purge_protection_enabled   = true               # Cannot be disabled once set

  # IM: RBAC authorization (preferred over access policies)
  enable_rbac_authorization = true

  # NS-2: Network isolation
  network_acls {
    default_action = "Deny"
    bypass         = "AzureServices"
  }
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "kv" {
  name                = "pe-kv-prod"
  location            = azurerm_resource_group.security.location
  resource_group_name = azurerm_resource_group.security.name
  subnet_id           = azurerm_subnet.security.id

  private_service_connection {
    name                           = "psc-kv-prod"
    private_connection_resource_id = azurerm_key_vault.main.id
    subresource_names              = ["vault"]
    is_manual_connection           = false
  }
}

# LT-3: Diagnostic Settings — ALL Key Vault operations
resource "azurerm_monitor_diagnostic_setting" "kv" {
  name                       = "diag-kv-prod"
  target_resource_id         = azurerm_key_vault.main.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  enabled_log { category = "AuditEvent" }
  enabled_log { category = "AzurePolicyEvaluationDetails" }

  metric { category = "AllMetrics" }
}

Azure Portal Instructions

Step-by-step instructions for configuring each Key Vault control through the Azure Portal.

NS-2 — Private Endpoints
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Settings, select Networking
  3. Select the Private endpoint connections tab
  4. Click + Create to add a private endpoint
  5. Select the VNet, subnet, and configure the private DNS zone (privatelink.vaultcore.azure.net)
  6. Go back to the Firewalls and virtual networks tab
  7. Set "Allow access from" to "Disable public access"
  8. Optionally check "Allow trusted Microsoft services to bypass this firewall"
  9. Click Save
PA-7 — Azure RBAC for Data Plane
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Settings, select Access configuration
  3. Under Permission model, select Azure role-based access control (recommended)
  4. Click Apply
  5. Navigate to Access control (IAM) on the Key Vault
  6. Click + Add role assignment
  7. Assign roles like "Key Vault Secrets User", "Key Vault Crypto Officer", or "Key Vault Certificates Officer" as needed
  8. Follow least-privilege principles — avoid assigning "Key Vault Administrator" broadly
DP-6 — Key Management
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Objects, select Keys
  3. Click + Generate/Import to create a new key
  4. For HSM-backed keys, set the key type to RSA-HSM or EC-HSM (requires Premium SKU)
  5. Set an activation and expiration date
  6. After creation, click on the key and select Rotation policy
  7. Configure the auto-rotation interval and notification period before expiry
  8. Click Save
DP-7 — Certificate Management
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Objects, select Certificates
  3. Click + Generate/Import
  4. Choose "Generate" for CA-integrated auto-renewal or "Import" for existing certificates
  5. For auto-renewal, select the issuer (DigiCert, GlobalSign, or Self-signed)
  6. Configure the certificate subject, validity period, and auto-renewal settings
  7. Set the Lifetime Action to auto-renew at a percentage of lifetime or days before expiry
  8. Click Create
DP-8 — Vault Protection
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Settings, select Properties
  3. Verify that Soft delete is Enabled (this cannot be disabled once set)
  4. Set Purge protection to Enabled (this prevents permanent deletion during the retention period)
  5. Set the soft delete retention period (default 90 days, recommended to keep at 90)
  6. Click Save
  7. Note: Purge protection cannot be disabled once enabled
LT-3 — Diagnostic Logging
  1. Navigate to your Key Vault resource in the Azure Portal
  2. In the left menu under Monitoring, select Diagnostic settings
  3. Click + Add diagnostic setting
  4. Give the setting a name (e.g., "kv-audit-logs")
  5. Select the log category: AuditEvent
  6. Also select AllMetrics under Metrics
  7. Select the destination: Send to Log Analytics workspace
  8. Select your Log Analytics workspace
  9. Click Save

Key Vault Best Practice

Use separate Key Vaults per workload and environment. A shared Key Vault creates a blast radius where compromise of one workload's RBAC grants access to all secrets. Exception: certificate management can share a dedicated Key Vault across workloads.

Azure Cache for Redis

Managed in-memory data store for caching, session management, and real-time analytics. Redis is often overlooked in security reviews, but it frequently caches session tokens, API responses, and temporary credentials — all high-value targets.

PropertyValue
Defender PlanN/A — monitor via diagnostic logs and Sentinel
Azure Policy Built-inYes — TLS version, private endpoint, disable non-SSL port
Recommended TierPremium (VNet injection, persistence, geo-replication) or Enterprise
Key FeatureEntra ID authentication to eliminate access key dependency

Baseline Controls

NS✓ Supported

Private Endpoint

○ ManualCustomer
IM✓ Supported

Entra ID Authentication

○ ManualCustomer
IM✓ Supported

Managed Identity

○ ManualCustomer
DP✓ Supported

TLS 1.2 Enforcement

● DefaultCustomer
DP✓ Supported

Encryption at Rest

● DefaultMicrosoft
BR✓ Supported

Data Persistence

○ ManualCustomer

Terraform: Hardened Redis Premium Cache

hcl
resource "azurerm_redis_cache" "main" {
  name                = "redis-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  capacity            = 1
  family              = "P"
  sku_name            = "Premium"

  # DP-3: TLS 1.2 enforcement
  minimum_tls_version = "1.2"
  enable_non_ssl_port = false

  # NS-2: Private endpoint access
  public_network_access_enabled = false

  redis_configuration {
    # BR: RDB persistence for data durability
    rdb_backup_enabled            = true
    rdb_backup_frequency          = 60
    rdb_storage_connection_string = azurerm_storage_account.redis_backup.primary_blob_connection_string
  }
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "redis" {
  name                = "pe-redis-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-redis-prod"
    private_connection_resource_id = azurerm_redis_cache.main.id
    subresource_names              = ["redisCache"]
    is_manual_connection           = false
  }
}
resource "azurerm_redis_cache" "main" {
  name                = "redis-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  capacity            = 1
  family              = "P"
  sku_name            = "Premium"

  # DP-3: TLS 1.2 enforcement
  minimum_tls_version = "1.2"
  enable_non_ssl_port = false

  # NS-2: Private endpoint access
  public_network_access_enabled = false

  redis_configuration {
    # BR: RDB persistence for data durability
    rdb_backup_enabled            = true
    rdb_backup_frequency          = 60
    rdb_storage_connection_string = azurerm_storage_account.redis_backup.primary_blob_connection_string
  }
}

# NS-2: Private Endpoint
resource "azurerm_private_endpoint" "redis" {
  name                = "pe-redis-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-redis-prod"
    private_connection_resource_id = azurerm_redis_cache.main.id
    subresource_names              = ["redisCache"]
    is_manual_connection           = false
  }
}

Azure Database for PostgreSQL

Managed PostgreSQL with built-in high availability, automated backups, and intelligent performance tuning. Flexible Server is the recommended deployment model — it supports VNet integration for true network isolation without needing private endpoints.

PropertyValue
Defender PlanDefender for open-source relational databases
Azure Policy Built-inYes — SSL enforcement, private access, CMK, Defender
Recommended ModelFlexible Server (VNet integration, Entra ID auth, zone-redundant HA)
Key FeatureVNet-injected deployment with Entra ID-only authentication

Baseline Controls

NS✓ Supported

Private Access (VNet)

○ ManualCustomer
IM✓ Supported

Entra ID Authentication

○ ManualCustomer
IM✓ Supported

Managed Identity

○ ManualCustomer
DP✓ Supported

Data in Transit (TLS 1.2)

● DefaultCustomer
DP✓ Supported

Encryption at Rest

● DefaultMicrosoft
DP✓ Supported

CMK Encryption

○ ManualCustomer
LT✓ Supported

Defender for PostgreSQL

○ ManualCustomer
BR✓ Supported

Automated Backups

● DefaultMicrosoft

Terraform: Hardened PostgreSQL Flexible Server

hcl
resource "azurerm_postgresql_flexible_server" "main" {
  name                = "psql-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  version             = "16"
  sku_name            = "GP_Standard_D4s_v3"

  # NS-1: VNet integration (private access)
  delegated_subnet_id = azurerm_subnet.postgresql.id
  private_dns_zone_id = azurerm_private_dns_zone.postgresql.id

  # DP-3: TLS enforcement
  zone                = "1"

  storage_mb          = 65536
  backup_retention_days = 35                    # BR-1: Max retention

  # DP-5: CMK encryption
  customer_managed_key {
    key_vault_key_id                  = azurerm_key_vault_key.postgresql.id
    primary_user_assigned_identity_id = azurerm_user_assigned_identity.postgresql.id
  }

  identity {
    type         = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.postgresql.id]
  }

  # IM-1: Entra ID authentication
  authentication {
    active_directory_auth_enabled = true
    password_auth_enabled         = false       # Entra ID only
    tenant_id                     = data.azurerm_client_config.current.tenant_id
  }

  high_availability {
    mode = "ZoneRedundant"
  }
}
resource "azurerm_postgresql_flexible_server" "main" {
  name                = "psql-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location
  version             = "16"
  sku_name            = "GP_Standard_D4s_v3"

  # NS-1: VNet integration (private access)
  delegated_subnet_id = azurerm_subnet.postgresql.id
  private_dns_zone_id = azurerm_private_dns_zone.postgresql.id

  # DP-3: TLS enforcement
  zone                = "1"

  storage_mb          = 65536
  backup_retention_days = 35                    # BR-1: Max retention

  # DP-5: CMK encryption
  customer_managed_key {
    key_vault_key_id                  = azurerm_key_vault_key.postgresql.id
    primary_user_assigned_identity_id = azurerm_user_assigned_identity.postgresql.id
  }

  identity {
    type         = "UserAssigned"
    identity_ids = [azurerm_user_assigned_identity.postgresql.id]
  }

  # IM-1: Entra ID authentication
  authentication {
    active_directory_auth_enabled = true
    password_auth_enabled         = false       # Entra ID only
    tenant_id                     = data.azurerm_client_config.current.tenant_id
  }

  high_availability {
    mode = "ZoneRedundant"
  }
}

Azure Data Factory

Cloud-native ETL/ELT service for data integration and transformation at scale. Data Factory pipelines often have broad access to production data stores — SQL, Storage, Cosmos DB, and on-premises sources — making pipeline security critical for data protection.

PropertyValue
Defender PlanN/A — monitor via diagnostic logs and Sentinel
Azure Policy Built-inYes — CMK, private endpoint, managed VNet IR, Git
Network ModelManaged VNet IR with managed private endpoints to data stores
Key FeatureManaged Identity auto-created for all linked service authentication

Baseline Controls

NS✓ Supported

Managed VNet

○ ManualCustomer
NS✓ Supported

Private Endpoint

○ ManualCustomer
IM✓ Supported

Managed Identity

● DefaultMicrosoft
DP✓ Supported

Encryption at Rest

● DefaultMicrosoft
DP✓ Supported

CMK Encryption

○ ManualCustomer
DS✓ Supported

Git Integration

○ ManualCustomer
LT✓ Supported

Diagnostic Logging

○ ManualCustomer

Terraform: Secure Data Factory with Managed VNet

hcl
resource "azurerm_data_factory" "main" {
  name                = "adf-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location

  # IM-3: System-assigned managed identity (auto-created)
  identity {
    type = "SystemAssigned"
  }

  # NS-2: Disable public access
  public_network_enabled = false

  # DS-6: Git integration for source control
  github_configuration {
    account_name    = "my-org"
    branch_name     = "main"
    git_url         = "https://github.com"
    repository_name = "adf-pipelines"
    root_folder     = "/"
  }

  managed_virtual_network_enabled = true        # NS-1: Managed VNet IR
}

# NS-1: Managed VNet Integration Runtime
resource "azurerm_data_factory_integration_runtime_azure" "managed" {
  name            = "ir-managed-vnet"
  data_factory_id = azurerm_data_factory.main.id
  location        = azurerm_resource_group.data.location

  virtual_network_enabled = true                # Uses managed VNet
}

# NS-2: Private Endpoint for Data Factory
resource "azurerm_private_endpoint" "adf" {
  name                = "pe-adf-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-adf-prod"
    private_connection_resource_id = azurerm_data_factory.main.id
    subresource_names              = ["dataFactory"]
    is_manual_connection           = false
  }
}
resource "azurerm_data_factory" "main" {
  name                = "adf-prod"
  resource_group_name = azurerm_resource_group.data.name
  location            = azurerm_resource_group.data.location

  # IM-3: System-assigned managed identity (auto-created)
  identity {
    type = "SystemAssigned"
  }

  # NS-2: Disable public access
  public_network_enabled = false

  # DS-6: Git integration for source control
  github_configuration {
    account_name    = "my-org"
    branch_name     = "main"
    git_url         = "https://github.com"
    repository_name = "adf-pipelines"
    root_folder     = "/"
  }

  managed_virtual_network_enabled = true        # NS-1: Managed VNet IR
}

# NS-1: Managed VNet Integration Runtime
resource "azurerm_data_factory_integration_runtime_azure" "managed" {
  name            = "ir-managed-vnet"
  data_factory_id = azurerm_data_factory.main.id
  location        = azurerm_resource_group.data.location

  virtual_network_enabled = true                # Uses managed VNet
}

# NS-2: Private Endpoint for Data Factory
resource "azurerm_private_endpoint" "adf" {
  name                = "pe-adf-prod"
  location            = azurerm_resource_group.data.location
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.data.id

  private_service_connection {
    name                           = "psc-adf-prod"
    private_connection_resource_id = azurerm_data_factory.main.id
    subresource_names              = ["dataFactory"]
    is_manual_connection           = false
  }
}

Data Factory Pipeline Permissions

Data Factory managed identity often has broad access across data stores. Apply least-privilege RBAC — grant only the specific roles needed (e.g., Storage Blob Data Reader, not Contributor). Audit pipeline activity logs regularly for unexpected data movement.