diff --git a/ocpp-server/README.md b/ocpp-server/README.md index 1f23eaa..c873e68 100644 --- a/ocpp-server/README.md +++ b/ocpp-server/README.md @@ -19,7 +19,9 @@ using 'main.bicep' param pubsubKeyVaultCertName = '' param webKeyVaultCertName ='' param keyVaultName = '' -param keyVaultIdentityRG ='' +param keyVaultRG ='' +param keyVaultIdentityName = '' +param keyVaultIdentityRG ='' param customDnsZoneName ='' param pubsubARecordName ='' param dnsZoneRG ='' diff --git a/ocpp-server/infra/main.bicep b/ocpp-server/infra/main.bicep index 6f5c474..1c1ed34 100644 --- a/ocpp-server/infra/main.bicep +++ b/ocpp-server/infra/main.bicep @@ -5,10 +5,12 @@ param pubsubKeyVaultCertName string param webKeyVaultCertName string @description('Name of the Key Vault where the certificates are stored') param keyVaultName string +@description('Resource Group name where the Key Vault was created') +param keyVaultRG string @description('User Assigned Managed Identity name with Get permissions fo the Key Vault Certificate') param keyVaultIdentityName string @description('Resource Group name where the User Assigned Managed Identity was created') -param keyVaultIdentityRG string = resourceGroup().name +param keyVaultIdentityRG string = keyVaultRG @description('Custom DNS Zone Name used for publishing the Web PubSub service endpoint securely') param customDnsZoneName string @description('A Record Name for the Web PubSub service endpoint, used as prefix of the DnsZoneName') @@ -23,11 +25,20 @@ param pubSubHubName string = 'OcppService' var pubsubHostName = '${pubsubARecordName}.${customDnsZoneName}' var webHostName = '${webARecordName}.${customDnsZoneName}' +// Add a Nat Gateway for outbound access +module natgw './modules/natgw.bicep' = { + name: 'natGwService' + params: { + natGwName: 'natgw-${uniqueString(resourceGroup().id)}' + location: resourceGroup().location + } +} // Creates a VNet with 3 subnets: default, gateway and private endpoints module virtualNetwork './modules/virtualNetwork.bicep' = { name: 'vNet' params: { virtualNetworkName: 'vnet-${uniqueString(resourceGroup().id)}' + natGatewayId: natgw.outputs.natGatewayId } } @@ -84,6 +95,17 @@ module webPrivateEndpoint './modules/privateEndpoint.bicep' = { } } +module storagePrivateEndpoint './modules/privateEndpoint.bicep' = { + name: 'webStoragePrivateEndpoint' + params: { + privateLinkResource: webApp.outputs.storageAccountId + subnetId: virtualNetwork.outputs.privateSubnetId + vnetId: virtualNetwork.outputs.vnetId + targetSubResource: 'blob' + endpointName: 'webStoragePrivate${uniqueString(resourceGroup().id)}' + } +} + module appGw './modules/appgw.bicep' = { name: 'appGwService' params: { @@ -96,6 +118,7 @@ module appGw './modules/appgw.bicep' = { webHostName: webHostName pubsubHostName: pubsubHostName keyVaultName: keyVaultName + keyVaultRG: keyVaultRG keyVaultIdentityName: keyVaultIdentityName keyVaultIdentityRG: keyVaultIdentityRG webServiceName: webApp.outputs.webSiteName @@ -123,3 +146,16 @@ module wwwdns './modules/dns.bicep' = if (customDnsZoneName != '') { ipTargetResourceId: appGw.outputs.publicIPAddressId } } + +module customDomain 'modules/customWebName.bicep' = if (customDnsZoneName != '') { + name: 'customDomain' + params: { + dnszoneName: customDnsZoneName + dnsZoneRG: dnsZoneRG + subdomain: 'www' + webSiteName: webApp.outputs.webSiteName + keyVaultName: keyVaultName + keyVaultRG: keyVaultRG + webKeyVaultCertName: webKeyVaultCertName + } +} diff --git a/ocpp-server/infra/main.parameters.bicepparam.example b/ocpp-server/infra/main.parameters.bicepparam.example index 5ce766c..5d36bef 100644 --- a/ocpp-server/infra/main.parameters.bicepparam.example +++ b/ocpp-server/infra/main.parameters.bicepparam.example @@ -3,7 +3,9 @@ using 'main.bicep' param pubsubKeyVaultCertName = '' param webKeyVaultCertName ='' param keyVaultName = '' -param keyVaultIdentityRG ='' +param keyVaultRG ='' +param keyVaultIdentityName = '' +param keyVaultIdentityRG ='' param customDnsZoneName ='' param pubsubARecordName ='' param dnsZoneRG ='' \ No newline at end of file diff --git a/ocpp-server/infra/modules/appgw.bicep b/ocpp-server/infra/modules/appgw.bicep index 7ca5720..da02b4d 100644 --- a/ocpp-server/infra/modules/appgw.bicep +++ b/ocpp-server/infra/modules/appgw.bicep @@ -14,6 +14,7 @@ param skuCapacity int = 1 param pubsubHostName string param webHostName string param keyVaultName string +param keyVaultRG string param keyVaultIdentityName string param keyVaultIdentityRG string param webServiceName string @@ -25,6 +26,7 @@ var ocppRuleSetName = 'ocppRuleSet' var pubsubBackendPoolName = 'pubsubBackend' var pubsubBackendSettingsName = 'pubsubBackendSettings' var pubsubProbeName = 'pubsubProbe' +var webProbeName = 'webProbe' var pubsubListenerName = 'pubsubListener' var webBackendPoolName = 'webBackend' var webListenerName = 'webListener' @@ -36,7 +38,7 @@ var isWildcard = (webKeyVaultCertName == pubsubKeyVaultCertName) resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { name: keyVaultName - scope: resourceGroup(keyVaultIdentityRG) + scope: resourceGroup(keyVaultRG) } resource pubsubKeyVaultCertificate 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = { @@ -162,6 +164,18 @@ resource appGw 'Microsoft.Network/applicationGateways@2023-02-01' = { } } } + { + name: webProbeName + properties: { + protocol: 'Https' + host: webHostName + path: '/' + interval: 30 + timeout: 30 + unhealthyThreshold: 3 + pickHostNameFromBackendHttpSettings: false + } + } ] backendHttpSettingsCollection: [ { @@ -180,11 +194,13 @@ resource appGw 'Microsoft.Network/applicationGateways@2023-02-01' = { { name: webBackendSettingsName properties: { - port: 80 - protocol: 'Http' + port: 443 + protocol: 'Https' cookieBasedAffinity: 'Disabled' - pickHostNameFromBackendAddress: true - requestTimeout: 180 //default KeepAliveInterval for websockets is 2 minutes, setting 3 minutes for app gateway + pickHostNameFromBackendAddress: false + probe: { + id: resourceId('Microsoft.Network/applicationGateways/probes', appgwName, webProbeName) + } } } ] diff --git a/ocpp-server/infra/modules/customWebName.bicep b/ocpp-server/infra/modules/customWebName.bicep new file mode 100644 index 0000000..bded9a0 --- /dev/null +++ b/ocpp-server/infra/modules/customWebName.bicep @@ -0,0 +1,75 @@ +param dnszoneName string +param dnsZoneRG string +param subdomain string = 'www' +param webSiteName string +param keyVaultName string +param keyVaultRG string +param webKeyVaultCertName string +param location string = resourceGroup().location + +var fqdn = '${subdomain}.${dnszoneName}' + +resource site 'Microsoft.Web/sites@2023-12-01' existing = { + name: webSiteName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { + name: keyVaultName + scope: resourceGroup(keyVaultRG) +} + +resource webKeyVaultCertificate 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' existing = { + name: webKeyVaultCertName + parent: keyVault +} + +// Add a TXT record to the DNS zone to verify the custom domain +module verification 'dnstxt.bicep' = { + name: 'dnsServiceWebTxt' + scope: resourceGroup(dnsZoneRG) + params: { + dnszoneName: dnszoneName + subdomain: 'asuid.${fqdn}' + value: site.properties.customDomainVerificationId + } +} + +// Enabling Managed certificate for a webapp requires 3 steps +// 1. Add custom domain to webapp with SSL in disabled state +// 2. Upload certificate for the domain +// 3. enable SSL +// +// The last step requires deploying again Microsoft.Web/sites/hostNameBindings - and ARM template forbids this in one deplyment, therefore we need to use modules to chain this. + +resource appCustomHost 'Microsoft.Web/sites/hostNameBindings@2020-06-01' = { + name: fqdn + parent: site + dependsOn: [verification] + properties: { + hostNameType: 'Verified' + sslState: 'Disabled' + customHostNameDnsRecordType: 'CName' + siteName: site.name + } +} + +resource appCustomHostCertificate 'Microsoft.Web/certificates@2020-06-01' = { + name: fqdn + location: location + dependsOn: [appCustomHost] + properties: any({ + keyVaultId: keyVault.id + keyVaultSecretName: webKeyVaultCertificate.name + serverFarmId: site.properties.serverFarmId + }) +} + +// we need to use a module to enable sni, as ARM forbids using resource with this same type-name combination twice in one deployment. +module appCustomHostEnable './sni-enable.bicep' = { + name: '${deployment().name}-${fqdn}-sni-enable' + params: { + appName: site.name + appHostname: appCustomHostCertificate.name + certificateThumbprint: appCustomHostCertificate.properties.thumbprint + } +} diff --git a/ocpp-server/infra/modules/dnstxt.bicep b/ocpp-server/infra/modules/dnstxt.bicep new file mode 100644 index 0000000..eb1e76a --- /dev/null +++ b/ocpp-server/infra/modules/dnstxt.bicep @@ -0,0 +1,20 @@ +param dnszoneName string +param subdomain string = 'www' +param value string + +resource dnsZone 'Microsoft.Network/dnszones@2023-07-01-preview' existing = { + name: dnszoneName +} + +resource dnsZoneNewTXTRecord 'Microsoft.Network/dnsZones/TXT@2023-07-01-preview' = { + parent: dnsZone + name: subdomain + properties: { + TTL: 300 + TXTRecords: [ + { + value: [value] + } + ] + } +} diff --git a/ocpp-server/infra/modules/natgw.bicep b/ocpp-server/infra/modules/natgw.bicep new file mode 100644 index 0000000..6e1f706 --- /dev/null +++ b/ocpp-server/infra/modules/natgw.bicep @@ -0,0 +1,34 @@ +param natGwName string +param sku string = 'Standard' +param tier string = 'Regional' +param idleTimeoutInMinutes int = 4 +param location string = resourceGroup().location + +resource publicIpPrefixes 'Microsoft.Network/publicIPPrefixes@2023-11-01' = { + name: 'ipPrefixes-${natGwName}' + location: location + sku: { + name: sku + tier: tier + } + properties: { + prefixLength: 28 + publicIPAddressVersion: 'IPv4' + natGateway: { + id: natGateway.id + } + } +} + +resource natGateway 'Microsoft.Network/natGateways@2023-11-01' = { + name: natGwName + location: location + sku: { + name: sku + } + properties: { + idleTimeoutInMinutes: idleTimeoutInMinutes + } +} + +output natGatewayId string = natGateway.id diff --git a/ocpp-server/infra/modules/privateEndpoint.bicep b/ocpp-server/infra/modules/privateEndpoint.bicep index ee77f2b..b1764b7 100644 --- a/ocpp-server/infra/modules/privateEndpoint.bicep +++ b/ocpp-server/infra/modules/privateEndpoint.bicep @@ -8,12 +8,14 @@ param privateLinkResource string @allowed([ 'webpubsub' 'sites' + 'blob' ]) param targetSubResource string var dnsByTarget = { webpubsub: 'privatelink.webpubsub.azure.com' sites: 'privatelink.azurewebsites.net' + blob: 'privatelink.blob.${environment().suffixes.storage}' } resource privateEndpoint 'Microsoft.Network/privateEndpoints@2021-05-01' = { diff --git a/ocpp-server/infra/modules/sni-enable.bicep b/ocpp-server/infra/modules/sni-enable.bicep new file mode 100644 index 0000000..8931e0b --- /dev/null +++ b/ocpp-server/infra/modules/sni-enable.bicep @@ -0,0 +1,11 @@ +param appName string +param appHostname string +param certificateThumbprint string + +resource appCustomHostEnable 'Microsoft.Web/sites/hostNameBindings@2020-06-01' = { + name: '${appName}/${appHostname}' + properties: { + sslState: 'SniEnabled' + thumbprint: certificateThumbprint + } +} diff --git a/ocpp-server/infra/modules/storage.bicep b/ocpp-server/infra/modules/storage.bicep index fa8e3b5..0dae90b 100644 --- a/ocpp-server/infra/modules/storage.bicep +++ b/ocpp-server/infra/modules/storage.bicep @@ -1,4 +1,4 @@ -param name string = 'storage${uniqueString(resourceGroup().id)}' +param name string = 'storage${uniqueString(resourceGroup().id)}' param location string = resourceGroup().location param sku string = 'Standard_LRS' param kind string = 'StorageV2' @@ -35,5 +35,5 @@ resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/container } output storageAccountName string = storageAccount.name +output storageAccountId string = storageAccount.id output blobContaineId string = blobContainer.id - diff --git a/ocpp-server/infra/modules/virtualNetwork.bicep b/ocpp-server/infra/modules/virtualNetwork.bicep index 233a5f5..6f95854 100644 --- a/ocpp-server/infra/modules/virtualNetwork.bicep +++ b/ocpp-server/infra/modules/virtualNetwork.bicep @@ -7,6 +7,7 @@ param privateSubnetName string = 'private-endpoints' param privateSubnetPrefix string = '10.1.1.0/24' param gatewaySubnetName string = 'gateway' param gatewaySubnetPrefix string = '10.1.2.0/24' +param natGatewayId string resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-09-01' = { name: virtualNetworkName @@ -20,6 +21,9 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-09-01' = { name: subnetName properties: { addressPrefix: subnetPrefix + natGateway: { + id: natGatewayId + } delegations: [ { name: 'Microsoft.Web/serverFarms' @@ -27,18 +31,24 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-09-01' = { serviceName: 'Microsoft.Web/serverFarms' } } - ] + ] } } { name: privateSubnetName properties: { + natGateway: { + id: natGatewayId + } addressPrefix: privateSubnetPrefix } } { name: gatewaySubnetName properties: { + natGateway: { + id: natGatewayId + } addressPrefix: gatewaySubnetPrefix } } diff --git a/ocpp-server/infra/modules/webapp.bicep b/ocpp-server/infra/modules/webapp.bicep index ac24406..bd82143 100644 --- a/ocpp-server/infra/modules/webapp.bicep +++ b/ocpp-server/infra/modules/webapp.bicep @@ -10,6 +10,21 @@ var appServicePlanName = toLower('AppServicePlan-${webAppName}') var webSiteName = toLower('wapp-${webAppName}') var appInsightsName = 'appInsights-${uniqueString(resourceGroup().id)}' +// links to existing services +resource pubSub 'Microsoft.SignalRService/webPubSub@2024-04-01-preview' existing = { + name: pubSubName +} + +resource vNet 'Microsoft.Network/virtualNetworks@2024-01-01' existing = { + name: vnetName +} + +resource subNet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = { + name: subnetName + parent: vNet +} + +// create an Application Insights resource resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = { name: appInsightsName location: resourceGroup().location @@ -19,15 +34,12 @@ resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = { } } -resource pubSub 'Microsoft.SignalRService/webPubSub@2024-04-01-preview' existing = { - name: pubSubName -} - +// create an App Service Plan resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { name: appServicePlanName location: location properties: { - reserved: true + reserved: true //needed for Linux apps } sku: { name: sku @@ -35,6 +47,7 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = { kind: 'linux' } +// create a storage account to deploy the app module storage 'storage.bicep' = { name: 'storage' params: { @@ -47,7 +60,9 @@ resource appService 'Microsoft.Web/sites@2020-06-01' = { location: location properties: { serverFarmId: appServicePlan.id + httpsOnly: true // Enable HTTPS only for improved security siteConfig: { + ftpsState: 'Disabled' linuxFxVersion: linuxFxVersion appSettings: [ // Application Insights needs these three settings to be activated @@ -74,15 +89,7 @@ resource appService 'Microsoft.Web/sites@2020-06-01' = { } } -resource vNet 'Microsoft.Network/virtualNetworks@2024-01-01' existing = { - name: vnetName -} - -resource subNet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = { - name: subnetName - parent: vNet -} - +// add vnet integration resource vnetconfig 'Microsoft.Web/sites/networkConfig@2022-09-01' = { name: 'virtualNetwork' parent: appService @@ -92,7 +99,26 @@ resource vnetconfig 'Microsoft.Web/sites/networkConfig@2022-09-01' = { } } -// enable ipSecurityRestrictions for the appService +// disallow ftp basic publishing credentials for improved security +resource ftpConfig 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = { + name: 'ftp' + parent: appService + properties: { + allow: false + } +} + +// disallow scm basic publishing credentials for improved security +resource scmConfig 'Microsoft.Web/sites/basicPublishingCredentialsPolicies@2023-12-01' = { + name: 'scm' + parent: appService + properties: { + allow: false + } +} + +// enable ipSecurityRestrictions for the appService, this will allow only the AzureWebPubSub service tag +// because AzureWebPubSub does not still have vNet integration, so it still needs to use the public endpoint resource ipSecurityRestrictions 'Microsoft.Web/sites/config@2023-12-01' = { name: 'web' parent: appService @@ -120,3 +146,4 @@ resource ipSecurityRestrictions 'Microsoft.Web/sites/config@2023-12-01' = { output webSiteName string = appService.name output appServiceId string = appService.id output storageName string = storage.outputs.storageAccountName +output storageAccountId string = storage.outputs.storageAccountId