#Requires -Version 5.1 #Requires -RunAsAdministrator <# .SYNOPSIS Stores a CI guest-VM credential into the LocalSystem keyring vault. .DESCRIPTION act_runner runs as the LocalSystem service account. Windows Credential Manager / keyring vaults are per-user, so a credential added with `cmdkey` or the Credential Manager UI under an interactive user is invisible to the runner. The orchestrator then fails with: Credential '' not found in keyring. This script writes the credential into the SYSTEM vault using the production venv's `keyring` itself (the exact backend `ci_orchestrator` reads with `keyring.get_credential(target, None)`), via a transient SYSTEM scheduled task. Using keyring for both write and read avoids cmdkey/keyring format and persistence mismatches. Known-issue references: plans/A1-closeout.md (KeyError: 'BuildVMGuest') plans/implementation-plan-A-B.md (keyring under SYSTEM) The plaintext password unavoidably transits a temporary .py file. The file is created with an Administrators/SYSTEM-only ACL, overwritten with random bytes, then deleted. The password is never placed in a scheduled-task argument or the command line. .PARAMETER Target Credential target name. Must match the runner env GITEA_CI_GUEST_CRED_TARGET. Default: BuildVMGuest .PARAMETER UserName Guest login username in the form the transport expects (e.g. 'WINBUILD\ci_build', '.\ci_build', or 'ci_build'). .PARAMETER Password Guest password as a SecureString. If omitted, prompted securely. .PARAMETER VenvPython Path to the production venv interpreter act_runner uses. Default: CI_VENV_PYTHON env var, else F:\CI\python\venv\Scripts\python.exe .PARAMETER SkipVerify Skip the post-write read-back verification (also run as SYSTEM). .EXAMPLE .\Set-CIGuestCredential.ps1 -UserName 'WINBUILD\ci_build' .EXAMPLE # Non-interactive (e.g. from another provisioning script) $pw = Read-Host 'pw' -AsSecureString .\Set-CIGuestCredential.ps1 -Target BuildVMGuest -UserName '.\ci_build' -Password $pw .EXAMPLE .\Set-CIGuestCredential.ps1 -UserName '.\ci_build' -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateNotNullOrEmpty()] [string] $Target = 'BuildVMGuest', [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $UserName, [System.Security.SecureString] $Password, [string] $VenvPython = $(if ($env:CI_VENV_PYTHON) { $env:CI_VENV_PYTHON } else { 'F:\CI\python\venv\Scripts\python.exe' }), [switch] $SkipVerify ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if (-not (Test-Path -LiteralPath $VenvPython)) { throw "venv interpreter not found: '$VenvPython'. Set -VenvPython or CI_VENV_PYTHON." } if (-not $Password) { $Password = Read-Host "Password for '$UserName'" -AsSecureString } # --- Run a script block as SYSTEM via a transient scheduled task ---------- function Invoke-AsSystem { param( [Parameter(Mandatory)] [string] $TaskName, [Parameter(Mandatory)] [string] $Execute, [Parameter(Mandatory)] [string] $Argument, [int] $TimeoutSec = 30 ) $action = New-ScheduledTaskAction -Execute $Execute -Argument $Argument $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' ` -LogonType ServiceAccount -RunLevel Highest Register-ScheduledTask -TaskName $TaskName -Action $action ` -Principal $principal -Force | Out-Null try { Start-ScheduledTask -TaskName $TaskName $deadline = (Get-Date).AddSeconds($TimeoutSec) do { Start-Sleep -Milliseconds 500 $info = Get-ScheduledTaskInfo -TaskName $TaskName $state = (Get-ScheduledTask -TaskName $TaskName).State } while ($state -eq 'Running' -and (Get-Date) -lt $deadline) return $info.LastTaskResult } finally { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false ` -ErrorAction SilentlyContinue } } # --- Harden + shred helper for the transient secret file ------------------ function Remove-Securely { # Best-effort cleanup helper; ShouldProcess prompting is undesirable # for an internal shred-and-delete of our own temp file. [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param([Parameter(Mandatory)] [string] $Path) if (-not (Test-Path -LiteralPath $Path)) { return } try { $len = (Get-Item -LiteralPath $Path).Length if ($len -gt 0) { $rnd = New-Object byte[] $len [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($rnd) [IO.File]::WriteAllBytes($Path, $rnd) } } catch { Write-Verbose "secure overwrite failed (continuing to delete): $_" } Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue } $tmpDir = $env:TEMP $setPy = Join-Path $tmpDir ("setcred_{0}.py" -f ([guid]::NewGuid().ToString('N'))) $verPy = Join-Path $tmpDir ("vercred_{0}.py" -f ([guid]::NewGuid().ToString('N'))) $verOut = Join-Path $tmpDir ("vercred_{0}.txt" -f ([guid]::NewGuid().ToString('N'))) if (-not $PSCmdlet.ShouldProcess("$Target ($UserName)", "store credential in SYSTEM keyring vault")) { return } try { # Decrypt SecureString only at the moment of writing the temp script. $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password) $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) # Python literals: pass via triple-quoted raw-ish strings. Reject quotes # that would break out of the literal rather than risk silent corruption. if ($UserName -match "'''" -or $plain -match "'''") { throw "UserName/Password contains a triple-quote sequence; unsupported." } $setBody = @" import keyring keyring.set_password(r'''$Target''', r'''$UserName''', r'''$plain''') print('OK') "@ Set-Content -LiteralPath $setPy -Value $setBody -Encoding UTF8 Remove-Variable plain -ErrorAction SilentlyContinue # Lock the secret file down to SYSTEM + Administrators before it runs. $acl = New-Object Security.AccessControl.FileSecurity $acl.SetAccessRuleProtection($true, $false) foreach ($id in 'SYSTEM', 'Administrators') { $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule( $id, 'FullControl', 'Allow'))) } Set-Acl -LiteralPath $setPy -AclObject $acl $rc = Invoke-AsSystem -TaskName 'CI-SetGuestCred' ` -Execute $VenvPython -Argument "`"$setPy`"" if ($rc -ne 0) { throw "keyring.set_password task exited with code $rc." } Write-Host "Stored '$Target' (user '$UserName') in the SYSTEM vault." ` -ForegroundColor Green } finally { Remove-Securely -Path $setPy } if ($SkipVerify) { return } try { $verBody = @" import keyring c = keyring.get_credential(r'''$Target''', None) open(r'''$verOut''', 'w', encoding='utf-8').write( 'NONE' if c is None else 'OK:' + c.username) "@ Set-Content -LiteralPath $verPy -Value $verBody -Encoding UTF8 Invoke-AsSystem -TaskName 'CI-VerifyGuestCred' ` -Execute $VenvPython -Argument "`"$verPy`"" | Out-Null $result = if (Test-Path -LiteralPath $verOut) { (Get-Content -LiteralPath $verOut -Raw).Trim() } else { 'NONE' } if ($result -like 'OK:*') { Write-Host "Verified: SYSTEM keyring returns user '$($result.Substring(3))'." ` -ForegroundColor Green } else { throw "Verification failed: SYSTEM keyring.get_credential('$Target', None) " + "returned None. Credential did not persist." } } finally { Remove-Item -LiteralPath $verPy -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $verOut -Force -ErrorAction SilentlyContinue }