From 114b87caf29321a4bf4a16d047ef04d4a8d322ff Mon Sep 17 00:00:00 2001 From: Devin Robison Date: Fri, 17 Apr 2026 11:11:10 -0600 Subject: [PATCH] fix(macos): require trusted SSH host keys (#68199) * fix(macos): require trusted SSH host keys * chore(changelog): add macOS SSH strict host-key entry --- CHANGELOG.md | 1 + apps/macos/Sources/OpenClaw/CommandResolver.swift | 10 +++++++--- .../Sources/OpenClaw/NodePairingApprovalPrompter.swift | 3 +-- apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift | 4 +--- apps/macos/Sources/OpenClaw/RemotePortTunnel.swift | 4 +--- .../Tests/OpenClawIPCTests/CommandResolverTests.swift | 3 +++ 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3163361e11e..7a2544280d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Telegram/streaming: clear the compaction replay guard after visible non-final boundaries so a post-tool assistant reply rotates to a fresh preview instead of editing the pre-compaction message. (#67993) Thanks @obviyus. - Matrix: fix `sessions_spawn --thread` subagent session spawning — thread binding creation, cleanup on session end, and completion-message delivery target resolution now work end-to-end. (#67643) Thanks @eejohnso-ops and @gumadeiras. - macOS/webchat: enable Undo and Redo in the composer text input by turning on the native `NSTextView` undo manager. (#34962) Thanks @tylerbittner. +- macOS/remote SSH: require an already-trusted host key on the macOS remote command, gateway probe, port tunnel, and pairing probe paths by switching `StrictHostKeyChecking=accept-new` to `StrictHostKeyChecking=yes` and centralizing the shared SSH option fragments in `CommandResolver`, so first-time macOS remote connections no longer silently accept an unknown host key and must be trusted ahead of time via `~/.ssh/known_hosts`. (#68199) ## 2026.4.15 diff --git a/apps/macos/Sources/OpenClaw/CommandResolver.swift b/apps/macos/Sources/OpenClaw/CommandResolver.swift index 06e5d5c32dc..718a303fc7a 100644 --- a/apps/macos/Sources/OpenClaw/CommandResolver.swift +++ b/apps/macos/Sources/OpenClaw/CommandResolver.swift @@ -3,6 +3,12 @@ import Foundation enum CommandResolver { private static let projectRootDefaultsKey = "openclaw.gatewayProjectRootPath" private static let helperName = "openclaw" + static let strictHostKeyCheckingSSHOptions = [ + "-o", "StrictHostKeyChecking=yes", + ] + static let updateHostKeysSSHOptions = [ + "-o", "UpdateHostKeys=yes", + ] static func gatewayEntrypoint(in root: URL) -> String? { let distEntry = root.appendingPathComponent("dist/index.js").path @@ -397,9 +403,7 @@ enum CommandResolver { """ let options: [String] = [ "-o", "BatchMode=yes", - "-o", "StrictHostKeyChecking=accept-new", - "-o", "UpdateHostKeys=yes", - ] + ] + self.strictHostKeyCheckingSSHOptions + self.updateHostKeysSSHOptions let args = self.sshArguments( target: parsed, identity: settings.identity, diff --git a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift index bd27e49626b..f70e0f579a7 100644 --- a/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/OpenClaw/NodePairingApprovalPrompter.swift @@ -483,8 +483,7 @@ final class NodePairingApprovalPrompter { "-o", "ConnectTimeout=5", "-o", "NumberOfPasswordPrompts=0", "-o", "PreferredAuthentications=publickey", - "-o", "StrictHostKeyChecking=accept-new", - ] + ] + CommandResolver.strictHostKeyCheckingSSHOptions guard let target = CommandResolver.makeSSHTarget(user: user, host: host, port: port) else { return false } diff --git a/apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift b/apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift index bde65c03495..78a865935d2 100644 --- a/apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift +++ b/apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift @@ -200,9 +200,7 @@ enum RemoteGatewayProbe { let options = [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", - "-o", "StrictHostKeyChecking=accept-new", - "-o", "UpdateHostKeys=yes", - ] + ] + CommandResolver.strictHostKeyCheckingSSHOptions + CommandResolver.updateHostKeysSSHOptions let args = CommandResolver.sshArguments( target: parsed, identity: identity, diff --git a/apps/macos/Sources/OpenClaw/RemotePortTunnel.swift b/apps/macos/Sources/OpenClaw/RemotePortTunnel.swift index 82adc209c16..79af1d61828 100644 --- a/apps/macos/Sources/OpenClaw/RemotePortTunnel.swift +++ b/apps/macos/Sources/OpenClaw/RemotePortTunnel.swift @@ -73,14 +73,12 @@ final class RemotePortTunnel { let options: [String] = [ "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", - "-o", "StrictHostKeyChecking=accept-new", - "-o", "UpdateHostKeys=yes", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3", "-o", "TCPKeepAlive=yes", "-N", "-L", "\(localPort):127.0.0.1:\(resolvedRemotePort)", - ] + ] + CommandResolver.strictHostKeyCheckingSSHOptions + CommandResolver.updateHostKeysSSHOptions let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) let args = CommandResolver.sshArguments( target: parsed, diff --git a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift index bb5d0b14f28..7cf471eadb7 100644 --- a/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift @@ -164,6 +164,9 @@ import Testing } else { #expect(Bool(false)) } + #expect(cmd.contains("StrictHostKeyChecking=yes")) + #expect(!cmd.contains("StrictHostKeyChecking=accept-new")) + #expect(cmd.contains("UpdateHostKeys=yes")) #expect(cmd.contains("-i")) #expect(cmd.contains("/tmp/id_ed25519")) if let script = cmd.last {