nebula-domain-join/install-nebula.ps1
Damien Coles 57a53e1fde fix domain auth at login: DNS persistence via scheduled task
Nebula recreates the nebula1 TUN adapter on every start, wiping DNS settings. This caused domain authentication to fail at the Windows login screen because Netlogon could not reach the DC.

Changes:

- install-nebula.ps1 now takes -DnsServer and -Domain parameters

- Changed service start type from delayed-auto to auto

- Creates set-dns-on-start.ps1 startup script and NebulaDNS scheduled task

- Sets ExpectedDialupDelay=60 in Netlogon registry

- Idempotency check verifies scheduled task and startup script exist
2026-02-12 15:42:23 -08:00

298 lines
10 KiB
PowerShell

#Requires -RunAsAdministrator
param(
[Parameter(Mandatory=$true)]
[string]$DnsServer,
[Parameter(Mandatory=$true)]
[string]$Domain
)
$ErrorActionPreference = "Stop"
# --- Configuration ---
$NebulaVersion = "1.10.3"
$NebulaUrl = "https://github.com/slackhq/nebula/releases/download/v$NebulaVersion/nebula-windows-amd64.zip"
# Verify running as Administrator
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "This script must be run as a machine Administrator."
exit 1
}
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$InstallDir = "C:\Program Files\Nebula"
$ConfigPath = "$InstallDir\config.yml"
$StartupScript = "$InstallDir\set-dns-on-start.ps1"
$ExpectedBinPath = "`"$InstallDir\nebula.exe`" -service run -config `"$ConfigPath`""
$TempZip = Join-Path $env:TEMP "nebula-windows-amd64.zip"
$TempExtract = Join-Path $env:TEMP "nebula-extract"
# Derive the NetBIOS domain name from the FQDN (first label, uppercased)
$NetBIOSDomain = ($Domain -split '\.')[0].ToUpper()
# --- Validate per-host files before making any changes ---
$missing = @()
$HostFiles = @("config.yml", "host.crt", "host.key")
foreach ($file in $HostFiles) {
if (-not (Test-Path (Join-Path $PWD $file))) {
$missing += "$file (expected in working directory: $PWD)"
}
}
# ca.crt must be provided alongside the script
if (-not (Test-Path (Join-Path $ScriptDir "ca.crt"))) {
$missing += "ca.crt (expected in script directory: $ScriptDir)"
}
if ($missing.Count -gt 0) {
Write-Error "Missing required files:`n - $($missing -join "`n - ")"
exit 1
}
# --- Check if already installed and running correctly ---
$existing = Get-Service -Name "nebula" -ErrorAction SilentlyContinue
if ($existing) {
$svcConfig = sc.exe qc nebula 2>&1 | Out-String
$correctBinary = $svcConfig -match [regex]::Escape($ExpectedBinPath)
if ($existing.Status -eq "Running" -and $correctBinary) {
# Check installed version
$installedVersion = & "$InstallDir\nebula.exe" -version 2>&1 | Select-String -Pattern "Version: (.+)" | ForEach-Object { $_.Matches[0].Groups[1].Value }
if ($installedVersion -eq $NebulaVersion) {
# Compare host files against installed files by hash
$allMatch = $true
foreach ($file in @("ca.crt")) {
$src = (Get-FileHash (Join-Path $ScriptDir $file)).Hash
$dst = (Get-FileHash (Join-Path $InstallDir $file) -ErrorAction SilentlyContinue).Hash
if ($src -ne $dst) { $allMatch = $false; break }
}
if ($allMatch) {
foreach ($file in $HostFiles) {
$src = (Get-FileHash (Join-Path $PWD $file)).Hash
$dst = (Get-FileHash (Join-Path $InstallDir $file) -ErrorAction SilentlyContinue).Hash
if ($src -ne $dst) { $allMatch = $false; break }
}
}
# Also verify the scheduled task and startup script exist
$taskExists = Get-ScheduledTask -TaskName "NebulaDNS" -ErrorAction SilentlyContinue
$scriptExists = Test-Path $StartupScript
if ($allMatch -and $taskExists -and $scriptExists) {
Write-Host "Nebula $NebulaVersion is already installed and running with matching files. No changes needed."
exit 0
}
}
Write-Host "Nebula is running but files or version have changed. Reinstalling..."
}
Write-Host "Stopping existing Nebula service..."
if ($existing.Status -eq "Running") {
sc.exe stop nebula | Out-Null
$timeout = 10
while ($timeout -gt 0) {
$svc = Get-Service -Name "nebula" -ErrorAction SilentlyContinue
if ($svc.Status -eq "Stopped") { break }
Start-Sleep -Seconds 1
$timeout--
}
if ($timeout -eq 0) {
Write-Error "Timed out waiting for Nebula service to stop."
exit 1
}
}
sc.exe delete nebula | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to remove existing Nebula service. A reboot may be required if it is marked for deletion."
exit 1
}
Start-Sleep -Seconds 1
}
# --- Download Nebula ---
Write-Host "Downloading Nebula v$NebulaVersion..."
try {
Invoke-WebRequest -Uri $NebulaUrl -OutFile $TempZip -UseBasicParsing
} catch {
Write-Error "Failed to download Nebula: $_"
exit 1
}
# --- Extract and clean up ---
if (Test-Path $TempExtract) { Remove-Item $TempExtract -Recurse -Force }
try {
Expand-Archive -Path $TempZip -DestinationPath $TempExtract -Force
} catch {
Write-Error "Failed to extract Nebula archive: $_"
exit 1
}
# Verify nebula.exe was extracted
if (-not (Test-Path (Join-Path $TempExtract "nebula.exe"))) {
Write-Error "nebula.exe not found in downloaded archive."
exit 1
}
# Remove nebula-cert.exe — not needed for this module
$certExe = Join-Path $TempExtract "nebula-cert.exe"
if (Test-Path $certExe) { Remove-Item $certExe -Force }
# --- Create install directory ---
if (-not (Test-Path $InstallDir)) {
try {
New-Item -ItemType Directory -Path $InstallDir | Out-Null
} catch {
Write-Error "Failed to create install directory: $_"
exit 1
}
}
# --- Copy files ---
try {
Write-Host "Copying Nebula files..."
Copy-Item (Join-Path $TempExtract "nebula.exe") $InstallDir -Force
Copy-Item (Join-Path $TempExtract "dist") $InstallDir -Recurse -Force
Copy-Item (Join-Path $ScriptDir "ca.crt") $InstallDir -Force
Write-Host "Copying host files..."
foreach ($file in $HostFiles) {
Copy-Item (Join-Path $PWD $file) $InstallDir -Force
}
} catch {
Write-Error "Failed to copy files: $_"
exit 1
}
# --- Clean up temp files ---
Remove-Item $TempZip -Force -ErrorAction SilentlyContinue
Remove-Item $TempExtract -Recurse -Force -ErrorAction SilentlyContinue
# --- Verify files landed in install directory ---
foreach ($file in @("nebula.exe", "ca.crt") + $HostFiles) {
if (-not (Test-Path (Join-Path $InstallDir $file))) {
Write-Error "File copy verification failed: $file not found in $InstallDir"
exit 1
}
}
if (-not (Test-Path (Join-Path $InstallDir "dist"))) {
Write-Error "File copy verification failed: dist\ not found in $InstallDir"
exit 1
}
# --- Install and configure service ---
Write-Host "Installing Nebula service..."
& "$InstallDir\nebula.exe" -service install -config "$ConfigPath"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to install Nebula service."
exit 1
}
# Wait for real network connectivity before starting, not just the TCP/IP stack
sc.exe config nebula depend= Tcpip/NlaSvc | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to set service dependencies."
exit 1
}
# Auto start — the NlaSvc dependency ensures the physical network is up first
sc.exe config nebula start= auto | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to set auto start."
exit 1
}
# --- Start and verify ---
Write-Host "Starting Nebula service..."
sc.exe start nebula | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to start Nebula service. Check Event Viewer (Application log, source 'nebula')."
exit 1
}
Start-Sleep -Seconds 3
$svc = Get-Service -Name "nebula"
if ($svc.Status -eq "Running") {
Write-Host "Nebula v$NebulaVersion service is running."
} else {
Write-Error "Nebula service failed to start. Check Event Viewer (Application log, source 'nebula')."
exit 1
}
# --- Configure DNS persistence across reboots ---
#
# Nebula recreates the nebula1 TUN adapter on every start, which wipes any
# DNS settings. This scheduled task runs at startup to re-apply DNS, wait
# for the domain controller to be reachable, then force Netlogon to
# rediscover the DC — ensuring domain authentication works at the login screen.
Write-Host "Creating startup DNS script..."
$startupScriptContent = @"
# Wait up to 120 seconds for the nebula1 adapter to appear
`$timeout = 120
while (`$timeout -gt 0) {
`$adapter = Get-NetAdapter -Name "nebula1" -ErrorAction SilentlyContinue
if (`$adapter -and `$adapter.Status -eq "Up") { break }
Start-Sleep -Seconds 2
`$timeout -= 2
}
if (`$timeout -le 0) { exit 1 }
Set-DnsClientServerAddress -InterfaceAlias "nebula1" -ServerAddresses "$DnsServer"
# Wait for DC to become reachable, then force Netlogon to rediscover
`$timeout = 180
while (`$timeout -gt 0) {
`$result = Resolve-DnsName "$Domain" -Server "$DnsServer" -ErrorAction SilentlyContinue
if (`$result) { break }
Start-Sleep -Seconds 5
`$timeout -= 5
}
if (`$timeout -gt 0) {
Clear-DnsClientCache
nltest /dsgetdc:$NetBIOSDomain /force 2>&1 | Out-Null
}
"@
Set-Content -Path $StartupScript -Value $startupScriptContent -Force
Write-Host "Registering NebulaDNS scheduled task..."
$existingTask = Get-ScheduledTask -TaskName "NebulaDNS" -ErrorAction SilentlyContinue
if ($existingTask) {
Unregister-ScheduledTask -TaskName "NebulaDNS" -Confirm:$false
}
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-ExecutionPolicy Bypass -File `"$StartupScript`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName "NebulaDNS" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Description "Sets DNS on the Nebula adapter and forces Netlogon DC rediscovery" | Out-Null
Write-Host "NebulaDNS scheduled task registered."
# --- Configure Netlogon to wait for slow network links ---
#
# ExpectedDialupDelay tells Netlogon to keep retrying DC discovery for the
# specified number of seconds. This covers the window between Windows boot
# and the Nebula tunnel becoming fully operational.
$netlogonKey = "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters"
Set-ItemProperty -Path $netlogonKey -Name "ExpectedDialupDelay" -Value 60 -Type DWord
Write-Host "Netlogon ExpectedDialupDelay set to 60 seconds."