Skip to main content
emnode
Compliance Low severity MCSB DP-5

Microsoft Defender for Cloud · Azure Database for MySQL

(Enable if required) MySQL servers should use customer-managed keys to encrypt data at rest

Written and reviewed by Emnode · Last reviewed

What does the recommendation "(Enable if required) MySQL servers should use customer-managed keys to encrypt data at rest" check?

Flags an Azure Database for MySQL single server (the Microsoft.DBforMySQL/servers resource type) whose data at rest is still protected only by the platform-managed key rather than a customer-managed key held in your own Azure Key Vault. The policy reads the server's key configuration (serverKeyType and the key vault key uri) and reports any server where the type is not set to a customer key. It does not apply to the newer Flexible Server deployment model, it does not verify that key rotation is actually happening, and it does not check that infrastructure double encryption or TLS in transit are configured. It is also audit-only by default: it reports the gap, it does not switch the key for you.

Why does "(Enable if required) MySQL servers should use customer-managed keys to encrypt data at rest" matter?

With the default service-managed key, Microsoft holds and rotates the key and you have no way to revoke access independently of deleting the server. For workloads under PCI DSS, HIPAA or a data-residency regime, that is often not acceptable: auditors expect the organisation that owns the data to also own the key lifecycle and the kill switch. A customer-managed key lets you revoke or disable the key in Key Vault to render the database unreadable during an incident, enforce separation of duties between DBAs and key custodians, and rotate on your own schedule. Without it, a regulated workload can fail an audit or breach a contractual encryption clause, and you lose the ability to cryptographically cut off access on demand.

How do I fix "(Enable if required) MySQL servers should use customer-managed keys to encrypt data at rest"?

  1. Prepare the Key Vault and identity: on the vault enable soft delete and purge protection (az keyvault update --name <vault> --enable-soft-delete true --enable-purge-protection true), create an RSA key, and grant the server's managed identity the key permissions get, list, wrapKey and unwrapKey via az keyvault set-policy.
  2. Bind the key to the server: run az mysql server key create --name <server> --resource-group <rg> --kid https://<vault>.vault.azure.net/keys/<key>/<version>, which sets serverKeyType to AzureKeyVault and points the server at your key.
  3. Make it the default and enforce it: assign the built-in policy by its display name so new servers are caught automatically, looking the definition id up at runtime rather than hardcoding a GUID: pid=$(az policy definition list --query "[?displayName=='MySQL servers should use customer-managed keys to encrypt data at rest'].name" -o tsv); az policy assignment create --name mysql-cmk --policy "$pid" --scope <scope>.

Remediation script · bash

# 1. Lock the wire and the network on a production PostgreSQL flexible server.
RG=db-prod-rg
SRV=ordersprod-pg

# Enforce encrypted connections (dynamic, no restart needed).
az postgres flexible-server parameter set \
  --resource-group "$RG" --server-name "$SRV" \
  --source user-override \
  --name require_secure_transport --value ON

# Pin the minimum TLS version.
az postgres flexible-server parameter set \
  --resource-group "$RG" --server-name "$SRV" \
  --source user-override \
  --name ssl_min_protocol_version --value TLSv1.2

# Disable public network access.
az postgres flexible-server update \
  --resource-group "$RG" --name "$SRV" \
  --public-access Disabled

# 2. Turn on the pgaudit trail and connection throttling.
for p in "pgaudit.log=role,ddl,misc" \
         "pgaudit.log_client=ON" \
         "pgaudit.log_level=log" \
         "pgaudit.log_statement=on" \
         "pgaudit.log_statement_once=on" \
         "connection_throttle.enable=on"; do
  az postgres flexible-server parameter set \
    --resource-group "$RG" --server-name "$SRV" \
    --source user-override \
    --name "${p%%=*}" --value "${p#*=}"
done

# 3. Provision a Microsoft Entra administrator and disable password sign-in.
az postgres flexible-server ad-admin create \
  --resource-group "$RG" --server-name "$SRV" \
  --display-name "dba-team" \
  --object-id "$(az ad group show --group dba-team --query id -o tsv)" \
  --type Group
az postgres flexible-server update \
  --resource-group "$RG" --name "$SRV" \
  --microsoft-entra-auth Enabled --password-auth Disabled

# 4. Ratchet it shut: deny any future PostgreSQL flexible server with public access.
# Look the built-in policy up by its EXACT display name, never paste a guessed GUID.
PID=$(az policy definition list \
  --query "[?displayName=='Public network access should be disabled for PostgreSQL flexible servers'].name" \
  -o tsv)
az policy assignment create \
  --name deny-public-pg-flex \
  --policy "$PID" \
  --params '{"effect":{"value":"Deny"}}' \
  --scope "/subscriptions/00000000-0000-0000-0000-000000000000"

Full walkthrough (console steps, edge cases and verification) in the lesson Harden Azure MySQL and PostgreSQL.

Is "(Enable if required) MySQL servers should use customer-managed keys to encrypt data at rest" a false positive?

A non-production single server holding only synthetic test data, where the platform-managed key already meets your risk policy, will still flag because the severity is Low and the recommendation name is prefixed '(Enable if required)': customer-managed keys are an optional control Azure only expects you to turn on where regulation or contract demands it. If you have made a documented decision that this server does not need bring-your-own-key, set the policy assignment to Disabled for that scope rather than carrying a permanent open finding. Note too that this control covers single server only, so a workload already migrated to Flexible Server is correctly out of scope here and is governed by the separate Flexible Server recommendation.