Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions run-samples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ TERRAFORM_SAMPLES=(
"samples/web-app-cosmosdb-mongodb-api/dotnet/terraform|bash deploy.sh"
"samples/web-app-managed-identity/python/terraform|bash deploy.sh"
"samples/web-app-managed-identity/dotnet/terraform|bash deploy.sh"
"samples/web-app-sql-database/python/terraform|bash deploy.sh"
"samples/web-app-sql-database/dotnet/terraform|bash deploy.sh"
"samples/web-app-sql-database/python/terraform|bash deploy.sh|bash ../scripts/validate.sh"
"samples/web-app-sql-database/dotnet/terraform|bash deploy.sh|bash ../scripts/validate.sh"
"samples/web-app-mysql-flexible-server/python/terraform|bash deploy.sh"
"samples/web-app-mysql-flexible-server/dotnet/terraform|bash deploy.sh"
"samples/web-app-postgresql-flexible-server/python/terraform|bash deploy.sh"
Expand Down
13 changes: 9 additions & 4 deletions samples/web-app-sql-database/dotnet/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Azure Web App with Azure SQL Database and Azure Key Vault

This sample demonstrates a ASP.NET Core Razor Pages single-page web application called *Vacation Planner* hosted on an [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview). The app runs on an Azure App Service Plan and stores activity data in an `activities` table within the `sampledb` database on an [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/) instance. The connection string of the SQL database is stored as a secret in [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview). The application also retrieves its certificate from Key Vault to serve traffic over HTTPS.
This sample demonstrates a ASP.NET Core Razor Pages single-page web application called *Vacation Planner* hosted on an [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview). The app runs on an Azure App Service Plan and stores activity data in an `activities` table within the `sampledb` database on an [Azure SQL Database](https://learn.microsoft.com/en-us/azure/azure-sql/database/) instance. The connection string of the SQL database is stored as a secret in [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview). The application also retrieves its certificate from Key Vault to serve traffic over HTTPS. The SQL server encrypts its databases at rest with [Transparent Data Encryption (TDE)](https://learn.microsoft.com/en-us/azure/azure-sql/database/transparent-data-encryption-byok-overview) protected by a customer-managed key: an RSA key in Key Vault that the server reaches through a user-assigned managed identity.


## Architecture
Expand All @@ -11,8 +11,9 @@ The following diagram illustrates the architecture of the solution:

- **Azure Web App**: Hosts the ASP.NET Core application
- **Azure App Service Plan**: Provides compute resources for the web app
- **Azure SQL Database**: Stores activity data in a relational table
- **Azure Key Vault**: Stores the database connection string and the certificate used to secure HTTPS traffic
- **Azure SQL Database**: Stores activity data in a relational table, encrypted at rest with TDE
- **Azure Key Vault**: Stores the database connection string, the certificate used to secure HTTPS traffic, and the RSA key that serves as the TDE protector of the SQL server
- **User-Assigned Managed Identity**: The identity the SQL server uses to wrap and unwrap its database encryption keys with the Key Vault key

## Prerequisites

Expand Down Expand Up @@ -44,12 +45,16 @@ The Vacation Planner Web App supports two common approaches for accessing Azure
This flexibility allows the app to run securely in Azure or in emulated environments like [LocalStack for Azure](https://docs.localstack.cloud/azure/). The client code supports both authentication modes using [`ClientSecretCredential`](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.clientsecretcredential) or [`DefaultAzureCredential`](https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential) from the Azure SDK.

## Azure Key Vault Integration
The application integrates with Azure Key Vault for managing secrets and certificates:
The application integrates with Azure Key Vault for managing secrets and certificates, and the SQL server uses a Key Vault key to protect its data at rest:

Secrets: The SQL connection string is stored as a secret in Key Vault. At runtime, the app retrieves it using the Azure Key Vault Secrets SDK. This is configured via the KEY_VAULT_NAME and SECRET_NAME environment variables.

Certificates: A self-signed certificate is created in Key Vault during deployment. The app exposes a GET /api/certificate endpoint that retrieves the certificate using the Azure Key Vault Certificates SDK and returns its name, confirming the integration works. This is configured via the KEYVAULT_URI and CERT_NAME environment variables.

Keys: An RSA key in Key Vault is the TDE protector of the SQL server, the customer-managed key that encrypts the database encryption key of every database on the server. The server reaches the key through its user-assigned managed identity, which holds the `get`, `wrapKey` and `unwrapKey` key permissions, and picks up new versions of the key automatically (auto-rotation). Azure requires soft delete and purge protection on the vault. With purge protection, a deleted vault cannot be purged: after the resource group is deleted, the vault stays soft-deleted for the 7-day retention period, and its name cannot be reused anywhere until then. To redeploy to Azure within that window, change `PREFIX` or `SUFFIX` in the deployment script you use and in `scripts/validate.sh` and `scripts/call-web-app.sh`.

On LocalStack, the emulator registers the key on the server and checks that the key exists, but it does not encrypt the database with it. The Azure CLI variant registers the key and the protector with `az resource create`, because `az sql server key create` and `az sql server tde-key set` only accept key ids on the public Key Vault domains and reject the ones the emulator issues.

## Deployment

Set up the Azure emulator using the LocalStack for Azure Docker image. Before starting, ensure you have a valid `LOCALSTACK_AUTH_TOKEN` to access the Azure emulator. Refer to the [Auth Token guide](https://docs.localstack.cloud/getting-started/auth-token/) to obtain your Auth Token and set it in the `LOCALSTACK_AUTH_TOKEN` environment variable. The Azure Docker image is available on the [LocalStack Docker Hub](https://hub.docker.com/r/localstack/localstack-azure). To pull the image, execute:
Expand Down
42 changes: 41 additions & 1 deletion samples/web-app-sql-database/dotnet/bicep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ The [deploy.sh](deploy.sh) script creates the [Azure Resource Group](https://lea
3. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application.
4. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the ASP.NET Core Razor Pages single-page application (*Vacation Planner*), connected to Azure SQL Database.
5. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository.
6. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret.
6. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret and the RSA key that serves as the [TDE protector](https://learn.microsoft.com/en-us/azure/azure-sql/database/transparent-data-encryption-byok-overview) of the SQL server, registered by the [transparent-data-encryption.bicep](modules/transparent-data-encryption.bicep) module.
7. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview): The identity the SQL server uses to reach its TDE protector key in Key Vault.

The web app allows users to plan and manage vacation activities, storing all activity data in the `Activities` table in the `PlannerDB` database. For more information, see [Azure Web App with Azure SQL Database and Azure Key Vault](../README.md).

Expand Down Expand Up @@ -100,6 +101,7 @@ SQL_DATABASE_NAME='PlannerDB'
WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}"
KEY_VAULT_NAME="${PREFIX}-kv-${SUFFIX}"
SECRET_NAME="${PREFIX}-secret-${SUFFIX}"
TDE_KEY_NAME="${PREFIX}-tde-key-${SUFFIX}"

# Check resource group
echo -e "[$RESOURCE_GROUP_NAME] resource group:\n"
Expand Down Expand Up @@ -130,6 +132,44 @@ az sql db show \
--resource-group "$RESOURCE_GROUP_NAME" \
--output table

# Check that the Key Vault key is the TDE protector of the Azure SQL Server
echo -e "\n[$SQL_SERVER_NAME] SQL server TDE protector:\n"
# Read the key through Azure Resource Manager: the Bicep variant grants the caller no Key Vault data-plane access.
KEY_VAULT_ID=$(az keyvault show \
--name "$KEY_VAULT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "id" \
--output tsv)
TDE_KEY_ID=$(az resource show \
--ids "$KEY_VAULT_ID/keys/$TDE_KEY_NAME" \
--api-version 2024-11-01 \
--query "properties.keyUriWithVersion" \
--output tsv)
TDE_PROTECTOR=$(az sql server tde-key show \
--server "$SQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--output json)
echo "$TDE_PROTECTOR" | jq '{serverKeyType, uri, autoRotationEnabled}'
if [[ "$(jq -r .serverKeyType <<< "$TDE_PROTECTOR")" != "AzureKeyVault" ||
"$(jq -r .uri <<< "$TDE_PROTECTOR")" != "$TDE_KEY_ID" ||
"$(jq -r .autoRotationEnabled <<< "$TDE_PROTECTOR")" != "true" ]]; then
echo "The TDE protector of [$SQL_SERVER_NAME] is not the auto-rotated Key Vault key [$TDE_KEY_ID]"
exit 1
fi

# Check that TDE is enabled on the Azure SQL Database
TDE_STATE=$(az sql db tde show \
--database "$SQL_DATABASE_NAME" \
--server "$SQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "state" \
--output tsv)
echo -e "\n[$SQL_DATABASE_NAME] SQL database TDE state: [$TDE_STATE]"
if [[ "$TDE_STATE" != "Enabled" ]]; then
echo "TDE is not enabled on [$SQL_DATABASE_NAME]"
exit 1
fi

# Check Azure Key Vault
echo -e "\n[$KEY_VAULT_NAME] Key Vault:\n"
az keyvault show \
Expand Down
58 changes: 50 additions & 8 deletions samples/web-app-sql-database/dotnet/bicep/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@ param administratorLoginPassword string = 'P@ssw0rd1234!'
@description('Conditional. The Azure Active Directory (AAD) administrator authentication. Required if no `administratorLogin` & `administratorLoginPassword` is provided.')
param administrators object?

@description('Specifies the conditional Developmentresource ID of a user-assigned identityDevelopment to be used by default. This is required if `userAssignedIdentities` is not empty.')
param primaryUserAssignedIdentityResourceId string?

@allowed([
'1.0'
'1.1'
Expand Down Expand Up @@ -325,15 +322,25 @@ var webAppName = '${prefix}-webapp-${suffix}'
var appServicePlanName = '${prefix}-app-service-plan-${suffix}'
var keyVaultName = '${prefix}-kv-${suffix}'
var sqlConnectionStringSecretName = '${prefix}-secret-${suffix}'
var identity = {
type: 'SystemAssigned'
}
var sqlServerIdentityName = '${prefix}-tde-identity-${suffix}'
var tdeKeyName = '${prefix}-tde-key-${suffix}'

resource sqlServerIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: sqlServerIdentityName
location: location
tags: tags
}

resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = {
name: sqlServerName
location: location
tags: tags
identity: identity
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${sqlServerIdentity.id}': {}
}
}
properties: {
administratorLogin: administratorLogin
administratorLoginPassword: administratorLoginPassword
Expand All @@ -342,7 +349,7 @@ resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' = {
isIPv6Enabled: isIPv6Enabled
version: version
minimalTlsVersion: minimalTlsVersion
primaryUserAssignedIdentityId: primaryUserAssignedIdentityResourceId
primaryUserAssignedIdentityId: sqlServerIdentity.id
publicNetworkAccess: publicNetworkAccess
restrictOutboundNetworkAccess: restrictOutboundNetworkAccess
}
Expand Down Expand Up @@ -453,10 +460,45 @@ resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
]
}
}
{
tenantId: subscription().tenantId
objectId: sqlServerIdentity.properties.principalId
permissions: {
keys: [
'get'
'wrapKey'
'unwrapKey'
]
}
}
]
enableRbacAuthorization: false
enableSoftDelete: true
softDeleteRetentionInDays: 7
enablePurgeProtection: true
}
}

resource tdeKey 'Microsoft.KeyVault/vaults/keys@2024-11-01' = {
parent: keyVault
name: tdeKeyName
properties: {
kty: 'RSA'
keySize: 2048
keyOps: [
'wrapKey'
'unwrapKey'
]
}
}

module transparentDataEncryption 'modules/transparent-data-encryption.bicep' = {
name: 'transparentDataEncryption'
params: {
sqlServerName: sqlServer.name
keyVaultName: keyVault.name
keyName: tdeKey.name
keyUri: tdeKey.properties.keyUriWithVersion
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@description('Specifies the name of the SQL logical server.')
param sqlServerName string

@description('Specifies the name of the Key Vault that holds the TDE protector key.')
param keyVaultName string

@description('Specifies the name of the Key Vault key used as the TDE protector.')
param keyName string

@description('Specifies the versioned URI of the Key Vault key used as the TDE protector.')
param keyUri string

resource sqlServer 'Microsoft.Sql/servers@2024-05-01-preview' existing = {
name: sqlServerName
}

// A server key must be named after the vault, key and key version it points to.
resource serverKey 'Microsoft.Sql/servers/keys@2023-08-01' = {
parent: sqlServer
name: '${keyVaultName}_${keyName}_${last(split(keyUri, '/'))}'
properties: {
serverKeyType: 'AzureKeyVault'
uri: keyUri
}
}

resource encryptionProtector 'Microsoft.Sql/servers/encryptionProtector@2023-08-01' = {
parent: sqlServer
name: 'current'
properties: {
serverKeyType: 'AzureKeyVault'
serverKeyName: serverKey.name
autoRotationEnabled: true
}
}
42 changes: 41 additions & 1 deletion samples/web-app-sql-database/dotnet/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ The [deploy.sh](deploy.sh) Bash script creates the following Azure resources usi
4. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The compute resource that hosts the web application.
5. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Hosts the ASP.NET Core Razor Pages single-page application (*Vacation Planner*), connected to Azure SQL Database.
6. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): (Optional) Configures automatic deployment from a public GitHub repository.
7. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret.
7. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview): Stores the SQL connection string in a secret and the RSA key that serves as the [TDE protector](https://learn.microsoft.com/en-us/azure/azure-sql/database/transparent-data-encryption-byok-overview) of the SQL server.
8. [User-Assigned Managed Identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview): The identity the SQL server uses to reach its TDE protector key in Key Vault.

The system implements a Vacation Planner web application that stores and retrieves activity data from Azure SQL Database. For more information, see [Azure Web App with Azure SQL Database and Azure Key Vault](../README.md).

Expand Down Expand Up @@ -100,6 +101,7 @@ SQL_DATABASE_NAME='PlannerDB'
WEB_APP_NAME="${PREFIX}-webapp-${SUFFIX}"
KEY_VAULT_NAME="${PREFIX}-kv-${SUFFIX}"
SECRET_NAME="${PREFIX}-secret-${SUFFIX}"
TDE_KEY_NAME="${PREFIX}-tde-key-${SUFFIX}"

# Check resource group
echo -e "[$RESOURCE_GROUP_NAME] resource group:\n"
Expand Down Expand Up @@ -130,6 +132,44 @@ az sql db show \
--resource-group "$RESOURCE_GROUP_NAME" \
--output table

# Check that the Key Vault key is the TDE protector of the Azure SQL Server
echo -e "\n[$SQL_SERVER_NAME] SQL server TDE protector:\n"
# Read the key through Azure Resource Manager: the Bicep variant grants the caller no Key Vault data-plane access.
KEY_VAULT_ID=$(az keyvault show \
--name "$KEY_VAULT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "id" \
--output tsv)
TDE_KEY_ID=$(az resource show \
--ids "$KEY_VAULT_ID/keys/$TDE_KEY_NAME" \
--api-version 2024-11-01 \
--query "properties.keyUriWithVersion" \
--output tsv)
TDE_PROTECTOR=$(az sql server tde-key show \
--server "$SQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--output json)
echo "$TDE_PROTECTOR" | jq '{serverKeyType, uri, autoRotationEnabled}'
if [[ "$(jq -r .serverKeyType <<< "$TDE_PROTECTOR")" != "AzureKeyVault" ||
"$(jq -r .uri <<< "$TDE_PROTECTOR")" != "$TDE_KEY_ID" ||
"$(jq -r .autoRotationEnabled <<< "$TDE_PROTECTOR")" != "true" ]]; then
echo "The TDE protector of [$SQL_SERVER_NAME] is not the auto-rotated Key Vault key [$TDE_KEY_ID]"
exit 1
fi

# Check that TDE is enabled on the Azure SQL Database
TDE_STATE=$(az sql db tde show \
--database "$SQL_DATABASE_NAME" \
--server "$SQL_SERVER_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "state" \
--output tsv)
echo -e "\n[$SQL_DATABASE_NAME] SQL database TDE state: [$TDE_STATE]"
if [[ "$TDE_STATE" != "Enabled" ]]; then
echo "TDE is not enabled on [$SQL_DATABASE_NAME]"
exit 1
fi

# Check Azure Key Vault
echo -e "\n[$KEY_VAULT_NAME] Key Vault:\n"
az keyvault show \
Expand Down
Loading
Loading