diff --git a/src/config/schema.ts b/src/config/schema.ts index 3a943da39..52d5200fb 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -106,6 +106,7 @@ const FIELD_LABELS: Record = { "gateway.remote.sshIdentity": "Remote Gateway SSH Identity", "gateway.remote.token": "Remote Gateway Token", "gateway.remote.password": "Remote Gateway Password", + "gateway.remote.tlsFingerprint": "Remote Gateway TLS Fingerprint", "gateway.auth.token": "Gateway Token", "gateway.auth.password": "Gateway Password", "tools.media.image.enabled": "Enable Image Understanding", @@ -311,6 +312,8 @@ const FIELD_HELP: Record = { "update.channel": 'Update channel for npm installs ("stable" or "beta").', "update.checkOnStart": "Check for npm updates when the gateway starts (default: true).", "gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).", + "gateway.remote.tlsFingerprint": + "Expected sha256 TLS fingerprint for the remote gateway (pin to avoid MITM).", "gateway.remote.sshTarget": "Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.", "gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).", @@ -558,6 +561,7 @@ const FIELD_HELP: Record = { const FIELD_PLACEHOLDERS: Record = { "gateway.remote.url": "ws://host:18789", + "gateway.remote.tlsFingerprint": "sha256:ab12cd34…", "gateway.remote.sshTarget": "user@host", "gateway.controlUi.basePath": "/clawdbot", }; diff --git a/src/config/types.gateway.ts b/src/config/types.gateway.ts index caa5ab22b..700a611af 100644 --- a/src/config/types.gateway.ts +++ b/src/config/types.gateway.ts @@ -82,6 +82,8 @@ export type GatewayRemoteConfig = { token?: string; /** Password for remote auth (when the gateway requires password auth). */ password?: string; + /** Expected TLS certificate fingerprint (sha256) for remote gateways. */ + tlsFingerprint?: string; /** SSH target for tunneling remote Gateway (user@host). */ sshTarget?: string; /** SSH identity file path for tunneling remote Gateway. */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 49416b3b1..fd000a27c 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -261,6 +261,7 @@ export const ClawdbotSchema = z url: z.string().optional(), token: z.string().optional(), password: z.string().optional(), + tlsFingerprint: z.string().optional(), sshTarget: z.string().optional(), sshIdentity: z.string().optional(), }) diff --git a/src/gateway/call.ts b/src/gateway/call.ts index c0557008e..3e630ecf5 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -22,6 +22,7 @@ export type CallGatewayOptions = { url?: string; token?: string; password?: string; + tlsFingerprint?: string; config?: ClawdbotConfig; method: string; params?: unknown; @@ -139,7 +140,16 @@ export async function callGateway(opts: CallGatewayOptions): Promis const useLocalTls = config.gateway?.tls?.enabled === true && !urlOverride && !remoteUrl && url.startsWith("wss://"); const tlsRuntime = useLocalTls ? await loadGatewayTlsRuntime(config.gateway?.tls) : undefined; - const tlsFingerprint = tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined; + const remoteTlsFingerprint = + isRemoteMode && !urlOverride && remoteUrl && typeof remote?.tlsFingerprint === "string" + ? remote.tlsFingerprint.trim() + : undefined; + const overrideTlsFingerprint = + typeof opts.tlsFingerprint === "string" ? opts.tlsFingerprint.trim() : undefined; + const tlsFingerprint = + overrideTlsFingerprint || + remoteTlsFingerprint || + (tlsRuntime?.enabled ? tlsRuntime.fingerprintSha256 : undefined); const token = (typeof opts.token === "string" && opts.token.trim().length > 0 ? opts.token.trim()