577 lines
17 KiB
Bicep
577 lines
17 KiB
Bicep
targetScope = 'resourceGroup'
|
|
|
|
@description('Short prefix used for every Azure resource.')
|
|
@minLength(3)
|
|
@maxLength(18)
|
|
param deploymentPrefix string = 'sgu-lab'
|
|
|
|
@description('Azure region for the virtual network, gateway, and VM.')
|
|
param location string = resourceGroup().location
|
|
|
|
@description('Windows Server VM administrator name. This must not be Administrator.')
|
|
@minLength(1)
|
|
@maxLength(20)
|
|
param administratorUsername string
|
|
|
|
@secure()
|
|
@description('Windows Server VM administrator password.')
|
|
param administratorPassword string
|
|
|
|
@description('Windows Server computer name; Active Directory limits this to 15 characters.')
|
|
@minLength(1)
|
|
@maxLength(15)
|
|
param computerName string = 'SGU-DC01'
|
|
|
|
@description('VM size for the Windows Server 2025 domain controller.')
|
|
param vmSize string = 'Standard_D2s_v5'
|
|
|
|
@description('Address space assigned to the Azure virtual network.')
|
|
param virtualNetworkAddressPrefix string = '10.77.0.0/16'
|
|
|
|
@description('Subnet that contains the domain controller.')
|
|
param domainControllerSubnetPrefix string = '10.77.0.0/24'
|
|
|
|
@description('Reserved Azure VPN Gateway subnet. Use /27 or larger.')
|
|
param gatewaySubnetPrefix string = '10.77.255.0/27'
|
|
|
|
@description('Deploy the optional Azure Point-to-Site VPN Gateway. Direct public enrollment does not require it.')
|
|
param deployVpnGateway bool = true
|
|
|
|
@description('Static private IP reserved on the Azure NIC for AD DS and DNS.')
|
|
param domainControllerPrivateIp string = '10.77.0.4'
|
|
|
|
@description('Point-to-site client pool. It must not overlap the VNet or local Hyper-V networks.')
|
|
param vpnClientAddressPoolPrefix string = '172.30.0.0/24'
|
|
|
|
@description('Name presented for the trusted P2S root certificate.')
|
|
param p2sRootCertificateName string = 'SGU-P2S-Root'
|
|
|
|
@description('Base64 DER bytes of the trusted P2S root certificate, without PEM markers.')
|
|
param p2sRootCertificateData string = ''
|
|
|
|
@description('Public IPv4 CIDRs allowed to enroll clients directly without VPN. Leave empty to expose no AD enrollment ports.')
|
|
param publicEnrollmentSourceAddressPrefixes array = []
|
|
|
|
@description('Optional public CIDR allowed to RDP to the VM public IP, for example 203.0.113.10/32. Leave empty to expose no management port.')
|
|
param administratorSourceAddressPrefix string = ''
|
|
|
|
@description('Deploy private Azure Files shares for SGU user roaming. Clients require P2S or another private route into the VNet.')
|
|
param deployUserRoaming bool = false
|
|
|
|
@description('Optional globally unique storage account name for roaming data. Leave empty to derive a stable name from the resource group.')
|
|
param userRoamingStorageAccountName string = ''
|
|
|
|
@description('Azure Files share used for AD/DO FSLogix profile containers.')
|
|
@minLength(3)
|
|
@maxLength(63)
|
|
param fsLogixProfilesShareName string = 'profiles'
|
|
|
|
@description('Azure Files share used for AL Documents and Desktop folder redirection.')
|
|
@minLength(3)
|
|
@maxLength(63)
|
|
param redirectedFoldersShareName string = 'redirected'
|
|
|
|
@description('Quota in GiB for the FSLogix profile-container share.')
|
|
@minValue(100)
|
|
@maxValue(102400)
|
|
param fsLogixProfilesQuotaGiB int = 1024
|
|
|
|
@description('Quota in GiB for the redirected-folders share.')
|
|
@minValue(100)
|
|
@maxValue(102400)
|
|
param redirectedFoldersQuotaGiB int = 1024
|
|
|
|
var virtualNetworkName = '${deploymentPrefix}-vnet'
|
|
var domainControllerSubnetName = 'DomainControllers'
|
|
var gatewaySubnetName = 'GatewaySubnet'
|
|
var networkSecurityGroupName = '${deploymentPrefix}-dc-nsg'
|
|
var domainControllerPublicIpName = '${deploymentPrefix}-dc-pip'
|
|
var gatewayPublicIpName = '${deploymentPrefix}-vpngw-pip'
|
|
var networkInterfaceName = '${deploymentPrefix}-dc-nic'
|
|
var virtualMachineName = '${deploymentPrefix}-dc'
|
|
var virtualNetworkGatewayName = '${deploymentPrefix}-vpngw'
|
|
var effectiveUserRoamingStorageAccountName = empty(userRoamingStorageAccountName)
|
|
? 'sguroam${uniqueString(resourceGroup().id)}'
|
|
: toLower(userRoamingStorageAccountName)
|
|
var userRoamingPrivateEndpointName = '${deploymentPrefix}-profiles-pe'
|
|
var storageEndpointSuffix = environment().suffixes.storage
|
|
var azureFilesPrivateDnsZoneName = 'privatelink.file.${storageEndpointSuffix}'
|
|
|
|
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
|
|
name: networkSecurityGroupName
|
|
location: location
|
|
properties: {
|
|
securityRules: concat(deployVpnGateway ? [
|
|
{
|
|
name: 'Allow-SGU-P2S-clients'
|
|
properties: {
|
|
priority: 100
|
|
access: 'Allow'
|
|
direction: 'Inbound'
|
|
protocol: '*'
|
|
sourcePortRange: '*'
|
|
destinationPortRange: '*'
|
|
sourceAddressPrefix: vpnClientAddressPoolPrefix
|
|
destinationAddressPrefix: domainControllerPrivateIp
|
|
description: 'AD, DNS, broker, monitoring, and RustDesk are reachable only through the authenticated P2S address pool.'
|
|
}
|
|
}
|
|
] : [], empty(publicEnrollmentSourceAddressPrefixes) ? [] : [
|
|
{
|
|
name: 'Allow-Direct-AD-TCP'
|
|
properties: {
|
|
priority: 110
|
|
access: 'Allow'
|
|
direction: 'Inbound'
|
|
protocol: 'Tcp'
|
|
sourcePortRange: '*'
|
|
destinationPortRanges: [
|
|
'53'
|
|
'88'
|
|
'135'
|
|
'389'
|
|
'443'
|
|
'445'
|
|
'464'
|
|
'636'
|
|
'3268'
|
|
'3269'
|
|
'21115-21117'
|
|
'49152-65535'
|
|
]
|
|
sourceAddressPrefixes: publicEnrollmentSourceAddressPrefixes
|
|
destinationAddressPrefix: domainControllerPrivateIp
|
|
description: 'AD, DoH, and RustDesk TCP access for explicitly authorized public enrollment networks.'
|
|
}
|
|
}
|
|
{
|
|
name: 'Allow-Direct-AD-UDP'
|
|
properties: {
|
|
priority: 120
|
|
access: 'Allow'
|
|
direction: 'Inbound'
|
|
protocol: 'Udp'
|
|
sourcePortRange: '*'
|
|
destinationPortRanges: [
|
|
'53'
|
|
'88'
|
|
'123'
|
|
'389'
|
|
'464'
|
|
'21116'
|
|
]
|
|
sourceAddressPrefixes: publicEnrollmentSourceAddressPrefixes
|
|
destinationAddressPrefix: domainControllerPrivateIp
|
|
description: 'AD, time, and RustDesk UDP access for explicitly authorized public enrollment networks.'
|
|
}
|
|
}
|
|
{
|
|
name: 'Allow-Direct-SGU-Enrollment-TCP'
|
|
properties: {
|
|
priority: 130
|
|
access: 'Allow'
|
|
direction: 'Inbound'
|
|
protocol: 'Tcp'
|
|
sourcePortRange: '*'
|
|
destinationPortRanges: [
|
|
'5985'
|
|
'8443'
|
|
]
|
|
sourceAddressPrefixes: publicEnrollmentSourceAddressPrefixes
|
|
destinationAddressPrefix: domainControllerPrivateIp
|
|
description: 'WinRM discovery and SGU broker access for direct enrollment.'
|
|
}
|
|
}
|
|
], empty(administratorSourceAddressPrefix) ? [] : [
|
|
{
|
|
name: 'Allow-RDP-from-administrator'
|
|
properties: {
|
|
priority: 140
|
|
access: 'Allow'
|
|
direction: 'Inbound'
|
|
protocol: 'Tcp'
|
|
sourcePortRange: '*'
|
|
destinationPortRange: '3389'
|
|
sourceAddressPrefix: administratorSourceAddressPrefix
|
|
destinationAddressPrefix: domainControllerPrivateIp
|
|
description: 'Optional bootstrap-only RDP access from one explicitly supplied public CIDR.'
|
|
}
|
|
}
|
|
])
|
|
}
|
|
}
|
|
|
|
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
|
|
name: virtualNetworkName
|
|
location: location
|
|
properties: {
|
|
addressSpace: {
|
|
addressPrefixes: [
|
|
virtualNetworkAddressPrefix
|
|
]
|
|
}
|
|
dhcpOptions: {
|
|
dnsServers: [
|
|
domainControllerPrivateIp
|
|
]
|
|
}
|
|
subnets: concat([
|
|
{
|
|
name: domainControllerSubnetName
|
|
properties: {
|
|
addressPrefix: domainControllerSubnetPrefix
|
|
privateEndpointNetworkPolicies: deployUserRoaming ? 'Disabled' : null
|
|
networkSecurityGroup: {
|
|
id: networkSecurityGroup.id
|
|
}
|
|
}
|
|
}
|
|
], deployVpnGateway ? [
|
|
{
|
|
name: gatewaySubnetName
|
|
properties: {
|
|
addressPrefix: gatewaySubnetPrefix
|
|
}
|
|
}
|
|
] : [])
|
|
}
|
|
}
|
|
|
|
resource userRoamingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = if (deployUserRoaming) {
|
|
name: effectiveUserRoamingStorageAccountName
|
|
location: location
|
|
tags: {
|
|
purpose: 'SGU-user-roaming'
|
|
}
|
|
sku: {
|
|
name: 'Standard_LRS'
|
|
}
|
|
kind: 'StorageV2'
|
|
properties: {
|
|
accessTier: 'Hot'
|
|
allowBlobPublicAccess: false
|
|
allowCrossTenantReplication: false
|
|
allowSharedKeyAccess: true
|
|
largeFileSharesState: 'Enabled'
|
|
minimumTlsVersion: 'TLS1_2'
|
|
publicNetworkAccess: 'Disabled'
|
|
supportsHttpsTrafficOnly: true
|
|
networkAcls: {
|
|
bypass: 'AzureServices'
|
|
defaultAction: 'Deny'
|
|
}
|
|
}
|
|
}
|
|
|
|
resource userRoamingFileService 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = if (deployUserRoaming) {
|
|
parent: userRoamingStorageAccount
|
|
name: 'default'
|
|
properties: {
|
|
shareDeleteRetentionPolicy: {
|
|
enabled: true
|
|
days: 14
|
|
}
|
|
}
|
|
}
|
|
|
|
resource fsLogixProfilesShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = if (deployUserRoaming) {
|
|
parent: userRoamingFileService
|
|
name: fsLogixProfilesShareName
|
|
properties: {
|
|
accessTier: 'TransactionOptimized'
|
|
enabledProtocols: 'SMB'
|
|
shareQuota: fsLogixProfilesQuotaGiB
|
|
}
|
|
}
|
|
|
|
resource redirectedFoldersShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = if (deployUserRoaming) {
|
|
parent: userRoamingFileService
|
|
name: redirectedFoldersShareName
|
|
properties: {
|
|
accessTier: 'TransactionOptimized'
|
|
enabledProtocols: 'SMB'
|
|
shareQuota: redirectedFoldersQuotaGiB
|
|
}
|
|
}
|
|
|
|
resource azureFilesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (deployUserRoaming) {
|
|
name: azureFilesPrivateDnsZoneName
|
|
location: 'global'
|
|
}
|
|
|
|
resource azureFilesPrivateDnsVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (deployUserRoaming) {
|
|
parent: azureFilesPrivateDnsZone
|
|
name: '${deploymentPrefix}-vnet-link'
|
|
location: 'global'
|
|
properties: {
|
|
registrationEnabled: false
|
|
virtualNetwork: {
|
|
id: virtualNetwork.id
|
|
}
|
|
}
|
|
}
|
|
|
|
resource userRoamingPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = if (deployUserRoaming) {
|
|
name: userRoamingPrivateEndpointName
|
|
location: location
|
|
properties: {
|
|
subnet: {
|
|
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, domainControllerSubnetName)
|
|
}
|
|
privateLinkServiceConnections: [
|
|
{
|
|
name: 'azure-files'
|
|
properties: {
|
|
groupIds: [
|
|
'file'
|
|
]
|
|
privateLinkServiceId: userRoamingStorageAccount.id
|
|
}
|
|
}
|
|
]
|
|
}
|
|
dependsOn: [
|
|
virtualNetwork
|
|
]
|
|
}
|
|
|
|
resource userRoamingPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = if (deployUserRoaming) {
|
|
parent: userRoamingPrivateEndpoint
|
|
name: 'default'
|
|
properties: {
|
|
privateDnsZoneConfigs: [
|
|
{
|
|
name: 'azure-files'
|
|
properties: {
|
|
privateDnsZoneId: azureFilesPrivateDnsZone.id
|
|
}
|
|
}
|
|
]
|
|
}
|
|
dependsOn: [
|
|
azureFilesPrivateDnsVnetLink
|
|
]
|
|
}
|
|
|
|
resource domainControllerPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = {
|
|
name: domainControllerPublicIpName
|
|
location: location
|
|
sku: {
|
|
name: 'Standard'
|
|
}
|
|
properties: {
|
|
publicIPAllocationMethod: 'Static'
|
|
publicIPAddressVersion: 'IPv4'
|
|
idleTimeoutInMinutes: 30
|
|
}
|
|
}
|
|
|
|
resource gatewayPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = if (deployVpnGateway) {
|
|
name: gatewayPublicIpName
|
|
location: location
|
|
zones: [
|
|
'1'
|
|
'2'
|
|
'3'
|
|
]
|
|
sku: {
|
|
name: 'Standard'
|
|
}
|
|
properties: {
|
|
publicIPAllocationMethod: 'Static'
|
|
publicIPAddressVersion: 'IPv4'
|
|
}
|
|
}
|
|
|
|
resource networkInterface 'Microsoft.Network/networkInterfaces@2024-05-01' = {
|
|
name: networkInterfaceName
|
|
location: location
|
|
properties: {
|
|
enableAcceleratedNetworking: true
|
|
dnsSettings: {
|
|
dnsServers: [
|
|
domainControllerPrivateIp
|
|
]
|
|
}
|
|
ipConfigurations: [
|
|
{
|
|
name: 'ipconfig1'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Static'
|
|
privateIPAddressVersion: 'IPv4'
|
|
privateIPAddress: domainControllerPrivateIp
|
|
subnet: {
|
|
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, domainControllerSubnetName)
|
|
}
|
|
publicIPAddress: {
|
|
id: domainControllerPublicIp.id
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
dependsOn: [
|
|
virtualNetwork
|
|
]
|
|
}
|
|
|
|
resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-07-01' = {
|
|
name: virtualMachineName
|
|
location: location
|
|
identity: {
|
|
type: 'SystemAssigned'
|
|
}
|
|
properties: {
|
|
hardwareProfile: {
|
|
vmSize: vmSize
|
|
}
|
|
securityProfile: {
|
|
securityType: 'TrustedLaunch'
|
|
uefiSettings: {
|
|
secureBootEnabled: true
|
|
vTpmEnabled: true
|
|
}
|
|
}
|
|
osProfile: {
|
|
computerName: computerName
|
|
adminUsername: administratorUsername
|
|
adminPassword: administratorPassword
|
|
windowsConfiguration: {
|
|
provisionVMAgent: true
|
|
enableAutomaticUpdates: true
|
|
patchSettings: {
|
|
patchMode: 'AutomaticByPlatform'
|
|
assessmentMode: 'AutomaticByPlatform'
|
|
enableHotpatching: false
|
|
}
|
|
}
|
|
}
|
|
storageProfile: {
|
|
imageReference: {
|
|
publisher: 'MicrosoftWindowsServer'
|
|
offer: 'WindowsServer'
|
|
sku: '2025-datacenter-azure-edition'
|
|
version: 'latest'
|
|
}
|
|
osDisk: {
|
|
createOption: 'FromImage'
|
|
// AD DS requires durable writes; the bootstrap stores NTDS on this disk.
|
|
caching: 'None'
|
|
managedDisk: {
|
|
storageAccountType: 'Premium_LRS'
|
|
}
|
|
deleteOption: 'Delete'
|
|
}
|
|
}
|
|
networkProfile: {
|
|
networkInterfaces: [
|
|
{
|
|
id: networkInterface.id
|
|
properties: {
|
|
primary: true
|
|
deleteOption: 'Delete'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
diagnosticsProfile: {
|
|
bootDiagnostics: {
|
|
enabled: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resource virtualNetworkGateway 'Microsoft.Network/virtualNetworkGateways@2024-05-01' = if (deployVpnGateway) {
|
|
name: virtualNetworkGatewayName
|
|
location: location
|
|
properties: {
|
|
gatewayType: 'Vpn'
|
|
vpnType: 'RouteBased'
|
|
activeActive: false
|
|
enableBgp: false
|
|
ipConfigurations: [
|
|
{
|
|
name: 'gateway-ipconfig'
|
|
properties: {
|
|
privateIPAllocationMethod: 'Dynamic'
|
|
subnet: {
|
|
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, gatewaySubnetName)
|
|
}
|
|
publicIPAddress: {
|
|
id: gatewayPublicIp.id
|
|
}
|
|
}
|
|
}
|
|
]
|
|
sku: {
|
|
name: 'VpnGw1AZ'
|
|
tier: 'VpnGw1AZ'
|
|
}
|
|
vpnClientConfiguration: {
|
|
vpnClientAddressPool: {
|
|
addressPrefixes: [
|
|
vpnClientAddressPoolPrefix
|
|
]
|
|
}
|
|
vpnClientProtocols: [
|
|
'IkeV2'
|
|
'OpenVPN'
|
|
]
|
|
vpnAuthenticationTypes: [
|
|
'Certificate'
|
|
]
|
|
vpnClientRootCertificates: [
|
|
{
|
|
name: p2sRootCertificateName
|
|
properties: {
|
|
publicCertData: p2sRootCertificateData
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
dependsOn: [
|
|
virtualNetwork
|
|
]
|
|
}
|
|
|
|
output domainControllerName string = virtualMachine.name
|
|
output domainControllerPrivateIp string = domainControllerPrivateIp
|
|
output domainControllerPublicIp string = domainControllerPublicIp.properties.ipAddress
|
|
output virtualNetworkName string = virtualNetwork.name
|
|
output virtualNetworkAddressPrefix string = virtualNetworkAddressPrefix
|
|
output vpnGatewayName string = deployVpnGateway ? virtualNetworkGateway.name : ''
|
|
output vpnClientAddressPoolPrefix string = vpnClientAddressPoolPrefix
|
|
output userRoamingEnabled bool = deployUserRoaming
|
|
output userRoamingStorageAccountName string = deployUserRoaming ? userRoamingStorageAccount.name : ''
|
|
output fsLogixProfilesSharePath string = deployUserRoaming ? '\\\\${userRoamingStorageAccount.name}.file.${storageEndpointSuffix}\\${fsLogixProfilesShare.name}' : ''
|
|
output redirectedFoldersSharePath string = deployUserRoaming ? '\\\\${userRoamingStorageAccount.name}.file.${storageEndpointSuffix}\\${redirectedFoldersShare.name}' : ''
|
|
output userRoamingSetupArguments array = deployUserRoaming ? [
|
|
'-SubscriptionId'
|
|
subscription().subscriptionId
|
|
'-ResourceGroupName'
|
|
resourceGroup().name
|
|
'-StorageAccountName'
|
|
userRoamingStorageAccount.name
|
|
'-FsLogixProfilesShareName'
|
|
fsLogixProfilesShare.name
|
|
'-RedirectedFoldersShareName'
|
|
redirectedFoldersShare.name
|
|
] : []
|
|
output serverBootstrapArguments array = concat([
|
|
'-ServerIPv4Address'
|
|
domainControllerPrivateIp
|
|
'-PrefixLength'
|
|
last(split(domainControllerSubnetPrefix, '/'))
|
|
'-NetworkConfigurationMode'
|
|
'PlatformManaged'
|
|
'-DnsForwarders'
|
|
'168.63.129.16'
|
|
], deployVpnGateway ? [
|
|
'-TrustedClientNetworks'
|
|
vpnClientAddressPoolPrefix
|
|
] : [], empty(publicEnrollmentSourceAddressPrefixes) ? [] : concat([
|
|
'-PublicEnrollmentNetworks'
|
|
], publicEnrollmentSourceAddressPrefixes))
|