Ansibles great. We like Ansible up in here. Managing all my linux vms with it is super simple. Windows though.........well I don't know. I haven't got that far yet. What I have managed to do though, is figure out a way to bootstrap a fresh Windows Server 2019 install and get it ready and willing to work Ansible. Securely. With minimal fuss.

You see, getting Windows to work with Ansible seems simple. You read all the guides, and they give a number of options and guides that almost have the whole story, but not quite. When working with Ansible and Windows, you have a few methods of implementing comms between the two. I tried all of them, but the one I settled on was WinRM with Kerberos Auth. Why? Well, it's the only one I could get to work reliably AND automate (mostly.)

So, before I get to showing off my bootrap script that kinda works okayish, here's what it'll do.

  1. Enable remote management so Server Manager can connect to it.
  2. Enable remote desktop
  3. Set remote desktop to work with NTLM which is slightly more secure than not using it.
  4. Allow RDP through the firewall.
  5. Set the windows update policy to download and notify only.
  6. Acquire windows updates and install them.
  7. Rename the computer.
  8. Activate Windows with a local key management server.
    What? You don't have one of those? Well why not? It's great........
  9. Set up networking (static ip, dns etc etc)
  10. Enables auto login (so I don't need to log in after each reboot that this script requires.)
  11. Reboot now so the activation and name change can propogate.
  12. Auto login and join the domain. (This requires manual intervention because I don't particularly fancy securely storing domain admin credentials with the script.)
  13. Reboot for the domain join to complete
  14. Auto login and inform the admin that they need to move the newly joined computer to the correct organisational unit and add the computer to the certificate auto enrol group. Once done, press enter to continue the script.
  15. Force a group policy update. This will cause my certificate authority to issue this computer with a valid computer certificate.
  16. Check that we have a valid certificate that matches the machine name, and if so, enable winrm with HTTPS.
  17. Allow port 5986 through the firewall.
  18. Delete the WinRM listener for HTTP (if it exists)
  19. Disable the auto login mechanism and prompt the user for a new Admin password.
  20. Profit.

DAAAAAAM. This project took 4 weekends worth of work. The complicated bits were from steps 14 and 15. These have a pre-requisite requirement for a Public Key Infrastructure to be in place. Lucky for you (me), I documented my process for setting this up, because it took a few tries before I had a nicely functioning set up.

Anyway, heres the WS2019 Provisioning Script.

functions.ps1

# https://www.codeproject.com/articles/223002/reboot-and-resume-powershell-script
# -------------------------------------
# Function Globals
# -------------------------------------
$global:started = $FALSE
$global:startingStep = $Step
$global:restartKey = "Restart-And-Resume"
$global:RegRunKey ="HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
$global:powershell = (Join-Path $env:windir "system32\WindowsPowerShell\v1.0\powershell.exe")
$global:RegLogonKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'

# -------------------------------------
# Collection of Utility functions.
# -------------------------------------
function Should-Run-Step([string] $prospectStep) 
{
	if ($global:startingStep -eq $prospectStep -or $global:started) {
		$global:started = $TRUE
	}
	return $global:started
}

function Wait-For-Keypress([string] $message, [bool] $shouldExit=$FALSE) 
{
	Write-Host "$message" -foregroundcolor yellow
	$key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
	if ($shouldExit) {
		exit
	}
}

function Test-Key([string] $path, [string] $key)
{
    return ((Test-Path $path) -and ((Get-Key $path $key) -ne $null))   
}

function Remove-Key([string] $path, [string] $key)
{
	Remove-ItemProperty -path $path -name $key
}

function Set-Key([string] $path, [string] $key, [string] $value) 
{
	Set-ItemProperty -path $path -name $key -value $value
}

function Get-Key([string] $path, [string] $key) 
{
	return (Get-ItemProperty $path).$key
}

function Restart-And-Run([string] $key, [string] $run) 
{
	Set-Key $global:RegRunKey $key $run
	Restart-Computer
	exit
} 

function Clear-Any-Restart([string] $key=$global:restartKey) 
{
	if (Test-Key $global:RegRunKey $key) {
		Remove-Key $global:RegRunKey $key
	}
}

function Restart-And-Resume([string] $script, [string] $step) 
{
	Restart-And-Run $global:restartKey "$global:powershell $script -Step $step"
}

# -------------------------------------
# Functions I hobbled together to auto log in and stuff
# -------------------------------------

function Enable-Auto-Login([string] $username, [string] $password)
{
	Set-ItemProperty -Path $global:RegLogonKey -Name 'AutoAdminLogon' -Value "1" -Type String 
	Set-ItemProperty -Path $global:RegLogonKey -Name 'DefaultUsername' -Value "$username" -type String 
	Set-ItemProperty -Path $global:RegLogonKey -Name 'DefaultPassword' -Value "$password" -type String
	Write-Warning "Auto-Login for $username configured."
}

function Disable-Auto-Login()
{
	Remove-ItemProperty -Path $global:RegLogonKey -Name 'AutoAdminLogon'
	Remove-ItemProperty -Path $global:RegLogonKey -Name 'DefaultUsername'
	Remove-ItemProperty -Path $global:RegLogonKey -Name 'DefaultPassword'
	Write-Warning "Auto-Login for $username disabled."
}

WS2019-Provision.ps1

param($Step="A")
# -------------------------------------
# Imports
# -------------------------------------
$script = $myInvocation.MyCommand.Definition
$scriptPath = Split-Path -parent $script
. (Join-Path $scriptpath functions.ps1)

# -------------------------------------
# Configuration Options
# -------------------------------------
$IP = "10.10.10.xx"
$Bitmask = 24
$Gateway = "10.10.10.1"
$DNS1 = "10.10.10.10"
$DNS2 = "10.10.10.11"
$IPType = "IPv4"
$NewComputerName = "NEW-NAME-HERE"
$ProductKey = ""
$Domain = "" #FQDN FORMAT
$KMS_IP = "10.10.10.70:1688"

$WindowsAdminUsername =  "$NewComputerName\Administrator"
$WindowsAdminPassword = 'Password123'

Clear-Any-Restart

# Initial house keeping and prep
if (Should-Run-Step "A") {
	# Enable Remote Management (For connecting via server manager)
	Write-Output "Enabling remote management"
	Configure-SMRemoting.exe -Enable

	# Enable remote desktop
	#Enable Remote Desktop connections
	Write-Output "Enabling remote desktop"
	Set-ItemProperty ‘HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\‘ -Name “fDenyTSConnections” -Value 0

	#Enable Network Level Authentication
	Write-Output "Setting remote desktop to only work with NTLM"
	Set-ItemProperty ‘HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp\‘ -Name “UserAuthentication” -Value 1

	#Enable Windows firewall rules to allow incoming RDP
	Write-Output "Allow RDP through Windows Firewall"
	Enable-NetFirewallRule -DisplayGroup “Remote Desktop”

	# Set Windows Update to Download Only and notify
	Write-Output "Setting Windows Update policy"
	Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU -Name AUOptions -Value 3

	# Download all current available updates and install them
	Write-Output "Acquiring updates...."
	$updates = Start-WUScan
	Write-Output "Installing Updates...."
	Install-WUUpdates -Updates $updates

	# Set the timezone
	Write-Output "Setting the timezone"
	Set-TimeZone "GMT Standard Time"

	# Rename Computer
	Write-Output "Renaming computer..."
	Rename-Computer -NewName $NewComputerName

	# Add the product key and activate windows
	Write-Output "Activating Windows...."
	DISM /online /Set-Edition:ServerStandard /ProductKey:$ProductKey /AcceptEula /NoRestart
	slmgr.vbs /skms $KMS_IP
	Slmgr.vbs -ato

	# Change the network settings (assign a static ip)
	# Retrieve the network adapter that you want to configure
	$adapter = Get-NetAdapter | ? {$_.Status -eq "up"}

	do {
		Start-Sleep -s 2
		Write-Output "Waiting for NIC to come online...."
	} while ($adapter.Status -eq "Down")

	# Remove any existing IP, gateway from our ipv4 adapter
	Write-Output "Clearing IP settings..."
	If (($adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) {
		$adapter | Remove-NetIPAddress -AddressFamily $IPType -Confirm:$false
	}
	If (($adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) {
		$adapter | Remove-NetRoute -AddressFamily $IPType -Confirm:$false
	}

	# Configure the IP address and default gateway
	Write-Output "Setting new IP/DNS/Gateway settings"
	$adapter | New-NetIPAddress `
	-AddressFamily $IPType `
	-IPAddress $IP `
	-PrefixLength $Bitmask `
	-DefaultGateway $Gateway
	# Configure the DNS client server IP addresses
	$adapter | Set-DnsClientServerAddress -ServerAddresses ($DNS1,$DNS2)

	# Enable auto login so we can do this thing handsfree
	Enable-Auto-Login $WindowsAdminUsername $WindowsAdminPassword

	Restart-And-Resume $script "B"
}

if (Should-Run-Step "B") {
	# Join the domain (REQUIRES INTERACTION - Area for improvement.......)
	Write-Output "Joining the domain..."
	Add-Computer -DomainName $Domain

	Restart-And-Resume $script "C"
}

if (Should-Run-Step "C") {
	# Wait for the admin to add the computer to the right ou
	Wait-For-Keypress "Add the computer to the Cert Auto Enrol Group and OU then press enter...." 

	# Force a gpudate
	Write-Output "Forcing a GPO update"
	gpupdate /force

	# Get the cert thumbprint
	Write-Output "Checking to see if a certificate has been generated"
	$cert = Get-ChildItem -Path 'Cert:LocalMachine\MY'
	if($cert.Subject -like "*$env:COMPUTERNAME*") {
		# Enable the WinRM listener
		#winrm create winrm/config/Listener?Address=*+Transport=HTTPS '@{Hostname="'$env:COMPUTERNAME'.'$DOMAIN'"; CertificateThumbprint="'$cert.Thumbprint'"}'
		Write-Output "Valid cert found. Enabling WINRM using Enterprise cert for ssl"
		winrm quickconfig -transport:https -Force

		# Enable 5986 through the firewall
		Write-Host "Allowing WinRM via HTTPS through firewall"
		New-NetFirewallRule -DisplayName 'WinRM HTTPS-In' -Name 'WinRM HTTPS-In' -Profile Any -LocalPort 5986 -Protocol TCP

		# Delete the HTTP listener (if it exists.)
		Write-Host "Removing the HTTP listener if it exists"
		winrm delete winrm/config/Listener?Address=*+Transport=HTTP

		# Seeing as we now have a valid cert, we may as well replace the self signed one RDP uses
		wmic /namespace:\\root\cimv2\TerminalServices PATH Win32_TSGeneralSetting Set SSLCertificateSHA1Hash=($cert.Thumbprint)
	}
	else {
		Wait-For-Keypress "Valid cert not found......."
	}

	# Disable the auto-login shenanigans
	Disable-Auto-Login
}

Wait-For-Keypress "Script finished. You should change the default admin password now."

There are a few caveats to using this script though. It assumes you have a pki infrastructure and a Windows Domain for the authentication as having all comms secured using trusted certs was one of the main goals here.