diff --git a/scripts/setup-gitea-windows-runner.ps1 b/scripts/setup-gitea-windows-runner.ps1 new file mode 100644 index 0000000..872f1f5 --- /dev/null +++ b/scripts/setup-gitea-windows-runner.ps1 @@ -0,0 +1,124 @@ +[CmdletBinding()] +param( + [string]$RegistrationToken, + + [string]$InstanceUrl = "https://gitea.officecom.cloud", + [string]$RunnerName = "officecom-oc-sentinel-windows-01", + [string]$RunnerVersion = "1.0.8", + [string]$RunnerAccount = "OCGiteaRunner", + [string]$InstallRoot = "$env:ProgramData\\OCGiteaRunner" +) + +$ErrorActionPreference = "Stop" + +function Test-IsAdministrator { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function New-RunnerPassword { + # The account is only used by Task Scheduler; no password is persisted in this script or repository. + $bytes = New-Object byte[] 36 + [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) + return [Convert]::ToBase64String($bytes).Replace('+', 'A').Replace('/', 'B').Replace('=', 'C') + "!9z" +} + +if (-not (Test-IsAdministrator)) { + throw "Run this script from an elevated PowerShell window (Run as administrator)." +} + +if ([string]::IsNullOrWhiteSpace($RegistrationToken)) { + $secureToken = Read-Host "Paste the repository runner registration token" -AsSecureString + $tokenPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken) + try { + $RegistrationToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($tokenPointer) + } + finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($tokenPointer) + } +} + +if ([string]::IsNullOrWhiteSpace($RegistrationToken)) { + throw "A repository runner registration token is required." +} + +$taskName = "OCSentinel Gitea Windows Runner" +$runnerPath = Join-Path $InstallRoot "gitea-runner.exe" +$configPath = Join-Path $InstallRoot "config.yaml" +$runnerStatePath = Join-Path $InstallRoot ".runner" +$logDirectory = Join-Path $InstallRoot "logs" +$downloadUrl = "https://gitea.com/gitea/act_runner/releases/download/v$RunnerVersion/gitea-runner-$RunnerVersion-windows-amd64.exe" +$checksumUrl = "$downloadUrl.sha256" +$accountQualifiedName = "$env:COMPUTERNAME\\$RunnerAccount" + +if (Test-Path -LiteralPath $runnerStatePath) { + throw "A runner is already registered at $InstallRoot. Remove it in Gitea first, then remove this directory if a new registration is needed." +} + +$existingAccount = Get-LocalUser -Name $RunnerAccount -ErrorAction SilentlyContinue +if ($existingAccount) { + throw "The local account '$RunnerAccount' already exists. Stop and remove the existing runner before reinstalling it." +} + +New-Item -ItemType Directory -Force -Path $InstallRoot, $logDirectory | Out-Null + +try { + Write-Host "Downloading Gitea runner $RunnerVersion..." + Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $runnerPath + $checksumText = (Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl).Content.Trim() + $expectedHash = ($checksumText -split '\s+')[0].ToLowerInvariant() + $actualHash = (Get-FileHash -LiteralPath $runnerPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actualHash -ne $expectedHash) { + throw "Runner checksum verification failed." + } + + $password = New-RunnerPassword + $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force + New-LocalUser -Name $RunnerAccount -Password $securePassword -Description "Restricted account for the OCSentinel Gitea Actions runner." -AccountNeverExpires | Out-Null + + $config = & $runnerPath generate-config + $config = $config -replace '(?m)^ labels:.*$', ' labels: ["windows:host"]' + Set-Content -LiteralPath $configPath -Value $config -Encoding utf8 + + # Build jobs run only with this non-administrative account and only for the repository runner token supplied. + $acl = Get-Acl -LiteralPath $InstallRoot + $acl.SetAccessRuleProtection($true, $false) + $systemRule = New-Object Security.AccessControl.FileSystemAccessRule("SYSTEM", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow") + $adminRule = New-Object Security.AccessControl.FileSystemAccessRule("BUILTIN\\Administrators", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow") + $runnerRule = New-Object Security.AccessControl.FileSystemAccessRule($accountQualifiedName, "Modify", "ContainerInherit,ObjectInherit", "None", "Allow") + $acl.AddAccessRule($systemRule) + $acl.AddAccessRule($adminRule) + $acl.AddAccessRule($runnerRule) + Set-Acl -LiteralPath $InstallRoot -AclObject $acl + + $credential = New-Object Management.Automation.PSCredential($accountQualifiedName, $securePassword) + $registerArgs = @( + "--config", "`"$configPath`"", "register", "--no-interactive", + "--instance", "`"$InstanceUrl`"", "--token", "`"$RegistrationToken`"", + "--name", "`"$RunnerName`"", "--labels", "windows:host" + ) -join " " + $registration = Start-Process -FilePath $runnerPath -ArgumentList $registerArgs -WorkingDirectory $InstallRoot -Credential $credential -Wait -PassThru + if ($registration.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $runnerStatePath)) { + throw "Runner registration failed with exit code $($registration.ExitCode)." + } + + $action = New-ScheduledTaskAction -Execute $runnerPath -Argument "--config `"$configPath`" daemon" -WorkingDirectory $InstallRoot + $trigger = New-ScheduledTaskTrigger -AtStartup + Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -User $accountQualifiedName -Password $password -RunLevel Limited -Description "Runs the repository-scoped OCSentinel Gitea Actions runner." -Force | Out-Null + Start-ScheduledTask -TaskName $taskName + + Start-Sleep -Seconds 3 + $task = Get-ScheduledTask -TaskName $taskName + Write-Host "Gitea runner installed successfully." + Write-Host "Runner: $RunnerName" + Write-Host "Labels: windows:host" + Write-Host "Task: $taskName ($($task.State))" + Write-Host "Install path: $InstallRoot" +} +catch { + if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $taskName -Confirm:$false + } + throw +}