From 985bf1c86f10c0021d82f764b3408bd7709960ee Mon Sep 17 00:00:00 2001 From: Daniel Scholl Date: Tue, 16 Jan 2024 20:12:21 -0600 Subject: [PATCH] Added additional bicep for keyvault, storage, database. --- README.md | 22 +- bicep/main.bicep | 453 +++++++++++++++++- bicep/main.parameters.json | 9 + bicep/modules/keyvault_secrets.bicep | 38 ++ bicep/modules/script-kv-certificate/README.md | 147 ++++++ .../modules/script-kv-certificate/main.bicep | 170 +++++++ bicep/modules/script-kv-certificate/main.json | 345 +++++++++++++ bicep/modules/script-kv-certificate/script.sh | 71 +++ .../test/main.test.bicep | 104 ++++ .../test/parameters.json | 15 + .../script-kv-certificate/version.json | 7 + bicep/modules/virtual_machine.bicep | 175 +++++++ 12 files changed, 1537 insertions(+), 19 deletions(-) create mode 100644 bicep/modules/keyvault_secrets.bicep create mode 100644 bicep/modules/script-kv-certificate/README.md create mode 100644 bicep/modules/script-kv-certificate/main.bicep create mode 100644 bicep/modules/script-kv-certificate/main.json create mode 100644 bicep/modules/script-kv-certificate/script.sh create mode 100644 bicep/modules/script-kv-certificate/test/main.test.bicep create mode 100644 bicep/modules/script-kv-certificate/test/parameters.json create mode 100644 bicep/modules/script-kv-certificate/version.json create mode 100644 bicep/modules/virtual_machine.bicep diff --git a/README.md b/README.md index 604a9385..5169ecb8 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,6 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fosdu-developer%2Fmain%2Fazuredeploy.json) - - [Open Subsurface Data Universe](https://osduforum.org) (OSDU) is a standard data platform that brings together a diverse array of subsurface and well data. It enables the energy industry to access and analyze data across various sources efficiently. This project aims to provide a streamlined approach for developing and working directly with [OSDU](https://community.opengroup.org/osdu/platform) using the [Azure Cloud Platform](https://azure.microsoft.com/). @@ -52,6 +49,19 @@ Once registered, refresh the Microsoft.ContainerService resource provider: az provider register --namespace Microsoft.ContainerService ``` + +## Templated Deployment + +The solution can be deployed directly with the ARM template but parameter options can be difficult to navigate. However, this method works just fine when leveraging a fully default deployment. + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fosdu-developer%2Fmain%2Fazuredeploy.json) + + + +## Azure Developer CLI - Workflow + +The recommended way for working with the solution is to leverage the Azure Developer CLI so that options can be better set, the solution modified or parameters changed in order to customize a deployment that has more flexability. + ### Enabling Alpha Features for Azure Developer CLI **Resource Group Scoped Deployments** @@ -99,9 +109,7 @@ APP_NAME= # <-- azd env set AZURE_CLIENT_ID $(az ad app list --display-name $APP_NAME --query "[].appId" -otsv) ``` - - -## Workspace +### Commands The solution template is provisioned using the azure developer cli. @@ -111,7 +119,7 @@ The solution template is provisioned using the azure developer cli. | Stop | `azd down --purge --force` | -### Infrastructure +## Infrastructure The following diagram repesents the infrastructure that is deployed by this solution. diff --git a/bicep/main.bicep b/bicep/main.bicep index f156c35d..e3b10cec 100644 --- a/bicep/main.bicep +++ b/bicep/main.bicep @@ -12,14 +12,14 @@ param enableTelemetry bool = false @description('Feature Flag to Enable a Pod Subnet') param enablePodSubnet bool = false -// @description('Boolean indicating whether the VNet is new or existing') -// param virtualNetworkNewOrExisting string = 'new' +@description('Boolean indicating whether the VNet is new or existing') +param virtualNetworkNewOrExisting string = 'new' -// @description('Name of the Virtual Network (Optional: If exiting Network is selected)') -// param virtualNetworkName string = 'osdu-network' +@description('Name of the Virtual Network (Optional: If exiting Network is selected)') +param virtualNetworkName string = 'osdu-network' -// @description('Resource group of the VNet (Optional: If exiting Network is selected)') -// param virtualNetworkResourceGroup string = 'osdu-network' +@description('Resource group of the VNet (Optional: If exiting Network is selected)') +param virtualNetworkResourceGroup string = 'osdu-network' @description('VNet address prefix') param virtualNetworkAddressPrefix string = '10.1.0.0/16' @@ -68,6 +68,19 @@ param remoteVpnPrefix string = '' param remoteNetworkPrefix string = '192.168.1.0/24' +///////////////// +// Security Blade +///////////////// +@description('Feature Flag to Enable Private Link') +param enablePrivateLink bool = false + +@description('Optional. Customer Managed Encryption Key.') +param cmekConfiguration object = { + kvUrl: '' + keyName: '' + identityId: '' +} + ///////////////// // Settings Blade @@ -75,8 +88,6 @@ param remoteNetworkPrefix string = '192.168.1.0/24' @description('Specify the AD Application Client Id.') param applicationClientId string -output applicationClientId string = applicationClientId - ///////////////// // Bastion Blade @@ -85,6 +96,12 @@ output applicationClientId string = applicationClientId @description('Feature Flag to Enable Bastion') param enableBastion bool = false +@description('Specifies the name of the administrator account of the virtual machine.') +param vmAdminUsername string = enableBastion ? 'azureUser' : newGuid() + +@description('Specifies the SSH Key or password for the virtual machine. SSH key is recommended.') +@secure() +param vmAdminPasswordOrKey string = enableBastion ? '' : newGuid() //*****************************************************************// // Common Section // @@ -209,10 +226,10 @@ module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.2.1' = { |__| \__| |_______| |__| \__/ \__/ \______/ | _| `._____||__|\__\ */ -// var vnetId = { -// new: virtualNetworkNewOrExisting == 'new' ? network.outputs.resourceId : null -// existing: resourceId(virtualNetworkResourceGroup, 'Microsoft.Network/virtualNetworks', virtualNetworkName) -// } +var vnetId = { + new: virtualNetworkNewOrExisting == 'new' ? network.outputs.resourceId : null + existing: resourceId(virtualNetworkResourceGroup, 'Microsoft.Network/virtualNetworks', virtualNetworkName) +} var nsgRules = { ssh_outbound: { @@ -619,3 +636,415 @@ module vpnGateway 'br/public:avm/res/network/vpn-gateway:0.1.0' = if (enableVpnG ] } } + + + + +/* + __ ___ ___________ ____ ____ ____ ___ __ __ __ .___________. +| |/ / | ____\ \ / / \ \ / / / \ | | | | | | | | +| ' / | |__ \ \/ / \ \/ / / ^ \ | | | | | | `---| |----` +| < | __| \_ _/ \ / / /_\ \ | | | | | | | | +| . \ | |____ | | \ / / _____ \ | `--' | | `----. | | +|__|\__\ |_______| |__| \__/ /__/ \__\ \______/ |_______| |__| +*/ + +var vaultDNSZoneName = 'privatelink.vaultcore.azure.net' + +module keyvault 'br:osdubicep.azurecr.io/public/azure-keyvault:1.0.7' = { + name: '${commonLayerConfig.name}-azure-keyvault' + params: { + resourceName: commonLayerConfig.name + location: location + + // Assign Tags + tags: { + layer: commonLayerConfig.displayName + } + + // Hook up Diagnostics + diagnosticWorkspaceId: logAnalytics.outputs.resourceId + diagnosticLogsRetentionInDays: 0 + + // Configure Access + accessPolicies: [ + { + principalId: stampIdentity.outputs.principalId + permissions: { + secrets: [ + 'get' + 'list' + 'set' + ] + certificates: [ + 'create' + 'get' + 'list' + ] + } + } + ] + + // Configure Secrets + secretsObject: { secrets: [ + // Misc Secrets + { + secretName: commonLayerConfig.secrets.tenantId + secretValue: subscription().tenantId + } + { + secretName: commonLayerConfig.secrets.subscriptionId + secretValue: subscription().subscriptionId + } + // Azure AD Secrets + { + secretName: commonLayerConfig.secrets.clientId + secretValue: applicationClientId + } + { + secretName: commonLayerConfig.secrets.applicationPrincipalId + secretValue: applicationClientId + } + // Managed Identity + { + secretName: commonLayerConfig.secrets.stampIdentity + secretValue: stampIdentity.outputs.principalId + } + ]} + + // Assign RBAC + roleAssignments: [ + { + roleDefinitionIdOrName: 'Reader' + principals: [ + { + id: stampIdentity.outputs.principalId + resourceId: stampIdentity.outputs.resourceId + } + ] + principalType: 'ServicePrincipal' + } + ] + } +} + +module keyvaultSecrets './modules/keyvault_secrets.bicep' = { + name: '${commonLayerConfig.name}-log-analytics-secrets' + params: { + // Persist Secrets to Vault + keyVaultName: keyvault.outputs.name + workspaceName: logAnalytics.outputs.name + workspaceIdName: commonLayerConfig.secrets.logAnalyticsId + workspaceKeySecretName: commonLayerConfig.secrets.logAnalyticsKey + } +} + +module sshKey 'br:osdubicep.azurecr.io/public/script-sshkeypair:1.0.3' = if (enableBastion) { + name: '${commonLayerConfig.name}-azure-keyvault-sshkey' + params: { + kvName: keyvault.outputs.name + location: location + + useExistingManagedIdentity: true + managedIdentityName: stampIdentity.outputs.name + existingManagedIdentitySubId: subscription().subscriptionId + existingManagedIdentityResourceGroupName:resourceGroup().name + + sshKeyName: 'PrivateLinkSSHKey-' + + cleanupPreference: 'Always' + } +} + +module certificates './modules/script-kv-certificate/main.bicep' = { + name: '${commonLayerConfig.name}-azure-keyvault-cert' + params: { + kvName: keyvault.outputs.name + location: location + + useExistingManagedIdentity: true + managedIdentityName: stampIdentity.outputs.name + existingManagedIdentitySubId: subscription().subscriptionId + existingManagedIdentityResourceGroupName: resourceGroup().name + + certificateNames: [ + 'https-certificate' + ] + initialScriptDelay: '0' + validity: 24 + } +} + +resource vaultDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (enablePrivateLink) { + name: vaultDNSZoneName + location: 'global' + properties: {} +} + +module vaultEndpoint 'br:osdubicep.azurecr.io/public/private-endpoint:1.0.1' = if (enablePrivateLink) { + name: '${commonLayerConfig.name}-azure-keyvault-endpoint' + params: { + resourceName: keyvault.outputs.name + subnetResourceId: '${vnetId[virtualNetworkNewOrExisting]}/subnets/${aksSubnetName}' + + groupIds: [ 'vault'] + privateDnsZoneGroup: { + privateDNSResourceIds: [vaultDNSZone.id] + } + serviceResourceId: keyvault.outputs.id + } + dependsOn: [ + network + vaultDNSZone + ] +} + +resource existingVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = { + name: keyvault.outputs.name +} + + +/* _______.___________. ______ .______ ___ _______ _______ + / | | / __ \ | _ \ / \ / _____|| ____| + | (----`---| |----`| | | | | |_) | / ^ \ | | __ | |__ + \ \ | | | | | | | / / /_\ \ | | |_ | | __| +.----) | | | | `--' | | |\ \----./ _____ \ | |__| | | |____ +|_______/ |__| \______/ | _| `._____/__/ \__\ \______| |_______| +*/ + +var storageDNSZoneForwarder = 'blob.${environment().suffixes.storage}' +var storageDnsZoneName = 'privatelink.${storageDNSZoneForwarder}' + +module configStorage 'br:osdubicep.azurecr.io/public/storage-account:1.0.7' = { + name: '${commonLayerConfig.name}-azure-storage' + params: { + resourceName: commonLayerConfig.name + location: location + + // Assign Tags + tags: { + layer: commonLayerConfig.displayName + } + + // Hook up Diagnostics + diagnosticWorkspaceId: logAnalytics.outputs.resourceId + diagnosticLogsRetentionInDays: 0 + + // Configure Service + sku: commonLayerConfig.storage.sku + tables: commonLayerConfig.storage.tables + + // Assign RBAC + roleAssignments: [ + { + roleDefinitionIdOrName: 'Contributor' + principals: [ + { + id: stampIdentity.outputs.principalId + resourceId: stampIdentity.outputs.resourceId + } + ] + principalType: 'ServicePrincipal' + } + ] + + // Hookup Customer Managed Encryption Key + cmekConfiguration: cmekConfiguration + + // Persist Secrets to Vault + keyVaultName: keyvault.outputs.name + storageAccountSecretName: commonLayerConfig.secrets.storageAccountName + storageAccountKeySecretName: commonLayerConfig.secrets.storageAccountKey + } +} + +resource storageDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (enablePrivateLink) { + name: storageDnsZoneName + location: 'global' + properties: {} +} + +module storageEndpoint 'br:osdubicep.azurecr.io/public/private-endpoint:1.0.1' = if (enablePrivateLink) { + name: '${commonLayerConfig.name}-azure-storage-endpoint' + params: { + resourceName: configStorage.outputs.name + subnetResourceId: '${vnetId[virtualNetworkNewOrExisting]}/subnets/${aksSubnetName}' + serviceResourceId: configStorage.outputs.id + groupIds: [ 'blob'] + privateDnsZoneGroup: { + privateDNSResourceIds: [storageDNSZone.id] + } + } + dependsOn: [ + network + storageDNSZone + ] +} + + +/* + _______ .______ ___ .______ __ __ + / _____|| _ \ / \ | _ \ | | | | +| | __ | |_) | / ^ \ | |_) | | |__| | +| | |_ | | / / /_\ \ | ___/ | __ | +| |__| | | |\ \----./ _____ \ | | | | | | + \______| | _| `._____/__/ \__\ | _| |__| |__| +*/ + +var cosmosDnsZoneName = 'privatelink.documents.azure.com' + +module database 'br:osdubicep.azurecr.io/public/cosmos-db:1.0.17' = { + name: '${commonLayerConfig.name}-cosmos-db' + params: { + resourceName: commonLayerConfig.name + resourceLocation: location + + // Assign Tags + tags: { + layer: commonLayerConfig.displayName + } + + // Hook up Diagnostics + diagnosticWorkspaceId: logAnalytics.outputs.resourceId + diagnosticLogsRetentionInDays: 0 + + // Configure Service + capabilitiesToAdd: [ + 'EnableGremlin' + ] + gremlinDatabases: [ + { + name: commonLayerConfig.database.name + graphs: commonLayerConfig.database.graphs + } + ] + throughput: commonLayerConfig.database.throughput + backupPolicyType: commonLayerConfig.database.backup + + // Assign RBAC + roleAssignments: [ + { + roleDefinitionIdOrName: 'Contributor' + principals: [ + { + id: stampIdentity.outputs.principalId + resourceId: stampIdentity.outputs.resourceId + } + ] + principalType: 'ServicePrincipal' + } + ] + + // Hookup Customer Managed Encryption Key + systemAssignedIdentity: false + userAssignedIdentities: !empty(cmekConfiguration.identityId) ? { + '${stampIdentity.outputs.resourceId}': {} + '${cmekConfiguration.identityId}': {} + } : { + '${stampIdentity.outputs.resourceId}': {} + } + defaultIdentity: !empty(cmekConfiguration.identityId) ? cmekConfiguration.identityId : '' + kvKeyUri: !empty(cmekConfiguration.kvUrl) && !empty(cmekConfiguration.keyName) ? '${cmekConfiguration.kvUrl}/keys/${cmekConfiguration.keyName}' : '' + + // Persist Secrets to Vault + keyVaultName: keyvault.outputs.name + databaseEndpointSecretName: commonLayerConfig.secrets.cosmosEndpoint + databasePrimaryKeySecretName: commonLayerConfig.secrets.cosmosPrimaryKey + databaseConnectionStringSecretName: commonLayerConfig.secrets.cosmosConnectionString + } +} + +resource cosmosDNSZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (enablePrivateLink) { + name: cosmosDnsZoneName + location: 'global' + properties: {} +} +module graphEndpoint 'br:osdubicep.azurecr.io/public/private-endpoint:1.0.1' = if (enablePrivateLink) { + name: '${commonLayerConfig.name}-cosmos-db-endpoint' + params: { + resourceName: database.outputs.name + subnetResourceId: '${vnetId[virtualNetworkNewOrExisting]}/subnets/${aksSubnetName}' + serviceResourceId: database.outputs.id + groupIds: [ 'sql'] + privateDnsZoneGroup: { + privateDNSResourceIds: [cosmosDNSZone.id] + } + } + dependsOn: [ + network + cosmosDNSZone + ] +} + + + +//*****************************************************************// +// Manage Section // +//*****************************************************************// + +///////////////////////////////// +// Configuration +///////////////////////////////// +var manageLayerConfig = { + name: 'manage' + displayName: 'Manage Resources' + machine: { + vmSize: 'Standard_DS3_v2' + imagePublisher: 'Canonical' + imageOffer: 'UbuntuServer' + imageSku: '18.04-LTS' + authenticationType: 'password' + } +} + +/*.______ ___ _______.___________. __ ______ .__ __. +| _ \ / \ / | || | / __ \ | \ | | +| |_) | / ^ \ | (----`---| |----`| | | | | | | \| | +| _ < / /_\ \ \ \ | | | | | | | | | . ` | +| |_) | / _____ \ .----) | | | | | | `--' | | |\ | +|______/ /__/ \__\ |_______/ |__| |__| \______/ |__| \__| +*/ + +module bastionHost 'br/public:avm/res/network/bastion-host:0.1.0' = if (enableBastion) { + name: '${manageLayerConfig.name}-bastion' + params: { + name: 'bh-${replace(manageLayerConfig.name, '-', '')}${uniqueString(deployment().name, manageLayerConfig.name)}' + vNetId: network.outputs.resourceId + location: location + enableTelemetry: enableTelemetry + } +} + +/* +.___ ___. ___ ______ __ __ __ .__ __. _______ +| \/ | / \ / || | | | | | | \ | | | ____| +| \ / | / ^ \ | ,----'| |__| | | | | \| | | |__ +| |\/| | / /_\ \ | | | __ | | | | . ` | | __| +| | | | / _____ \ | `----.| | | | | | | |\ | | |____ +|__| |__| /__/ \__\ \______||__| |__| |__| |__| \__| |_______| + +*/ + +module virtualMachine './modules/virtual_machine.bicep' = if (enableBastion) { + name: 'virtualMachine' + params: { + vmName: '${manageLayerConfig.name}-vm' + vmSize: manageLayerConfig.machine.vmSize + + // Assign Tags + tags: { + layer: manageLayerConfig.displayName + } + + vmSubnetId: '${vnetId[virtualNetworkNewOrExisting]}/subnets/${vmSubnetName}' + vmAdminPasswordOrKey: empty(vmAdminPasswordOrKey) ? existingVault.getSecret('PrivateLinkSSHKey-public') : vmAdminPasswordOrKey + vmAdminUsername: vmAdminUsername + workspaceName: logAnalytics.outputs.name + authenticationType: empty(vmAdminPasswordOrKey) ? 'sshPublicKey' : 'password' + } + dependsOn: [ + logAnalytics + bastionHost + sshKey + ] +} diff --git a/bicep/main.parameters.json b/bicep/main.parameters.json index 335d8a9f..40b7c0bd 100644 --- a/bicep/main.parameters.json +++ b/bicep/main.parameters.json @@ -13,6 +13,15 @@ }, "enableBastion": { "value": "${ENABLE_BASTION}" + }, + "vpnSharedKey": { + "value": "${VPN_SHARED_KEY}" + }, + "remoteVpnPrefix": { + "value": "${REMOTE_VPN_PREFIX}" + }, + "remoteNetworkPrefix": { + "value": "${REMOTE_NETWORK_PREFIX}" } } } \ No newline at end of file diff --git a/bicep/modules/keyvault_secrets.bicep b/bicep/modules/keyvault_secrets.bicep new file mode 100644 index 00000000..f416576f --- /dev/null +++ b/bicep/modules/keyvault_secrets.bicep @@ -0,0 +1,38 @@ +@description('Conditional. The name of the parent key vault. Required if the template is used in a standalone deployment.') +param keyVaultName string + +@description('Conditional. The name of the Analytics Workspace. Required if the template is used in a standalone deployment.') +param workspaceName string + +@description('Required. The name of the secret.') +param workspaceIdName string + +@description('Required. The name of the secret.') +param workspaceKeySecretName string + +resource logAnaltyics 'Microsoft.OperationalInsights/workspaces@2021-06-01' existing = { + name: workspaceName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource keySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: workspaceKeySecretName + parent: keyVault + + properties: { + value: logAnaltyics.listKeys().primarySharedKey + } +} + +resource idSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: workspaceIdName + parent: keyVault + + properties: { + value: logAnaltyics.id + } +} + diff --git a/bicep/modules/script-kv-certificate/README.md b/bicep/modules/script-kv-certificate/README.md new file mode 100644 index 00000000..ee6f6268 --- /dev/null +++ b/bicep/modules/script-kv-certificate/README.md @@ -0,0 +1,147 @@ +# Key Vault Certificate + +This module creates a Key Vault Certificate and stores it in an Azure Key Vault + +## Details + +{{Add detailed information about the module}} + +## Parameters + +| Name | Type | Required | Description | +| :----------------------------------------- | :------------: | :------: | :------------------------------------------------------------------------------------------------------------ | +| `kvName` | `string` | Yes | The name of the Azure Key Vault | +| `location` | `string` | Yes | The location to deploy the resources to | +| `forceUpdateTag` | `string` | No | How the deployment script should be forced to execute | +| `rbacRolesNeededOnKV` | `string` | No | The RoleDefinitionId required for the DeploymentScript resource to interact with KeyVault | +| `useExistingManagedIdentity` | `bool` | No | Does the Managed Identity already exists, or should be created | +| `managedIdentityName` | `string` | No | Name of the Managed Identity resource | +| `existingManagedIdentitySubId` | `string` | No | For an existing Managed Identity, the Subscription Id it is located in | +| `existingManagedIdentityResourceGroupName` | `string` | No | For an existing Managed Identity, the Resource Group it is located in | +| `certificateNames` | `array` | Yes | The names of the certificate to create. Use when creating many certificates. | +| `certificateCommonNames` | `array` | No | The common names of the certificate to create. Use when creating many certificates. | +| `initialScriptDelay` | `string` | No | A delay before the script import operation starts. Primarily to allow Azure AAD Role Assignments to propagate | +| `cleanupPreference` | `string` | No | When the script resource is cleaned up | +| `issuerName` | `string` | No | Self, or user defined {IssuerName} for certificate signing | +| `issuerProvider` | `string` | No | Certificate Issuer Provider, DigiCert, GlobalSign, or internal options may be used. | +| `disabled` | `bool` | No | Create certificate in disabled state. Default: false | +| `accountId` | `string` | No | Account ID of Certificate Issuer Account | +| `issuerPassword` | `securestring` | No | Password of Certificate Issuer Account | +| `organizationId` | `string` | No | Organization ID of Certificate Issuer Account | +| `isCrossTenant` | `bool` | No | Override this parameter if using this in cross tenant scenarios | +| `reuseKey` | `bool` | No | The default policy might cause errors about CSR being used before, so set this to false if that happens | +| `validity` | `int` | No | Optional. Override default validityInMonths 12 value | +| `performRoleAssignment` | `bool` | No | Set to false to disable role assignments within this module. Default: true | + +## Outputs + +| Name | Type | Description | +| :-------------------------------- | :-----: | :------------------------------------------------- | +| `certificateNames` | `array` | Certificate names | +| `certificateSecretIds` | `array` | KeyVault secret ids to the created version | +| `certificateSecretIdUnversioneds` | `array` | KeyVault secret ids which uses the unversioned uri | +| `certificateThumbpints` | `array` | Certificate Thumbprints | +| `certificateThumbprintHexs` | `array` | Certificate Thumbprints (in hex) | + +## Examples + +### Single KeyVault Certificate + +Creates a single self-signed certificate in Azure KeyVault. + +```bicep +param location string = resourceGroup().location +param akvName string = 'yourAzureKeyVault' +param certificateName string = 'myapp' + +module kvCert 'br/public:deployment-scripts/create-kv-certificate:3.4' = { + name: 'akvCertSingle' + params: { + akvName: akvName + location: location + certificateName: certificateName + } +} +output SecretId string = akvCertSingle.outputs.certificateSecretId +output Thumbprint string = akvCertSingle.outputs.certificateThumbprintHex + +``` + +### Single KeyVault Certificate with fqdn common name + +Creates a single self-signed certificate in Azure KeyVault using a specific certificate common name. + +```bicep +param location string = resourceGroup().location +param akvName string = 'yourAzureKeyVault' +param certificateName string = 'myapp' +param certificateCommonName string = '${certificateName}.mydomain.local' + +module kvCert 'br/public:deployment-scripts/create-kv-certificate:3.4' = { + name: 'akvCertSingle' + params: { + akvName: akvName + location: location + certificateNames: [certificateName] + certificateCommonNames: [certificateCommonName] + } +} +output SecretId string = akvCertSingle.outputs.certificateSecretIds[0] +output Thumbprint string = akvCertSingle.outputs.certificateThumbprintHexs[0] + +``` + +### Multiple KeyVault Certificates + +Create multiple self-signed certificates in Azure KeyVault + +```bicep +param location string = resourceGroup().location +param akvName string = 'yourAzureKeyVault' +param certificateNames array = [ + 'myapp' + 'myotherapp' +] + +module kvCert 'br/public:deployment-scripts/create-kv-certificate:3.1.1' = { + name: 'akvCert-${certificateName}' + params: { + akvName: akvName + location: location + certificateNames: certificateNames + } +} + +@description('Array of info from each Certificate') +output createdCertificates array = [for (certificateName, i) in certificateNames: { + certificateName: kvCert.outputs.certificateNames[i] + certificateSecretId: kvCert.outputs.certificateSecretIds[i] + certificateThumbprint: kvCert.outputs.certificateThumbprintHexs[i] +}] +``` + +### Create Signed Certificate + +Using `DigiCert` or `GlobalSign` first requires account setup described [here](https://learn.microsoft.com/en-us/azure/key-vault/certificates/how-to-integrate-certificate-authority) + +```bicep +param accountId +@secure +param issuerPassword +param organizationId + +module signedCert 'br/public:deployment-scripts/create-kv-certificate:3.1.1' = { + name: 'akvCert-${certificateName}' + params: { + akvName: akvName + location: location + certificateName: [certificateName] + certificateCommonName: ['customdomain.com'] + issuerName: 'MyCert' + issuerProvider: 'DigiCert' + accountId: accountId + issuerPassword: issuerPassword + organizationId: organizationId + } +}] +``` \ No newline at end of file diff --git a/bicep/modules/script-kv-certificate/main.bicep b/bicep/modules/script-kv-certificate/main.bicep new file mode 100644 index 00000000..2cd2c3fd --- /dev/null +++ b/bicep/modules/script-kv-certificate/main.bicep @@ -0,0 +1,170 @@ +metadata name = 'Key Vault Certificate' +metadata description = 'This module creates a Key Vault Certificate and stores it in an Azure Key Vault' +metadata owner = 'azure-global-energy' + +@description('The name of the Azure Key Vault') +param kvName string + +@description('The location to deploy the resources to') +param location string + +@description('How the deployment script should be forced to execute') +param forceUpdateTag string = utcNow() + +@description('The RoleDefinitionId required for the DeploymentScript resource to interact with KeyVault') +param rbacRolesNeededOnKV string = 'a4417e6f-fecd-4de8-b567-7b0420556985' //KeyVault Certificate Officer + +@description('Does the Managed Identity already exists, or should be created') +param useExistingManagedIdentity bool = false + +@description('Name of the Managed Identity resource') +param managedIdentityName string = 'id-KeyVaultCertificateCreator-${location}' + +@description('For an existing Managed Identity, the Subscription Id it is located in') +param existingManagedIdentitySubId string = subscription().subscriptionId + +@description('For an existing Managed Identity, the Resource Group it is located in') +param existingManagedIdentityResourceGroupName string = resourceGroup().name + +@description('The names of the certificate to create. Use when creating many certificates.') +param certificateNames array + +@description('The common names of the certificate to create. Use when creating many certificates.') +param certificateCommonNames array = certificateNames + +@description('A delay before the script import operation starts. Primarily to allow Azure AAD Role Assignments to propagate') +param initialScriptDelay string = '0' + +@allowed([ + 'OnSuccess' + 'OnExpiration' + 'Always' +]) +@description('When the script resource is cleaned up') +param cleanupPreference string = 'OnSuccess' + +@description('Self, or user defined {IssuerName} for certificate signing') +param issuerName string = 'Self' + +@description('Certificate Issuer Provider, DigiCert, GlobalSign, or internal options may be used.') +param issuerProvider string = '' + +@description('Create certificate in disabled state. Default: false') +param disabled bool = false + +@description('Account ID of Certificate Issuer Account') +param accountId string = '' + +@description('Password of Certificate Issuer Account') +@secure() +param issuerPassword string = '' + +@description('Organization ID of Certificate Issuer Account') +param organizationId string = '' + +@description('Override this parameter if using this in cross tenant scenarios') +param isCrossTenant bool = false + +@description('The default policy might cause errors about CSR being used before, so set this to false if that happens') +param reuseKey bool = true + +@minValue(1) +@maxValue(1200) +@description('Optional. Override default validityInMonths 12 value') +param validity int = 12 + +@description('Set to false to disable role assignments within this module. Default: true') +param performRoleAssignment bool = true + +resource akv 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: kvName +} + +@description('A new managed identity that will be created in this Resource Group, this is the default option') +resource newDepScriptId 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = if (!useExistingManagedIdentity) { + name: managedIdentityName + location: location +} + +@description('An existing managed identity that could exist in another sub/rg') +resource existingDepScriptId 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = if (useExistingManagedIdentity) { + name: managedIdentityName + scope: resourceGroup(existingManagedIdentitySubId, existingManagedIdentityResourceGroupName) +} + +var delegatedManagedIdentityResourceId = useExistingManagedIdentity ? existingDepScriptId.id : newDepScriptId.id + +resource rbacKv 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (performRoleAssignment) { + name: guid(akv.id, rbacRolesNeededOnKV, managedIdentityName, string(useExistingManagedIdentity)) + scope: akv + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', rbacRolesNeededOnKV) + principalId: useExistingManagedIdentity ? existingDepScriptId.properties.principalId : newDepScriptId.properties.principalId + principalType: 'ServicePrincipal' + delegatedManagedIdentityResourceId: isCrossTenant ? delegatedManagedIdentityResourceId : null + } +} + +resource createImportCerts 'Microsoft.Resources/deploymentScripts@2020-10-01' = [for (certificateName, index) in certificateNames: { + name: 'AKV-Cert-${akv.name}-${replace(replace(certificateName, ':', ''), '/', '-')}' + location: location + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${useExistingManagedIdentity ? existingDepScriptId.id : newDepScriptId.id}': {} + } + } + kind: 'AzureCLI' + dependsOn: [ + rbacKv + ] + properties: { + forceUpdateTag: forceUpdateTag + azCliVersion: '2.35.0' + timeout: 'PT10M' + retentionInterval: 'P1D' + environmentVariables: [ + { name: 'akvName', value: kvName } + { name: 'certName', value: certificateName } + { name: 'certCommonName', value: certificateCommonNames[index] } + { name: 'initialDelay', value: initialScriptDelay } + { name: 'issuerName', value: issuerName } + { name: 'issuerProvider', value: issuerProvider } + { name: 'disabled', value: toLower(string(disabled)) } + { name: 'retryMax', value: '10' } + { name: 'retrySleep', value: '5s' } + { name: 'accountId', value: accountId } + { name: 'issuerPassword', secureValue: issuerPassword } + { name: 'organizationId', value: organizationId } + { name: 'reuseKey', value: toLower(string(reuseKey)) } + { name: 'validity', value: string(validity) } + ] + scriptContent: loadTextContent('script.sh') + cleanupPreference: cleanupPreference + } +}] + +@description('Certificate names') +output certificateNames array = [for (certificateName, index) in certificateNames: [ + createImportCerts[index].properties.outputs.name +]] + +@description('KeyVault secret ids to the created version') +output certificateSecretIds array = [for (certificateName, index) in certificateNames: [ + createImportCerts[index].properties.outputs.certSecretId.versioned +]] + +@description('KeyVault secret ids which uses the unversioned uri') +output certificateSecretIdUnversioneds array = [for (certificateName, index) in certificateNames: [ + createImportCerts[index].properties.outputs.certSecretId.unversioned +]] + +@description('Certificate Thumbprints') +output certificateThumbpints array = [for (certificateName, index) in certificateNames: [ + createImportCerts[index].properties.outputs.thumbprint +]] + +@description('Certificate Thumbprints (in hex)') +output certificateThumbprintHexs array = [for (certificateName, index) in certificateNames: [ + createImportCerts[index].properties.outputs.thumbprintHex +]] diff --git a/bicep/modules/script-kv-certificate/main.json b/bicep/modules/script-kv-certificate/main.json new file mode 100644 index 00000000..9e89dc20 --- /dev/null +++ b/bicep/modules/script-kv-certificate/main.json @@ -0,0 +1,345 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.23.1.45101", + "templateHash": "11944081156064634174" + }, + "name": "Key Vault Certificate", + "description": "This module creates a Key Vault Certificate and stores it in an Azure Key Vault", + "owner": "azure-global-energy" + }, + "parameters": { + "kvName": { + "type": "string", + "metadata": { + "description": "The name of the Azure Key Vault" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "The location to deploy the resources to" + } + }, + "forceUpdateTag": { + "type": "string", + "defaultValue": "[utcNow()]", + "metadata": { + "description": "How the deployment script should be forced to execute" + } + }, + "rbacRolesNeededOnKV": { + "type": "string", + "defaultValue": "a4417e6f-fecd-4de8-b567-7b0420556985", + "metadata": { + "description": "The RoleDefinitionId required for the DeploymentScript resource to interact with KeyVault" + } + }, + "useExistingManagedIdentity": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Does the Managed Identity already exists, or should be created" + } + }, + "managedIdentityName": { + "type": "string", + "defaultValue": "[format('id-KeyVaultCertificateCreator-{0}', parameters('location'))]", + "metadata": { + "description": "Name of the Managed Identity resource" + } + }, + "existingManagedIdentitySubId": { + "type": "string", + "defaultValue": "[subscription().subscriptionId]", + "metadata": { + "description": "For an existing Managed Identity, the Subscription Id it is located in" + } + }, + "existingManagedIdentityResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "For an existing Managed Identity, the Resource Group it is located in" + } + }, + "certificateNames": { + "type": "array", + "metadata": { + "description": "The names of the certificate to create. Use when creating many certificates." + } + }, + "certificateCommonNames": { + "type": "array", + "defaultValue": "[parameters('certificateNames')]", + "metadata": { + "description": "The common names of the certificate to create. Use when creating many certificates." + } + }, + "initialScriptDelay": { + "type": "string", + "defaultValue": "0", + "metadata": { + "description": "A delay before the script import operation starts. Primarily to allow Azure AAD Role Assignments to propagate" + } + }, + "cleanupPreference": { + "type": "string", + "defaultValue": "OnSuccess", + "allowedValues": [ + "OnSuccess", + "OnExpiration", + "Always" + ], + "metadata": { + "description": "When the script resource is cleaned up" + } + }, + "issuerName": { + "type": "string", + "defaultValue": "Self", + "metadata": { + "description": "Self, or user defined {IssuerName} for certificate signing" + } + }, + "issuerProvider": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Certificate Issuer Provider, DigiCert, GlobalSign, or internal options may be used." + } + }, + "disabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Create certificate in disabled state. Default: false" + } + }, + "accountId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Account ID of Certificate Issuer Account" + } + }, + "issuerPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Password of Certificate Issuer Account" + } + }, + "organizationId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Organization ID of Certificate Issuer Account" + } + }, + "isCrossTenant": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Override this parameter if using this in cross tenant scenarios" + } + }, + "reuseKey": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "The default policy might cause errors about CSR being used before, so set this to false if that happens" + } + }, + "validity": { + "type": "int", + "defaultValue": 12, + "minValue": 1, + "maxValue": 1200, + "metadata": { + "description": "Optional. Override default validityInMonths 12 value" + } + }, + "performRoleAssignment": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Set to false to disable role assignments within this module. Default: true" + } + } + }, + "variables": { + "$fxv#0": "#!/bin/bash\nset -e\ninitialDelay=\"${initialDelay:-5}\"\nretryMax=\"${retryMax:-5}\"\ncertName=\"${certName:-default-cert}\"\ncertCommonName=\"${certCommonName:-default}\"\nvalidity=\"${validity:-12}\"\nakvName=\"${akvName:-keyvault}\"\nissuerName=\"${issuerName:-}\"\nreuseKey=\"${reuseKey:-true}\"\nretrySleep=\"${retrySleep:-5}\"\n\necho \"Waiting on Identity RBAC replication (\\\"$initialDelay\\\")\"\nsleep \"$initialDelay\"\n\n#Retry loop to catch errors (usually RBAC delays)\nretryLoopCount=0\nuntil [ \"$retryLoopCount\" -ge \"$retryMax\" ]\ndo\n echo \"Creating AKV Cert $certName with CN $certCommonName (attempt $retryLoopCount)...\"\n\n if [ -z \"$issuerName\" ] || [ -z \"$issuerProvider\" ]; then\n policy=$(az keyvault certificate get-default-policy \\\n | sed -e s/\\\"validityInMonths\\\":\\ 12/\\\"validityInMonths\\\":\\ \"${validity}\"/g \\\n | sed -e s/CN=CLIGetDefaultPolicy/CN=\"${certCommonName}\"/g )\n else\n if [ \"$issuerProvider\" == \"DigiCert\" ] || [ \"$issuerProvider\" == \"GlobalCert\" ]; then\n az keyvault certificate issuer create \\\n --vault-name \"$akvName\" \\\n --issuer-name \"$issuerName\" \\\n --provider-name \"$issuerProvider\" \\\n --account-id \"$accountId\" \\\n --password \"$issuerPassword\" \\\n --organizatiion-id \"$organizationId\"\n else\n az keyvault certificate issuer create \\\n --vault-name \"$akvName\" \\\n --issuer-name \"$issuerName\" \\\n --provider-name \"$issuerProvider\"\n fi\n policy=$(az keyvault certificate get-default-policy \\\n | sed -e s/\\\"validityInMonths\\\":\\ 12/\\\"validityInMonths\\\":\\ \"${validity}\"/g \\\n | sed -e s/CN=CLIGetDefaultPolicy/CN=\"${certCommonName}\"/g \\\n | sed -e s/\\\"name\\\":\\ \\\"Self\\\"/\\\"name\\\":\\ \\\"\"${issuerName}\"\\\"/g \\\n | sed -e s/\\\"reuseKey\\\":\\ true/\\\"reuseKey\\\":\\ \"${reuseKey}\"/g )\n fi\n az keyvault certificate create \\\n --vault-name \"$akvName\" \\\n -n \"$certName\" \\\n -p \"$policy\" \\\n --disabled \"$disabled\" \\\n && break\n\n sleep \"$retrySleep\"\n retryLoopCount=$((retryLoopCount+1))\ndone\n\necho \"Getting Certificate $certName\";\nretryLoopCount=0\ncreatedCert=$(az keyvault certificate show -n \"$certName\" --vault-name \"$akvName\" -o json)\nwhile [ -z \"$(echo \"$createdCert\" | jq -r '.x509ThumbprintHex')\" ] && [ $retryLoopCount -lt \"$retryMax\" ]\ndo\n echo \"Waiting for cert creation (attempt $retryLoopCount)...\"\n sleep $retrySleep\n createdCert=$(az keyvault certificate show -n $certName --vault-name $akvName -o json)\n retryLoopCount=$((retryLoopCount+1))\ndone\n\nunversionedSecretId=$(echo $createdCert | jq -r \".sid\" | cut -d'/' -f-5) # remove the version from the url;\njsonOutputString=$(echo $createdCert | jq --arg usid $unversionedSecretId '{name: .name ,certSecretId: {versioned: .sid, unversioned: $usid }, thumbprint: .x509Thumbprint, thumbprintHex: .x509ThumbprintHex}')\necho $jsonOutputString > $AZ_SCRIPTS_OUTPUT_PATH", + "delegatedManagedIdentityResourceId": "[if(parameters('useExistingManagedIdentity'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('existingManagedIdentitySubId'), parameters('existingManagedIdentityResourceGroupName')), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')))]" + }, + "resources": [ + { + "condition": "[not(parameters('useExistingManagedIdentity'))]", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2018-11-30", + "name": "[parameters('managedIdentityName')]", + "location": "[parameters('location')]", + "metadata": { + "description": "A new managed identity that will be created in this Resource Group, this is the default option" + } + }, + { + "condition": "[parameters('performRoleAssignment')]", + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('kvName'))]", + "name": "[guid(resourceId('Microsoft.KeyVault/vaults', parameters('kvName')), parameters('rbacRolesNeededOnKV'), parameters('managedIdentityName'), string(parameters('useExistingManagedIdentity')))]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('rbacRolesNeededOnKV'))]", + "principalId": "[if(parameters('useExistingManagedIdentity'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('existingManagedIdentitySubId'), parameters('existingManagedIdentityResourceGroupName')), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')), '2018-11-30').principalId, reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')), '2018-11-30').principalId)]", + "principalType": "ServicePrincipal", + "delegatedManagedIdentityResourceId": "[if(parameters('isCrossTenant'), variables('delegatedManagedIdentityResourceId'), null())]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]" + ] + }, + { + "copy": { + "name": "createImportCerts", + "count": "[length(parameters('certificateNames'))]" + }, + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))]", + "location": "[parameters('location')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[format('{0}', if(parameters('useExistingManagedIdentity'), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('existingManagedIdentitySubId'), parameters('existingManagedIdentityResourceGroupName')), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))))]": {} + } + }, + "kind": "AzureCLI", + "properties": { + "forceUpdateTag": "[parameters('forceUpdateTag')]", + "azCliVersion": "2.35.0", + "timeout": "PT10M", + "retentionInterval": "P1D", + "environmentVariables": [ + { + "name": "akvName", + "value": "[parameters('kvName')]" + }, + { + "name": "certName", + "value": "[parameters('certificateNames')[copyIndex()]]" + }, + { + "name": "certCommonName", + "value": "[parameters('certificateCommonNames')[copyIndex()]]" + }, + { + "name": "initialDelay", + "value": "[parameters('initialScriptDelay')]" + }, + { + "name": "issuerName", + "value": "[parameters('issuerName')]" + }, + { + "name": "issuerProvider", + "value": "[parameters('issuerProvider')]" + }, + { + "name": "disabled", + "value": "[toLower(string(parameters('disabled')))]" + }, + { + "name": "retryMax", + "value": "10" + }, + { + "name": "retrySleep", + "value": "5s" + }, + { + "name": "accountId", + "value": "[parameters('accountId')]" + }, + { + "name": "issuerPassword", + "secureValue": "[parameters('issuerPassword')]" + }, + { + "name": "organizationId", + "value": "[parameters('organizationId')]" + }, + { + "name": "reuseKey", + "value": "[toLower(string(parameters('reuseKey')))]" + }, + { + "name": "validity", + "value": "[string(parameters('validity'))]" + } + ], + "scriptContent": "[variables('$fxv#0')]", + "cleanupPreference": "[parameters('cleanupPreference')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('managedIdentityName'))]", + "[extensionResourceId(resourceId('Microsoft.KeyVault/vaults', parameters('kvName')), 'Microsoft.Authorization/roleAssignments', guid(resourceId('Microsoft.KeyVault/vaults', parameters('kvName')), parameters('rbacRolesNeededOnKV'), parameters('managedIdentityName'), string(parameters('useExistingManagedIdentity'))))]" + ] + } + ], + "outputs": { + "certificateNames": { + "type": "array", + "metadata": { + "description": "Certificate names" + }, + "copy": { + "count": "[length(parameters('certificateNames'))]", + "input": "[createArray(reference(resourceId('Microsoft.Resources/deploymentScripts', format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))), '2020-10-01').outputs.name)]" + } + }, + "certificateSecretIds": { + "type": "array", + "metadata": { + "description": "KeyVault secret ids to the created version" + }, + "copy": { + "count": "[length(parameters('certificateNames'))]", + "input": "[createArray(reference(resourceId('Microsoft.Resources/deploymentScripts', format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))), '2020-10-01').outputs.certSecretId.versioned)]" + } + }, + "certificateSecretIdUnversioneds": { + "type": "array", + "metadata": { + "description": "KeyVault secret ids which uses the unversioned uri" + }, + "copy": { + "count": "[length(parameters('certificateNames'))]", + "input": "[createArray(reference(resourceId('Microsoft.Resources/deploymentScripts', format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))), '2020-10-01').outputs.certSecretId.unversioned)]" + } + }, + "certificateThumbpints": { + "type": "array", + "metadata": { + "description": "Certificate Thumbprints" + }, + "copy": { + "count": "[length(parameters('certificateNames'))]", + "input": "[createArray(reference(resourceId('Microsoft.Resources/deploymentScripts', format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))), '2020-10-01').outputs.thumbprint)]" + } + }, + "certificateThumbprintHexs": { + "type": "array", + "metadata": { + "description": "Certificate Thumbprints (in hex)" + }, + "copy": { + "count": "[length(parameters('certificateNames'))]", + "input": "[createArray(reference(resourceId('Microsoft.Resources/deploymentScripts', format('create-cert-{0}-{1}', parameters('kvName'), replace(replace(parameters('certificateNames')[copyIndex()], ':', ''), '/', '-'))), '2020-10-01').outputs.thumbprintHex)]" + } + } + } +} \ No newline at end of file diff --git a/bicep/modules/script-kv-certificate/script.sh b/bicep/modules/script-kv-certificate/script.sh new file mode 100644 index 00000000..93838ad5 --- /dev/null +++ b/bicep/modules/script-kv-certificate/script.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -e +initialDelay="${initialDelay:-5}" +retryMax="${retryMax:-5}" +certName="${certName:-default-cert}" +certCommonName="${certCommonName:-default}" +validity="${validity:-12}" +akvName="${akvName:-keyvault}" +issuerName="${issuerName:-}" +reuseKey="${reuseKey:-true}" +retrySleep="${retrySleep:-5}" + +echo "Waiting on Identity RBAC replication (\"$initialDelay\")" +sleep "$initialDelay" + +#Retry loop to catch errors (usually RBAC delays) +retryLoopCount=0 +until [ "$retryLoopCount" -ge "$retryMax" ] +do + echo "Creating AKV Cert $certName with CN $certCommonName (attempt $retryLoopCount)..." + + if [ -z "$issuerName" ] || [ -z "$issuerProvider" ]; then + policy=$(az keyvault certificate get-default-policy \ + | sed -e s/\"validityInMonths\":\ 12/\"validityInMonths\":\ "${validity}"/g \ + | sed -e s/CN=CLIGetDefaultPolicy/CN="${certCommonName}"/g ) + else + if [ "$issuerProvider" == "DigiCert" ] || [ "$issuerProvider" == "GlobalCert" ]; then + az keyvault certificate issuer create \ + --vault-name "$akvName" \ + --issuer-name "$issuerName" \ + --provider-name "$issuerProvider" \ + --account-id "$accountId" \ + --password "$issuerPassword" \ + --organizatiion-id "$organizationId" + else + az keyvault certificate issuer create \ + --vault-name "$akvName" \ + --issuer-name "$issuerName" \ + --provider-name "$issuerProvider" + fi + policy=$(az keyvault certificate get-default-policy \ + | sed -e s/\"validityInMonths\":\ 12/\"validityInMonths\":\ "${validity}"/g \ + | sed -e s/CN=CLIGetDefaultPolicy/CN="${certCommonName}"/g \ + | sed -e s/\"name\":\ \"Self\"/\"name\":\ \""${issuerName}"\"/g \ + | sed -e s/\"reuseKey\":\ true/\"reuseKey\":\ "${reuseKey}"/g ) + fi + az keyvault certificate create \ + --vault-name "$akvName" \ + -n "$certName" \ + -p "$policy" \ + --disabled "$disabled" \ + && break + + sleep "$retrySleep" + retryLoopCount=$((retryLoopCount+1)) +done + +echo "Getting Certificate $certName"; +retryLoopCount=0 +createdCert=$(az keyvault certificate show -n "$certName" --vault-name "$akvName" -o json) +while [ -z "$(echo "$createdCert" | jq -r '.x509ThumbprintHex')" ] && [ $retryLoopCount -lt "$retryMax" ] +do + echo "Waiting for cert creation (attempt $retryLoopCount)..." + sleep $retrySleep + createdCert=$(az keyvault certificate show -n $certName --vault-name $akvName -o json) + retryLoopCount=$((retryLoopCount+1)) +done + +unversionedSecretId=$(echo $createdCert | jq -r ".sid" | cut -d'/' -f-5) # remove the version from the url; +jsonOutputString=$(echo $createdCert | jq --arg usid $unversionedSecretId '{name: .name ,certSecretId: {versioned: .sid, unversioned: $usid }, thumbprint: .x509Thumbprint, thumbprintHex: .x509ThumbprintHex}') +echo $jsonOutputString > $AZ_SCRIPTS_OUTPUT_PATH \ No newline at end of file diff --git a/bicep/modules/script-kv-certificate/test/main.test.bicep b/bicep/modules/script-kv-certificate/test/main.test.bicep new file mode 100644 index 00000000..5bd3e973 --- /dev/null +++ b/bicep/modules/script-kv-certificate/test/main.test.bicep @@ -0,0 +1,104 @@ +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(10) +@description('Used to name all resources') +param resourceName string + +@description('Registry Location.') +param location string = resourceGroup().location + +//Prerequisites +module identity '../../user-managed-identity/main.bicep' = { + name: 'user-managed-identity' + params: { + resourceName: resourceName + location: location + } +} + +module kv '../../azure-keyvault/main.bicep' = { + name: 'azure_keyvault' + params: { + resourceName: resourceName + location: location + secretsObject: { secrets: [] } + + // Add Role Assignment + roleAssignments: [ + { + roleDefinitionIdOrName: 'Key Vault Administrator' + principals: [ + { + id: identity.outputs.principalId + } + ] + principalType: 'ServicePrincipal' + } + ] + } +} + +//Test 1. Just a single certificate +module kvCertSingle '../main.bicep' = { + name: 'kvCertSingle' + params: { + kvName: kv.outputs.name + location: location + + useExistingManagedIdentity: true + managedIdentityName: identity.outputs.name + existingManagedIdentitySubId: subscription().subscriptionId + existingManagedIdentityResourceGroupName:resourceGroup().name + + certificateNames: [ 'mysingleapp' ] + certificateCommonNames: [ 'mysingleapp.mydomain.local' ] + validity: 11 + disabled: true + } +} +output singleSecretId string = kvCertSingle.outputs.certificateSecretIds[0][0] +output singleThumbprint string = kvCertSingle.outputs.certificateThumbprintHexs[0][0] + +//Test 2. Array of certificates +var certificateNames = [ + 'myapp' + 'myotherapp' +] + +module kvCertMultiple '../main.bicep' = { + name: 'kvCertMultiple-${uniqueString(kv.name)}' + params: { + kvName: kv.name + location: location + + useExistingManagedIdentity: true + managedIdentityName: identity.outputs.name + existingManagedIdentitySubId: subscription().subscriptionId + existingManagedIdentityResourceGroupName:resourceGroup().name + + certificateNames: certificateNames + initialScriptDelay: '0' + validity: 24 + } +} + +// Test 3. Test a signed cert +// module akvCertSigned '../main.bicep' = { +// name: 'akvCertSigned' +// params: { +// akvName: akv.name +// location: location +// certificateName: 'mysignedcert' +// certificateCommonName: 'sample-cert.gaming.azure.com' +// issuerName: 'Signed' +// issuerProvider: 'OneCertV2-PublicCA' +// } +// } + +@description('Array of info from each Certificate') +output createdCertificates array = [for (certificateName, i) in certificateNames: { + certificateName: certificateName + certificateSecretId: kvCertMultiple.outputs.certificateSecretIds[i] + certificateThumbprint: kvCertMultiple.outputs.certificateThumbprintHexs[i] +}] diff --git a/bicep/modules/script-kv-certificate/test/parameters.json b/bicep/modules/script-kv-certificate/test/parameters.json new file mode 100644 index 00000000..4d4c4d1d --- /dev/null +++ b/bicep/modules/script-kv-certificate/test/parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "template": "../main.bicep" + }, + "parameters": { + "location": { + "value": "southcentralus" + }, + "resourceName": { + "value": "adme" + } + } +} \ No newline at end of file diff --git a/bicep/modules/script-kv-certificate/version.json b/bicep/modules/script-kv-certificate/version.json new file mode 100644 index 00000000..e215964e --- /dev/null +++ b/bicep/modules/script-kv-certificate/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://aka.ms/bicep-registry-module-version-file-schema#", + "version": "3.4", + "pathFilters": [ + "./main.json" + ] +} \ No newline at end of file diff --git a/bicep/modules/virtual_machine.bicep b/bicep/modules/virtual_machine.bicep new file mode 100644 index 00000000..19baadf9 --- /dev/null +++ b/bicep/modules/virtual_machine.bicep @@ -0,0 +1,175 @@ +// Parameters +@description('Specifies the name of the virtual machine.') +param vmName string = 'simpleVM' + +@description('Specifies the size of the virtual machine.') +param vmSize string = 'Standard_D2s_v3' + +@description('Specifies the resource id of the subnet hosting the virtual machine.') +param vmSubnetId string + +@description('Specifies the image publisher of the disk image used to create the virtual machine.') +param imagePublisher string = 'Canonical' + +@description('Specifies the offer of the platform image or marketplace image used to create the virtual machine.') +param imageOffer string = '0001-com-ubuntu-server-jammy' + +@description('Specifies the Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version.') +param imageSku string = '22_04-lts-gen2' + +@description('Specifies the type of authentication when accessing the Virtual Machine. SSH key is recommended.') +@allowed([ + 'sshPublicKey' + 'password' +]) +param authenticationType string = 'password' + +@description('Specifies the name of the administrator account of the virtual machine.') +param vmAdminUsername string + +@description('Specifies the SSH Key or password for the virtual machine. SSH key is recommended.') +@secure() +param vmAdminPasswordOrKey string + +@description('Specifies the storage account type for OS and data disk.') +@allowed([ + 'Premium_LRS' + 'StandardSSD_LRS' + 'Standard_LRS' + 'UltraSSD_LRS' +]) +param diskStorageAccountType string = 'Standard_LRS' + +@description('Specifies the number of data disks of the virtual machine.') +@minValue(0) +@maxValue(64) +param numDataDisks int = 0 + +@description('Specifies the size in GB of the OS disk of the VM.') +param osDiskSize int = 30 + +@description('Specifies the size in GB of the OS disk of the virtual machine.') +param dataDiskSize int = 50 + +@description('Specifies the caching requirements for the data disks.') +param dataDiskCaching string = 'ReadWrite' + +@description('Specifies the name of the Log Analytics workspace.') +param workspaceName string + +@description('Specifies the location.') +param location string = resourceGroup().location + +@description('Specifies the resource tags.') +param tags object + +// Variables +var vmNicName = '${vmName}Nic' +var linuxConfiguration = { + disablePasswordAuthentication: true + ssh: { + publicKeys: [ + { + path: '/home/${vmAdminUsername}/.ssh/authorized_keys' + keyData: vmAdminPasswordOrKey + } + ] + } + provisionVMAgent: true +} + +// Resources +resource virtualMachineNic 'Microsoft.Network/networkInterfaces@2021-08-01' = { + name: vmNicName + location: location + tags: tags + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Dynamic' + subnet: { + id: vmSubnetId + } + } + } + ] + } +} + + + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2021-11-01' = { + name: vmName + location: location + tags: tags + properties: { + hardwareProfile: { + vmSize: vmSize + } + osProfile: { + computerName: vmName + adminUsername: vmAdminUsername + adminPassword: vmAdminPasswordOrKey + linuxConfiguration: (authenticationType == 'password') ? null : linuxConfiguration + } + storageProfile: { + imageReference: { + publisher: imagePublisher + offer: imageOffer + sku: imageSku + version: 'latest' + } + osDisk: { + name: '${vmName}_OSDisk' + caching: 'ReadWrite' + createOption: 'FromImage' + diskSizeGB: osDiskSize + managedDisk: { + storageAccountType: diskStorageAccountType + } + } + dataDisks: [for j in range(0, numDataDisks): { + caching: dataDiskCaching + diskSizeGB: dataDiskSize + lun: j + name: '${vmName}-DataDisk${j}' + createOption: 'Empty' + managedDisk: { + storageAccountType: diskStorageAccountType + } + }] + } + networkProfile: { + networkInterfaces: [ + { + id: virtualMachineNic.id + } + ] + } + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = { + name: workspaceName +} + +resource omsAgentForLinux 'Microsoft.Compute/virtualMachines/extensions@2023-03-01' = { + parent: virtualMachine + name: 'LogAnalytics' + location: location + properties: { + publisher: 'Microsoft.EnterpriseCloud.Monitoring' + type: 'OmsAgentForLinux' + typeHandlerVersion: '1.17' + settings: { + workspaceId: logAnalyticsWorkspace.properties.customerId + stopOnMultipleConnections: false + } + protectedSettings: { + workspaceKey: logAnalyticsWorkspace.listKeys().primarySharedKey //listKeys(logAnalyticsWorkspace.id, logAnalyticsWorkspace.apiVersion).primarySharedKey + } + } +} +