Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/gate13_gcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
"windows_image": "windows-server-2025-dc-v20260814",
"windows_image_project": "windows-cloud",
"windows_machine_type": "e2-standard-8",
"windows_startup_commit": "e60c3577c7205ff434cad6e9396f89555626aceb",
"windows_startup_commit": "67fffa61a372a17ce7c2d8a76ac0792095e35aa3",
"zone": "us-central1-b"
}
9 changes: 9 additions & 0 deletions docs/GATE13_WINDOWS_SSH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Gate 13 Windows SSH

Gate 13 SSH enters the machine's normal Windows PowerShell explicitly because Windows OpenSSH may start commands in cmd.exe. Script commands use UTF-16LE EncodedCommand to preserve quotes and shell metacharacters. Profiles remain enabled; the harness does not replace HOME or isolate PowerShell. The stage uses its fixed script path, and scheduled qualification and cleanup children also retain profiles.

The zero-input launcher reads optional `ssh_private_key` (absolute local path) and `ssh_public_fingerprint` (`SHA256:` public-key fingerprint from `ssh-keygen -lf PATH.pub -E sha256`) from `config/gate13_gcp.json`. Without these fields it uses the existing `~/.ssh/google_compute_engine` identity. It does not select the last project metadata key. Every SSH and SCP operation passes the same explicit private-key path. Before client creation the provider derives its public key from that private key, verifies an existing .pub file matches, and checks the optional fingerprint. Missing configured identities and mismatches stop before metadata installation. Do not put private-key contents in configuration.

Client metadata contains only that exact public key. Startup refreshes authorized keys even when the ready marker already exists. The installer replaces stale explicit ACL entries, disables inheritance, and verifies the resulting SID-based allow list. The ordinary user owns its profile key; program-data key files permit only Administrators and SYSTEM, preserving the existing OpenSSH Match configuration. IAP transport and existing host-key verification remain unchanged.

Local qualification exercises native cmd.exe-to-PowerShell execution, real temporary ssh-keygen identities, and native ACL replacement on owned temporary files. These tests do not establish a new GCE/IAP cloud acceptance run.
107 changes: 81 additions & 26 deletions scripts/gate13_gcp_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ def json(self, argv: Sequence[str | os.PathLike[str]], *, action: str, timeout:
raise CommandError(f"{action} returned invalid JSON") from exc


def _windows_powershell_command(script: str) -> str:
"""Cross cmd.exe without parsing script metacharacters; retain normal profiles."""
encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
return f"powershell.exe -NoLogo -NonInteractive -EncodedCommand {encoded}"


def _ssh_public_identity(value: str) -> tuple[str, str]:
fields = value.strip().split()
if len(value.splitlines()) != 1 or len(fields) < 2 or fields[0] not in {"ssh-rsa", "ssh-ed25519"}:
raise Gate13CloudError("SSH public key must contain one supported key")
try:
blob = base64.b64decode(fields[1], validate=True)
except ValueError as exc:
raise Gate13CloudError("SSH public key encoding is invalid") from exc
type_length = int.from_bytes(blob[:4], "big")
if blob[4 : 4 + type_length] != fields[0].encode("ascii"):
raise Gate13CloudError("SSH public key algorithm does not match encoded key")
if len(blob) < 32:
raise Gate13CloudError("SSH public key is truncated")
fingerprint = "SHA256:" + base64.b64encode(hashlib.sha256(blob).digest()).decode("ascii").rstrip("=")
return " ".join(fields[:2]), fingerprint


def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
Expand Down Expand Up @@ -476,21 +499,35 @@ class GcpConfig:
route_wheel_path: str
route_wheel_sha256: str
route_wheel_bytes: int
ssh_private_key: str | None = None
ssh_public_fingerprint: str | None = None

@classmethod
def load(cls, path: Path) -> "GcpConfig":
value = _strict_object(path.read_text(encoding="utf-8"), "GCP one-click configuration")
expected = set(cls.__dataclass_fields__)
optional = {"ssh_private_key", "ssh_public_fingerprint"}
expected = set(cls.__dataclass_fields__) - optional
string_fields = expected - {"route_wheel_bytes"}
if (
set(value) != expected
not expected.issubset(value)
or set(value) - expected - optional
or any(
value.get(field) is not None and (not isinstance(value[field], str) or not value[field])
for field in optional
)
or not all(isinstance(value[field], str) and value[field] for field in string_fields)
or not isinstance(value["route_wheel_bytes"], int)
or isinstance(value["route_wheel_bytes"], bool)
or value["route_wheel_bytes"] <= 0
):
raise Gate13CloudError("GCP one-click configuration fields are invalid")
result = cls(**value)
if result.ssh_private_key is not None and not Path(result.ssh_private_key).is_absolute():
raise Gate13CloudError("configured SSH private key must be absolute")
if result.ssh_public_fingerprint is not None and not re.fullmatch(
r"SHA256:[A-Za-z0-9+/]{43}", result.ssh_public_fingerprint
):
raise Gate13CloudError("configured SSH public fingerprint is invalid")
for commit in (
result.route_source_commit,
result.catalog_source_commit,
Expand Down Expand Up @@ -523,9 +560,18 @@ def __init__(
signed_url: Callable[[PackageArtifact], str],
progress: Callable[[str], None] = print,
sleeper: Callable[[float], None] = time.sleep,
ssh_private_key: Path | None = None,
ssh_public_fingerprint: str | None = None,
) -> None:
if not _RUN_RE.fullmatch(run_id):
raise Gate13CloudError("GCP run ID is invalid")
self.ssh_key_explicit = ssh_private_key is not None or config.ssh_private_key is not None
self.ssh_private_key = (
ssh_private_key
or (Path(config.ssh_private_key) if config.ssh_private_key else None)
or Path.home() / ".ssh" / "google_compute_engine"
).resolve()
self.ssh_public_fingerprint = ssh_public_fingerprint or config.ssh_public_fingerprint
self.run_id = run_id
self.repository_root = repository_root
self.output_root = output_root
Expand Down Expand Up @@ -596,6 +642,8 @@ def _ssh(
"--zone",
self.config.zone,
"--tunnel-through-iap",
"--ssh-key-file",
os.fspath(self.ssh_private_key),
"--quiet",
"--command",
command,
Expand All @@ -622,6 +670,8 @@ def _scp(
"--zone",
self.config.zone,
"--tunnel-through-iap",
"--ssh-key-file",
os.fspath(self.ssh_private_key),
"--quiet",
action=action,
timeout=timeout,
Expand Down Expand Up @@ -710,25 +760,35 @@ def _wait_ssh(
raise Gate13CloudError(f"{action} exceeded its time bound")

def _ensure_ssh_key(self) -> Path:
root = Path.home() / ".ssh"
private = root / "google_compute_engine"
public = private.with_suffix(".pub")
root.mkdir(mode=0o700, exist_ok=True)
if public.is_file() and 32 <= public.stat().st_size <= 16_384:
return public
if private.is_file():
result = self.runner.run(
["ssh-keygen", "-y", "-f", private], action="Deriving the GCP SSH public key", timeout=60
)
public.write_text(result.stdout.strip() + "\n", encoding="ascii", newline="\n")
else:
private = self.ssh_private_key
public = private.with_suffix(private.suffix + ".pub")
if not private.is_file() and self.ssh_key_explicit:
raise Gate13CloudError("configured SSH private key is unavailable")
private.parent.mkdir(mode=0o700, exist_ok=True)
if not private.is_file():
if public.exists() or self.ssh_public_fingerprint is not None:
raise Gate13CloudError("configured SSH private key is unavailable")
self.runner.run(
["ssh-keygen", "-t", "rsa", "-b", "3072", "-N", "", "-f", private],
action="Creating the GCP SSH key",
timeout=120,
)
if not public.is_file():
raise Gate13CloudError("GCP SSH public key is unavailable")
result = self.runner.run(
["ssh-keygen", "-y", "-f", private],
action="Checking the exact GCP SSH public key",
timeout=60,
)
identity, fingerprint = _ssh_public_identity(result.stdout.strip())
if self.ssh_public_fingerprint is not None and fingerprint != self.ssh_public_fingerprint:
raise Gate13CloudError("configured SSH public fingerprint does not match private key")
if public.exists():
if not public.is_file() or public.stat().st_size > 16_384:
raise Gate13CloudError("GCP SSH public key is invalid")
existing, _ = _ssh_public_identity(public.read_text(encoding="ascii").strip())
if existing != identity:
raise Gate13CloudError("GCP SSH public key does not match private key")
else:
public.write_text(identity + "\n", encoding="ascii", newline="\n")
return public

def _resource_absence(self) -> tuple[list[str], list[str], list[str]]:
Expand Down Expand Up @@ -1750,13 +1810,12 @@ def prepare_client(self, platform: str, package: PackageArtifact) -> Mapping[str
name = self.clients[platform]
if platform == "windows":
user = "Gate13Admin"
ready_command = (
"powershell.exe -NoLogo -NoProfile -NonInteractive -Command "
"\"if (!(Test-Path -LiteralPath 'C:\\Gate13Bootstrap\\ready.txt' "
ready_command = _windows_powershell_command(
"if (!(Test-Path -LiteralPath 'C:\\Gate13Bootstrap\\ready.txt' "
"-PathType Leaf)) { exit 1 }; "
"$p=@(Get-Process explorer -IncludeUserName -ErrorAction SilentlyContinue | "
"Where-Object {$_.UserName -like '*\\M'}); "
'if ($p.Count -ne 1 -or $p[0].SessionId -lt 1) { exit 1 }"'
"if ($p.Count -ne 1 -or $p[0].SessionId -lt 1) { exit 1 }"
)
else:
user = None
Expand Down Expand Up @@ -1788,10 +1847,7 @@ def prepare_client(self, platform: str, package: PackageArtifact) -> Mapping[str
timeout=900,
)
if platform == "windows":
command = (
"powershell.exe -NoLogo -NoProfile -NonInteractive "
"-ExecutionPolicy Bypass -File C:\\Gate13Run\\stage.ps1"
)
command = "powershell.exe -NoLogo -NonInteractive " "-ExecutionPolicy Bypass -File C:\\Gate13Run\\stage.ps1"
else:
command = "sudo bash /tmp/stage.sh"
result = self._ssh(
Expand Down Expand Up @@ -1902,8 +1958,7 @@ def _capture_host_failure(self, platform: str, instance: str) -> Path:
"[Console]::Out.Write([IO.File]::ReadAllText("
f"'C:\\Gate13Run\\{filename}'))"
)
encoded = base64.b64encode(script.encode("utf-16-le")).decode("ascii")
command = f"powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand {encoded}"
command = _windows_powershell_command(script)
user = "Gate13Admin"
else:
command = f"sudo cat /qualification/{filename}"
Expand Down
3 changes: 1 addition & 2 deletions scripts/gate13_host_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"PROGRAMFILES(X86)",
"PROGRAMW6432",
"PUBLIC",
"PSModulePath",
"SYSTEMDRIVE",
"SYSTEMROOT",
"TEMP",
Expand Down Expand Up @@ -544,7 +545,6 @@ def _entrypoint_argv(config: HostJobConfig) -> list[str]:
return [
r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
Expand Down Expand Up @@ -869,7 +869,6 @@ def _powershell_argv(script: str) -> list[str]:
return [
r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-EncodedCommand",
encoded,
Expand Down
70 changes: 52 additions & 18 deletions scripts/gate13_windows_client_startup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ $bootstrapRoot = "C:\Gate13Bootstrap"
$runRoot = "C:\Gate13Run"
$downloadRoot = "C:\Gate13Download"
New-Item -ItemType Directory -Force -Path $bootstrapRoot, $runRoot, $downloadRoot | Out-Null
function Set-Gate13KeyAcl {
param([string]$Path, [string[]]$AllowedSids, [switch]$Directory)
$item = Get-Item -LiteralPath $Path -Force
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "SSH key path is a reparse point" }
if ($Directory) { $acl = [Security.AccessControl.DirectorySecurity]::new() }
else { $acl = [Security.AccessControl.FileSecurity]::new() }
$acl.SetAccessRuleProtection($true, $false)
$acl.SetOwner([Security.Principal.SecurityIdentifier]::new($AllowedSids[0]))
foreach ($sid in $AllowedSids) {
$identity = [Security.Principal.SecurityIdentifier]::new($sid)
if ($Directory) {
$rule = [Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
} else {
$rule = [Security.AccessControl.FileSystemAccessRule]::new($identity, 'FullControl', 'Allow')
}
$acl.AddAccessRule($rule)
}
$item.SetAccessControl($acl)
$actual = $item.GetAccessControl()
$rules = @($actual.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]))
if (!$actual.AreAccessRulesProtected -or $rules.Count -ne $AllowedSids.Count) { throw "SSH key ACL verification failed" }
foreach ($rule in $rules) {
if ($rule.IsInherited -or $rule.IdentityReference.Value -notin $AllowedSids -or $rule.AccessControlType -ne 'Allow' -or $rule.FileSystemRights -ne 'FullControl') { throw "SSH key ACL verification failed" }
}
}

function Install-Gate13SshKey {
$publicKey = (Invoke-RestMethod -Headers $metadataHeaders -Uri "$metadataRoot/gate13-ssh-public-key").Trim()
if ($publicKey -notmatch '^ssh-(rsa|ed25519) [A-Za-z0-9+/]+={0,2}( [^\r\n]*)?$') { throw "Expected one exact SSH public key" }
$ordinarySid = (Get-LocalUser -Name 'M').SID.Value
$sshRoot = 'C:\Users\M\.ssh'
$programDataSsh = Join-Path $env:ProgramData 'ssh'
New-Item -ItemType Directory -Force -Path $sshRoot, $programDataSsh | Out-Null
Set-Gate13KeyAcl -Path $sshRoot -AllowedSids @($ordinarySid, 'S-1-5-18') -Directory
foreach ($name in @('authorized_keys')) {
$path = Join-Path $sshRoot $name
if (Test-Path -LiteralPath $path) { Set-Gate13KeyAcl -Path $path -AllowedSids @($ordinarySid, 'S-1-5-18') }
[IO.File]::WriteAllText($path, $publicKey + "`n", [Text.UTF8Encoding]::new($false))
Set-Gate13KeyAcl -Path $path -AllowedSids @($ordinarySid, 'S-1-5-18')
}
foreach ($name in @('administrators_authorized_keys', 'communityai_gate13_m_authorized_keys')) {
$path = Join-Path $programDataSsh $name
if (Test-Path -LiteralPath $path) { Set-Gate13KeyAcl -Path $path -AllowedSids @('S-1-5-32-544', 'S-1-5-18') }
[IO.File]::WriteAllText($path, $publicKey + "`n", [Text.UTF8Encoding]::new($false))
Set-Gate13KeyAcl -Path $path -AllowedSids @('S-1-5-32-544', 'S-1-5-18')
}
}

$readyMarker = Join-Path $bootstrapRoot "ready.txt"
if (Test-Path -LiteralPath $readyMarker -PathType Leaf) {
Set-Service -Name sshd -StartupType Automatic
Expand All @@ -17,6 +65,7 @@ if (Test-Path -LiteralPath $readyMarker -PathType Leaf) {
} else {
Set-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -Enabled True -Profile Any
}
Install-Gate13SshKey
return
}

Expand Down Expand Up @@ -59,23 +108,8 @@ if ($openSshMemberNames -notcontains "$env:COMPUTERNAME\M") {
}
Remove-LocalGroupMember -Group "Administrators" -Member "M" -ErrorAction SilentlyContinue

$publicKey = (Invoke-RestMethod -Headers $metadataHeaders -Uri "$metadataRoot/gate13-ssh-public-key").Trim()
$profileRoot = "C:\Users\M"
$sshRoot = Join-Path $profileRoot ".ssh"
New-Item -ItemType Directory -Force -Path $profileRoot, $sshRoot | Out-Null
$authorizedKeys = Join-Path $sshRoot "authorized_keys"
[IO.File]::WriteAllText($authorizedKeys, $publicKey + "`n", [Text.UTF8Encoding]::new($false))
& icacls.exe $sshRoot /inheritance:r /grant:r "M:(OI)(CI)F" "SYSTEM:(OI)(CI)F" | Out-Null
& icacls.exe $authorizedKeys /inheritance:r /grant:r "M:F" "SYSTEM:F" | Out-Null

$programDataSsh = Join-Path $env:ProgramData "ssh"
New-Item -ItemType Directory -Force -Path $programDataSsh | Out-Null
$administratorKeys = Join-Path $programDataSsh "administrators_authorized_keys"
$ordinaryKeys = Join-Path $programDataSsh "communityai_gate13_m_authorized_keys"
[IO.File]::WriteAllText($administratorKeys, $publicKey + "`n", [Text.UTF8Encoding]::new($false))
[IO.File]::WriteAllText($ordinaryKeys, $publicKey + "`n", [Text.UTF8Encoding]::new($false))
& icacls.exe $administratorKeys /inheritance:r /grant:r "Administrators:F" "SYSTEM:F" | Out-Null
& icacls.exe $ordinaryKeys /inheritance:r /grant:r "Administrators:F" "SYSTEM:F" | Out-Null
Install-Gate13SshKey
$programDataSsh = Join-Path $env:ProgramData 'ssh'
$sshdConfig = Join-Path $programDataSsh "sshd_config"
if (-not (Test-Path -LiteralPath $sshdConfig -PathType Leaf)) {
Copy-Item -LiteralPath "$env:WINDIR\System32\OpenSSH\sshd_config_default" -Destination $sshdConfig
Expand Down Expand Up @@ -132,7 +166,7 @@ Set-ItemProperty -Path $winlogon -Name DefaultUserName -Value "M" -Type String
Set-ItemProperty -Path $winlogon -Name DefaultDomainName -Value $env:COMPUTERNAME -Type String
Set-ItemProperty -Path $winlogon -Name DefaultPassword -Value $plainPassword -Type String
Set-ItemProperty -Path $winlogon -Name AutoLogonCount -Value 1 -Type DWord
$clearArgument = '-NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 60; $p = ''HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon''; Remove-ItemProperty -Path $p -Name DefaultPassword -ErrorAction SilentlyContinue; Set-ItemProperty -Path $p -Name AutoAdminLogon -Value ''0'' -Type String"'
$clearArgument = '-WindowStyle Hidden -Command "Start-Sleep -Seconds 60; $p = ''HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon''; Remove-ItemProperty -Path $p -Name DefaultPassword -ErrorAction SilentlyContinue; Set-ItemProperty -Path $p -Name AutoAdminLogon -Value ''0'' -Type String"'
$clearAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $clearArgument
$clearTrigger = New-ScheduledTaskTrigger -AtLogOn -User "M"
Register-ScheduledTask -TaskName "Gate13ClearAutoLogon" -Action $clearAction -Trigger $clearTrigger -User "SYSTEM" -RunLevel Highest -Force | Out-Null
Expand Down
Loading
Loading