# Installs the Homelab Manager agent on Windows as a scheduled task that runs as SYSTEM. # # Run in an ELEVATED Windows PowerShell (Run as administrator), with your server's URL and this # server's token from the Servers page: # # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # $env:API_URL = 'https://homelab.example.lan'; $env:API_TOKEN = 'hlm_xxx' # iex ((New-Object Net.WebClient).DownloadString("$env:API_URL/agent/windows/install.ps1")) # # If Homelab Manager uses a self-signed certificate, also set API_INSECURE (this skips certificate checks # for every request the agent makes - only on a trusted LAN) and skip the check for the download itself, # since fetching this very script hits the same certificate: # # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } # $env:API_URL = 'https://homelab.example.lan'; $env:API_TOKEN = 'hlm_xxx'; $env:API_INSECURE = 'true' # iex ((New-Object Net.WebClient).DownloadString("$env:API_URL/agent/windows/install.ps1")) # # Optional: INTERVAL_MINUTES (default 15). # # NOTE: keep this file plain ASCII (it is downloaded as text). $ErrorActionPreference = 'Stop' $script:TaskName = 'Homelab Manager Agent' $script:InstallDir = Join-Path $env:ProgramData 'HomelabManager' function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() return ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Test-Truthy([string]$Value) { return ($Value -match '^(1|true|yes)$') } # Downloads a file. Windows PowerShell 5.1 needs TLS 1.2 switched on and takes the self-signed-certificate override through # ServicePointManager; PowerShell 7 ignores that setting and has its own switch. function Save-Url([string]$Url, [string]$Path, [bool]$Insecure) { if ($PSVersionTable.PSVersion.Major -ge 6) { $params = @{ Uri = $Url; OutFile = $Path } if ($Insecure) { $params['SkipCertificateCheck'] = $true } Invoke-WebRequest @params return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if ($Insecure) { [Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } $client = New-Object System.Net.WebClient try { $client.DownloadFile($Url, $Path) } finally { $client.Dispose() } } # Extra principals (as "*SID:(F)") allowed to write the credentials file. Empty in real use - an elevated installer already # holds Administrators - and only there so a test running without elevation can still write to its own temporary file. $script:ExtraConfigGrants = @() # The credentials file holds the token, so only SYSTEM and Administrators may read it. Identified by SID so this works on any Windows language. function Save-AgentConfig([string]$Path, [string]$ApiUrl, [string]$ApiToken, [bool]$Insecure) { $config = [ordered]@{ apiUrl = $ApiUrl; apiToken = $ApiToken; insecure = $Insecure } # Write with restrictive permissions already in place, so the token is never readable by anyone else, even briefly. [System.IO.File]::WriteAllText($Path, '', (New-Object System.Text.UTF8Encoding($false))) $grants = @('*S-1-5-18:(F)', '*S-1-5-32-544:(F)') + @($script:ExtraConfigGrants) & icacls.exe $Path /inheritance:r /grant:r $grants | Out-Null if ($LASTEXITCODE -ne 0) { throw "Couldn't restrict permissions on $Path (icacls exit $LASTEXITCODE)." } [System.IO.File]::WriteAllText($Path, (ConvertTo-Json -InputObject $config), (New-Object System.Text.UTF8Encoding($false))) } # The pieces of the scheduled task, built but not registered. function New-AgentTaskParts([string]$AgentScript, [int]$IntervalMinutes) { $argument = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $AgentScript $repeat = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) $boot = New-ScheduledTaskTrigger -AtStartup $boot.Delay = 'PT2M' # let the network come up before the first report return @{ Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argument Triggers = @($repeat, $boot) Principal = New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount -RunLevel Highest Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 5) } } function Install-Agent { if (-not (Test-Administrator)) { throw 'This installer must be run as Administrator (it registers a scheduled task that runs as SYSTEM). Open PowerShell with "Run as administrator" and try again.' } $apiUrl = ([string]$env:API_URL).TrimEnd('/') $apiToken = [string]$env:API_TOKEN if (-not $apiUrl) { throw 'Set API_URL to your Homelab Manager URL, e.g. $env:API_URL = ''https://homelab.example.lan''' } if (-not $apiToken) { throw 'Set API_TOKEN to the per-server token from the Servers page, e.g. $env:API_TOKEN = ''hlm_xxx''' } $insecure = Test-Truthy $env:API_INSECURE $interval = 15 if ($env:INTERVAL_MINUTES) { if (-not ([int]::TryParse($env:INTERVAL_MINUTES, [ref]$interval)) -or $interval -lt 1 -or $interval -gt 1440) { throw 'INTERVAL_MINUTES must be a whole number from 1 to 1440.' } } Write-Output "Installing Homelab Manager agent from $apiUrl ..." New-Item -ItemType Directory -Force -Path $script:InstallDir | Out-Null $agentScript = Join-Path $script:InstallDir 'homelab-manager-agent.ps1' Save-Url "$apiUrl/agent/windows/report-tasks.ps1" $agentScript $insecure Save-AgentConfig (Join-Path $script:InstallDir 'agent.json') $apiUrl $apiToken $insecure # Reinstalling replaces the task rather than failing on it. if (Get-ScheduledTask -TaskName $script:TaskName -ErrorAction SilentlyContinue) { Unregister-ScheduledTask -TaskName $script:TaskName -Confirm:$false } $parts = New-AgentTaskParts $agentScript $interval $null = Register-ScheduledTask -TaskName $script:TaskName -Action $parts.Action -Trigger $parts.Triggers -Principal $parts.Principal -Settings $parts.Settings -Description 'Reports scheduled tasks and system information to Homelab Manager.' Write-Output 'Installed. Running an initial report now...' & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $agentScript if ($LASTEXITCODE -ne 0) { Write-Warning "The initial report failed (see above). The agent is installed and will keep trying every $interval minute(s) - check API_URL and API_TOKEN." } Write-Output "Done. The agent reports every $interval minute(s) through the '$script:TaskName' scheduled task." Write-Output "To remove it later, run in an elevated PowerShell: iex ((New-Object Net.WebClient).DownloadString('$apiUrl/agent/windows/uninstall.ps1'))" } # Dot-sourcing (for tests) defines the functions without running anything. if ($MyInvocation.InvocationName -ne '.') { Install-Agent }