From 06929f27fac3f478277414b49c1faa0c78b3bb11 Mon Sep 17 00:00:00 2001 From: David Watson Date: Thu, 27 Feb 2025 08:57:37 +1100 Subject: [PATCH 01/10] private endpoint for Cosmos --- infra/main.bicep | 24 ++++++-- infra/private_endpoints.bicep | 113 ++++++++++++++++++++++++++++++++++ infra/resources.bicep | 22 ++++++- 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 infra/private_endpoints.bicep diff --git a/infra/main.bicep b/infra/main.bicep index 9240a2c90..40792228e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,7 +1,13 @@ targetScope = 'subscription' // Activates/Deactivates Authentication using keys. If true it will enforce RBAC using managed identities -param disableLocalAuth bool = false +@allowed([true, false]) +@description('Enables/Disables Authentication using keys. If true it will enforce RBAC using managed identity and disable key auth on backend resouces') +param disableLocalAuth bool + +@allowed([false, true]) +@description('Enables/Disables Private Endpoints for backend Azure resources. If true, it will create a virtual network and subnets to host the private endpoints.') +param usePrivateEndpoints bool @minLength(1) @maxLength(64) @@ -22,6 +28,16 @@ param location string }) param openAILocation string +// DALL-E v3 only supported in limited regions for now +@description('Location for the OpenAI DALL-E 3 instance resource group') +@allowed(['swedencentral', 'eastus', 'australiaeast']) +@metadata({ + azd: { + type: 'location' + } +}) +param dalleLocation string + param openAISku string = 'S0' param openAIApiVersion string ='2024-08-01-preview' @@ -33,10 +49,7 @@ param embeddingDeploymentName string = 'embedding' param embeddingDeploymentCapacity int = 120 param embeddingModelName string = 'text-embedding-ada-002' -// DALL-E v3 only supported in limited regions for now -@description('Location for the OpenAI DALL-E 3 instance resource group') -@allowed(['swedencentral', 'eastus', 'australiaeast']) -param dalleLocation string + param dalleDeploymentCapacity int = 1 param dalleDeploymentName string = 'dall-e-3' @@ -92,6 +105,7 @@ module resources 'resources.bicep' = { storageServiceImageContainerName: storageServiceImageContainerName location: location disableLocalAuth:disableLocalAuth + usePrivateEndpoints: usePrivateEndpoints } } diff --git a/infra/private_endpoints.bicep b/infra/private_endpoints.bicep new file mode 100644 index 000000000..10c650a93 --- /dev/null +++ b/infra/private_endpoints.bicep @@ -0,0 +1,113 @@ +@minLength(1) +@description('Primary location for all resources') +param location string + +param name string +param resourceToken string + +param cosmosServiceId string +param openAiServiceId string +param dalleServiceId string +param formRecognizerServiceId string +param speechServiceId string +param storageAccountId string + +param tags object + +var subnetNamePrivateEndpoints = 'privateEndpoints' +var subnetNameAppServiceBackend = 'appServiceBackend' + +var virtualNetworkName = toLower('${name}-vnet-${resourceToken}') + +resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { + name: virtualNetworkName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + '192.168.0.0/16' + ] + } + } +} + +resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNamePrivateEndpoints + properties: { + addressPrefix: '192.168.0.0/24' + privateEndpointNetworkPolicies: 'Disabled' + } +} + +resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNameAppServiceBackend + properties: { + addressPrefix: '192.168.1.0/24' + delegations: [ + { + name: 'delegation' + properties: { + serviceName: 'Microsoft.Web/serverFarms' + } + } + ] + } +} + +resource privateEndpoint_cosmos 'Microsoft.Network/privateEndpoints@2021-08-01' = { + name: toLower('${name}-pe-cosmos-${resourceToken}') + location: location + properties: { + subnet: { + id: subnet_privateEndpoint.id + } + privateLinkServiceConnections: [ + { + name: 'cosmos-connection' + properties: { + privateLinkServiceId: cosmosServiceId + groupIds: [ + 'Sql' + ] + } + } + ] + } +} + +resource privateDns_cosmos 'Microsoft.Network/privateDnsZones@2020-06-01' = { + name: 'privatelink.documents.azure.com' + location: 'global' +} + +resource privateDns_ZoneLink_Cosmos 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { + parent: privateDns_cosmos + name: '${privateDns_cosmos.name}-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: virtualNetwork.id + } + } +} + +resource privateEndpoints_DNS_cosmos 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + name: 'privateEndpoints_DNS_cosmos' + parent: privateEndpoint_cosmos + properties: { + privateDnsZoneConfigs: [ + { + name: privateDns_cosmos.name + properties: { + privateDnsZoneId: privateDns_cosmos.id + } + } + ] + } +} + +output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/resources.bicep b/infra/resources.bicep index ff8053977..24077d02b 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -32,6 +32,7 @@ param storageServiceImageContainerName string param location string = resourceGroup().location param disableLocalAuth bool= false +param usePrivateEndpoints bool = false @secure() param nextAuthHash string = uniqueString(newGuid()) @@ -89,6 +90,22 @@ var llmDeployments = [ } ] +module privateEndpoints 'private_endpoints.bicep' = if (usePrivateEndpoints) { + name: 'private-endpoints' + params: { + location: location + name: name + resourceToken: resourceToken + tags: tags + cosmosServiceId: cosmosDbAccount.id + openAiServiceId: azureopenai.id + dalleServiceId: azureopenaidalle.id + formRecognizerServiceId: formRecognizer.id + speechServiceId: speechService.id + storageAccountId: storage.id + } +} + resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { name: appservice_name location: location @@ -213,13 +230,15 @@ var appSettingsWithLocalAuth = disableLocalAuth ? [] : [ } ] -resource webApp 'Microsoft.Web/sites@2020-06-01' = { +resource webApp 'Microsoft.Web/sites@2024-04-01' = { name: webapp_name location: location tags: union(tags, { 'azd-service-name': 'frontend' }) properties: { serverFarmId: appServicePlan.id httpsOnly: true + virtualNetworkSubnetId: usePrivateEndpoints ? privateEndpoints.outputs.appServiceSubnetId : null + vnetRouteAllEnabled: usePrivateEndpoints ? false : null siteConfig: { linuxFxVersion: 'node|18-lts' alwaysOn: true @@ -360,6 +379,7 @@ resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { properties: { databaseAccountOfferType: 'Standard' disableLocalAuth: disableLocalAuth + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' locations: [ { locationName: location From 9d0c7ad590ef99dec344e10c8c29848dafcd9b79 Mon Sep 17 00:00:00 2001 From: David Watson Date: Thu, 27 Feb 2025 14:37:12 +1100 Subject: [PATCH 02/10] additional pe services --- infra/private_endpoint_services.bicep | 65 +++++++++++++++ infra/private_endpoints.bicep | 113 -------------------------- infra/private_endpoints_core.bicep | 107 ++++++++++++++++++++++++ infra/resources.bicep | 21 +++-- 4 files changed, 184 insertions(+), 122 deletions(-) create mode 100644 infra/private_endpoint_services.bicep delete mode 100644 infra/private_endpoints.bicep create mode 100644 infra/private_endpoints_core.bicep diff --git a/infra/private_endpoint_services.bicep b/infra/private_endpoint_services.bicep new file mode 100644 index 000000000..095461355 --- /dev/null +++ b/infra/private_endpoint_services.bicep @@ -0,0 +1,65 @@ +param serviceId string + +// leave blank if not DNS zone required (e.g. if multiple services share a DNS zone) +param dnsZoneName string = '' +param virtualNetworkId string +param privateEndpointSubnetId string +param groupId string + +var idElements = split(serviceId, '/') +var idElementsLength = length(idElements) + +var serviceName = idElementsLength == 1 ? serviceId : idElements[idElementsLength-1] + +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { + name: toLower('${serviceName}-pe') + location: resourceGroup().location + properties: { + subnet: { + id: privateEndpointSubnetId + } + privateLinkServiceConnections: [ + { + name: toLower('${serviceName}-pe-connections') + properties: { + privateLinkServiceId: serviceId + groupIds: [ + groupId + ] + } + } + ] + } +} + +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(dnsZoneName)) { + name: dnsZoneName + location: 'global' +} + +resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(dnsZoneName)){ + parent: privateDnsZone + name: '${privateDnsZone.name}-link' + location: 'global' + properties: { + registrationEnabled: false + virtualNetwork: { + id: virtualNetworkId + } + } +} + +resource privateEndpointsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { + name: '${serviceName}-dns-zone-group' + parent: privateEndpoint + properties: { + privateDnsZoneConfigs: [ + { + name: privateDnsZone.name + properties: { + privateDnsZoneId: privateDnsZone.id + } + } + ] + } +} diff --git a/infra/private_endpoints.bicep b/infra/private_endpoints.bicep deleted file mode 100644 index 10c650a93..000000000 --- a/infra/private_endpoints.bicep +++ /dev/null @@ -1,113 +0,0 @@ -@minLength(1) -@description('Primary location for all resources') -param location string - -param name string -param resourceToken string - -param cosmosServiceId string -param openAiServiceId string -param dalleServiceId string -param formRecognizerServiceId string -param speechServiceId string -param storageAccountId string - -param tags object - -var subnetNamePrivateEndpoints = 'privateEndpoints' -var subnetNameAppServiceBackend = 'appServiceBackend' - -var virtualNetworkName = toLower('${name}-vnet-${resourceToken}') - -resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { - name: virtualNetworkName - location: location - tags: tags - properties: { - addressSpace: { - addressPrefixes: [ - '192.168.0.0/16' - ] - } - } -} - -resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { - parent: virtualNetwork - name: subnetNamePrivateEndpoints - properties: { - addressPrefix: '192.168.0.0/24' - privateEndpointNetworkPolicies: 'Disabled' - } -} - -resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { - parent: virtualNetwork - name: subnetNameAppServiceBackend - properties: { - addressPrefix: '192.168.1.0/24' - delegations: [ - { - name: 'delegation' - properties: { - serviceName: 'Microsoft.Web/serverFarms' - } - } - ] - } -} - -resource privateEndpoint_cosmos 'Microsoft.Network/privateEndpoints@2021-08-01' = { - name: toLower('${name}-pe-cosmos-${resourceToken}') - location: location - properties: { - subnet: { - id: subnet_privateEndpoint.id - } - privateLinkServiceConnections: [ - { - name: 'cosmos-connection' - properties: { - privateLinkServiceId: cosmosServiceId - groupIds: [ - 'Sql' - ] - } - } - ] - } -} - -resource privateDns_cosmos 'Microsoft.Network/privateDnsZones@2020-06-01' = { - name: 'privatelink.documents.azure.com' - location: 'global' -} - -resource privateDns_ZoneLink_Cosmos 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { - parent: privateDns_cosmos - name: '${privateDns_cosmos.name}-link' - location: 'global' - properties: { - registrationEnabled: false - virtualNetwork: { - id: virtualNetwork.id - } - } -} - -resource privateEndpoints_DNS_cosmos 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-05-01' = { - name: 'privateEndpoints_DNS_cosmos' - parent: privateEndpoint_cosmos - properties: { - privateDnsZoneConfigs: [ - { - name: privateDns_cosmos.name - properties: { - privateDnsZoneId: privateDns_cosmos.id - } - } - ] - } -} - -output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/private_endpoints_core.bicep b/infra/private_endpoints_core.bicep new file mode 100644 index 000000000..209a2d385 --- /dev/null +++ b/infra/private_endpoints_core.bicep @@ -0,0 +1,107 @@ +@minLength(1) +@description('Primary location for all resources') +param location string + +param name string +param resourceToken string + +param cosmos_id string +param openai_id string +param openai_dalle_id string +param form_recognizer_id string +param speech_service_id string +param search_service_id string +param storage_id string +param keyVault_id string + +param tags object + +var subnetNamePrivateEndpoints = 'privateEndpoints' +var subnetNameAppServiceBackend = 'appServiceBackend' + +var virtualNetworkName = toLower('${name}-vnet-${resourceToken}') + +var privateEndpointSpecs = [ + { + serviceId: cosmos_id + dnsZoneName: 'privatelink.documents.azure.com' + groupId: 'Sql' + } + { + serviceId: openai_id + dnsZoneName: 'privatelink.openai.azure.com' + groupId: 'account' + } + { + serviceId: openai_dalle_id + groupId: 'account' + } + { + serviceId: storage_id + dnsZoneName: 'privatelink.blob.core.windows.net' + groupId: 'blob' + } + { + serviceId: search_service_id + dnsZoneName: '.blob.core.windows.net' + groupId: 'account' + } + // speech service is called from the browser so no private endpoint + // { + // serviceId: speech_service_id + // dnsZoneName: 'privatelink.cognitiveservices.azure.com' + // groupId: 'account' + // } +] + +resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { + name: virtualNetworkName + location: location + tags: tags + properties: { + addressSpace: { + addressPrefixes: [ + '192.168.0.0/16' + ] + } + } +} + +resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNamePrivateEndpoints + properties: { + addressPrefix: '192.168.0.0/24' + privateEndpointNetworkPolicies: 'Disabled' + } +} + +resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = { + parent: virtualNetwork + name: subnetNameAppServiceBackend + properties: { + addressPrefix: '192.168.1.0/24' + delegations: [ + { + name: 'delegation' + properties: { + serviceName: 'Microsoft.Web/serverFarms' + } + } + ] + } +} + +module privateEndpoints 'private_endpoint_services.bicep' = [for (privateEndpointSpec,i) in privateEndpointSpecs: { + name: 'private-endpoint-${i}' + params: { + serviceId: privateEndpointSpec.serviceId + dnsZoneName: privateEndpointSpec.dnsZoneName + virtualNetworkId: virtualNetwork.id + privateEndpointSubnetId: subnet_privateEndpoint.id + groupId: privateEndpointSpec.groupId + } + } +] + +output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/resources.bicep b/infra/resources.bicep index 24077d02b..d3b06d99a 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -90,19 +90,20 @@ var llmDeployments = [ } ] -module privateEndpoints 'private_endpoints.bicep' = if (usePrivateEndpoints) { +module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints) { name: 'private-endpoints' params: { location: location name: name resourceToken: resourceToken tags: tags - cosmosServiceId: cosmosDbAccount.id - openAiServiceId: azureopenai.id - dalleServiceId: azureopenaidalle.id - formRecognizerServiceId: formRecognizer.id - speechServiceId: speechService.id - storageAccountId: storage.id + cosmos_id: cosmosDbAccount.id + openai_id: azureopenai.id + openai_dalle_id: azureopenaidalle.id + form_recognizer_id: formRecognizer.id + speech_service_id: speechService.id + storage_id: storage.id + keyVault_id: kv.id } } @@ -456,6 +457,7 @@ resource searchService 'Microsoft.Search/searchServices@2022-09-01' = { publicNetworkAccess: 'enabled' replicaCount: 1 disableLocalAuth: disableLocalAuth + } sku: { name: searchServiceSkuName @@ -469,8 +471,9 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'OpenAI' properties: { customSubDomainName: openai_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' disableLocalAuth: disableLocalAuth + } sku: { name: openAiSkuName @@ -529,7 +532,7 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'SpeechServices' properties: { customSubDomainName: speech_service_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' /* TODO: disableLocalAuth: disableLocalAuth*/ } sku: { From 12c4baba70e05b7100de8cdfecf136b8e0c05780 Mon Sep 17 00:00:00 2001 From: David Watson Date: Thu, 27 Feb 2025 14:37:40 +1100 Subject: [PATCH 03/10] add search service id --- infra/resources.bicep | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/resources.bicep b/infra/resources.bicep index d3b06d99a..200ce6c56 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -104,6 +104,7 @@ module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints speech_service_id: speechService.id storage_id: storage.id keyVault_id: kv.id + search_service_id: searchService.id } } From 12781304dcad8786c7b0814239383d47825689a0 Mon Sep 17 00:00:00 2001 From: David Watson Date: Thu, 27 Feb 2025 18:03:46 +1100 Subject: [PATCH 04/10] additional services --- infra/private_endpoints_core.bicep | 47 ++++++++++++++++--- ...bicep => private_endpoints_services.bicep} | 10 ++-- infra/resources.bicep | 11 +++-- 3 files changed, 53 insertions(+), 15 deletions(-) rename infra/{private_endpoint_services.bicep => private_endpoints_services.bicep} (84%) diff --git a/infra/private_endpoints_core.bicep b/infra/private_endpoints_core.bicep index 209a2d385..e36449342 100644 --- a/infra/private_endpoints_core.bicep +++ b/infra/private_endpoints_core.bicep @@ -32,10 +32,6 @@ var privateEndpointSpecs = [ dnsZoneName: 'privatelink.openai.azure.com' groupId: 'account' } - { - serviceId: openai_dalle_id - groupId: 'account' - } { serviceId: storage_id dnsZoneName: 'privatelink.blob.core.windows.net' @@ -43,7 +39,17 @@ var privateEndpointSpecs = [ } { serviceId: search_service_id - dnsZoneName: '.blob.core.windows.net' + dnsZoneName: 'privatelink.search.windows.net' + groupId: 'searchService' + } + { + serviceId: keyVault_id + dnsZoneName: 'privatelink.vaultcore.azure.net' + groupId: 'vault' + } + { + serviceId: form_recognizer_id + dnsZoneName: 'privatelink.cognitiveservices.azure.com' groupId: 'account' } // speech service is called from the browser so no private endpoint @@ -54,6 +60,15 @@ var privateEndpointSpecs = [ // } ] +// specified separately so that we can ensure the private DNS zones are created before these private endpoints +var privateEndpointSpecs_noDNSZone = [ + { + serviceId: openai_dalle_id + dnsZoneName: 'privatelink.openai.azure.com' + groupId: 'account' + } +] + resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { name: virtualNetworkName location: location @@ -92,11 +107,13 @@ resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@202 } } -module privateEndpoints 'private_endpoint_services.bicep' = [for (privateEndpointSpec,i) in privateEndpointSpecs: { +module privateEndpoints 'private_endpoints_services.bicep' = [ + for (privateEndpointSpec, i) in privateEndpointSpecs: { name: 'private-endpoint-${i}' params: { serviceId: privateEndpointSpec.serviceId dnsZoneName: privateEndpointSpec.dnsZoneName + createDnsZone: true virtualNetworkId: virtualNetwork.id privateEndpointSubnetId: subnet_privateEndpoint.id groupId: privateEndpointSpec.groupId @@ -104,4 +121,22 @@ module privateEndpoints 'private_endpoint_services.bicep' = [for (privateEndpoin } ] +// created after the previous private endpoints to ensure the private DNS zones are created first +module privateEndpoints_noDNSZone 'private_endpoints_services.bicep' = [ + for (privateEndpointSpec, i) in privateEndpointSpecs_noDNSZone: { + name: 'private-endpoint-noDns-${i}' + dependsOn: [ + privateEndpoints + ] + params: { + serviceId: privateEndpointSpec.serviceId + dnsZoneName: privateEndpointSpec.dnsZoneName + createDnsZone: false + virtualNetworkId: virtualNetwork.id + privateEndpointSubnetId: subnet_privateEndpoint.id + groupId: privateEndpointSpec.groupId + } + } +] + output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/private_endpoint_services.bicep b/infra/private_endpoints_services.bicep similarity index 84% rename from infra/private_endpoint_services.bicep rename to infra/private_endpoints_services.bicep index 095461355..f74cbe26b 100644 --- a/infra/private_endpoint_services.bicep +++ b/infra/private_endpoints_services.bicep @@ -1,7 +1,7 @@ param serviceId string -// leave blank if not DNS zone required (e.g. if multiple services share a DNS zone) -param dnsZoneName string = '' +param dnsZoneName string +param createDnsZone bool = true param virtualNetworkId string param privateEndpointSubnetId string param groupId string @@ -32,12 +32,12 @@ resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-08-01' = { } } -resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (!empty(dnsZoneName)) { +resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (createDnsZone) { name: dnsZoneName location: 'global' } -resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (!empty(dnsZoneName)){ +resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (createDnsZone){ parent: privateDnsZone name: '${privateDnsZone.name}-link' location: 'global' @@ -57,7 +57,7 @@ resource privateEndpointsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privat { name: privateDnsZone.name properties: { - privateDnsZoneId: privateDnsZone.id + privateDnsZoneId: resourceId('Microsoft.Network/privateDnsZones', privateDnsZone.name) } } ] diff --git a/infra/resources.bicep b/infra/resources.bicep index 200ce6c56..df4918df3 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -306,6 +306,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { enabledForDeployment: false enabledForDiskEncryption: true enabledForTemplateDeployment: false + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' } resource AZURE_OPENAI_API_KEY 'secrets' = if (!disableLocalAuth) { @@ -441,7 +442,7 @@ resource formRecognizer 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'FormRecognizer' properties: { customSubDomainName: form_recognizer_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' disableLocalAuth: disableLocalAuth } sku: { @@ -455,7 +456,7 @@ resource searchService 'Microsoft.Search/searchServices@2022-09-01' = { tags: tags properties: { partitionCount: 1 - publicNetworkAccess: 'enabled' + publicNetworkAccess: usePrivateEndpoints ? 'disabled' : 'enabled' replicaCount: 1 disableLocalAuth: disableLocalAuth @@ -502,7 +503,7 @@ resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'OpenAI' properties: { customSubDomainName: openai_dalle_name - publicNetworkAccess: 'Enabled' + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' disableLocalAuth: disableLocalAuth } sku: { @@ -533,7 +534,8 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { kind: 'SpeechServices' properties: { customSubDomainName: speech_service_name - publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + // called from the browser so public endpoint is required + publicNetworkAccess: 'Enabled' /* TODO: disableLocalAuth: disableLocalAuth*/ } sku: { @@ -550,6 +552,7 @@ resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { sku: storageServiceSku properties:{ allowSharedKeyAccess: !disableLocalAuth + publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' } resource blobServices 'blobServices' = { From f10baf1e9ab0d9c8f75c413731c5992d6bbf7344 Mon Sep 17 00:00:00 2001 From: oliverlabs <70239916+oliverlabs@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:40:17 +0000 Subject: [PATCH 05/10] remove unused speech service parameter and add network security group to subnets; add soft delete for key vaults and TLS encryption for storage accounts --- infra/private_endpoints_core.bicep | 16 +- infra/resources.bicep | 305 +++++++++++++++-------------- 2 files changed, 173 insertions(+), 148 deletions(-) diff --git a/infra/private_endpoints_core.bicep b/infra/private_endpoints_core.bicep index e36449342..30043878d 100644 --- a/infra/private_endpoints_core.bicep +++ b/infra/private_endpoints_core.bicep @@ -9,7 +9,7 @@ param cosmos_id string param openai_id string param openai_dalle_id string param form_recognizer_id string -param speech_service_id string +// param speech_service_id string param search_service_id string param storage_id string param keyVault_id string @@ -69,6 +69,12 @@ var privateEndpointSpecs_noDNSZone = [ } ] +resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2021-08-01' = { + name: toLower('${name}-nsg-${resourceToken}') + location: location + tags: tags +} + resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { name: virtualNetworkName location: location @@ -88,6 +94,9 @@ resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024- properties: { addressPrefix: '192.168.0.0/24' privateEndpointNetworkPolicies: 'Disabled' + networkSecurityGroup: { + id: networkSecurityGroup.id + } } } @@ -104,6 +113,9 @@ resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@202 } } ] + networkSecurityGroup: { + id: networkSecurityGroup.id + } } } @@ -137,6 +149,6 @@ module privateEndpoints_noDNSZone 'private_endpoints_services.bicep' = [ groupId: privateEndpointSpec.groupId } } -] +] output appServiceSubnetId string = subnet_appServiceBackend.id diff --git a/infra/resources.bicep b/infra/resources.bicep index df4918df3..8cd5aacd7 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -4,14 +4,14 @@ param resourceToken string param openai_api_version string param openAiLocation string -param openAiSkuName string -param chatGptDeploymentCapacity int +param openAiSkuName string +param chatGptDeploymentCapacity int param chatGptDeploymentName string -param chatGptModelName string +param chatGptModelName string param chatGptModelVersion string -param embeddingDeploymentName string +param embeddingDeploymentName string param embeddingDeploymentCapacity int -param embeddingModelName string +param embeddingModelName string param dalleLocation string param dalleDeploymentCapacity int @@ -31,7 +31,7 @@ param storageServiceImageContainerName string param location string = resourceGroup().location -param disableLocalAuth bool= false +param disableLocalAuth bool = false param usePrivateEndpoints bool = false @secure() @@ -52,13 +52,16 @@ var appservice_name = toLower('${name}-app-${resourceToken}') var clean_name = replace(replace(name, '-', ''), '_', '') var storage_prefix = take(clean_name, 8) var storage_name = toLower('${storage_prefix}sto${resourceToken}') -// keyvault name must be less than 24 chars - token is 13 -var kv_prefix = take(name, 7) -var keyVaultName = toLower('${kv_prefix}-kv-${resourceToken}') +// keyvault name must be less than 24 chars - token is 13, 'kv' is 2 +var kv_prefix = take(clean_name, 7) +var keyVaultName = toLower('${kv_prefix}kv${resourceToken}') var la_workspace_name = toLower('${name}-la-${resourceToken}') var diagnostic_setting_name = 'AppServiceConsoleLogs' -var keyVaultSecretsOfficerRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7') +var keyVaultSecretsOfficerRole = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7' +) var validStorageServiceImageContainerName = toLower(replace(storageServiceImageContainerName, '-', '')) @@ -101,7 +104,6 @@ module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints openai_id: azureopenai.id openai_dalle_id: azureopenaidalle.id form_recognizer_id: formRecognizer.id - speech_service_id: speechService.id storage_id: storage.id keyVault_id: kv.id search_service_id: searchService.id @@ -126,112 +128,114 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { } var appSettingsCommon = [ - { - name: 'USE_MANAGED_IDENTITIES' - value: disableLocalAuth - } - - { - name: 'AZURE_KEY_VAULT_NAME' - value: keyVaultName - } - { - name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' - value: 'true' - } - { - name: 'AZURE_OPENAI_API_INSTANCE_NAME' - value: openai_name - } - { - name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME' - value: chatGptDeploymentName - } - { - name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME' - value: embeddingDeploymentName - } - { - name: 'AZURE_OPENAI_API_VERSION' - value: openai_api_version - } - { - name: 'AZURE_OPENAI_DALLE_API_INSTANCE_NAME' - value: openai_dalle_name - } - { - name: 'AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME' - value: dalleDeploymentName - } - { - name: 'AZURE_OPENAI_DALLE_API_VERSION' - value: dalleApiVersion - } - { - name: 'NEXTAUTH_SECRET' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::NEXTAUTH_SECRET.name})' - } - { - name: 'NEXTAUTH_URL' - value: 'https://${webapp_name}.azurewebsites.net' - } - { - name: 'AZURE_COSMOSDB_URI' - value: cosmosDbAccount.properties.documentEndpoint - } - { - name: 'AZURE_SEARCH_NAME' - value: search_name - } - { - name: 'AZURE_SEARCH_INDEX_NAME' - value: searchServiceIndexName - } - { - name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT' - value: 'https://${form_recognizer_name}.cognitiveservices.azure.com/' - } - { - name: 'AZURE_SPEECH_REGION' - value: location - } - { - name: 'AZURE_SPEECH_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SPEECH_KEY.name})' - } - { - name: 'AZURE_STORAGE_ACCOUNT_NAME' - value: storage_name - } - ] + { + name: 'USE_MANAGED_IDENTITIES' + value: disableLocalAuth + } -var appSettingsWithLocalAuth = disableLocalAuth ? [] : [ { - name: 'AZURE_OPENAI_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' + name: 'AZURE_KEY_VAULT_NAME' + value: keyVaultName + } + { + name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' + value: 'true' + } + { + name: 'AZURE_OPENAI_API_INSTANCE_NAME' + value: openai_name } { - name: 'AZURE_OPENAI_DALLE_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_DALLE_API_KEY.name})' + name: 'AZURE_OPENAI_API_DEPLOYMENT_NAME' + value: chatGptDeploymentName } { - name: 'AZURE_COSMOSDB_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_COSMOSDB_KEY.name})' + name: 'AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME' + value: embeddingDeploymentName } { - name: 'AZURE_SEARCH_API_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SEARCH_API_KEY.name})' - } + name: 'AZURE_OPENAI_API_VERSION' + value: openai_api_version + } + { + name: 'AZURE_OPENAI_DALLE_API_INSTANCE_NAME' + value: openai_dalle_name + } + { + name: 'AZURE_OPENAI_DALLE_API_DEPLOYMENT_NAME' + value: dalleDeploymentName + } { - name: 'AZURE_DOCUMENT_INTELLIGENCE_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_DOCUMENT_INTELLIGENCE_KEY.name})' + name: 'AZURE_OPENAI_DALLE_API_VERSION' + value: dalleApiVersion } { - name: 'AZURE_STORAGE_ACCOUNT_KEY' - value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_STORAGE_ACCOUNT_KEY.name})' + name: 'NEXTAUTH_SECRET' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::NEXTAUTH_SECRET.name})' + } + { + name: 'NEXTAUTH_URL' + value: 'https://${webapp_name}.azurewebsites.net' + } + { + name: 'AZURE_COSMOSDB_URI' + value: cosmosDbAccount.properties.documentEndpoint + } + { + name: 'AZURE_SEARCH_NAME' + value: search_name + } + { + name: 'AZURE_SEARCH_INDEX_NAME' + value: searchServiceIndexName + } + { + name: 'AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT' + value: 'https://${form_recognizer_name}.cognitiveservices.azure.com/' + } + { + name: 'AZURE_SPEECH_REGION' + value: location + } + { + name: 'AZURE_SPEECH_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SPEECH_KEY.name})' + } + { + name: 'AZURE_STORAGE_ACCOUNT_NAME' + value: storage_name } ] +var appSettingsWithLocalAuth = disableLocalAuth + ? [] + : [ + { + name: 'AZURE_OPENAI_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_API_KEY.name})' + } + { + name: 'AZURE_OPENAI_DALLE_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_OPENAI_DALLE_API_KEY.name})' + } + { + name: 'AZURE_COSMOSDB_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_COSMOSDB_KEY.name})' + } + { + name: 'AZURE_SEARCH_API_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_SEARCH_API_KEY.name})' + } + { + name: 'AZURE_DOCUMENT_INTELLIGENCE_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_DOCUMENT_INTELLIGENCE_KEY.name})' + } + { + name: 'AZURE_STORAGE_ACCOUNT_KEY' + value: '@Microsoft.KeyVault(VaultName=${kv.name};SecretName=${kv::AZURE_STORAGE_ACCOUNT_KEY.name})' + } + ] + resource webApp 'Microsoft.Web/sites@2024-04-01' = { name: webapp_name location: location @@ -250,7 +254,7 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = { appSettings: concat(appSettingsCommon, appSettingsWithLocalAuth) } } - identity: { type: 'SystemAssigned'} + identity: { type: 'SystemAssigned' } resource configLogs 'config' = { name: 'logs' @@ -293,7 +297,7 @@ resource kvFunctionAppPermissions 'Microsoft.Authorization/roleAssignments@2020- } } -resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { +resource kv 'Microsoft.KeyVault/vaults@2024-12-01-preview' = { name: keyVaultName location: location properties: { @@ -306,6 +310,9 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { enabledForDeployment: false enabledForDiskEncryption: true enabledForTemplateDeployment: false + enableSoftDelete: true + softDeleteRetentionInDays: 90 + enablePurgeProtection: true publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' } @@ -317,7 +324,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_OPENAI_DALLE_API_KEY 'secrets' = if (!disableLocalAuth){ + resource AZURE_OPENAI_DALLE_API_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-OPENAI-DALLE-API-KEY' properties: { contentType: 'text/plain' @@ -333,7 +340,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_COSMOSDB_KEY 'secrets' = if (!disableLocalAuth){ + resource AZURE_COSMOSDB_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-COSMOSDB-KEY' properties: { contentType: 'text/plain' @@ -341,7 +348,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_DOCUMENT_INTELLIGENCE_KEY 'secrets' = if (!disableLocalAuth){ + resource AZURE_DOCUMENT_INTELLIGENCE_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-DOCUMENT-INTELLIGENCE-KEY' properties: { contentType: 'text/plain' @@ -357,7 +364,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_SEARCH_API_KEY 'secrets' = if (!disableLocalAuth){ + resource AZURE_SEARCH_API_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-SEARCH-API-KEY' properties: { contentType: 'text/plain' @@ -365,7 +372,7 @@ resource kv 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { } } - resource AZURE_STORAGE_ACCOUNT_KEY 'secrets' = if (!disableLocalAuth){ + resource AZURE_STORAGE_ACCOUNT_KEY 'secrets' = if (!disableLocalAuth) { name: 'AZURE-STORAGE-ACCOUNT-KEY' properties: { contentType: 'text/plain' @@ -459,7 +466,6 @@ resource searchService 'Microsoft.Search/searchServices@2022-09-01' = { publicNetworkAccess: usePrivateEndpoints ? 'disabled' : 'enabled' replicaCount: 1 disableLocalAuth: disableLocalAuth - } sku: { name: searchServiceSkuName @@ -475,7 +481,6 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { customSubDomainName: openai_name publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' disableLocalAuth: disableLocalAuth - } sku: { name: openAiSkuName @@ -483,18 +488,22 @@ resource azureopenai 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } @batchSize(1) -resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in llmDeployments: { - parent: azureopenai - name: deployment.name - properties: { - model: deployment.model - /*raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null*/ - } - sku: contains(deployment, 'sku') ? deployment.sku : { - name: 'Standard' - capacity: deployment.capacity +resource llmdeployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [ + for deployment in llmDeployments: { + parent: azureopenai + name: deployment.name + properties: { + model: deployment.model + /*raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null*/ + } + sku: contains(deployment, 'sku') + ? deployment.sku + : { + name: 'Standard' + capacity: deployment.capacity + } } -}] +] resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { name: openai_dalle_name @@ -525,8 +534,6 @@ resource azureopenaidalle 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } } - - resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { name: speech_service_name location: location @@ -544,15 +551,16 @@ resource speechService 'Microsoft.CognitiveServices/accounts@2023-05-01' = { } // TODO: define good default Sku and settings for storage account -resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { +resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = { name: storage_name location: location tags: tags kind: 'StorageV2' sku: storageServiceSku - properties:{ + properties: { allowSharedKeyAccess: !disableLocalAuth publicNetworkAccess: usePrivateEndpoints ? 'Disabled' : 'Enabled' + minimumTlsVersion: 'TLS1_2' } resource blobServices 'blobServices' = { @@ -566,17 +574,16 @@ resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { } } - //RBAC Roles for managed identity authentication var cosmosDbContributorRoleId = '5bd9cd88-fe45-4216-938b-f97437e15450' // Replace with actual role ID for Cosmos DB. -var cosmosDbOperatorRoleId= '230815da-be43-4aae-9cb4-875f7bd000aa' +var cosmosDbOperatorRoleId = '230815da-be43-4aae-9cb4-875f7bd000aa' var cognitiveServicesContributorRoleId = '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' // Replace with actual role ID for Cognitive Services. -var cognitiveServicesUserRoleId='a97b65f3-24c7-4388-baec-2e87135dc908' +var cognitiveServicesUserRoleId = 'a97b65f3-24c7-4388-baec-2e87135dc908' var storageBlobDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Replace with actual role ID for Blob Data Contributor. var searchServiceContributorRoleId = '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Replace with actual role ID for Azure Search. -var cognitiveServicesOpenAIContributorRoleId='a001fd3d-188f-4b5d-821b-7da978bf7442' -var searchIndexDataContributorRoleId='8ebe5a00-799e-43f5-93ac-243d3dce84a7' +var cognitiveServicesOpenAIContributorRoleId = 'a001fd3d-188f-4b5d-821b-7da978bf7442' +var searchIndexDataContributorRoleId = '8ebe5a00-799e-43f5-93ac-243d3dce84a7' var targetUserPrincipal = webApp.identity.principalId // These are only deployed if local authentication has been disabled in the parameters @@ -590,7 +597,6 @@ resource cosmosDbRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04 } } - resource cosmosDbRoleAssignmentOperator 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { name: guid(cosmosDbAccount.id, cosmosDbOperatorRoleId, 'role-assignment-cosmosDb') scope: cosmosDbAccount @@ -605,7 +611,10 @@ resource cognitiveServicesRoleAssignment 'Microsoft.Authorization/roleAssignment scope: resourceGroup() properties: { principalId: targetUserPrincipal - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesContributorRoleId) + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + cognitiveServicesContributorRoleId + ) } } @@ -614,27 +623,31 @@ resource cognitiveServicesOpenAIContributorRoleAssignment 'Microsoft.Authorizati scope: azureopenai properties: { principalId: targetUserPrincipal - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIContributorRoleId) + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + cognitiveServicesOpenAIContributorRoleId + ) } } -resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { +resource cognitiveServicesUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { name: guid(formRecognizer.id, cognitiveServicesUserRoleId, 'role-assignment-cognitiveServices') - scope: resourceGroup() + scope: resourceGroup() properties: { principalId: targetUserPrincipal roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleId) } } - - resource storageBlobDataContributorRole 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (disableLocalAuth) { name: guid(storage.id, storageBlobDataContributorRoleId, 'role-assignment-storage') scope: storage properties: { principalId: targetUserPrincipal - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributorRoleId) + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + storageBlobDataContributorRoleId + ) } } @@ -651,17 +664,18 @@ resource searchServiceIndexDataContributorRoleAssignment 'Microsoft.Authorizatio scope: searchService properties: { principalId: targetUserPrincipal - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', searchIndexDataContributorRoleId) + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + searchIndexDataContributorRoleId + ) } } //Special case for cosmosdb - @description('Name of the role definition.') param roleDefinitionName string = 'Azure Cosmos DB for NoSQL Data Plane Owner' - -resource definition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-05-15'= if (disableLocalAuth) { +resource definition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-05-15' = if (disableLocalAuth) { name: guid(cosmosDbAccount.id, roleDefinitionName) parent: cosmosDbAccount properties: { @@ -682,7 +696,7 @@ resource definition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@20 } } -resource assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15'= if (disableLocalAuth) { +resource assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = if (disableLocalAuth) { name: guid(definition.id, webApp.name, cosmosDbAccount.id) parent: cosmosDbAccount properties: { @@ -690,7 +704,6 @@ resource assignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@20 roleDefinitionId: definition.id scope: cosmosDbAccount.id } - } output url string = 'https://${webApp.properties.defaultHostName}' From 01384d8337c6e16ff5caa6c7cd98a412fc319b46 Mon Sep 17 00:00:00 2001 From: David Watson Date: Mon, 3 Mar 2025 21:10:29 +1100 Subject: [PATCH 06/10] Documentation updates, paramaterize the vnet and subnet ranges --- docs/4-deploy-to-azure.md | 16 +++++++++++ docs/images/10-private-endpoints.md | 40 ++++++++++++++++++++++++++++ docs/images/private-endpoints.png | Bin 0 -> 104919 bytes infra/main.bicep | 40 +++++++++++++++++++++++----- infra/private_endpoints_core.bicep | 10 ++++--- infra/resources.bicep | 7 +++++ 6 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 docs/images/10-private-endpoints.md create mode 100644 docs/images/private-endpoints.png diff --git a/docs/4-deploy-to-azure.md b/docs/4-deploy-to-azure.md index 6a81fd305..f6e0de719 100644 --- a/docs/4-deploy-to-azure.md +++ b/docs/4-deploy-to-azure.md @@ -19,6 +19,22 @@ To deploy the application to Azure using the Azure Developer CLI, follow the ste 2. Run `azd auth login` to authenticate with Azure 3. Run `azd up` to provision and deploy the application +In both cases you will be prompted for some configuration values. These are described below: + +| Prompt - azd init | Description | +|--------|-------------| +| Enter a new environment name                                            | The name of the azd environment. The application will be deployed into a resource group called `rg-`, and this value is also used in the name of all the created Azure resources.| + +| Prompt - azd up | Description | +|--------|-------------| +| Select an Azure Subscription to use| Select the Azure subscription you want to deploy the application to | +|Select an Azure location to use| Select the Azure region you want to deploy the Azure services to. This location is used by all services except OpenAI deployments - these are set separately (below) | +| Enter a value for the 'dalleLocation' infrastructure parameter| Select the Azure region you want to deploy the DALL-E model to. The number of regions that support DALL-E is currently limited | +| Enter a value for the 'disableLocalAuth' infrastructure parameter | Set to `true` to use Managed Identities for authentication, or `false` to use keys. See [Managed Identities](9-managed-identities.md) for more information. if you are unsure we recommend you select `true` | +| Enter a value for the 'openAILocation' infrastructure parameter: | Select the Azure region you want to deploy the OpenAI service to. | +|Enter a value for the 'usePrivateEndpoints' infrastructure parameter: | Set to `false` to deploy the application without Private Endpoints, or `true` to use Private Endpoints. See [Private Endpoints](10-private-endpoints.md) for more information. If you are unsure we recommend you select `false` | + + ## Option 2: GitHub Actions The following steps describes how the application can be deployed to Azure App service using GitHub Actions. diff --git a/docs/images/10-private-endpoints.md b/docs/images/10-private-endpoints.md new file mode 100644 index 000000000..06fd8737a --- /dev/null +++ b/docs/images/10-private-endpoints.md @@ -0,0 +1,40 @@ +# Securing Azure Chat Resources with Private Endpoints + +## Overview + +You can enhance the security of the Azure Chat application by using Private Endpoints. Private Endpoints provide a secure and private connection to Azure services, ensuring that traffic between the Web App that hosts the application and the supporting Azure services remains within the Microsoft network. Implementing Private Endpoints also allows for the removal of any public network access to the these services. + +The included bicep template can optionally be configured to make a number of key changes to the deployed Azure resources to enable Private Endpoints: +1. Deploy a Virtual Network and 2 subnets - one for the Web App backend, and one for the Private Endpoints. +1. Deploy Private Endpoints for the following services: + - OpenAI Service + - Cosmos DB + - Storage Account + - AI Search Service + - AI Document Intelligence + - Key Vault +1. Configure the Web App to use the Virtual Network for outgoing requests +1. Remove public access to all of the above services - only clients and applications within the Virtual Network will be able to access these services. + +![Private Endpoints image](/docs/images/private-endpoints.png) + +Using Private Endpoints for these services is a recommended best practice for production deployments of Azure Chat, and it can also be useful in Azure environments where policies are in place to disable public access to some services. There are some additional considerations to be aware of when using Private Endpoints however: +- **Local Developemnt**: If you deploy the Azure resources with Private Endpoints it will be more difficult to use these services if you are running the application locally - you will need to use a development environment that is connected to the Virtual Network. +- **Resource Access in the Portal** - If you deploy the Azure resources with Private Endpoints you will need to use a development environment that is connected to the Virtual Network to access the data plane for any of these services (e.g. the Cosmos DB Azure Portal Data Explorer). + +## How to enable Private Endpoints + +The addition of Private Endpoints and it's supportted configuration is controlled by the `usePrivateEndpoints` parameter in the bicep template. To enable Private Endpoints, set this parameter to `true`. If you are using the Azure Developer CLI to deploy the application see the [Deploy to Azure](4-deploy-to-azure.md) page for more details on how to do this. + +## Additional Configuration + +By default the Virtual Network that is deployed when you set `usePrivateEndpoints` to `true` has the following properies: +- **Address Space**: `192.168.0.0/16` +- **Subnet for Private Endpoints**: `privateEndpoints` - `192.168.0.0/24` +- **Subnet for Web App**: `appServiceBackend` - `192.168.1.0/24` + +The address spaces for each of the subnets can be changed by setting the `privateEndpointVNetPrefix`, `privateEndpointSubnetAddressPrefix` and `appServiceBackendSubnetAddressPrefix` parameters in the bicep template. + +If you want to deploy these resources into an existing Virtual Network you will need to modify the `private_endpoints_core.bicep` template - a parameterised version of this is not currently available. If you do this note that the deployment includes Private DNS Zones for each of the services that are deployed with Private Endpoints - if your Virtual Network uses a custom DNS server you will need to ensure that the DNS server can resolve the Private DNS Zones. + + diff --git a/docs/images/private-endpoints.png b/docs/images/private-endpoints.png new file mode 100644 index 0000000000000000000000000000000000000000..58d5e308eaf86a4bd23cf78439a88a9b00999e07 GIT binary patch literal 104919 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 z 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 literal 0 HcmV?d00001 diff --git a/infra/main.bicep b/infra/main.bicep index 40792228e..e9a84bec7 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -20,7 +20,30 @@ param location string // azure open ai -- regions currently support gpt-4o global-standard @description('Location for the OpenAI resource group') -@allowed(['australiaeast', 'brazilsouth', 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'germanywestcentral', 'japaneast', 'koreacentral', 'northcentralus', 'norwayeast', 'polandcentral', 'spaincentral', 'southafricanorth', 'southcentralus', 'southindia', 'swedencentral', 'switzerlandnorth', 'uksouth', 'westeurope', 'westus', 'westus3']) +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadaeast' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'norwayeast' + 'polandcentral' + 'spaincentral' + 'southafricanorth' + 'southcentralus' + 'southindia' + 'swedencentral' + 'switzerlandnorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus3' +]) @metadata({ azd: { type: 'location' @@ -39,7 +62,7 @@ param openAILocation string param dalleLocation string param openAISku string = 'S0' -param openAIApiVersion string ='2024-08-01-preview' +param openAIApiVersion string = '2024-08-01-preview' param chatGptDeploymentCapacity int = 30 param chatGptDeploymentName string = 'gpt-4o' @@ -49,8 +72,6 @@ param embeddingDeploymentName string = 'embedding' param embeddingDeploymentCapacity int = 120 param embeddingModelName string = 'text-embedding-ada-002' - - param dalleDeploymentCapacity int = 1 param dalleDeploymentName string = 'dall-e-3' param dalleModelName string = 'dall-e-3' @@ -61,11 +82,15 @@ param searchServiceIndexName string = 'azure-chat' param searchServiceSkuName string = 'standard' // TODO: define good default Sku and settings for storage account -param storageServiceSku object = { name: 'Standard_LRS' } +param storageServiceSku object = { name: 'Standard_LRS' } param storageServiceImageContainerName string = 'images' param resourceGroupName string = '' +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + var resourceToken = toLower(uniqueString(subscription().id, name, location)) var tags = { 'azd-env-name': name } @@ -104,8 +129,11 @@ module resources 'resources.bicep' = { storageServiceSku: storageServiceSku storageServiceImageContainerName: storageServiceImageContainerName location: location - disableLocalAuth:disableLocalAuth + disableLocalAuth: disableLocalAuth usePrivateEndpoints: usePrivateEndpoints + privateEndpointVNetPrefix: privateEndpointVNetPrefix + privateEndpointSubnetAddressPrefix: privateEndpointSubnetAddressPrefix + appServiceBackendSubnetAddressPrefix: appServiceBackendSubnetAddressPrefix } } diff --git a/infra/private_endpoints_core.bicep b/infra/private_endpoints_core.bicep index e36449342..ddb0e83bd 100644 --- a/infra/private_endpoints_core.bicep +++ b/infra/private_endpoints_core.bicep @@ -16,6 +16,10 @@ param keyVault_id string param tags object +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + var subnetNamePrivateEndpoints = 'privateEndpoints' var subnetNameAppServiceBackend = 'appServiceBackend' @@ -76,7 +80,7 @@ resource virtualNetwork 'Microsoft.Network/VirtualNetworks@2021-08-01' = { properties: { addressSpace: { addressPrefixes: [ - '192.168.0.0/16' + privateEndpointVNetPrefix ] } } @@ -86,7 +90,7 @@ resource subnet_privateEndpoint 'Microsoft.Network/virtualNetworks/subnets@2024- parent: virtualNetwork name: subnetNamePrivateEndpoints properties: { - addressPrefix: '192.168.0.0/24' + addressPrefix: privateEndpointSubnetAddressPrefix privateEndpointNetworkPolicies: 'Disabled' } } @@ -95,7 +99,7 @@ resource subnet_appServiceBackend 'Microsoft.Network/virtualNetworks/subnets@202 parent: virtualNetwork name: subnetNameAppServiceBackend properties: { - addressPrefix: '192.168.1.0/24' + addressPrefix: appServiceBackendSubnetAddressPrefix delegations: [ { name: 'delegation' diff --git a/infra/resources.bicep b/infra/resources.bicep index df4918df3..c4f8a8613 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -39,6 +39,10 @@ param nextAuthHash string = uniqueString(newGuid()) param tags object = {} +param privateEndpointVNetPrefix string = '192.168.0.0/16' +param privateEndpointSubnetAddressPrefix string = '192.168.0.0/24' +param appServiceBackendSubnetAddressPrefix string = '192.168.1.0/24' + var openai_name = toLower('${name}-aillm-${resourceToken}') var openai_dalle_name = toLower('${name}-aidalle-${resourceToken}') @@ -105,6 +109,9 @@ module privateEndpoints 'private_endpoints_core.bicep' = if (usePrivateEndpoints storage_id: storage.id keyVault_id: kv.id search_service_id: searchService.id + privateEndpointVNetPrefix: privateEndpointVNetPrefix + privateEndpointSubnetAddressPrefix: privateEndpointSubnetAddressPrefix + appServiceBackendSubnetAddressPrefix: appServiceBackendSubnetAddressPrefix } } From 0a204684ab4b5424432b810ca1ac26c594086c9d Mon Sep 17 00:00:00 2001 From: David Watson Date: Mon, 3 Mar 2025 22:55:59 +1100 Subject: [PATCH 07/10] move pe doc to right location --- docs/{images => }/10-private-endpoints.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{images => }/10-private-endpoints.md (100%) diff --git a/docs/images/10-private-endpoints.md b/docs/10-private-endpoints.md similarity index 100% rename from docs/images/10-private-endpoints.md rename to docs/10-private-endpoints.md From e51c98ec78394e2ba9d122f12be29703b9bc2572 Mon Sep 17 00:00:00 2001 From: Oliver Gulich <70239916+oliverlabs@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:58:40 +0000 Subject: [PATCH 08/10] Disable Local Auth cosmos db --- infra/resources.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/resources.bicep b/infra/resources.bicep index 8cd5aacd7..02c4d6456 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -31,7 +31,7 @@ param storageServiceImageContainerName string param location string = resourceGroup().location -param disableLocalAuth bool = false +param disableLocalAuth bool = true param usePrivateEndpoints bool = false @secure() From ac1d33c64bc0eb6c7f61ff8830652c3fc99f5daf Mon Sep 17 00:00:00 2001 From: Oliver Gulich <70239916+oliverlabs@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:07:42 +0000 Subject: [PATCH 09/10] Update resources.bicep use private endpoints --- infra/resources.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/resources.bicep b/infra/resources.bicep index 02c4d6456..df151b169 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -32,7 +32,7 @@ param storageServiceImageContainerName string param location string = resourceGroup().location param disableLocalAuth bool = true -param usePrivateEndpoints bool = false +param usePrivateEndpoints bool = true @secure() param nextAuthHash string = uniqueString(newGuid()) From 1cd8e2f6b50fc0473904633be9ae45329fa42e49 Mon Sep 17 00:00:00 2001 From: David Watson Date: Thu, 6 Mar 2025 16:03:07 +1100 Subject: [PATCH 10/10] Fix small issue in bicep templates for PEs, update app reg script to austomatically pull the app name from azd env --- infra/private_endpoints_services.bicep | 4 ++-- infra/resources.bicep | 2 +- scripts/appreg_setup.ps1 | 4 ++++ scripts/appreg_setup.sh | 22 +++++++++++++++++++--- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/infra/private_endpoints_services.bicep b/infra/private_endpoints_services.bicep index f74cbe26b..6e2b734c2 100644 --- a/infra/private_endpoints_services.bicep +++ b/infra/private_endpoints_services.bicep @@ -55,9 +55,9 @@ resource privateEndpointsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privat properties: { privateDnsZoneConfigs: [ { - name: privateDnsZone.name + name: dnsZoneName properties: { - privateDnsZoneId: resourceId('Microsoft.Network/privateDnsZones', privateDnsZone.name) + privateDnsZoneId: resourceId('Microsoft.Network/privateDnsZones', dnsZoneName) } } ] diff --git a/infra/resources.bicep b/infra/resources.bicep index 7ae4f96de..4b796a526 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -253,7 +253,7 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = { virtualNetworkSubnetId: usePrivateEndpoints ? privateEndpoints.outputs.appServiceSubnetId : null vnetRouteAllEnabled: usePrivateEndpoints ? false : null siteConfig: { - linuxFxVersion: 'node|18-lts' + linuxFxVersion: 'NODE|22-lts' alwaysOn: true appCommandLine: 'next start' ftpsState: 'Disabled' diff --git a/scripts/appreg_setup.ps1 b/scripts/appreg_setup.ps1 index 1af3221da..80f8700bf 100644 --- a/scripts/appreg_setup.ps1 +++ b/scripts/appreg_setup.ps1 @@ -6,6 +6,10 @@ param ( ) if (-not $webappname) { + $webappname=(azd env get-value AZURE_WEBAPP_NAME).Trim() +} + +if (-not $webappname -or $webappname -like "*ERROR*") { Write-Host "`n Usage: .\appreg_setup.ps1 -webappname [-showsecret] `n" Write-Host "No arguments provided. Please provide the web app name from the Azure portal (e.g. azurechat-ulg3yy5ybjhdq)." Write-Host "The -showsecret flag will display the client secret in the console output." diff --git a/scripts/appreg_setup.sh b/scripts/appreg_setup.sh index 46cbd51a0..c0eb05598 100755 --- a/scripts/appreg_setup.sh +++ b/scripts/appreg_setup.sh @@ -13,6 +13,9 @@ while [[ "$#" -gt 0 ]]; do -showsecret) showsecret_flag="true" ;; + -localredirect) + local_redirect_flag="true" + ;; *) echo "Unknown parameter passed: $1" exit 1 @@ -22,8 +25,13 @@ while [[ "$#" -gt 0 ]]; do done if [[ -z "$webappname" ]]; then + webappname=$(azd env get-value AZURE_WEBAPP_NAME) +fi + +if [[ -z "$webappname" ]] || [[ $webappname == *"ERROR"* ]]; then + echo "" - echo "Usage: $0 -w [-showsecret]" + echo "Usage: $0 -w [-showsecret] [-localredirect]" echo "No arguments provided. Please provide the web app name from the Azure portal (e.g. azurechat-ulg3yy5ybjhdq)." exit 1 fi @@ -51,8 +59,16 @@ echo "Done." redirecturl="https://${webappname}.azurewebsites.net/api/auth/callback/azure-ad" graphurl="https://graph.microsoft.com/v1.0/applications/${objectid}" -echo "> Updating redirect url to $redirecturl..." -az rest --method PATCH --uri $graphurl --body "{'web':{'redirectUris':['${redirecturl}']}}" + +if [[ "$local_redirect_flag" == "true" ]]; then + echo "> Updating redirect url to $redirecturl and http://localhost:3000/api/auth/callback/azure-ad..." + redirectBody="{'web':{'redirectUris':['${redirecturl}','http://localhost:3000/api/auth/callback/azure-ad']}}" +else + echo "> Updating redirect url to $redirecturl..." + redirectBody="{'web':{'redirectUris':['${redirecturl}']}}" +fi + +az rest --method PATCH --uri $graphurl --body $redirectBody echo "Done." rg=$(az webapp list --query "[?name=='${webappname}'].resourceGroup" --output tsv | tr -d '[:space:]')