# OpenClaw (Mac Studio) administrator PowerShell remote access - a.kieley.dev # Run in an elevated (Run as administrator) Windows PowerShell 5.1+ window. # # What it does: # 1. Installs Tailscale and joins this PC to James's tailnet (browser sign-in). # 2. Installs/enables OpenSSH Server, starts it automatically at boot. # 3. Trusts ONE public key (OpenClaw on the Mac Studio) for administrator logins. # 4. Key-only auth (passwords disabled), PowerShell as the SSH shell. # 5. Firewall: SSH (TCP 22) allowed only from the Tailscale range 100.64.0.0/10. # Undo: see the "Remove access" section on https://a.kieley.dev # # Only a PUBLIC key is in this script. No secrets. $ErrorActionPreference = 'Stop' $OpenClawKey = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILPVqmrVmynre7vQXH6UKQC0mBrju7pbr+WFwGd0iQwc openclaw-mac-studio-windows-admin' function Step($m) { Write-Host "==> $m" -ForegroundColor Cyan } $id = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not ([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this in PowerShell opened with "Run as administrator".' } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # --- 1. Tailscale ----------------------------------------------------------- $ts = 'C:\Program Files\Tailscale\tailscale.exe' if (-not (Test-Path $ts)) { Step 'Installing Tailscale' $msi = Join-Path $env:TEMP 'tailscale-setup.msi' $arch = if ([Environment]::Is64BitOperatingSystem) { if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' } } else { 'x86' } Invoke-WebRequest -UseBasicParsing "https://pkgs.tailscale.com/stable/tailscale-setup-latest-$arch.msi" -OutFile $msi $sig = Get-AuthenticodeSignature $msi if ($sig.Status -ne 'Valid' -or $sig.SignerCertificate.Subject -notmatch 'Tailscale') { throw "Tailscale installer signature check failed: $($sig.Status)" } Start-Process msiexec.exe -Wait -ArgumentList "/i `"$msi`" /quiet /norestart TS_UNATTENDEDMODE=always" } # Keep Tailscale connected even when nobody is signed in to Windows (system policy). New-Item -Path 'HKLM:\SOFTWARE\Policies\Tailscale' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Tailscale' -Name UnattendedMode -Value 'always' -PropertyType String -Force | Out-Null # Read the tailnet address from the network adapter: the tailscale CLI refuses # (401 "already in use by") when another Windows user owns the Tailscale session. function Get-TsIp { Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -like '100.*' -and $_.InterfaceAlias -like '*Tailscale*' } | Select-Object -First 1 -ExpandProperty IPAddress } $tsIp = Get-TsIp if ($tsIp) { Step "Tailscale already connected ($tsIp)" } else { Step 'Connecting to tailnet (sign in as elderjameskieley if a browser opens / open the printed URL)' & $ts up --unattended --timeout 5m $tsIp = Get-TsIp if (-not $tsIp) { throw 'Tailscale is not connected. Click the Tailscale icon by the clock, log in as elderjameskieley, then re-run the snippet.' } } # --- 2. OpenSSH Server ------------------------------------------------------ Step 'Installing OpenSSH Server' $cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' | Select-Object -First 1 if ($cap.State -ne 'Installed') { Add-WindowsCapability -Online -Name $cap.Name | Out-Null } Set-Service sshd -StartupType Automatic $sshDir = "$env:ProgramData\ssh" $sshBin = "$env:SystemRoot\System32\OpenSSH" function Show-SshdDiagnostics { $ErrorActionPreference = 'Continue' Write-Host '--- sshd diagnostics (send a photo of this to OpenClaw) ---' -ForegroundColor Yellow Write-Host "OS: $([Environment]::OSVersion.VersionString) sshd: $((Get-Item "$sshBin\sshd.exe").VersionInfo.FileVersion)" Get-Service sshd | Format-Table Name, Status, StartType -AutoSize | Out-String | Write-Host Write-Host 'sshd -t:'; & "$sshBin\sshd.exe" -t 2>&1 | Out-String | Write-Host Write-Host 'Port 22 listeners:' Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue | ForEach-Object { " pid $($_.OwningProcess) $((Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Path)" } | Write-Host Write-Host 'Recent OpenSSH / service errors:' Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 5 -ErrorAction SilentlyContinue | ForEach-Object { " $($_.TimeCreated) $($_.Message)" } | Write-Host Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'; StartTime=(Get-Date).AddMinutes(-15)} -ErrorAction SilentlyContinue | Where-Object { $_.Message -match 'sshd|OpenSSH' } | Select-Object -First 3 | ForEach-Object { " $($_.Message)" } | Write-Host Write-Host 'Folder:'; icacls.exe $sshDir | Out-String | Write-Host Write-Host 'ListenAddress / logging lines:'; Get-Content "$sshDir\sshd_config" -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\s*(ListenAddress|Port|SyslogFacility|LogLevel|Match|AllowUsers|AllowGroups)' } | Write-Host Write-Host 'This PC IPv4:'; (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object IPAddress -ne '127.0.0.1').IPAddress -join ', ' | Write-Host if (Test-Path "$sshDir\logs\sshd.log") { Write-Host 'sshd.log (last 12):'; Get-Content "$sshDir\logs\sshd.log" -Tail 12 | Write-Host } Write-Host 'Foreground sshd -d (6s):' $o = Join-Path $env:TEMP 'sshd-debug.txt' $p = Start-Process "$sshBin\sshd.exe" -ArgumentList '-d', '-E', $o -PassThru -WindowStyle Hidden Start-Sleep 6; if (-not $p.HasExited) { Stop-Process -Id $p.Id -Force } if (Test-Path $o) { Get-Content $o -Tail 15 | Write-Host } } function Repair-Sshd { $ErrorActionPreference = 'Continue' Step 'sshd did not start - repairing config, host keys and folder permissions' if (-not (Test-Path $sshDir)) { New-Item -ItemType Directory -Path $sshDir | Out-Null } icacls.exe $sshDir /inheritance:r /grant '*S-1-5-18:(OI)(CI)F' /grant '*S-1-5-32-544:(OI)(CI)F' /grant '*S-1-5-11:(OI)(CI)RX' | Out-Null if (-not (Test-Path "$sshDir\sshd_config")) { Copy-Item "$sshBin\sshd_config_default" "$sshDir\sshd_config" } Disable-StaleListenAddress if (-not (Test-Path "$sshDir\logs")) { New-Item -ItemType Directory -Path "$sshDir\logs" | Out-Null } Push-Location $env:ProgramData; & "$sshBin\ssh-keygen.exe" -A 2>&1 | Out-Null; Pop-Location Get-ChildItem "$sshDir\ssh_host_*_key" -ErrorAction SilentlyContinue | ForEach-Object { icacls.exe $_.FullName /inheritance:r /grant '*S-1-5-18:F' /grant '*S-1-5-32-544:F' | Out-Null } } # An old ListenAddress pinned to an IP this PC no longer has makes sshd exit at startup. # Listen on all addresses instead; the firewall rule below limits who can connect. function Disable-StaleListenAddress { $f = "$sshDir\sshd_config" if (-not (Test-Path $f)) { return } $lines = Get-Content $f if (-not ($lines -match '^\s*ListenAddress\s')) { return } Copy-Item $f "$f.before-openclaw-$(Get-Date -Format yyyyMMddHHmmss)" $lines = $lines | ForEach-Object { if ($_ -match '^\s*ListenAddress\s') { "# (disabled by OpenClaw setup) $_" } else { $_ } } Set-Content -Path $f -Value $lines -Encoding ascii Step 'Removed pinned ListenAddress from sshd_config (backup kept)' } function Start-Sshd { Disable-StaleListenAddress try { Restart-Service sshd -ErrorAction Stop; return } catch { } Repair-Sshd try { Restart-Service sshd -ErrorAction Stop; Step 'sshd repaired and running' } catch { Show-SshdDiagnostics; throw 'OpenSSH Server still will not start. Send OpenClaw a photo of the diagnostics above.' } } Start-Sshd # first start generates host keys + default sshd_config # --- 3. Administrator key --------------------------------------------------- Step 'Trusting the OpenClaw administrator key' $akf = "$env:ProgramData\ssh\administrators_authorized_keys" $existing = if (Test-Path $akf) { Get-Content $akf } else { @() } if ($existing -notcontains $OpenClawKey) { Add-Content -Path $akf -Value $OpenClawKey -Encoding ascii } icacls.exe $akf /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F' | Out-Null # --- 4. sshd hardening + PowerShell shell ----------------------------------- Step 'Hardening sshd' $cfg = "$env:ProgramData\ssh\sshd_config" $c = Get-Content $cfg | Where-Object { $_ -notmatch '^\s*(PasswordAuthentication|PubkeyAuthentication|PermitEmptyPasswords)\s' } $c = @('PubkeyAuthentication yes', 'PasswordAuthentication no', 'PermitEmptyPasswords no') + $c Set-Content -Path $cfg -Value $c -Encoding ascii New-Item -Path 'HKLM:\SOFTWARE\OpenSSH' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force | Out-Null & "$sshBin\sshd.exe" -t if ($LASTEXITCODE -ne 0) { Show-SshdDiagnostics; throw 'sshd_config test failed' } Start-Sshd # --- 5. Firewall: tailnet only ---------------------------------------------- Step 'Firewall: SSH from Tailscale only' Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue | Disable-NetFirewallRule Remove-NetFirewallRule -Name 'OpenClaw-SSH-Tailscale' -ErrorAction SilentlyContinue New-NetFirewallRule -Name 'OpenClaw-SSH-Tailscale' -DisplayName 'OpenClaw SSH (Tailscale only)' -Direction Inbound -Protocol TCP -LocalPort 22 -RemoteAddress '100.64.0.0/10' -Action Allow -Profile Any | Out-Null # --- 6. Keep it reachable --------------------------------------------------- powercfg /change standby-timeout-ac 0 | Out-Null $fp = (& "$env:SystemRoot\System32\OpenSSH\ssh-keygen.exe" -lf "$env:ProgramData\ssh\ssh_host_ed25519_key.pub") Write-Host '' Write-Host 'DONE. Tell OpenClaw:' -ForegroundColor Green Write-Host " computer : $env:COMPUTERNAME" Write-Host " user : $env:USERNAME" Write-Host " tailscale: $tsIp" Write-Host " host key : $fp"