From 456d2bd5d9b63bcbd66205342bfe15e1a4fa1270 Mon Sep 17 00:00:00 2001 From: "Jianing Wang (MSFT)" <141212663+jianingwang123@users.noreply.github.com> Date: Sun, 13 Oct 2024 02:31:37 +0800 Subject: [PATCH] feat: Add the creation of `Microsoft.KeyVault/vaults/secrets` in Sql Server Module - `avm/res/sql/server` (#2859) ## Description Fixes #2608 Set `azure/login@v2` to use federated credential. (Due to some security policies, we are currently unable to use client secret for azure login. After completing all changes to this PR, we will restore the login method. This is currently only for testing the github pipeline.) ## Pipeline Reference | Pipeline | | -------- | | [![avm.res.sql.server](https://github.com/jianingwang123/bicep-registry-modules/actions/workflows/avm.res.sql.server.yml/badge.svg?branch=fix%2F2608)](https://github.com/jianingwang123/bicep-registry-modules/actions/workflows/avm.res.sql.server.yml) | ## Type of Change - [ ] Update to CI Environment or utilities (Non-module affecting changes) - [x] Azure Verified Module updates: - [ ] Bugfix containing backwards-compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `version.json`: - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `version.json`. - [ ] Breaking changes and I have bumped the MAJOR version in `version.json`. - [ ] Update to documentation ## Checklist - [x] I'm sure there are no other open Pull Requests for the same update/change - [x] I have run `Set-AVMModule` locally to generate the supporting module files. - [x] My corresponding pipelines / checks run clean and green without any errors or warnings @jongio - for notification. --------- Co-authored-by: zedy --- avm/res/sql/server/README.md | 169 ++++++++++- avm/res/sql/server/main.bicep | 55 ++++ avm/res/sql/server/main.json | 262 ++++++++++++++++-- .../sql/server/modules/keyVaultExport.bicep | 62 +++++ .../tests/e2e/kvSecrets/dependencies.bicep | 21 ++ .../tests/e2e/kvSecrets/main.test.bicep | 73 +++++ 6 files changed, 606 insertions(+), 36 deletions(-) create mode 100644 avm/res/sql/server/modules/keyVaultExport.bicep create mode 100644 avm/res/sql/server/tests/e2e/kvSecrets/dependencies.bicep create mode 100644 avm/res/sql/server/tests/e2e/kvSecrets/main.test.bicep diff --git a/avm/res/sql/server/README.md b/avm/res/sql/server/README.md index e38347c937..92be8ea923 100644 --- a/avm/res/sql/server/README.md +++ b/avm/res/sql/server/README.md @@ -19,6 +19,7 @@ This module deploys an Azure SQL Server. | `Microsoft.Authorization/locks` | [2020-05-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2020-05-01/locks) | | `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | | `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | +| `Microsoft.KeyVault/vaults/secrets` | [2023-07-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.KeyVault/2023-07-01/vaults/secrets) | | `Microsoft.Network/privateEndpoints` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints) | | `Microsoft.Network/privateEndpoints/privateDnsZoneGroups` | [2023-11-01](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Network/2023-11-01/privateEndpoints/privateDnsZoneGroups) | | `Microsoft.Sql/servers` | [2023-08-01-preview](https://learn.microsoft.com/en-us/azure/templates/Microsoft.Sql/servers) | @@ -45,10 +46,11 @@ The following section provides usage examples for the module, which were used to - [With an administrator](#example-1-with-an-administrator) - [With audit settings](#example-2-with-audit-settings) - [Using only defaults](#example-3-using-only-defaults) -- [Using large parameter set](#example-4-using-large-parameter-set) -- [With a secondary database](#example-5-with-a-secondary-database) -- [With vulnerability assessment](#example-6-with-vulnerability-assessment) -- [WAF-aligned](#example-7-waf-aligned) +- [Deploying with a key vault reference to save secrets](#example-4-deploying-with-a-key-vault-reference-to-save-secrets) +- [Using large parameter set](#example-5-using-large-parameter-set) +- [With a secondary database](#example-6-with-a-secondary-database) +- [With vulnerability assessment](#example-7-with-vulnerability-assessment) +- [WAF-aligned](#example-8-waf-aligned) ### Example 1: _With an administrator_ @@ -310,7 +312,115 @@ param location = ''

-### Example 4: _Using large parameter set_ +### Example 4: _Deploying with a key vault reference to save secrets_ + +This instance deploys the module saving all its secrets in a key vault. + + +

+ +via Bicep module + +```bicep +module server 'br/public:avm/res/sql/server:' = { + name: 'serverDeployment' + params: { + // Required parameters + name: 'sqlkvs001' + // Non-required parameters + administratorLogin: 'adminUserName' + administratorLoginPassword: '' + databases: [ + { + name: 'myDatabase' + } + ] + location: '' + secretsExportConfiguration: { + keyVaultResourceId: '' + sqlAdminPasswordSecretName: 'adminLoginPasswordKey' + sqlAzureConnectionStringSercretName: 'sqlConnectionStringKey' + } + } +} +``` + +
+

+ +

+ +via JSON parameters file + +```json +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + // Required parameters + "name": { + "value": "sqlkvs001" + }, + // Non-required parameters + "administratorLogin": { + "value": "adminUserName" + }, + "administratorLoginPassword": { + "value": "" + }, + "databases": { + "value": [ + { + "name": "myDatabase" + } + ] + }, + "location": { + "value": "" + }, + "secretsExportConfiguration": { + "value": { + "keyVaultResourceId": "", + "sqlAdminPasswordSecretName": "adminLoginPasswordKey", + "sqlAzureConnectionStringSercretName": "sqlConnectionStringKey" + } + } + } +} +``` + +
+

+ +

+ +via Bicep parameters file + +```bicep-params +using 'br/public:avm/res/sql/server:' + +// Required parameters +param name = 'sqlkvs001' +// Non-required parameters +param administratorLogin = 'adminUserName' +param administratorLoginPassword = '' +param databases = [ + { + name: 'myDatabase' + } +] +param location = '' +param secretsExportConfiguration = { + keyVaultResourceId: '' + sqlAdminPasswordSecretName: 'adminLoginPasswordKey' + sqlAzureConnectionStringSercretName: 'sqlConnectionStringKey' +} +``` + +
+

+ +### Example 5: _Using large parameter set_ This instance deploys the module with most of its features enabled. @@ -833,7 +943,7 @@ param vulnerabilityAssessmentsObj = {

-### Example 5: _With a secondary database_ +### Example 6: _With a secondary database_ This instance deploys the module with a secondary database. @@ -956,7 +1066,7 @@ param tags = {

-### Example 6: _With vulnerability assessment_ +### Example 7: _With vulnerability assessment_ This instance deploys the module with a vulnerability assessment. @@ -1133,7 +1243,7 @@ param vulnerabilityAssessmentsObj = {

-### Example 7: _WAF-aligned_ +### Example 8: _WAF-aligned_ This instance deploys the module in alignment with the best-practices of the Azure Well-Architected Framework. @@ -1579,6 +1689,7 @@ param vulnerabilityAssessmentsObj = { | [`publicNetworkAccess`](#parameter-publicnetworkaccess) | string | Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and neither firewall rules nor virtual network rules are set. | | [`restrictOutboundNetworkAccess`](#parameter-restrictoutboundnetworkaccess) | string | Whether or not to restrict outbound network access for this server. | | [`roleAssignments`](#parameter-roleassignments) | array | Array of role assignments to create. | +| [`secretsExportConfiguration`](#parameter-secretsexportconfiguration) | object | Key vault reference and secret settings for the module's secrets export. | | [`securityAlertPolicies`](#parameter-securityalertpolicies) | array | The security alert policies to create in the server. | | [`tags`](#parameter-tags) | object | Tags of the resource. | | [`virtualNetworkRules`](#parameter-virtualnetworkrules) | array | The virtual network rules to create in the server. | @@ -2411,6 +2522,47 @@ The principal type of the assigned principal ID. ] ``` +### Parameter: `secretsExportConfiguration` + +Key vault reference and secret settings for the module's secrets export. + +- Required: No +- Type: object + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`keyVaultResourceId`](#parameter-secretsexportconfigurationkeyvaultresourceid) | string | The resource ID of the key vault where to store the secrets of this module. | + +**Optional parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`sqlAdminPasswordSecretName`](#parameter-secretsexportconfigurationsqladminpasswordsecretname) | string | The sqlAdminPassword secret name to create. | +| [`sqlAzureConnectionStringSercretName`](#parameter-secretsexportconfigurationsqlazureconnectionstringsercretname) | string | The sqlAzureConnectionString secret name to create. | + +### Parameter: `secretsExportConfiguration.keyVaultResourceId` + +The resource ID of the key vault where to store the secrets of this module. + +- Required: Yes +- Type: string + +### Parameter: `secretsExportConfiguration.sqlAdminPasswordSecretName` + +The sqlAdminPassword secret name to create. + +- Required: No +- Type: string + +### Parameter: `secretsExportConfiguration.sqlAzureConnectionStringSercretName` + +The sqlAzureConnectionString secret name to create. + +- Required: No +- Type: string + ### Parameter: `securityAlertPolicies` The security alert policies to create in the server. @@ -2446,6 +2598,7 @@ The vulnerability assessment configuration. | Output | Type | Description | | :-- | :-- | :-- | +| `exportedSecrets` | | A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name. | | `location` | string | The location the resource was deployed into. | | `name` | string | The name of the deployed SQL server. | | `privateEndpoints` | array | The private endpoints of the SQL server. | diff --git a/avm/res/sql/server/main.bicep b/avm/res/sql/server/main.bicep index e628668146..6a3921b4eb 100644 --- a/avm/res/sql/server/main.bicep +++ b/avm/res/sql/server/main.bicep @@ -105,6 +105,9 @@ param vulnerabilityAssessmentsObj object = {} @description('Optional. The audit settings configuration.') param auditSettings auditSettingsType? +@description('Optional. Key vault reference and secret settings for the module\'s secrets export.') +param secretsExportConfiguration secretsExportConfigurationType? + var builtInRoleNames = { Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') @@ -491,6 +494,36 @@ module server_audit_settings 'audit-settings/main.bicep' = if (!empty(auditSetti } } +module secretsExport 'modules/keyVaultExport.bicep' = if (secretsExportConfiguration != null) { + name: '${uniqueString(deployment().name, location)}-secrets-kv' + scope: resourceGroup( + split((secretsExportConfiguration.?keyVaultResourceId ?? '//'), '/')[2], + split((secretsExportConfiguration.?keyVaultResourceId ?? '////'), '/')[4] + ) + params: { + keyVaultName: last(split(secretsExportConfiguration.?keyVaultResourceId ?? '//', '/')) + secretsToSet: union( + [], + contains(secretsExportConfiguration!, 'sqlAdminPasswordSecretName') + ? [ + { + name: secretsExportConfiguration!.sqlAdminPasswordSecretName + value: administratorLoginPassword + } + ] + : [], + contains(secretsExportConfiguration!, 'sqlAzureConnectionStringSercretName') + ? [ + { + name: secretsExportConfiguration!.sqlAzureConnectionStringSercretName + value: 'Server=${server.properties.fullyQualifiedDomainName}; Database=${!empty(databases) ? databases[0].name : ''}; User=${administratorLogin}; Password=${administratorLoginPassword}' + } + ] + : [] + ) + } +} + @description('The name of the deployed SQL server.') output name string = server.name @@ -506,6 +539,11 @@ output systemAssignedMIPrincipalId string = server.?identity.?principalId ?? '' @description('The location the resource was deployed into.') output location string = server.location +@description('A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret\'s name.') +output exportedSecrets secretsOutputType = (secretsExportConfiguration != null) + ? toObject(secretsExport.outputs.secretsSet, secret => last(split(secret.secretResourceId, '/')), secret => secret) + : {} + @description('The private endpoints of the SQL server.') output privateEndpoints array = [ for (pe, i) in (!empty(privateEndpoints) ? array(privateEndpoints) : []): { @@ -681,3 +719,20 @@ type auditSettingsType = { @description('Optional. Specifies the identifier key of the auditing storage account.') storageAccountResourceId: string? } + +type secretsExportConfigurationType = { + @description('Required. The resource ID of the key vault where to store the secrets of this module.') + keyVaultResourceId: string + + @description('Optional. The sqlAdminPassword secret name to create.') + sqlAdminPasswordSecretName: string? + + @description('Optional. The sqlAzureConnectionString secret name to create.') + sqlAzureConnectionStringSercretName: string? +}? + +import { secretSetType } from 'modules/keyVaultExport.bicep' +type secretsOutputType = { + @description('An exported secret\'s references.') + *: secretSetType +} diff --git a/avm/res/sql/server/main.json b/avm/res/sql/server/main.json index b13a78c08f..af15b03a96 100644 --- a/avm/res/sql/server/main.json +++ b/avm/res/sql/server/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "13031224188572751832" + "version": "0.30.23.60470", + "templateHash": "779644860652942835" }, "name": "Azure SQL Servers", "description": "This module deploys an Azure SQL Server.", @@ -434,6 +434,64 @@ } } } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the key vault where to store the secrets of this module." + } + }, + "sqlAdminPasswordSecretName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The sqlAdminPassword secret name to create." + } + }, + "sqlAzureConnectionStringSercretName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The sqlAzureConnectionString secret name to create." + } + } + }, + "nullable": true + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/secretSetType", + "metadata": { + "description": "An exported secret's references." + } + } + }, + "secretSetType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "modules/keyVaultExport.bicep" + } + } } }, "parameters": { @@ -614,6 +672,13 @@ "metadata": { "description": "Optional. The audit settings configuration." } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } } }, "variables": { @@ -780,8 +845,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "6019999815954957727" + "version": "0.30.23.60470", + "templateHash": "11407307502843440892" }, "name": "SQL Server Database", "description": "This module deploys an Azure SQL Server Database.", @@ -1258,8 +1323,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "8635162595153731245" + "version": "0.30.23.60470", + "templateHash": "10836519140305169908" }, "name": "Azure SQL Server Database Short Term Backup Retention Policies", "description": "This module deploys an Azure SQL Server Database Short-Term Backup Retention Policy.", @@ -1360,8 +1425,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "2778016138108001251" + "version": "0.30.23.60470", + "templateHash": "10064519186693398262" }, "name": "SQL Server Database Long Term Backup Retention Policies", "description": "This module deploys an Azure SQL Server Database Long-Term Backup Retention Policy.", @@ -1538,8 +1603,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "18037703368269722870" + "version": "0.30.23.60470", + "templateHash": "1486548652639885128" }, "name": "SQL Server Elastic Pool", "description": "This module deploys an Azure SQL Server Elastic Pool.", @@ -2520,8 +2585,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "7779473510493338097" + "version": "0.30.23.60470", + "templateHash": "6449556555046717103" }, "name": "Azure SQL Server Firewall Rule", "description": "This module deploys an Azure SQL Server Firewall Rule.", @@ -2626,8 +2691,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "7859066741604114060" + "version": "0.30.23.60470", + "templateHash": "4969955763304077350" }, "name": "Azure SQL Server Virtual Network Rules", "description": "This module deploys an Azure SQL Server Virtual Network Rule.", @@ -2734,8 +2799,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "6025191760768766090" + "version": "0.30.23.60470", + "templateHash": "15406914222375641032" }, "name": "Azure SQL Server Security Alert Policies", "description": "This module deploys an Azure SQL Server Security Alert Policy.", @@ -2885,8 +2950,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "5682596516926040129" + "version": "0.30.23.60470", + "templateHash": "11004049200994426011" }, "name": "Azure SQL Server Vulnerability Assessments", "description": "This module deploys an Azure SQL Server Vulnerability Assessment.", @@ -2988,8 +3053,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17251889896692066430" + "version": "0.30.23.60470", + "templateHash": "13956215614091387428" } }, "parameters": { @@ -3077,8 +3142,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "5863771213375512760" + "version": "0.30.23.60470", + "templateHash": "17839617504395216689" }, "name": "Azure SQL Server Keys", "description": "This module deploys an Azure SQL Server Key.", @@ -3196,8 +3261,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "6914924378490463775" + "version": "0.30.23.60470", + "templateHash": "11473914706327458055" }, "name": "Azure SQL Server Encryption Protector", "description": "This module deploys an Azure SQL Server Encryption Protector.", @@ -3329,8 +3394,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "4165841300638093382" + "version": "0.30.23.60470", + "templateHash": "2456263707393734456" }, "name": "Azure SQL Server Audit Settings", "description": "This module deploys an Azure SQL Server Audit Settings.", @@ -3468,8 +3533,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "17251889896692066430" + "version": "0.30.23.60470", + "templateHash": "13956215614091387428" } }, "parameters": { @@ -3528,6 +3593,140 @@ "dependsOn": [ "server" ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(coalesce(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '//'), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '////'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(coalesce(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '//'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'sqlAdminPasswordSecretName'), createArray(createObject('name', parameters('secretsExportConfiguration').sqlAdminPasswordSecretName, 'value', parameters('administratorLoginPassword'))), createArray()), if(contains(parameters('secretsExportConfiguration'), 'sqlAzureConnectionStringSercretName'), createArray(createObject('name', parameters('secretsExportConfiguration').sqlAzureConnectionStringSercretName, 'value', format('Server={0}; Database={1}; User={2}; Password={3}', reference('server').fullyQualifiedDomainName, if(not(empty(parameters('databases'))), parameters('databases')[0].name, ''), parameters('administratorLogin'), parameters('administratorLoginPassword')))), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "16142913599202614386" + } + }, + "definitions": { + "secretSetType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the secrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2022-07-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2023-07-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + }, + "dependsOn": [ + "keyVault" + ] + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]" + } + } + } + } + } + }, + "dependsOn": [ + "server" + ] } }, "outputs": { @@ -3566,6 +3765,13 @@ }, "value": "[reference('server', '2023-08-01-preview', 'full').location]" }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, "privateEndpoints": { "type": "array", "metadata": { diff --git a/avm/res/sql/server/modules/keyVaultExport.bicep b/avm/res/sql/server/modules/keyVaultExport.bicep new file mode 100644 index 0000000000..c4ff7c2f9d --- /dev/null +++ b/avm/res/sql/server/modules/keyVaultExport.bicep @@ -0,0 +1,62 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. The name of the Key Vault to set the secrets in.') +param keyVaultName string + +@description('Required. The secrets to set in the Key Vault.') +param secretsToSet secretToSetType[] + +// ============= // +// Resources // +// ============= // + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource secrets 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = [ + for secret in secretsToSet: { + name: secret.name + parent: keyVault + properties: { + value: secret.value + } + } +] + +// =========== // +// Outputs // +// =========== // + +@description('The references to the secrets exported to the provided Key Vault.') +output secretsSet secretSetType[] = [ + #disable-next-line outputs-should-not-contain-secrets // Only returning the references, not a secret value + for index in range(0, length(secretsToSet ?? [])): { + secretResourceId: secrets[index].id + secretUri: secrets[index].properties.secretUri + } +] + +// =============== // +// Definitions // +// =============== // + +@export() +type secretSetType = { + @description('The resourceId of the exported secret.') + secretResourceId: string + + @description('The secret URI of the exported secret.') + secretUri: string +} + +type secretToSetType = { + @description('Required. The name of the secret to set.') + name: string + + @description('Required. The value of the secret to set.') + @secure() + value: string +} diff --git a/avm/res/sql/server/tests/e2e/kvSecrets/dependencies.bicep b/avm/res/sql/server/tests/e2e/kvSecrets/dependencies.bicep new file mode 100644 index 0000000000..2d7a1701e3 --- /dev/null +++ b/avm/res/sql/server/tests/e2e/kvSecrets/dependencies.bicep @@ -0,0 +1,21 @@ +@description('Optional. The location to deploy to.') +param location string = resourceGroup().location + +@description('Required. The name of the keyVault to create.') +param keyVaultName string + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyVaultName + location: location + properties: { + sku: { + family: 'A' + name: 'standard' + } + tenantId: tenant().tenantId + enableRbacAuthorization: true + } +} + +@description('The id of the Key Vault created.') +output keyVaultResourceId string = keyVault.id diff --git a/avm/res/sql/server/tests/e2e/kvSecrets/main.test.bicep b/avm/res/sql/server/tests/e2e/kvSecrets/main.test.bicep new file mode 100644 index 0000000000..31e94ee0a9 --- /dev/null +++ b/avm/res/sql/server/tests/e2e/kvSecrets/main.test.bicep @@ -0,0 +1,73 @@ +targetScope = 'subscription' + +metadata name = 'Deploying with a key vault reference to save secrets' +metadata description = 'This instance deploys the module saving all its secrets in a key vault.' + +// ========== // +// Parameters // +// ========== // + +@description('Optional. The name of the resource group to deploy for testing purposes.') +@maxLength(90) +param resourceGroupName string = 'dep-${namePrefix}-sql.servers-${serviceShort}-rg' + +@description('Optional. The location to deploy resources to.') +param resourceLocation string = deployment().location + +@description('Optional. A short identifier for the kind of deployment. Should be kept short to not run into resource-name length-constraints.') +param serviceShort string = 'sqlkvs' + +@description('Optional. The password to leverage for the login.') +@secure() +param password string = newGuid() + +@description('Optional. A token to inject into the name of each resource.') +param namePrefix string = '#_namePrefix_#' + +// ============ // +// Dependencies // +// ============ // + +// General resources +// ================= +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: resourceLocation +} + +module nestedDependencies 'dependencies.bicep' = { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-nestedDependencies' + params: { + keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' + location: resourceLocation + } +} + +// ============== // +// Test Execution // +// ============== // + +@batchSize(1) +module testDeployment '../../../main.bicep' = [ + for iteration in ['init', 'idem']: { + scope: resourceGroup + name: '${uniqueString(deployment().name, resourceLocation)}-test-${serviceShort}-${iteration}' + params: { + name: '${namePrefix}${serviceShort}001' + location: resourceLocation + administratorLogin: 'adminUserName' + administratorLoginPassword: password + secretsExportConfiguration: { + keyVaultResourceId: nestedDependencies.outputs.keyVaultResourceId + sqlAdminPasswordSecretName: 'adminLoginPasswordKey' + sqlAzureConnectionStringSercretName: 'sqlConnectionStringKey' + } + databases: [ + { + name: 'myDatabase' + } + ] + } + } +]