nebula-domain-join/install-nebula.ps1

299 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
}
}
# Lock down the directory — only SYSTEM and Administrators need access.
# Program Files subdirectories inherit broad ACLs (Users: Read & Execute).
# Take ownership, disable inheritance, and strip everything except SYSTEM and Admins.
takeown /F $InstallDir /R /A /D Y | Out-Null
icacls $InstallDir /inheritance:d /T /Q
icacls $InstallDir /remove:g "BUILTIN\Users" /T /Q
icacls $InstallDir /remove:g "CREATOR OWNER" /T /Q
icacls $InstallDir /remove:g "NT SERVICE\TrustedInstaller" /T /Q
icacls $InstallDir /remove:g "APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES" /T /Q
icacls $InstallDir /remove:g "APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES" /T /Q
# --- 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."