[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 }