Files

407 lines
12 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 = ''
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'
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
]
}
subnets: concat([
{
name: domainControllerSubnetName
properties: {
addressPrefix: domainControllerSubnetPrefix
networkSecurityGroup: {
id: networkSecurityGroup.id
}
}
}
], deployVpnGateway ? [
{
name: gatewaySubnetName
properties: {
addressPrefix: gatewaySubnetPrefix
}
}
] : [])
}
}
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 serverBootstrapArguments array = concat([
'-ServerIPv4Address'
domainControllerPrivateIp
'-PrefixLength'
last(split(domainControllerSubnetPrefix, '/'))
'-NetworkConfigurationMode'
'PlatformManaged'
'-DnsForwarders'
'168.63.129.16'
], deployVpnGateway ? [
'-TrustedClientNetworks'
vpnClientAddressPoolPrefix
] : [], empty(publicEnrollmentSourceAddressPrefixes) ? [] : concat([
'-PublicEnrollmentNetworks'
], publicEnrollmentSourceAddressPrefixes))