Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a24c25a3fb |
@@ -97,6 +97,9 @@ the latest .NET 10 x64 runtime. The broker is published self-contained.
|
|||||||
Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review
|
Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review
|
||||||
[docs/security.md](docs/security.md) before production deployment and
|
[docs/security.md](docs/security.md) before production deployment and
|
||||||
[docs/architecture.md](docs/architecture.md) for the component contract.
|
[docs/architecture.md](docs/architecture.md) for the component contract.
|
||||||
|
For a public Azure VM connected to local Hyper-V clients through Azure VPN
|
||||||
|
Gateway, use [docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). AD
|
||||||
|
ports remain private even though the VM owns a public IP.
|
||||||
|
|
||||||
Never disable the built-in Microsoft password Credential Provider. It is the
|
Never disable the built-in Microsoft password Credential Provider. It is the
|
||||||
supported recovery path if a third-party provider fails to load.
|
supported recovery path if a third-party provider fails to load.
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# Active Directory SGU en Azure con VPN Point-to-Site
|
||||||
|
|
||||||
|
Esta variante conserva Active Directory en una VM Windows Server 2025 con IP
|
||||||
|
pública de Azure, pero **no publica Active Directory en Internet**. La IP pública
|
||||||
|
sirve para el ciclo de vida y, opcionalmente, RDP desde un único CIDR
|
||||||
|
administrativo. DNS, Kerberos, LDAP, SMB, RPC, WinRM, Auth Broker, monitoreo y
|
||||||
|
RustDesk viajan por Azure VPN Gateway Point-to-Site (P2S).
|
||||||
|
|
||||||
|
La plantilla crea:
|
||||||
|
|
||||||
|
- VNet `10.77.0.0/16`, subnet del DC `10.77.0.0/24` y `GatewaySubnet`;
|
||||||
|
- Windows Server 2025 con IP privada estática `10.77.0.4` reservada en la NIC;
|
||||||
|
- IP pública Standard para la VM, protegida por NSG;
|
||||||
|
- VPN Gateway `VpnGw1` con IKEv2/SSTP y autenticación por certificados;
|
||||||
|
- pool P2S `172.30.0.0/24`, autorizado en los firewalls SGU;
|
||||||
|
- DNS de la NIC del servidor apuntando a `10.77.0.4`.
|
||||||
|
|
||||||
|
Los prefijos son parámetros. Deben ser RFC1918 y no deben solaparse con las
|
||||||
|
redes usadas por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales.
|
||||||
|
|
||||||
|
## 1. Crear la autoridad P2S y el certificado de administración
|
||||||
|
|
||||||
|
En la estación administrativa donde está el repositorio:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$p2s = .\scripts\New-SguAzureP2sCertificates.ps1 `
|
||||||
|
-ClientName 'AdminWorkstation'
|
||||||
|
```
|
||||||
|
|
||||||
|
Se pide una contraseña para proteger el PFX. La clave privada de la autoridad
|
||||||
|
raíz permanece no exportable en `Cert:\CurrentUser\My`; Azure recibe solamente
|
||||||
|
el `.cer` público. El PFX es una credencial de acceso a la VNet: se debe copiar
|
||||||
|
únicamente a la VM correspondiente y eliminarse de ubicaciones compartidas
|
||||||
|
después de importarlo.
|
||||||
|
|
||||||
|
## 2. Desplegar Azure
|
||||||
|
|
||||||
|
Requisitos: Azure CLI, una sesión iniciada con `az login`, permisos para crear
|
||||||
|
red, gateway, IP pública y VM, y una suscripción seleccionable.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
|
||||||
|
-SubscriptionId '00000000-0000-0000-0000-000000000000' `
|
||||||
|
-ResourceGroupName 'rg-sgu-lab' `
|
||||||
|
-Location 'centralus' `
|
||||||
|
-AdministratorUsername 'azureadmin' `
|
||||||
|
-P2sRootCertificatePath $p2s.RootCertificatePath
|
||||||
|
```
|
||||||
|
|
||||||
|
La contraseña local de la VM se solicita como `SecureString`, se coloca sólo en
|
||||||
|
un archivo temporal con ACL exclusiva para el usuario actual y se elimina al
|
||||||
|
terminar. No aparece en los argumentos de Azure CLI ni queda guardada en el
|
||||||
|
repositorio.
|
||||||
|
|
||||||
|
Por omisión ningún puerto administrativo de la VM se abre desde Internet. Para
|
||||||
|
habilitar temporalmente RDP durante el bootstrap, indique exclusivamente su IP
|
||||||
|
pública actual:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
-AdministratorSourceAddressPrefix '203.0.113.10/32'
|
||||||
|
```
|
||||||
|
|
||||||
|
No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante
|
||||||
|
más que la VM; el comando espera hasta que Azure entregue un resultado final.
|
||||||
|
|
||||||
|
## 3. Descargar P2S y entrar por la IP privada
|
||||||
|
|
||||||
|
Cuando el gateway esté `Succeeded`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$vpn = .\scripts\Get-SguAzureP2sPackage.ps1 `
|
||||||
|
-SubscriptionId '00000000-0000-0000-0000-000000000000' `
|
||||||
|
-ResourceGroupName 'rg-sgu-lab' `
|
||||||
|
-VpnGatewayName $azure.VpnGatewayName
|
||||||
|
|
||||||
|
.\scripts\Install-SguAzureP2sClient.ps1 `
|
||||||
|
-VpnProfilePackagePath $vpn.PackagePath `
|
||||||
|
-ClientCertificatePfxPath $p2s.ClientCertificatePath `
|
||||||
|
-ClientRootCertificatePath $p2s.RootCertificatePath `
|
||||||
|
-Connect
|
||||||
|
```
|
||||||
|
|
||||||
|
Con el túnel conectado, use RDP contra `10.77.0.4` y habilite la redirección de
|
||||||
|
una unidad local para copiar `sgu-server-bootstrap-VERSION.zip` a la VM. La NIC
|
||||||
|
ya apunta a su futura dirección DNS propia, por lo que la resolución pública no
|
||||||
|
está disponible hasta que el bootstrap instale DNS y sus reenviadores. Así no es
|
||||||
|
necesario abrir 3389 en la IP pública. La opción
|
||||||
|
`AdministratorSourceAddressPrefix` queda como ruta de recuperación temporal,
|
||||||
|
no como el camino normal.
|
||||||
|
|
||||||
|
## 4. Ejecutar el bootstrap dentro de Windows Server
|
||||||
|
|
||||||
|
Descargue y extraiga `sgu-server-bootstrap-VERSION.zip` dentro de la VM. La IP
|
||||||
|
que recibe el bootstrap es la **privada** de la NIC, nunca la pública:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
Start-SguAzureServerBootstrap.cmd 10.77.0.4 172.30.0.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
El modo `PlatformManaged` comprueba que Azure ya asignó `10.77.0.4/24`, pero no
|
||||||
|
deshabilita DHCP, no reemplaza la ruta predeterminada y no reinicia el adaptador.
|
||||||
|
El DNS de AD publica únicamente la dirección privada. `168.63.129.16` se usa
|
||||||
|
como reenviador DNS de la plataforma Azure.
|
||||||
|
|
||||||
|
Después del reinicio de promoción, verificar:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Content C:\ProgramData\SGU\Bootstrap\Server\bootstrap-complete.json
|
||||||
|
Resolve-DnsName _ldap._tcp.dc._msdcs.lci.lasalle.mx -Type SRV -Server 10.77.0.4
|
||||||
|
```
|
||||||
|
|
||||||
|
El JSON debe indicar `NetworkConfigurationMode = PlatformManaged`, el pool P2S
|
||||||
|
en `TrustedClientNetworks` y ambos prefijos en `AllowedRemoteAddresses`.
|
||||||
|
|
||||||
|
## 5. Emitir un certificado y enrolar cada VM Hyper-V
|
||||||
|
|
||||||
|
En la estación administrativa, emita una credencial distinta por equipo:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$w11 = .\scripts\New-SguAzureP2sCertificates.ps1 -ClientName 'Windows11'
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie a la VM Windows 11 de Hyper-V:
|
||||||
|
|
||||||
|
- `sgu-client-bootstrap-VERSION.zip` extraído;
|
||||||
|
- `$vpn.PackagePath`;
|
||||||
|
- `$w11.ClientCertificatePath`;
|
||||||
|
- `sgu-azure-p2s-root.cer`.
|
||||||
|
|
||||||
|
Desde la carpeta extraída del bootstrap de cliente, instale P2S y enrole:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\sgu-azure-vpn-client.zip C:\SGU\sgu-azure-p2s-Windows11.pfx C:\SGU\sgu-azure-p2s-root.cer
|
||||||
|
```
|
||||||
|
|
||||||
|
En una sola ejecución el comando:
|
||||||
|
|
||||||
|
1. importa el certificado de cliente en `LocalMachine\My` sin dejar la
|
||||||
|
contraseña en disco;
|
||||||
|
2. instala un perfil IKEv2 de todos los usuarios llamado `SGU Azure P2S`;
|
||||||
|
3. agrega la ruta `10.77.0.0/16` y una regla NRPT que envía sólo
|
||||||
|
`.lci.lasalle.mx` al DNS `10.77.0.4`;
|
||||||
|
4. conecta P2S con certificado de máquina;
|
||||||
|
5. registra mTLS, instala SGU/RustDesk y une el equipo al dominio;
|
||||||
|
6. reinicia Windows.
|
||||||
|
|
||||||
|
Si la red local bloquea IKEv2 (UDP 500/4500), el paquete de Azure también
|
||||||
|
incluye un perfil SSTP sobre TCP 443, pero ese fallback todavía requiere
|
||||||
|
instalación manual con el instalador oficial incluido en `WindowsAmd64`.
|
||||||
|
|
||||||
|
Windows 11 Pro admite unión a AD y VPN nativa, pero Microsoft no licencia el
|
||||||
|
**Always On VPN device tunnel** para Pro. Por ello el perfil se instala para
|
||||||
|
todos los usuarios y se puede seleccionar desde el control de red de la
|
||||||
|
pantalla de inicio de sesión; antes del primer logon de una cuenta de dominio,
|
||||||
|
conecte `SGU Azure P2S` allí. Enterprise/Education pueden recibir posteriormente
|
||||||
|
un device tunnel Always On, pero eso no es requisito del enrolamiento SGU.
|
||||||
|
|
||||||
|
Validación dentro del cliente, con la VPN conectada:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-VpnConnection -Name 'SGU Azure P2S' -AllUserConnection
|
||||||
|
Get-DnsClientNrptRule | Where-Object DisplayName -like 'SGU Azure P2S*'
|
||||||
|
Test-NetConnection 10.77.0.4 -Port 5985
|
||||||
|
Resolve-DnsName _ldap._tcp.dc._msdcs.lci.lasalle.mx -Type SRV
|
||||||
|
nltest.exe /dsgetdc:lci.lasalle.mx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seguridad y referencias
|
||||||
|
|
||||||
|
No agregue reglas NSG públicas para 53, 88, 135, 389, 445, 464, 636, 3268,
|
||||||
|
3269 ni RPC dinámico. El conjunto de puertos necesario para una unión de dominio
|
||||||
|
es precisamente la razón de encapsularlo en P2S.
|
||||||
|
|
||||||
|
- [Azure VPN Gateway P2S con certificados](https://learn.microsoft.com/en-us/azure/vpn-gateway/point-to-site-certificate-gateway)
|
||||||
|
- [Cliente P2S nativo de Windows](https://learn.microsoft.com/en-us/azure/vpn-gateway/point-to-site-vpn-client-certificate)
|
||||||
|
- [Instalación de certificados P2S](https://learn.microsoft.com/en-us/azure/vpn-gateway/point-to-site-how-to-vpn-client-install-azure-cert)
|
||||||
|
- [Puertos necesarios para unir un dominio](https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/active-directory-domain-join-troubleshooting-guidance)
|
||||||
|
- [Requisitos de edición de Windows](https://learn.microsoft.com/en-us/windows/security/licensing-and-edition-requirements)
|
||||||
|
- [Limitación de Always On device tunnel](https://learn.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-always-on-device-tunnel)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Recuperación desde cero y alta en una sola ejecución
|
# Recuperación desde cero y alta en una sola ejecución
|
||||||
|
|
||||||
Los releases entregan tres ZIP independientes. Cada uno contiene sus binarios,
|
Los releases entregan cuatro ZIP independientes. Cada uno contiene sus binarios,
|
||||||
scripts, instalador offline requerido y un manifiesto SHA-256 interno. No contienen
|
scripts, instalador offline requerido y un manifiesto SHA-256 interno. No contienen
|
||||||
contraseñas, claves privadas ni certificados reutilizables.
|
contraseñas, claves privadas ni certificados reutilizables.
|
||||||
|
|
||||||
@@ -17,6 +17,11 @@ Compatible con Windows Server con Windows PowerShell 5.1. El servidor necesita
|
|||||||
una interfaz privada para el dominio y, para autenticar contra SGU, salida HTTPS
|
una interfaz privada para el dominio y, para autenticar contra SGU, salida HTTPS
|
||||||
por esa u otra interfaz.
|
por esa u otra interfaz.
|
||||||
|
|
||||||
|
Cuando el servidor vive en Azure, no se configura la IP dentro del sistema
|
||||||
|
operativo. La NIC reserva la IP privada y se usa el modo `PlatformManaged`; el
|
||||||
|
procedimiento completo, incluidos VPN Gateway y los clientes Hyper-V, está en
|
||||||
|
[azure-vpn-deployment.md](azure-vpn-deployment.md).
|
||||||
|
|
||||||
1. Descargar y extraer `sgu-server-bootstrap-VERSION.zip`.
|
1. Descargar y extraer `sgu-server-bootstrap-VERSION.zip`.
|
||||||
2. Abrir el directorio extraído.
|
2. Abrir el directorio extraído.
|
||||||
3. Ejecutar, indicando la IP fija que tendrá el controlador:
|
3. Ejecutar, indicando la IP fija que tendrá el controlador:
|
||||||
@@ -186,4 +191,4 @@ la línea de comandos. Para empaquetar recursos institucionales adicionales:
|
|||||||
-ServerContentPath C:\Preparacion\Packages
|
-ServerContentPath C:\Preparacion\Packages
|
||||||
```
|
```
|
||||||
|
|
||||||
`SHA256SUMS-VERSION.txt` permite comprobar los tres ZIP antes de usarlos.
|
`SHA256SUMS-VERSION.txt` permite comprobar los cuatro ZIP antes de usarlos.
|
||||||
|
|||||||
+17
-5
@@ -1,5 +1,14 @@
|
|||||||
# Security model
|
# Security model
|
||||||
|
|
||||||
|
## Public Azure deployment
|
||||||
|
|
||||||
|
Owning a public Azure IP does not make the domain controller an Internet-facing
|
||||||
|
directory service. The supported cloud topology exposes no AD DS, DNS, SMB,
|
||||||
|
RPC, WinRM, broker, monitoring, or RustDesk port publicly. Hyper-V and later
|
||||||
|
physical Windows clients enter the VNet through certificate-authenticated Azure
|
||||||
|
VPN Gateway P2S; the Azure NSG and Windows firewall accept the P2S pool and the
|
||||||
|
private VNet only. See [azure-vpn-deployment.md](azure-vpn-deployment.md).
|
||||||
|
|
||||||
## Password handling
|
## Password handling
|
||||||
|
|
||||||
- The Credential Provider receives the password in Lithnet's secure password
|
- The Credential Provider receives the password in Lithnet's secure password
|
||||||
@@ -33,15 +42,18 @@
|
|||||||
- Administrative enrichment first verifies the employee number and reads
|
- Administrative enrichment first verifies the employee number and reads
|
||||||
employee type/status, email, job title, and department from the incident
|
employee type/status, email, job title, and department from the incident
|
||||||
overview. Only after that match, it reads given names and paternal/maternal
|
overview. Only after that match, it reads given names and paternal/maternal
|
||||||
surnames from the personal page plus street, exterior/interior number,
|
surnames and the normalized `Male`/`Female` value from the personal page plus
|
||||||
neighborhood, locality, state, and postal code from the location page.
|
street, exterior/interior number, neighborhood, locality, state, and postal
|
||||||
- Administrative birth date, RFC, CURP, sex, blood type, marital status,
|
code from the location page. AD stores only the controlled `SGU-Gender` line,
|
||||||
|
not the original HTML field.
|
||||||
|
- Administrative birth date, RFC, CURP, blood type, marital status,
|
||||||
nationality, telephone, email lists, housing type, and emergency-contact
|
nationality, telephone, email lists, housing type, and emergency-contact
|
||||||
fields are ignored.
|
fields are ignored.
|
||||||
- Student enrichment reads only the matching student number, given names,
|
- Student enrichment reads only the matching student number, given names,
|
||||||
paternal/maternal surnames, email, career, street, neighborhood,
|
paternal/maternal surnames, email, career, street, neighborhood,
|
||||||
city/municipality, state, and postal code from known element IDs.
|
city/municipality, state, postal code, and normalized sex from known element
|
||||||
- Student CURP, birth date, sex, blood type, marital status, telephone, mobile,
|
IDs.
|
||||||
|
- Student CURP, birth date, blood type, marital status, telephone, mobile,
|
||||||
guardian, medical, financial, and academic-history values are ignored.
|
guardian, medical, financial, and academic-history values are ignored.
|
||||||
- Professor enrichment keeps the menu display name as its base. From the payroll
|
- Professor enrichment keeps the menu display name as its base. From the payroll
|
||||||
consultation header it reads only a matching employee number, name, email,
|
consultation header it reads only a matching employee number, name, email,
|
||||||
|
|||||||
@@ -0,0 +1,323 @@
|
|||||||
|
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('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('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([
|
||||||
|
{
|
||||||
|
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(administratorSourceAddressPrefix) ? [] : [
|
||||||
|
{
|
||||||
|
name: 'Allow-RDP-from-administrator'
|
||||||
|
properties: {
|
||||||
|
priority: 110
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
name: domainControllerSubnetName
|
||||||
|
properties: {
|
||||||
|
addressPrefix: domainControllerSubnetPrefix
|
||||||
|
networkSecurityGroup: {
|
||||||
|
id: networkSecurityGroup.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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' = {
|
||||||
|
name: gatewayPublicIpName
|
||||||
|
location: location
|
||||||
|
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'
|
||||||
|
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' = {
|
||||||
|
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: 'VpnGw1'
|
||||||
|
tier: 'VpnGw1'
|
||||||
|
}
|
||||||
|
vpnClientConfiguration: {
|
||||||
|
vpnClientAddressPool: {
|
||||||
|
addressPrefixes: [
|
||||||
|
vpnClientAddressPoolPrefix
|
||||||
|
]
|
||||||
|
}
|
||||||
|
vpnClientProtocols: [
|
||||||
|
'IkeV2'
|
||||||
|
'SSTP'
|
||||||
|
]
|
||||||
|
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 = virtualNetworkGateway.name
|
||||||
|
output vpnClientAddressPoolPrefix string = vpnClientAddressPoolPrefix
|
||||||
|
output serverBootstrapArguments array = [
|
||||||
|
'-ServerIPv4Address'
|
||||||
|
domainControllerPrivateIp
|
||||||
|
'-PrefixLength'
|
||||||
|
last(split(domainControllerSubnetPrefix, '/'))
|
||||||
|
'-NetworkConfigurationMode'
|
||||||
|
'PlatformManaged'
|
||||||
|
'-TrustedClientNetworks'
|
||||||
|
vpnClientAddressPoolPrefix
|
||||||
|
'-DnsForwarders'
|
||||||
|
'168.63.129.16'
|
||||||
|
]
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$SubscriptionId,
|
||||||
|
[string]$ResourceGroupName = 'rg-sgu-lab',
|
||||||
|
[string]$Location = 'centralus',
|
||||||
|
[string]$DeploymentPrefix = 'sgu-lab',
|
||||||
|
[Parameter(Mandatory)][string]$AdministratorUsername,
|
||||||
|
[securestring]$AdministratorPassword,
|
||||||
|
[Parameter(Mandatory)][string]$P2sRootCertificatePath,
|
||||||
|
[string]$ComputerName = 'SGU-DC01',
|
||||||
|
[string]$VmSize = 'Standard_D2s_v5',
|
||||||
|
[string]$VirtualNetworkAddressPrefix = '10.77.0.0/16',
|
||||||
|
[string]$DomainControllerSubnetPrefix = '10.77.0.0/24',
|
||||||
|
[ipaddress]$DomainControllerPrivateIp = '10.77.0.4',
|
||||||
|
[string]$GatewaySubnetPrefix = '10.77.255.0/27',
|
||||||
|
[string]$VpnClientAddressPoolPrefix = '172.30.0.0/24',
|
||||||
|
[string]$AdministratorSourceAddressPrefix = '',
|
||||||
|
[string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep')
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'Azure CLI is required. Install it from https://aka.ms/installazurecliwindows and run az login.'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $TemplateFile -PathType Leaf)) {
|
||||||
|
throw "Azure Bicep template not found: $TemplateFile"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $P2sRootCertificatePath -PathType Leaf)) {
|
||||||
|
throw "P2S root certificate not found: $P2sRootCertificatePath"
|
||||||
|
}
|
||||||
|
if (-not $AdministratorPassword) {
|
||||||
|
$AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootCertificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
||||||
|
(Resolve-Path -LiteralPath $P2sRootCertificatePath).Path)
|
||||||
|
if (-not ($rootCertificate.Extensions | Where-Object {
|
||||||
|
$_.Oid -and $_.Oid.Value -eq '2.5.29.19' -and $_.Format($false) -match 'CA' })) {
|
||||||
|
throw 'P2sRootCertificatePath must contain a certificate-authority certificate.'
|
||||||
|
}
|
||||||
|
$rootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData)
|
||||||
|
|
||||||
|
$account = & az account show --output json 2>$null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw 'Azure CLI is not signed in. Run az login, then retry.'
|
||||||
|
}
|
||||||
|
& az account set --subscription $SubscriptionId --only-show-errors
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Could not select Azure subscription $SubscriptionId."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", 'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway')) {
|
||||||
|
& az group create --name $ResourceGroupName --location $Location --only-show-errors --output none
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Could not create or update resource group $ResourceGroupName."
|
||||||
|
}
|
||||||
|
|
||||||
|
$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ("sgu-azure-" + [Guid]::NewGuid().ToString('N'))
|
||||||
|
$parametersPath = Join-Path $temporaryRoot 'parameters.json'
|
||||||
|
$passwordPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($AdministratorPassword)
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $temporaryRoot -Force | Out-Null
|
||||||
|
$acl = Get-Acl -LiteralPath $temporaryRoot
|
||||||
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
|
[Security.Principal.WindowsIdentity]::GetCurrent().User,
|
||||||
|
[Security.AccessControl.FileSystemRights]::FullControl,
|
||||||
|
[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',
|
||||||
|
[Security.AccessControl.PropagationFlags]::None,
|
||||||
|
[Security.AccessControl.AccessControlType]::Allow))
|
||||||
|
Set-Acl -LiteralPath $temporaryRoot -AclObject $acl
|
||||||
|
|
||||||
|
$plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($passwordPointer)
|
||||||
|
$parameters = [ordered]@{
|
||||||
|
'$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#'
|
||||||
|
contentVersion = '1.0.0.0'
|
||||||
|
parameters = [ordered]@{
|
||||||
|
deploymentPrefix = @{ value = $DeploymentPrefix }
|
||||||
|
location = @{ value = $Location }
|
||||||
|
administratorUsername = @{ value = $AdministratorUsername }
|
||||||
|
administratorPassword = @{ value = $plainPassword }
|
||||||
|
computerName = @{ value = $ComputerName }
|
||||||
|
vmSize = @{ value = $VmSize }
|
||||||
|
virtualNetworkAddressPrefix = @{ value = $VirtualNetworkAddressPrefix }
|
||||||
|
domainControllerSubnetPrefix = @{ value = $DomainControllerSubnetPrefix }
|
||||||
|
gatewaySubnetPrefix = @{ value = $GatewaySubnetPrefix }
|
||||||
|
domainControllerPrivateIp = @{ value = $DomainControllerPrivateIp.IPAddressToString }
|
||||||
|
vpnClientAddressPoolPrefix = @{ value = $VpnClientAddressPoolPrefix }
|
||||||
|
p2sRootCertificateData = @{ value = $rootCertificateData }
|
||||||
|
administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[IO.File]::WriteAllText(
|
||||||
|
$parametersPath,
|
||||||
|
($parameters | ConvertTo-Json -Depth 8),
|
||||||
|
[Text.UTF8Encoding]::new($false))
|
||||||
|
$plainPassword = $null
|
||||||
|
$parameters.parameters.administratorPassword.value = $null
|
||||||
|
|
||||||
|
$deploymentName = 'sgu-{0}' -f (Get-Date -Format 'yyyyMMdd-HHmmss')
|
||||||
|
$deploymentOutput = & az deployment group create `
|
||||||
|
--name $deploymentName `
|
||||||
|
--resource-group $ResourceGroupName `
|
||||||
|
--template-file (Resolve-Path -LiteralPath $TemplateFile).Path `
|
||||||
|
--parameters "@$parametersPath" `
|
||||||
|
--only-show-errors `
|
||||||
|
--output json
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw 'Azure deployment failed. Review the Azure CLI error above; no bootstrap credential was persisted by this script.'
|
||||||
|
}
|
||||||
|
$deployment = ($deploymentOutput -join [Environment]::NewLine) | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($passwordPointer -ne [IntPtr]::Zero) {
|
||||||
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordPointer)
|
||||||
|
}
|
||||||
|
$AdministratorPassword = $null
|
||||||
|
if ($temporaryRoot -and (Test-Path -LiteralPath $temporaryRoot)) {
|
||||||
|
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = @{}
|
||||||
|
foreach ($property in $deployment.properties.outputs.PSObject.Properties) {
|
||||||
|
$values[$property.Name] = $property.Value.value
|
||||||
|
}
|
||||||
|
[pscustomobject]@{
|
||||||
|
ResourceGroupName = $ResourceGroupName
|
||||||
|
DeploymentName = $deploymentName
|
||||||
|
DomainControllerName = $values.domainControllerName
|
||||||
|
DomainControllerPrivateIp = $values.domainControllerPrivateIp
|
||||||
|
DomainControllerPublicIp = $values.domainControllerPublicIp
|
||||||
|
VpnGatewayName = $values.vpnGatewayName
|
||||||
|
VpnClientAddressPoolPrefix = $values.vpnClientAddressPoolPrefix
|
||||||
|
ServerBootstrapArguments = $values.serverBootstrapArguments
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ param(
|
|||||||
[string]$NewComputerName,
|
[string]$NewComputerName,
|
||||||
[string]$NetworkInterfaceAlias = 'Ethernet',
|
[string]$NetworkInterfaceAlias = 'Ethernet',
|
||||||
[string[]]$DomainDnsServerAddresses = @('192.168.50.10'),
|
[string[]]$DomainDnsServerAddresses = @('192.168.50.10'),
|
||||||
|
[ValidateSet('Direct', 'AzureP2S')]
|
||||||
|
[string]$ConnectivityMode = 'Direct',
|
||||||
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||||
[string]$DotNetRuntimeInstallerPath,
|
[string]$DotNetRuntimeInstallerPath,
|
||||||
[string]$RustDeskServerAddress,
|
[string]$RustDeskServerAddress,
|
||||||
@@ -90,9 +92,21 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
|
|||||||
# The broker uses a domain DNS name even before the machine joins the
|
# The broker uses a domain DNS name even before the machine joins the
|
||||||
# domain. Point at AD DNS first so the provider-first health check works on
|
# domain. Point at AD DNS first so the provider-first health check works on
|
||||||
# a completely clean Windows installation.
|
# a completely clean Windows installation.
|
||||||
|
if ($ConnectivityMode -eq 'Direct') {
|
||||||
Set-DnsClientServerAddress `
|
Set-DnsClientServerAddress `
|
||||||
-InterfaceAlias $NetworkInterfaceAlias `
|
-InterfaceAlias $NetworkInterfaceAlias `
|
||||||
-ServerAddresses $DomainDnsServerAddresses
|
-ServerAddresses $DomainDnsServerAddresses
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
|
||||||
|
$nrptRule = Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object DisplayName -eq $nrptDisplayName |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $nrptRule -or
|
||||||
|
@($DomainDnsServerAddresses | Where-Object { @($nrptRule.NameServers) -contains $_ }).Count -eq 0) {
|
||||||
|
throw "AzureP2S enrollment requires the managed NRPT rule '$nrptDisplayName'. Run Install-SguAzureP2sClient.ps1 first."
|
||||||
|
}
|
||||||
|
}
|
||||||
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
|
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
& (Join-Path $PSScriptRoot 'Install-CredentialProvider.ps1') @installParams | Out-Null
|
& (Join-Path $PSScriptRoot 'Install-CredentialProvider.ps1') @installParams | Out-Null
|
||||||
@@ -159,6 +173,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before jo
|
|||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
||||||
DomainName = $DomainName
|
DomainName = $DomainName
|
||||||
|
ConnectivityMode = $ConnectivityMode
|
||||||
ProviderValidatedBeforeJoin = $true
|
ProviderValidatedBeforeJoin = $true
|
||||||
RustDesk = $rustDeskResult
|
RustDesk = $rustDeskResult
|
||||||
RestartRequired = [bool]$SkipRestart
|
RestartRequired = [bool]$SkipRestart
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$SubscriptionId,
|
||||||
|
[Parameter(Mandatory)][string]$ResourceGroupName,
|
||||||
|
[Parameter(Mandatory)][string]$VpnGatewayName,
|
||||||
|
[string]$OutputPath = (Join-Path $PSScriptRoot '..\artifacts\azure-p2s\sgu-azure-vpn-client.zip')
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
if (-not (Get-Command az -ErrorAction SilentlyContinue)) {
|
||||||
|
throw 'Azure CLI is required.'
|
||||||
|
}
|
||||||
|
& az account set --subscription $SubscriptionId --only-show-errors
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Could not select Azure subscription $SubscriptionId."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($OutputPath, 'Generate and download the Azure P2S client package')) {
|
||||||
|
$downloadUriText = & az network vnet-gateway vpn-client generate `
|
||||||
|
--resource-group $ResourceGroupName `
|
||||||
|
--name $VpnGatewayName `
|
||||||
|
--processor-architecture Amd64 `
|
||||||
|
--authentication-method EAPTLS `
|
||||||
|
--only-show-errors `
|
||||||
|
--output tsv
|
||||||
|
$downloadUriText = ($downloadUriText -join '').Trim()
|
||||||
|
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($downloadUriText)) {
|
||||||
|
throw 'Azure did not generate a P2S client package URL.'
|
||||||
|
}
|
||||||
|
$downloadUri = $null
|
||||||
|
if (-not [uri]::TryCreate($downloadUriText, [UriKind]::Absolute, [ref]$downloadUri) -or
|
||||||
|
$downloadUri.Scheme -ne 'https') {
|
||||||
|
throw 'Azure returned an invalid VPN client package URL.'
|
||||||
|
}
|
||||||
|
$resolvedOutputPath = [IO.Path]::GetFullPath($OutputPath)
|
||||||
|
New-Item -ItemType Directory -Path (Split-Path $resolvedOutputPath -Parent) -Force | Out-Null
|
||||||
|
Invoke-WebRequest -Uri $downloadUri -OutFile $resolvedOutputPath -UseBasicParsing
|
||||||
|
if ((Get-Item -LiteralPath $resolvedOutputPath).Length -lt 1024) {
|
||||||
|
throw 'The downloaded VPN client package is unexpectedly small.'
|
||||||
|
}
|
||||||
|
[pscustomobject]@{
|
||||||
|
PackagePath = $resolvedOutputPath
|
||||||
|
Sha256 = (Get-FileHash -LiteralPath $resolvedOutputPath -Algorithm SHA256).Hash
|
||||||
|
VpnGatewayName = $VpnGatewayName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,9 @@ param(
|
|||||||
[int]$PrefixLength = 24,
|
[int]$PrefixLength = 24,
|
||||||
[string]$NetworkInterfaceAlias,
|
[string]$NetworkInterfaceAlias,
|
||||||
[ipaddress]$DefaultGateway,
|
[ipaddress]$DefaultGateway,
|
||||||
|
[ValidateSet('GuestStatic', 'PlatformManaged')]
|
||||||
|
[string]$NetworkConfigurationMode = 'GuestStatic',
|
||||||
|
[string[]]$TrustedClientNetworks = @(),
|
||||||
[ipaddress[]]$DnsForwarders = @(),
|
[ipaddress[]]$DnsForwarders = @(),
|
||||||
[string]$DomainName = 'lci.lasalle.mx',
|
[string]$DomainName = 'lci.lasalle.mx',
|
||||||
[string]$DomainNetbios = 'LCI',
|
[string]$DomainNetbios = 'LCI',
|
||||||
@@ -69,6 +72,67 @@ function Get-DomainBaseDn {
|
|||||||
return (($DnsDomainName -split '\.') | ForEach-Object { "DC=$_" }) -join ','
|
return (($DnsDomainName -split '\.') | ForEach-Object { "DC=$_" }) -join ','
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-PrivateIPv4Address {
|
||||||
|
param([Parameter(Mandatory)][ipaddress]$Address)
|
||||||
|
|
||||||
|
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
$bytes = $Address.GetAddressBytes()
|
||||||
|
return $bytes[0] -eq 10 -or
|
||||||
|
($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
|
||||||
|
($bytes[0] -eq 192 -and $bytes[1] -eq 168)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NetworkCidr {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][ipaddress]$Address,
|
||||||
|
[Parameter(Mandatory)][ValidateRange(1, 32)][int]$NetworkPrefixLength
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
||||||
|
throw 'Only IPv4 networks are supported by the SGU bootstrap.'
|
||||||
|
}
|
||||||
|
$addressBytes = $Address.GetAddressBytes()
|
||||||
|
$networkBytes = [byte[]]::new(4)
|
||||||
|
$remainingBits = $NetworkPrefixLength
|
||||||
|
for ($index = 0; $index -lt 4; $index++) {
|
||||||
|
$mask = if ($remainingBits -ge 8) {
|
||||||
|
255
|
||||||
|
}
|
||||||
|
elseif ($remainingBits -le 0) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
256 - [Math]::Pow(2, 8 - $remainingBits)
|
||||||
|
}
|
||||||
|
$networkBytes[$index] = [byte]($addressBytes[$index] -band [int]$mask)
|
||||||
|
$remainingBits -= 8
|
||||||
|
}
|
||||||
|
return "$(($networkBytes | ForEach-Object { [string]$_ }) -join '.')/$NetworkPrefixLength"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-PrivateNetworkCidr {
|
||||||
|
param([Parameter(Mandatory)][string]$Cidr)
|
||||||
|
|
||||||
|
if ($Cidr -notmatch '^([^/]+)/(\d{1,2})$') {
|
||||||
|
throw "Trusted client network '$Cidr' must use IPv4 CIDR notation, for example 172.30.0.0/24."
|
||||||
|
}
|
||||||
|
$address = $null
|
||||||
|
if (-not [ipaddress]::TryParse($Matches[1], [ref]$address) -or
|
||||||
|
$address.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
|
||||||
|
throw "Trusted client network '$Cidr' is not a valid IPv4 network."
|
||||||
|
}
|
||||||
|
$networkPrefixLength = [int]$Matches[2]
|
||||||
|
if ($networkPrefixLength -lt 1 -or $networkPrefixLength -gt 32) {
|
||||||
|
throw "Trusted client network '$Cidr' has an invalid prefix length."
|
||||||
|
}
|
||||||
|
if (-not (Test-PrivateIPv4Address -Address $address)) {
|
||||||
|
throw "Trusted client network '$Cidr' is not private RFC1918 space. The bootstrap never exposes AD services to public client addresses."
|
||||||
|
}
|
||||||
|
return ConvertTo-NetworkCidr -Address $address -NetworkPrefixLength $networkPrefixLength
|
||||||
|
}
|
||||||
|
|
||||||
function Resolve-PrivateInterfaceAlias {
|
function Resolve-PrivateInterfaceAlias {
|
||||||
param([string]$RequestedAlias)
|
param([string]$RequestedAlias)
|
||||||
|
|
||||||
@@ -147,6 +211,27 @@ function Set-StaticDomainAddress {
|
|||||||
-ServerAddresses $Address.IPAddressToString
|
-ServerAddresses $Address.IPAddressToString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Assert-PlatformManagedDomainAddress {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$InterfaceAlias,
|
||||||
|
[Parameter(Mandatory)][ipaddress]$Address,
|
||||||
|
[Parameter(Mandatory)][int]$NetworkPrefixLength
|
||||||
|
)
|
||||||
|
|
||||||
|
$adapter = Get-NetAdapter -Name $InterfaceAlias -ErrorAction Stop
|
||||||
|
$matchingAddress = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
|
||||||
|
-IPAddress $Address.IPAddressToString -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object PrefixLength -eq $NetworkPrefixLength |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $matchingAddress) {
|
||||||
|
$observed = @(Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 `
|
||||||
|
-ErrorAction SilentlyContinue |
|
||||||
|
Where-Object PrefixOrigin -ne 'WellKnown' |
|
||||||
|
ForEach-Object { "$($_.IPAddress)/$($_.PrefixLength)" }) -join ', '
|
||||||
|
throw "PlatformManaged mode expected $Address/$NetworkPrefixLength on $InterfaceAlias, but found: $observed. Configure a static private IP on the Azure NIC before running the bootstrap; do not assign it inside Windows."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Register-ResumeTask {
|
function Register-ResumeTask {
|
||||||
param([Parameter(Mandatory)][string]$ScriptPath)
|
param([Parameter(Mandatory)][string]$ScriptPath)
|
||||||
|
|
||||||
@@ -310,6 +395,8 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
|
|||||||
$PrefixLength = [int]$existingState.PrefixLength
|
$PrefixLength = [int]$existingState.PrefixLength
|
||||||
$NetworkInterfaceAlias = [string]$existingState.NetworkInterfaceAlias
|
$NetworkInterfaceAlias = [string]$existingState.NetworkInterfaceAlias
|
||||||
$DefaultGateway = if ($existingState.DefaultGateway) { [ipaddress][string]$existingState.DefaultGateway } else { $null }
|
$DefaultGateway = if ($existingState.DefaultGateway) { [ipaddress][string]$existingState.DefaultGateway } else { $null }
|
||||||
|
$NetworkConfigurationMode = if ($existingState.NetworkConfigurationMode) { [string]$existingState.NetworkConfigurationMode } else { 'GuestStatic' }
|
||||||
|
$TrustedClientNetworks = if ($existingState.TrustedClientNetworks) { @($existingState.TrustedClientNetworks | ForEach-Object { [string]$_ }) } else { @() }
|
||||||
$DnsForwarders = @($existingState.DnsForwarders | ForEach-Object { [ipaddress][string]$_ })
|
$DnsForwarders = @($existingState.DnsForwarders | ForEach-Object { [ipaddress][string]$_ })
|
||||||
$DomainName = [string]$existingState.DomainName
|
$DomainName = [string]$existingState.DomainName
|
||||||
$DomainNetbios = [string]$existingState.DomainNetbios
|
$DomainNetbios = [string]$existingState.DomainNetbios
|
||||||
@@ -321,6 +408,16 @@ if ($Resume -or (-not $ServerIPv4Address -and $existingState)) {
|
|||||||
if (-not $ServerIPv4Address) {
|
if (-not $ServerIPv4Address) {
|
||||||
$ServerIPv4Address = [ipaddress](Read-Host 'Fixed IPv4 address for this domain controller')
|
$ServerIPv4Address = [ipaddress](Read-Host 'Fixed IPv4 address for this domain controller')
|
||||||
}
|
}
|
||||||
|
if (-not (Test-PrivateIPv4Address -Address $ServerIPv4Address)) {
|
||||||
|
throw 'ServerIPv4Address must be the private address of the domain controller. An Azure public IP is never assigned to AD or published in domain DNS.'
|
||||||
|
}
|
||||||
|
$domainSubnet = ConvertTo-NetworkCidr -Address $ServerIPv4Address `
|
||||||
|
-NetworkPrefixLength $PrefixLength
|
||||||
|
$TrustedClientNetworks = @($TrustedClientNetworks |
|
||||||
|
ForEach-Object { ConvertTo-PrivateNetworkCidr -Cidr $_ } |
|
||||||
|
Where-Object { $_ -ne $domainSubnet } |
|
||||||
|
Select-Object -Unique)
|
||||||
|
$allowedRemoteAddresses = @($domainSubnet) + $TrustedClientNetworks
|
||||||
|
|
||||||
$sourceRoot = $PSScriptRoot
|
$sourceRoot = $PSScriptRoot
|
||||||
if (-not $Resume) {
|
if (-not $Resume) {
|
||||||
@@ -383,6 +480,8 @@ if (-not $existingState) {
|
|||||||
PrefixLength = $PrefixLength
|
PrefixLength = $PrefixLength
|
||||||
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
||||||
DefaultGateway = if ($DefaultGateway) { $DefaultGateway.IPAddressToString } else { $null }
|
DefaultGateway = if ($DefaultGateway) { $DefaultGateway.IPAddressToString } else { $null }
|
||||||
|
NetworkConfigurationMode = $NetworkConfigurationMode
|
||||||
|
TrustedClientNetworks = $TrustedClientNetworks
|
||||||
DnsForwarders = @($DnsForwarders | ForEach-Object IPAddressToString)
|
DnsForwarders = @($DnsForwarders | ForEach-Object IPAddressToString)
|
||||||
DomainName = $DomainName
|
DomainName = $DomainName
|
||||||
DomainNetbios = $DomainNetbios
|
DomainNetbios = $DomainNetbios
|
||||||
@@ -396,9 +495,16 @@ if (-not $existingState) {
|
|||||||
[Text.UTF8Encoding]::new($false))
|
[Text.UTF8Encoding]::new($false))
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-BootstrapLog "Configuring $NetworkInterfaceAlias as $ServerIPv4Address/$PrefixLength."
|
if ($NetworkConfigurationMode -eq 'PlatformManaged') {
|
||||||
Set-StaticDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
|
Write-BootstrapLog "Validating platform-managed address $ServerIPv4Address/$PrefixLength on $NetworkInterfaceAlias without changing DHCP, routes, or the Azure NIC."
|
||||||
|
Assert-PlatformManagedDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
|
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-BootstrapLog "Configuring $NetworkInterfaceAlias as $ServerIPv4Address/$PrefixLength."
|
||||||
|
Set-StaticDomainAddress -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength -Gateway $DefaultGateway
|
-Address $ServerIPv4Address -NetworkPrefixLength $PrefixLength -Gateway $DefaultGateway
|
||||||
|
}
|
||||||
|
|
||||||
$computer = Get-CimInstance Win32_ComputerSystem
|
$computer = Get-CimInstance Win32_ComputerSystem
|
||||||
if (-not $computer.PartOfDomain) {
|
if (-not $computer.PartOfDomain) {
|
||||||
@@ -472,8 +578,17 @@ Wait-ActiveDirectoryReady -ExpectedBaseDn $baseDn
|
|||||||
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
-ErrorAction SilentlyContinue
|
-ErrorAction SilentlyContinue
|
||||||
if (-not $domainProfile -or $domainProfile.NetworkCategory -ne 'DomainAuthenticated') {
|
if (-not $domainProfile -or $domainProfile.NetworkCategory -ne 'DomainAuthenticated') {
|
||||||
|
if ($NetworkConfigurationMode -eq 'GuestStatic') {
|
||||||
Write-BootstrapLog "Refreshing $NetworkInterfaceAlias so Windows detects the domain network profile."
|
Write-BootstrapLog "Refreshing $NetworkInterfaceAlias so Windows detects the domain network profile."
|
||||||
Restart-NetAdapter -Name $NetworkInterfaceAlias -Confirm:$false
|
Restart-NetAdapter -Name $NetworkInterfaceAlias -Confirm:$false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# Restarting an Azure NIC from inside the guest can sever the only
|
||||||
|
# management path. Refresh NLA instead; this does not change the
|
||||||
|
# platform-managed address, DHCP lease, route, or link state.
|
||||||
|
Write-BootstrapLog 'Refreshing Network Location Awareness without restarting the Azure adapter.'
|
||||||
|
Restart-Service NlaSvc -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
for ($attempt = 1; $attempt -le 15; $attempt++) {
|
for ($attempt = 1; $attempt -le 15; $attempt++) {
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
$domainProfile = Get-NetConnectionProfile -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
@@ -577,7 +692,7 @@ if (Test-Path -LiteralPath $brokerConfigurationPath -PathType Leaf) {
|
|||||||
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
|
-RemoteDesktopGroupDn $remoteDesktopGroup.DistinguishedName `
|
||||||
-DefaultCompany 'La Salle' `
|
-DefaultCompany 'La Salle' `
|
||||||
-FirewallLocalAddress $ServerIPv4Address `
|
-FirewallLocalAddress $ServerIPv4Address `
|
||||||
-FirewallRemoteAddress "$($ServerIPv4Address.IPAddressToString)/$PrefixLength" `
|
-FirewallRemoteAddress $allowedRemoteAddresses `
|
||||||
-CreateMissingOus `
|
-CreateMissingOus `
|
||||||
-DisableCertificateRevocationCheckForLab | Out-Null
|
-DisableCertificateRevocationCheckForLab | Out-Null
|
||||||
|
|
||||||
@@ -591,9 +706,8 @@ foreach ($hostRecord in $hostRecords) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$privateSubnet = "$($ServerIPv4Address.IPAddressToString)/$PrefixLength"
|
|
||||||
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') `
|
& (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1') `
|
||||||
-AllowedRemoteAddress $privateSubnet | Out-Null
|
-AllowedRemoteAddress $allowedRemoteAddresses | Out-Null
|
||||||
|
|
||||||
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
|
$contentPath = Join-Path $bootstrapRoot 'payload\server-content\Packages'
|
||||||
if (Test-Path -LiteralPath $contentPath -PathType Container) {
|
if (Test-Path -LiteralPath $contentPath -PathType Container) {
|
||||||
@@ -613,7 +727,7 @@ if (-not $packageFirewallRule) {
|
|||||||
-Protocol TCP `
|
-Protocol TCP `
|
||||||
-LocalPort 445 `
|
-LocalPort 445 `
|
||||||
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
||||||
-RemoteAddress $privateSubnet `
|
-RemoteAddress $allowedRemoteAddresses `
|
||||||
-Profile Any | Out-Null
|
-Profile Any | Out-Null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -621,7 +735,7 @@ else {
|
|||||||
$packageFirewallRule | Get-NetFirewallAddressFilter |
|
$packageFirewallRule | Get-NetFirewallAddressFilter |
|
||||||
Set-NetFirewallAddressFilter `
|
Set-NetFirewallAddressFilter `
|
||||||
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
-LocalAddress $ServerIPv4Address.IPAddressToString `
|
||||||
-RemoteAddress $privateSubnet | Out-Null
|
-RemoteAddress $allowedRemoteAddresses | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
$collectorFqdn = "$env:COMPUTERNAME.$DomainName"
|
$collectorFqdn = "$env:COMPUTERNAME.$DomainName"
|
||||||
@@ -642,7 +756,7 @@ $userPolicyParameters = @{
|
|||||||
|
|
||||||
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
|
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
|
||||||
-ServerAddress $rustDeskDnsName `
|
-ServerAddress $rustDeskDnsName `
|
||||||
-FirewallRemoteAddress $privateSubnet
|
-FirewallRemoteAddress $allowedRemoteAddresses
|
||||||
$rustDeskManagementRoot = Join-Path $env:ProgramData 'SGU\RustDesk'
|
$rustDeskManagementRoot = Join-Path $env:ProgramData 'SGU\RustDesk'
|
||||||
New-Item -ItemType Directory -Path $rustDeskManagementRoot -Force | Out-Null
|
New-Item -ItemType Directory -Path $rustDeskManagementRoot -Force | Out-Null
|
||||||
foreach ($scriptName in @(
|
foreach ($scriptName in @(
|
||||||
@@ -680,6 +794,9 @@ $validation = [ordered]@{
|
|||||||
ComputerName = $env:COMPUTERNAME
|
ComputerName = $env:COMPUTERNAME
|
||||||
DomainName = $DomainName
|
DomainName = $DomainName
|
||||||
ServerIPv4Address = $ServerIPv4Address.IPAddressToString
|
ServerIPv4Address = $ServerIPv4Address.IPAddressToString
|
||||||
|
NetworkConfigurationMode = $NetworkConfigurationMode
|
||||||
|
TrustedClientNetworks = $TrustedClientNetworks
|
||||||
|
AllowedRemoteAddresses = $allowedRemoteAddresses
|
||||||
BrokerDnsName = $brokerDnsName
|
BrokerDnsName = $brokerDnsName
|
||||||
BrokerCertificateThumbprint = $serverCertificate.Thumbprint
|
BrokerCertificateThumbprint = $serverCertificate.Thumbprint
|
||||||
BrokerService = (Get-Service SGUAuthBroker).Status.ToString()
|
BrokerService = (Get-Service SGUAuthBroker).Status.ToString()
|
||||||
@@ -714,7 +831,8 @@ if ($validation.BrokerService -ne 'Running' -or
|
|||||||
-not $validation.RustDeskHbbsListening -or
|
-not $validation.RustDeskHbbsListening -or
|
||||||
-not $validation.RustDeskHbbrListening -or
|
-not $validation.RustDeskHbbrListening -or
|
||||||
$validation.EventCollector -ne 'Running' -or
|
$validation.EventCollector -ne 'Running' -or
|
||||||
-not $validation.EventSubscription) {
|
-not $validation.EventSubscription -or
|
||||||
|
$validation.DomainNetworkProfile -ne 'DomainAuthenticated') {
|
||||||
throw 'Server finalization did not pass service validation. Review bootstrap.log and re-run the bootstrap.'
|
throw 'Server finalization did not pass service validation. Review bootstrap.log and re-run the bootstrap.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$VpnProfilePackagePath,
|
||||||
|
[Parameter(Mandatory)][string]$ClientCertificatePfxPath,
|
||||||
|
[securestring]$ClientCertificatePfxPassword,
|
||||||
|
[Parameter(Mandatory)][string]$ClientRootCertificatePath,
|
||||||
|
[string]$ConnectionName = 'SGU Azure P2S',
|
||||||
|
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
|
||||||
|
[ipaddress]$DomainControllerIPv4Address = '10.77.0.4',
|
||||||
|
[string]$DomainName = 'lci.lasalle.mx',
|
||||||
|
[switch]$Connect
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
foreach ($path in @($VpnProfilePackagePath,$ClientCertificatePfxPath,$ClientRootCertificatePath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||||
|
throw "Required P2S file not found: $path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $ClientCertificatePfxPassword) {
|
||||||
|
$ClientCertificatePfxPassword = Read-Host 'Password protecting the P2S client PFX' -AsSecureString
|
||||||
|
}
|
||||||
|
|
||||||
|
$temporaryRoot = Join-Path $env:ProgramData ("SGU\AzureP2S\Import-" + [Guid]::NewGuid().ToString('N'))
|
||||||
|
try {
|
||||||
|
Expand-Archive -LiteralPath $VpnProfilePackagePath -DestinationPath $temporaryRoot -Force
|
||||||
|
$vpnSettingsPath = Get-ChildItem -LiteralPath $temporaryRoot -Recurse -Filter VpnSettings.xml -File |
|
||||||
|
Select-Object -First 1 -ExpandProperty FullName
|
||||||
|
if (-not $vpnSettingsPath) {
|
||||||
|
throw 'The Azure package does not contain Generic\VpnSettings.xml. Generate it with IKEv2 enabled.'
|
||||||
|
}
|
||||||
|
[xml]$vpnSettings = Get-Content -LiteralPath $vpnSettingsPath -Raw
|
||||||
|
$vpnServerNode = $vpnSettings.SelectSingleNode('//*[local-name()="VpnServer"]')
|
||||||
|
if (-not $vpnServerNode -or [string]::IsNullOrWhiteSpace($vpnServerNode.InnerText)) {
|
||||||
|
throw 'VpnSettings.xml does not contain the Azure VPN gateway FQDN.'
|
||||||
|
}
|
||||||
|
$vpnServer = $vpnServerNode.InnerText.Trim()
|
||||||
|
|
||||||
|
$serverRootPath = Get-ChildItem -LiteralPath (Split-Path $vpnSettingsPath -Parent) `
|
||||||
|
-Filter VpnServerRoot.cer -File | Select-Object -First 1 -ExpandProperty FullName
|
||||||
|
if ($serverRootPath) {
|
||||||
|
Import-Certificate -FilePath $serverRootPath -CertStoreLocation Cert:\LocalMachine\Root | Out-Null
|
||||||
|
}
|
||||||
|
$clientRoot = Import-Certificate -FilePath $ClientRootCertificatePath `
|
||||||
|
-CertStoreLocation Cert:\LocalMachine\Root | Select-Object -First 1
|
||||||
|
$clientCertificates = @(Import-PfxCertificate -FilePath $ClientCertificatePfxPath `
|
||||||
|
-Password $ClientCertificatePfxPassword -CertStoreLocation Cert:\LocalMachine\My)
|
||||||
|
$clientCertificate = $clientCertificates |
|
||||||
|
Where-Object {
|
||||||
|
$_.HasPrivateKey -and
|
||||||
|
$_.NotAfter -gt (Get-Date) -and
|
||||||
|
@($_.EnhancedKeyUsageList | ForEach-Object ObjectId) -contains '1.3.6.1.5.5.7.3.2'
|
||||||
|
} |
|
||||||
|
Sort-Object NotAfter -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $clientCertificate) {
|
||||||
|
throw 'The imported PFX does not contain a valid Client Authentication certificate with a private key.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($ConnectionName, 'Install an all-user IKEv2 Azure P2S connection using a machine certificate')) {
|
||||||
|
$existingConnection = Get-VpnConnection -Name $ConnectionName -AllUserConnection `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if ($existingConnection) {
|
||||||
|
Remove-VpnConnection -Name $ConnectionName -AllUserConnection -Force
|
||||||
|
}
|
||||||
|
Add-VpnConnection `
|
||||||
|
-Name $ConnectionName `
|
||||||
|
-ServerAddress $vpnServer `
|
||||||
|
-TunnelType Ikev2 `
|
||||||
|
-AuthenticationMethod MachineCertificate `
|
||||||
|
-MachineCertificateIssuerFilter $clientRoot `
|
||||||
|
-MachineCertificateEKUFilter '1.3.6.1.5.5.7.3.2' `
|
||||||
|
-EncryptionLevel Required `
|
||||||
|
-SplitTunneling `
|
||||||
|
-AllUserConnection `
|
||||||
|
-DnsSuffix $DomainName `
|
||||||
|
-Force | Out-Null
|
||||||
|
foreach ($prefix in $AzureNetworkPrefixes) {
|
||||||
|
Add-VpnConnectionRoute -ConnectionName $ConnectionName `
|
||||||
|
-DestinationPrefix $prefix -AllUserConnection -PassThru | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
|
||||||
|
Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object DisplayName -eq $nrptDisplayName |
|
||||||
|
Remove-DnsClientNrptRule -Force
|
||||||
|
Add-DnsClientNrptRule `
|
||||||
|
-Namespace ".$DomainName" `
|
||||||
|
-NameServers $DomainControllerIPv4Address.IPAddressToString `
|
||||||
|
-DisplayName $nrptDisplayName `
|
||||||
|
-Comment 'Managed by SGU Azure P2S bootstrap; routes only the AD namespace to the domain controller.' | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Connect) {
|
||||||
|
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Windows could not connect $ConnectionName. Verify UDP 500/4500 (IKEv2) or use the Azure-generated SSTP profile when the local network blocks IKEv2."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection
|
||||||
|
[pscustomobject]@{
|
||||||
|
ConnectionName = $connection.Name
|
||||||
|
ServerAddress = $connection.ServerAddress
|
||||||
|
TunnelType = $connection.TunnelType
|
||||||
|
AllUserConnection = $true
|
||||||
|
AuthenticationMethod = $connection.AuthenticationMethod
|
||||||
|
ConnectionStatus = $connection.ConnectionStatus
|
||||||
|
ClientCertificateThumbprint = $clientCertificate.Thumbprint
|
||||||
|
DomainControllerIPv4Address = $DomainControllerIPv4Address.IPAddressToString
|
||||||
|
DomainDnsNamespace = ".$DomainName"
|
||||||
|
AzureNetworkPrefixes = $AzureNetworkPrefixes
|
||||||
|
AvailableBeforeLogon = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$ClientCertificatePfxPassword = $null
|
||||||
|
if (Test-Path -LiteralPath $temporaryRoot) {
|
||||||
|
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ param(
|
|||||||
|
|
||||||
[string]$InstallRoot = "$env:ProgramFiles\SGU\RustDeskServer",
|
[string]$InstallRoot = "$env:ProgramFiles\SGU\RustDeskServer",
|
||||||
[string]$DataRoot = "$env:ProgramData\SGU\RustDesk\Server",
|
[string]$DataRoot = "$env:ProgramData\SGU\RustDesk\Server",
|
||||||
[string]$FirewallRemoteAddress = '192.168.50.0/24',
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string[]]$FirewallRemoteAddress = @('192.168.50.0/24'),
|
||||||
[uri]$DownloadUri = 'https://github.com/rustdesk/rustdesk-server/releases/download/1.1.16/rustdesk-server-windows-x86_64-unsigned.zip',
|
[uri]$DownloadUri = 'https://github.com/rustdesk/rustdesk-server/releases/download/1.1.16/rustdesk-server-windows-x86_64-unsigned.zip',
|
||||||
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
[ValidatePattern('^[A-Fa-f0-9]{64}$')]
|
||||||
[string]$ExpectedSha256 = 'B865A3A62FC8755B45480C508F1C4871C3338590408DDA8C58C7E9C373B7ADB0'
|
[string]$ExpectedSha256 = 'B865A3A62FC8755B45480C508F1C4871C3338590408DDA8C58C7E9C373B7ADB0'
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ param(
|
|||||||
[string]$DomainNetbios = 'LCI',
|
[string]$DomainNetbios = 'LCI',
|
||||||
[string]$ComputerOuDn,
|
[string]$ComputerOuDn,
|
||||||
[string]$NewComputerName,
|
[string]$NewComputerName,
|
||||||
|
[ValidateSet('Direct', 'AzureP2S')]
|
||||||
|
[string]$ConnectivityMode = 'Direct',
|
||||||
|
[string]$VpnConnectionName = 'SGU Azure P2S',
|
||||||
|
[string]$VpnProfilePackagePath,
|
||||||
|
[string]$VpnClientCertificatePfxPath,
|
||||||
|
[securestring]$VpnClientCertificatePfxPassword,
|
||||||
|
[string]$VpnClientRootCertificatePath,
|
||||||
|
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
|
||||||
[switch]$SkipRestart
|
[switch]$SkipRestart
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,6 +103,30 @@ function Test-TcpPort {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Connect-SguAzureP2s {
|
||||||
|
param([Parameter(Mandatory)][string]$ConnectionName)
|
||||||
|
|
||||||
|
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $connection) {
|
||||||
|
throw "The all-user VPN connection '$ConnectionName' is not installed. Run Install-SguAzureP2sClient.ps1 in this VM first."
|
||||||
|
}
|
||||||
|
if ($connection.TunnelType -notcontains 'Ikev2' -and $connection.TunnelType -ne 'Ikev2') {
|
||||||
|
throw "The VPN connection '$ConnectionName' is not configured for IKEv2."
|
||||||
|
}
|
||||||
|
if ($connection.ConnectionStatus -ne 'Connected') {
|
||||||
|
& "$env:SystemRoot\System32\rasdial.exe" $ConnectionName
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Could not connect the Azure P2S profile '$ConnectionName'. Verify the machine certificate and that UDP 500/4500 is permitted by the local network."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$connection = Get-VpnConnection -Name $ConnectionName -AllUserConnection
|
||||||
|
if ($connection.ConnectionStatus -ne 'Connected') {
|
||||||
|
throw "The Azure P2S profile '$ConnectionName' did not reach Connected state."
|
||||||
|
}
|
||||||
|
return $connection
|
||||||
|
}
|
||||||
|
|
||||||
Assert-Administrator
|
Assert-Administrator
|
||||||
$operatingSystem = Get-CimInstance Win32_OperatingSystem
|
$operatingSystem = Get-CimInstance Win32_OperatingSystem
|
||||||
if ([int]$operatingSystem.ProductType -ne 1) {
|
if ([int]$operatingSystem.ProductType -ne 1) {
|
||||||
@@ -135,9 +167,52 @@ if (-not $runtimeInstaller) {
|
|||||||
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
|
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
|
||||||
}
|
}
|
||||||
|
|
||||||
$NetworkInterfaceAlias = Resolve-ClientInterfaceAlias -RequestedAlias $NetworkInterfaceAlias
|
if ($ConnectivityMode -eq 'AzureP2S') {
|
||||||
Set-DnsClientServerAddress -InterfaceAlias $NetworkInterfaceAlias `
|
$existingVpnConnection = Get-VpnConnection -Name $VpnConnectionName -AllUserConnection `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $existingVpnConnection) {
|
||||||
|
$installerPath = Join-Path $packageRoot 'Install-SguAzureP2sClient.ps1'
|
||||||
|
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
|
||||||
|
throw 'Install-SguAzureP2sClient.ps1 is missing from the client bootstrap package.'
|
||||||
|
}
|
||||||
|
foreach ($vpnInput in @(
|
||||||
|
@{ Name = 'VpnProfilePackagePath'; Value = $VpnProfilePackagePath },
|
||||||
|
@{ Name = 'VpnClientCertificatePfxPath'; Value = $VpnClientCertificatePfxPath },
|
||||||
|
@{ Name = 'VpnClientRootCertificatePath'; Value = $VpnClientRootCertificatePath })) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$vpnInput.Value)) {
|
||||||
|
throw "$($vpnInput.Name) is required the first time an Azure P2S client is enrolled."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$vpnInstallParameters = @{
|
||||||
|
VpnProfilePackagePath = $VpnProfilePackagePath
|
||||||
|
ClientCertificatePfxPath = $VpnClientCertificatePfxPath
|
||||||
|
ClientRootCertificatePath = $VpnClientRootCertificatePath
|
||||||
|
ConnectionName = $VpnConnectionName
|
||||||
|
AzureNetworkPrefixes = $AzureNetworkPrefixes
|
||||||
|
DomainControllerIPv4Address = $DomainControllerIPv4Address
|
||||||
|
DomainName = $DomainName
|
||||||
|
}
|
||||||
|
if ($VpnClientCertificatePfxPassword) {
|
||||||
|
$vpnInstallParameters.ClientCertificatePfxPassword = $VpnClientCertificatePfxPassword
|
||||||
|
}
|
||||||
|
& $installerPath @vpnInstallParameters | Out-Null
|
||||||
|
}
|
||||||
|
$vpnConnection = Connect-SguAzureP2s -ConnectionName $VpnConnectionName
|
||||||
|
$nrptDisplayName = "SGU Azure P2S DNS - $DomainName"
|
||||||
|
$nrptRule = Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object DisplayName -eq $nrptDisplayName |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $nrptRule -or
|
||||||
|
@($nrptRule.NameServers) -notcontains $DomainControllerIPv4Address.IPAddressToString) {
|
||||||
|
throw "The SGU NRPT rule for $DomainName is missing or does not point to $DomainControllerIPv4Address. Re-run Install-SguAzureP2sClient.ps1."
|
||||||
|
}
|
||||||
|
$NetworkInterfaceAlias = $vpnConnection.Name
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$NetworkInterfaceAlias = Resolve-ClientInterfaceAlias -RequestedAlias $NetworkInterfaceAlias
|
||||||
|
Set-DnsClientServerAddress -InterfaceAlias $NetworkInterfaceAlias `
|
||||||
-ServerAddresses $DomainControllerIPv4Address.IPAddressToString
|
-ServerAddresses $DomainControllerIPv4Address.IPAddressToString
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port 5985)) {
|
if (-not (Test-TcpPort -Address $DomainControllerIPv4Address -Port 5985)) {
|
||||||
throw "The domain controller at $DomainControllerIPv4Address is not accepting WinRM on TCP 5985. Run the server bootstrap first and verify the selected IP."
|
throw "The domain controller at $DomainControllerIPv4Address is not accepting WinRM on TCP 5985. Run the server bootstrap first and verify the selected IP."
|
||||||
@@ -292,6 +367,7 @@ try {
|
|||||||
ComputerOuDn = $ComputerOuDn
|
ComputerOuDn = $ComputerOuDn
|
||||||
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
NetworkInterfaceAlias = $NetworkInterfaceAlias
|
||||||
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
|
DomainDnsServerAddresses = @($DomainControllerIPv4Address.IPAddressToString)
|
||||||
|
ConnectivityMode = $ConnectivityMode
|
||||||
RemoteDesktopPrincipal = "$DomainNetbios\SG-Laboratorio-Usuarios-RDP"
|
RemoteDesktopPrincipal = "$DomainNetbios\SG-Laboratorio-Usuarios-RDP"
|
||||||
DotNetRuntimeInstallerPath = $runtimeInstaller.FullName
|
DotNetRuntimeInstallerPath = $runtimeInstaller.FullName
|
||||||
RustDeskServerAddress = $serverIdentity.RustDeskServerAddress
|
RustDeskServerAddress = $serverIdentity.RustDeskServerAddress
|
||||||
@@ -356,6 +432,7 @@ finally {
|
|||||||
}
|
}
|
||||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
$DomainCredential = $null
|
$DomainCredential = $null
|
||||||
|
$VpnClientCertificatePfxPassword = $null
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($SkipRestart) {
|
if ($SkipRestart) {
|
||||||
@@ -365,6 +442,8 @@ if ($SkipRestart) {
|
|||||||
ProviderInstalled = $true
|
ProviderInstalled = $true
|
||||||
ClientCertificateRegistered = $true
|
ClientCertificateRegistered = $true
|
||||||
BrokerEndpoint = $brokerEndpoint
|
BrokerEndpoint = $brokerEndpoint
|
||||||
|
ConnectivityMode = $ConnectivityMode
|
||||||
|
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
|
||||||
RestartRequired = $true
|
RestartRequired = $true
|
||||||
RustDesk = if ($result) { $result.RustDesk } else { $null }
|
RustDesk = if ($result) { $result.RustDesk } else { $null }
|
||||||
EnrollmentResult = $result
|
EnrollmentResult = $result
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$')]
|
||||||
|
[string]$ClientName,
|
||||||
|
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\artifacts\azure-p2s'),
|
||||||
|
[securestring]$ClientPfxPassword,
|
||||||
|
[string]$RootSubject = 'CN=SGU Azure P2S Root',
|
||||||
|
[ValidateRange(1, 10)]
|
||||||
|
[int]$ClientValidityYears = 2,
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$resolvedOutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||||
|
New-Item -ItemType Directory -Path $resolvedOutputDirectory -Force | Out-Null
|
||||||
|
$rootCertificatePath = Join-Path $resolvedOutputDirectory 'sgu-azure-p2s-root.cer'
|
||||||
|
$clientCertificatePath = Join-Path $resolvedOutputDirectory "sgu-azure-p2s-$ClientName.pfx"
|
||||||
|
if ((Test-Path -LiteralPath $clientCertificatePath -PathType Leaf) -and -not $Force) {
|
||||||
|
throw "$clientCertificatePath already exists. Use -Force only when you intend to replace that exported client credential."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $ClientPfxPassword) {
|
||||||
|
$ClientPfxPassword = Read-Host 'Password that will protect the exported P2S client certificate' -AsSecureString
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootCertificate = Get-ChildItem Cert:\CurrentUser\My |
|
||||||
|
Where-Object {
|
||||||
|
$_.Subject -eq $RootSubject -and
|
||||||
|
$_.HasPrivateKey -and
|
||||||
|
$_.NotAfter -gt (Get-Date).AddYears($ClientValidityYears)
|
||||||
|
} |
|
||||||
|
Sort-Object NotAfter -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $rootCertificate) {
|
||||||
|
if (-not $PSCmdlet.ShouldProcess($RootSubject, 'Create a non-exportable Azure P2S root certificate authority')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$rootCertificate = New-SelfSignedCertificate `
|
||||||
|
-Type Custom `
|
||||||
|
-Subject $RootSubject `
|
||||||
|
-CertStoreLocation Cert:\CurrentUser\My `
|
||||||
|
-KeyAlgorithm RSA `
|
||||||
|
-KeyLength 4096 `
|
||||||
|
-HashAlgorithm SHA256 `
|
||||||
|
-KeySpec Signature `
|
||||||
|
-KeyExportPolicy NonExportable `
|
||||||
|
-KeyUsage CertSign,CRLSign,DigitalSignature `
|
||||||
|
-NotAfter (Get-Date).AddYears(10) `
|
||||||
|
-TextExtension @('2.5.29.19={critical}{text}ca=1&pathlength=1')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $PSCmdlet.ShouldProcess($ClientName, 'Issue and export an Azure P2S machine certificate')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientSubject = "CN=SGU Azure P2S $ClientName"
|
||||||
|
$clientCertificate = New-SelfSignedCertificate `
|
||||||
|
-Type Custom `
|
||||||
|
-Subject $clientSubject `
|
||||||
|
-DnsName "sgu-p2s-$ClientName" `
|
||||||
|
-Signer $rootCertificate `
|
||||||
|
-CertStoreLocation Cert:\CurrentUser\My `
|
||||||
|
-KeyAlgorithm RSA `
|
||||||
|
-KeyLength 3072 `
|
||||||
|
-HashAlgorithm SHA256 `
|
||||||
|
-KeySpec Signature `
|
||||||
|
-KeyExportPolicy Exportable `
|
||||||
|
-KeyUsage DigitalSignature `
|
||||||
|
-NotAfter (Get-Date).AddYears($ClientValidityYears) `
|
||||||
|
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.2')
|
||||||
|
|
||||||
|
Export-Certificate -Cert $rootCertificate -FilePath $rootCertificatePath -Force | Out-Null
|
||||||
|
Export-PfxCertificate -Cert $clientCertificate -FilePath $clientCertificatePath `
|
||||||
|
-Password $ClientPfxPassword -ChainOption BuildChain -CryptoAlgorithmOption AES256_SHA256 `
|
||||||
|
-Force | Out-Null
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
RootCertificatePath = $rootCertificatePath
|
||||||
|
RootCertificateThumbprint = $rootCertificate.Thumbprint
|
||||||
|
RootCertificateData = [Convert]::ToBase64String($rootCertificate.RawData)
|
||||||
|
ClientName = $ClientName
|
||||||
|
ClientCertificatePath = $clientCertificatePath
|
||||||
|
ClientCertificateThumbprint = $clientCertificate.Thumbprint
|
||||||
|
ClientCertificateExpires = $clientCertificate.NotAfter
|
||||||
|
RootPrivateKeyExportable = $false
|
||||||
|
}
|
||||||
@@ -81,16 +81,18 @@ New-Item -ItemType Directory -Path $resolvedOutputRoot -Force | Out-Null
|
|||||||
$clientRoot = Join-Path $resolvedOutputRoot "sgu-client-bootstrap-$Version"
|
$clientRoot = Join-Path $resolvedOutputRoot "sgu-client-bootstrap-$Version"
|
||||||
$serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version"
|
$serverRoot = Join-Path $resolvedOutputRoot "sgu-server-bootstrap-$Version"
|
||||||
$linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version"
|
$linuxClientRoot = Join-Path $resolvedOutputRoot "sgu-linux-client-bootstrap-$Version"
|
||||||
|
$azureRoot = Join-Path $resolvedOutputRoot "sgu-azure-infrastructure-$Version"
|
||||||
$clientZip = "$clientRoot.zip"
|
$clientZip = "$clientRoot.zip"
|
||||||
$serverZip = "$serverRoot.zip"
|
$serverZip = "$serverRoot.zip"
|
||||||
$linuxClientZip = "$linuxClientRoot.zip"
|
$linuxClientZip = "$linuxClientRoot.zip"
|
||||||
foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$clientZip,$serverZip,$linuxClientZip)) {
|
$azureZip = "$azureRoot.zip"
|
||||||
|
foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$azureRoot,$clientZip,$serverZip,$linuxClientZip,$azureZip)) {
|
||||||
if (Test-Path -LiteralPath $target) {
|
if (Test-Path -LiteralPath $target) {
|
||||||
throw "Release target already exists: $target"
|
throw "Release target already exists: $target"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot -Force | Out-Null
|
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot,$azureRoot -Force | Out-Null
|
||||||
$welcomeFontNames = @(
|
$welcomeFontNames = @(
|
||||||
'IndivisaTextSans-Regular.otf',
|
'IndivisaTextSans-Regular.otf',
|
||||||
'IndivisaTextSans-Bold.otf',
|
'IndivisaTextSans-Bold.otf',
|
||||||
@@ -103,6 +105,10 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Invoke-SguClientBootstrap.ps
|
|||||||
-Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1')
|
-Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1')
|
||||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') `
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguClientEnrollment.cmd') `
|
||||||
-Destination (Join-Path $clientRoot 'Start-SguClientEnrollment.cmd')
|
-Destination (Join-Path $clientRoot 'Start-SguClientEnrollment.cmd')
|
||||||
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureClientEnrollment.cmd') `
|
||||||
|
-Destination (Join-Path $clientRoot 'Start-SguAzureClientEnrollment.cmd')
|
||||||
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Install-SguAzureP2sClient.ps1') `
|
||||||
|
-Destination (Join-Path $clientRoot 'Install-SguAzureP2sClient.ps1')
|
||||||
$clientScripts = @(
|
$clientScripts = @(
|
||||||
'Enable-LabRemoteAccess.ps1',
|
'Enable-LabRemoteAccess.ps1',
|
||||||
'Enable-SguClientMonitoring.ps1',
|
'Enable-SguClientMonitoring.ps1',
|
||||||
@@ -163,6 +169,8 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Initialize-SguDomainControll
|
|||||||
-Destination (Join-Path $serverRoot 'Initialize-SguDomainController.ps1')
|
-Destination (Join-Path $serverRoot 'Initialize-SguDomainController.ps1')
|
||||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguServerBootstrap.cmd') `
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguServerBootstrap.cmd') `
|
||||||
-Destination (Join-Path $serverRoot 'Start-SguServerBootstrap.cmd')
|
-Destination (Join-Path $serverRoot 'Start-SguServerBootstrap.cmd')
|
||||||
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstrap.cmd') `
|
||||||
|
-Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd')
|
||||||
$serverScripts = @(
|
$serverScripts = @(
|
||||||
'Deploy-AuthBroker.ps1',
|
'Deploy-AuthBroker.ps1',
|
||||||
'Enable-SguServerRemoteManagement.ps1',
|
'Enable-SguServerRemoteManagement.ps1',
|
||||||
@@ -211,10 +219,32 @@ Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -Package
|
|||||||
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
|
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
|
||||||
-CompressionLevel Optimal
|
-CompressionLevel Optimal
|
||||||
|
|
||||||
|
# Azure infrastructure is packaged separately because it runs on the trusted
|
||||||
|
# administrator workstation, not inside the domain controller or a client.
|
||||||
|
$azureScriptsRoot = Join-Path $azureRoot 'scripts'
|
||||||
|
$azureInfrastructureRoot = Join-Path $azureRoot 'infra\azure'
|
||||||
|
New-Item -ItemType Directory -Path $azureScriptsRoot,$azureInfrastructureRoot -Force | Out-Null
|
||||||
|
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'infra\azure\main.bicep') `
|
||||||
|
-Destination (Join-Path $azureInfrastructureRoot 'main.bicep')
|
||||||
|
foreach ($scriptName in @(
|
||||||
|
'New-SguAzureP2sCertificates.ps1',
|
||||||
|
'Deploy-SguAzureInfrastructure.ps1',
|
||||||
|
'Get-SguAzureP2sPackage.ps1',
|
||||||
|
'Install-SguAzureP2sClient.ps1')) {
|
||||||
|
Copy-RequiredFile -Source (Join-Path $PSScriptRoot $scriptName) `
|
||||||
|
-Destination (Join-Path $azureScriptsRoot $scriptName)
|
||||||
|
}
|
||||||
|
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') `
|
||||||
|
-Destination (Join-Path $azureRoot 'README.md')
|
||||||
|
Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure
|
||||||
|
Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip `
|
||||||
|
-CompressionLevel Optimal
|
||||||
|
|
||||||
$checksums = @(
|
$checksums = @(
|
||||||
("{0} {1}" -f (Get-FileHash -LiteralPath $clientZip -Algorithm SHA256).Hash, (Split-Path $clientZip -Leaf))
|
("{0} {1}" -f (Get-FileHash -LiteralPath $clientZip -Algorithm SHA256).Hash, (Split-Path $clientZip -Leaf))
|
||||||
("{0} {1}" -f (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash, (Split-Path $serverZip -Leaf))
|
("{0} {1}" -f (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash, (Split-Path $serverZip -Leaf))
|
||||||
("{0} {1}" -f (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash, (Split-Path $linuxClientZip -Leaf))
|
("{0} {1}" -f (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash, (Split-Path $linuxClientZip -Leaf))
|
||||||
|
("{0} {1}" -f (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash, (Split-Path $azureZip -Leaf))
|
||||||
)
|
)
|
||||||
$checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
|
$checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
|
||||||
[IO.File]::WriteAllLines($checksumsPath, $checksums, [Text.UTF8Encoding]::new($false))
|
[IO.File]::WriteAllLines($checksumsPath, $checksums, [Text.UTF8Encoding]::new($false))
|
||||||
@@ -227,6 +257,8 @@ $checksumsPath = Join-Path $resolvedOutputRoot "SHA256SUMS-$Version.txt"
|
|||||||
LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash
|
LinuxClientSha256 = (Get-FileHash -LiteralPath $linuxClientZip -Algorithm SHA256).Hash
|
||||||
ServerPackage = $serverZip
|
ServerPackage = $serverZip
|
||||||
ServerSha256 = (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash
|
ServerSha256 = (Get-FileHash -LiteralPath $serverZip -Algorithm SHA256).Hash
|
||||||
|
AzureInfrastructurePackage = $azureZip
|
||||||
|
AzureInfrastructureSha256 = (Get-FileHash -LiteralPath $azureZip -Algorithm SHA256).Hash
|
||||||
Checksums = $checksumsPath
|
Checksums = $checksumsPath
|
||||||
RuntimeInstaller = $runtimeInstaller.Name
|
RuntimeInstaller = $runtimeInstaller.Name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ $assetPaths = @(
|
|||||||
(Join-Path $ReleaseDirectory "sgu-client-bootstrap-$Version.zip"),
|
(Join-Path $ReleaseDirectory "sgu-client-bootstrap-$Version.zip"),
|
||||||
(Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"),
|
(Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"),
|
||||||
(Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"),
|
(Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"),
|
||||||
|
(Join-Path $ReleaseDirectory "sgu-azure-infrastructure-$Version.zip"),
|
||||||
(Join-Path $ReleaseDirectory "SHA256SUMS-$Version.txt")
|
(Join-Path $ReleaseDirectory "SHA256SUMS-$Version.txt")
|
||||||
)
|
)
|
||||||
foreach ($assetPath in $assetPaths) {
|
foreach ($assetPath in $assetPaths) {
|
||||||
@@ -112,6 +113,9 @@ Bootstrap reproducible para el laboratorio SGU.
|
|||||||
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
|
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
|
||||||
- `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque.
|
- `sgu-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque.
|
||||||
- `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows.
|
- `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows.
|
||||||
|
- `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente.
|
||||||
|
- El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet.
|
||||||
|
- Los Windows 11 Pro pueden instalar un perfil IKEv2 de todos los usuarios con certificado de máquina, DNS dividido para `lci.lasalle.mx` y ejecutarlo desde la pantalla de inicio de sesión antes de autenticar una cuenta de dominio nueva.
|
||||||
- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea estos grupos de seguridad de forma idempotente.
|
- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea estos grupos de seguridad de forma idempotente.
|
||||||
- El enriquecimiento obtiene el sexo de los módulos SGU de personal/alumnos, lo conserva como la línea administrada `SGU-Gender: Male|Female` en Notas de AD y adapta el fondo de Windows/Linux; cuando falta utiliza redacción neutral.
|
- El enriquecimiento obtiene el sexo de los módulos SGU de personal/alumnos, lo conserva como la línea administrada `SGU-Gender: Male|Female` en Notas de AD y adapta el fondo de Windows/Linux; cuando falta utiliza redacción neutral.
|
||||||
- El servidor configura WEF/WEC para registrar sesiones y fallos, inventariar el estado alcanzable de las máquinas cada cinco minutos y conservar durante 183 días tanto esos eventos como el diagnóstico estructurado del Auth Broker.
|
- El servidor configura WEF/WEC para registrar sesiones y fallos, inventariar el estado alcanzable de las máquinas cada cinco minutos y conservar durante 183 días tanto esos eventos como el diagnóstico estructurado del Auth Broker.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set "SGU_BOOTSTRAP_IP=%~1"
|
||||||
|
set "SGU_VPN_PACKAGE=%~2"
|
||||||
|
set "SGU_VPN_PFX=%~3"
|
||||||
|
set "SGU_VPN_ROOT=%~4"
|
||||||
|
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
|
||||||
|
exit /b %errorlevel%
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set "SGU_BOOTSTRAP_IP=%~1"
|
||||||
|
set "SGU_VPN_POOL=%~2"
|
||||||
|
if "%SGU_VPN_POOL%"=="" set "SGU_VPN_POOL=172.30.0.0/24"
|
||||||
|
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Initialize-SguDomainController.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-NetworkConfigurationMode','PlatformManaged','-TrustedClientNetworks',$env:SGU_VPN_POOL,'-DnsForwarders','168.63.129.16'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-ServerIPv4Address',$env:SGU_BOOTSTRAP_IP) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
|
||||||
|
exit /b %errorlevel%
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$serverBootstrapPath = Join-Path $repositoryRoot 'scripts\Initialize-SguDomainController.ps1'
|
||||||
|
$clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1'
|
||||||
|
$azureClientPath = Join-Path $repositoryRoot 'scripts\Install-SguAzureP2sClient.ps1'
|
||||||
|
$bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep'
|
||||||
|
|
||||||
|
$tokens = $null
|
||||||
|
$parseErrors = $null
|
||||||
|
$serverAst = [Management.Automation.Language.Parser]::ParseFile(
|
||||||
|
$serverBootstrapPath,
|
||||||
|
[ref]$tokens,
|
||||||
|
[ref]$parseErrors)
|
||||||
|
if ($parseErrors.Count -gt 0) {
|
||||||
|
throw ($parseErrors -join [Environment]::NewLine)
|
||||||
|
}
|
||||||
|
$networkFunctionNames = @(
|
||||||
|
'Test-PrivateIPv4Address',
|
||||||
|
'ConvertTo-NetworkCidr',
|
||||||
|
'ConvertTo-PrivateNetworkCidr'
|
||||||
|
)
|
||||||
|
$networkFunctions = $serverAst.FindAll({
|
||||||
|
param($node)
|
||||||
|
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
|
||||||
|
$networkFunctionNames -contains $node.Name
|
||||||
|
}, $true)
|
||||||
|
Invoke-Expression (($networkFunctions | ForEach-Object { $_.Extent.Text }) -join [Environment]::NewLine)
|
||||||
|
|
||||||
|
Describe 'SGU public-cloud network safety' {
|
||||||
|
It 'canonicalizes a host address to its IPv4 network' {
|
||||||
|
ConvertTo-NetworkCidr -Address ([ipaddress]'10.77.0.4') `
|
||||||
|
-NetworkPrefixLength 24 | Should Be '10.77.0.0/24'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'canonicalizes the trusted P2S pool' {
|
||||||
|
ConvertTo-PrivateNetworkCidr -Cidr '172.30.4.19/16' |
|
||||||
|
Should Be '172.30.0.0/16'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'rejects a public trusted-client CIDR' {
|
||||||
|
$wasRejected = $false
|
||||||
|
try {
|
||||||
|
ConvertTo-PrivateNetworkCidr -Cidr '8.8.8.0/24' | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$wasRejected = $true
|
||||||
|
}
|
||||||
|
$wasRejected | Should Be $true
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'exposes explicit Azure modes on both bootstraps' {
|
||||||
|
((Get-Command $serverBootstrapPath).Parameters.Keys -contains
|
||||||
|
'NetworkConfigurationMode') | Should Be $true
|
||||||
|
((Get-Command $serverBootstrapPath).Parameters.Keys -contains
|
||||||
|
'TrustedClientNetworks') | Should Be $true
|
||||||
|
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
|
||||||
|
'ConnectivityMode') | Should Be $true
|
||||||
|
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
|
||||||
|
'VpnProfilePackagePath') | Should Be $true
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'uses an all-user machine-certificate VPN profile' {
|
||||||
|
$source = Get-Content -LiteralPath $azureClientPath -Raw
|
||||||
|
$source | Should Match '-AuthenticationMethod MachineCertificate'
|
||||||
|
$source | Should Match '-AllUserConnection'
|
||||||
|
$source | Should Match 'Add-DnsClientNrptRule'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'limits optional public administration to RDP' {
|
||||||
|
$template = Get-Content -LiteralPath $bicepPath -Raw
|
||||||
|
$template | Should Match "name: 'Allow-RDP-from-administrator'"
|
||||||
|
$template | Should Match "destinationPortRange: '3389'"
|
||||||
|
$template | Should Not Match "sourceAddressPrefix: '0\.0\.0\.0/0'"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user