fix(cloud): security review of A6: offer and save only keys the proxy swaps; no desktop keys over SSH into a cloud workspace; orcad on a cloud VM drops carried keys

- 'Save as personal Cloud secret' is offered, and accepted by main, only for names the egress proxy
  swaps (a shared list the control-plane test holds equal to the proxy's); anything else would sit
  on the VM in full, so main answers not_swappable.
- SSH launches into an Orca Cloud workspace (orca-* aliases or an 'orca ssh-proxy' ProxyCommand
  surfaced through ~/.ssh/config Includes) get no desktop keys.
- Defense in depth on the VM: orcad with ORCA_IS_CLOUD=1 drops a carried keyEnv.
This commit is contained in:
Neil
2026-11-10 02:37:29 -08:00
parent f364c85f87
commit dd4baa98ed
13 changed files with 194 additions and 15 deletions
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { compileNetworkPolicy } from '../src/network-policy'
import { SWAPPED_CREDENTIALS, credentialTargets } from '../src/provider-env'
import { AGENT_KEY_NAMES } from '../src/secrets'
import { cloudSecretStoredOnVm } from '../../../../src/shared/cloud-secrets/cloud-secret-rules'
import { CLOUD_SWAPPED_CREDENTIALS, cloudSecretStoredOnVm } from '../../../../src/shared/cloud-secrets/cloud-secret-rules'
import { egressSecretHosts, encodeEgressSecretHosts } from '../src/secret-egress'
import { ENV_COMMAND } from '../src/sandbox-vm-commands'
import { createHarness } from './harness'
@@ -286,5 +286,7 @@ describe('every agent credential is either swapped or marked "stored on workspac
expect(swapped !== cloudSecretStoredOnVm(name), `${name}: swapped=${swapped}`).toBe(true)
}
expect(SWAPPED_CREDENTIALS.includes('COPILOT_GITHUB_TOKEN')).toBe(true)
// The desktop's mirror (what it offers to save from this computer) matches the proxy exactly.
expect([...CLOUD_SWAPPED_CREDENTIALS].sort()).toEqual([...SWAPPED_CREDENTIALS].sort())
})
})
@@ -160,6 +160,10 @@ export function registerCloudSecretsHandlers(userDataPath: string): void {
if (nameError) {
return { ok: false, error: nameError }
}
// Only keys the workspace gets as a stand-in: anything else would sit there in full.
if (!cloudSecretSwapped(envName)) {
return { ok: false, error: 'not_swappable' }
}
const value = await desktopKeyValueForCloud(agent, envName)
if (!value) {
return { ok: false, error: 'not_found' }
@@ -21,8 +21,19 @@ vi.mock('../agent-api-keys/agent-api-key-launch-env', () => ({
resolveAgentApiKeyLaunchEnv: async (...args: unknown[]) => {
launchCalls.push(args)
return launchEnv
},
withAgentApiKeyLaunchEnv: async (env: Record<string, string> | undefined, ...args: unknown[]) => {
launchCalls.push(args)
return { ...env, ...launchEnv }
}
}))
const sshTargets = new Map<
string,
{ host: string; label: string; configHost?: string; proxyCommand?: string }
>()
vi.mock('../ssh/targets/ssh-target-registry', () => ({
getSshTargetRegistryStore: () => ({ getSshTarget: (id: string) => sshTargets.get(id) })
}))
const { desktopKeyValueForCloud, isOrcaCloudRuntimeEnvironment } =
await import('./cloud-desktop-keys')
@@ -32,6 +43,7 @@ beforeEach(() => {
environments.clear()
launchEnv = {}
launchCalls.length = 0
sshTargets.clear()
})
describe('isOrcaCloudRuntimeEnvironment', () => {
@@ -90,3 +102,31 @@ describe('withHostAgentApiKeyEnv', () => {
expect(readStatus).not.toHaveBeenCalled()
})
})
describe('withHostAgentApiKeyLaunchEnv (SSH)', () => {
it('sends no desktop keys over SSH into an Orca Cloud workspace, and does elsewhere', async () => {
const { withHostAgentApiKeyLaunchEnv } = await import('./cloud-desktop-keys')
launchEnv = { CURSOR_API_KEY: 'key_real' }
sshTargets.set('t-cloud', {
host: 'orca-api-7f3a',
label: 'orca-api-7f3a',
configHost: 'orca-api-7f3a'
})
sshTargets.set('t-proxy', {
host: 'box',
label: 'box',
proxyCommand: '/Applications/Orca.app/bin/orca ssh-proxy env-1'
})
sshTargets.set('t-devbox', { host: 'devbox.lan', label: 'devbox' })
expect(await withHostAgentApiKeyLaunchEnv({ A: '1' }, 'cursor', 't-cloud')).toEqual({ A: '1' })
expect(await withHostAgentApiKeyLaunchEnv({ A: '1' }, 'cursor', 't-proxy')).toEqual({ A: '1' })
expect(launchCalls).toEqual([])
expect(await withHostAgentApiKeyLaunchEnv({ A: '1' }, 'cursor', 't-devbox')).toEqual({
A: '1',
CURSOR_API_KEY: 'key_real'
})
expect(await withHostAgentApiKeyLaunchEnv(undefined, 'cursor', null)).toEqual({
CURSOR_API_KEY: 'key_real'
})
})
})
+38 -1
View File
@@ -2,7 +2,13 @@
// host those keys ride each agent launch as plain env. An Orca Cloud VM must never hold a model key,
// so they are not sent there at all: the user saves a key as a personal Cloud secret instead (one key
// per click), and the VM then gets a placeholder the egress proxy swaps.
import { resolveAgentApiKeyLaunchEnv } from '../agent-api-keys/agent-api-key-launch-env'
import {
resolveAgentApiKeyLaunchEnv,
withAgentApiKeyLaunchEnv
} from '../agent-api-keys/agent-api-key-launch-env'
import { isCloudSshAlias } from '../cloud-ssh/cloud-ssh-config'
import { getSshTargetRegistryStore } from '../ssh/targets/ssh-target-registry'
import { getConnectionExecutionHostId } from '../../shared/execution-host'
import { withAgentApiKeyRuntimeEnv } from '../agent-api-keys/agent-api-key-runtime-call'
import { listEphemeralVmRuntimes } from '../../shared/ephemeral-vm-runtime-store'
import { resolveEnvironment } from '../../shared/remote-runtime/host-store/runtime-environment-store'
@@ -78,3 +84,34 @@ export async function withHostAgentApiKeyEnv(
}
return withAgentApiKeyRuntimeEnv(environmentId, method, params, readStatus)
}
/**
* An SSH target that reaches an Orca Cloud workspace (`orca ssh` writes `orca-*` aliases that proxy
* through the Orca CLI; OpenSSH config Includes can surface them as targets).
*/
export function isOrcaCloudSshConnection(connectionId: string | null | undefined): boolean {
if (!connectionId) {
return false
}
const target = getSshTargetRegistryStore()?.getSshTarget(connectionId)
if (!target) {
return false
}
return (
[target.configHost, target.host, target.label].some(
(value) => value !== undefined && isCloudSshAlias(value)
) || /\bssh-proxy\b/.test(target.proxyCommand ?? '')
)
}
/** A local or SSH launch env with this computer's Agent API Keys, except over SSH into an Orca Cloud workspace. */
export async function withHostAgentApiKeyLaunchEnv(
env: Record<string, string> | undefined,
agent: TuiAgent | undefined,
connectionId: string | null | undefined
): Promise<Record<string, string> | undefined> {
if (isOrcaCloudSshConnection(connectionId)) {
return env
}
return withAgentApiKeyLaunchEnv(env, agent, getConnectionExecutionHostId(connectionId))
}
+2 -7
View File
@@ -1,6 +1,5 @@
import { withClaudeAgentViewPlugin } from '../../../shared/claude-agent-view-launch'
import { getConnectionExecutionHostId } from '../../../shared/execution-host'
import { withAgentApiKeyLaunchEnv } from '../../agent-api-keys/agent-api-key-launch-env'
import { withHostAgentApiKeyLaunchEnv } from '../../hosted-sandbox/cloud-desktop-keys'
import { resolveStartupShell } from '../../../shared/agent-launch/startup/tui-agent-startup-shell'
import { resolveLocalWindowsAgentStartupShell } from '../../../shared/windows-terminal-shell'
import { getAppEnvironment } from '../../../shared/app-environment'
@@ -156,11 +155,7 @@ export async function assemblePtySpawnLaunchOptions(input: PtySpawnLaunchOptions
const env =
input.preAdoptedStablePane || input.launchCommand === undefined
? openCodeLaunch.env
: await withAgentApiKeyLaunchEnv(
openCodeLaunch.env,
launchAgent,
getConnectionExecutionHostId(connectionId)
)
: await withHostAgentApiKeyLaunchEnv(openCodeLaunch.env, launchAgent, connectionId)
const pluginDir = !connectionId ? env?.ORCA_CLAUDE_AGENT_VIEW_PLUGIN_DIR : undefined
const launchCommand =
pluginDir && openCodeLaunch.command
@@ -0,0 +1,25 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { withKeyEnvFolded } from './agent-session'
afterEach(() => {
vi.unstubAllEnvs()
})
describe('carried agent keys on the host', () => {
it('folds them into the launch env on an ordinary host', () => {
expect(withKeyEnvFolded({ keyEnv: { GEMINI_API_KEY: 'AIzaSy-real' } })).toEqual({
keyEnv: undefined,
accountEnv: { GEMINI_API_KEY: 'AIzaSy-real' }
})
})
it('drops them on an Orca Cloud VM', () => {
vi.stubEnv('ORCA_IS_CLOUD', '1')
expect(
withKeyEnvFolded({ keyEnv: { GEMINI_API_KEY: 'AIzaSy-real' }, accountEnv: { X: '1' } })
).toEqual({
keyEnv: undefined,
accountEnv: { X: '1' }
})
})
})
@@ -34,12 +34,17 @@ function withExecutionHostAgentPresentation<T extends { presentation?: 'backgrou
}
/** Carried keys ride the account env the launch already merges over its plan; nothing downstream keeps the request. */
function withKeyEnvFolded<
export function withKeyEnvFolded<
T extends { accountEnv?: Record<string, string>; keyEnv?: Record<string, string> }
>(params: T): T {
if (!params.keyEnv) {
return params
}
// An Orca Cloud VM never takes a desktop's keys (they would sit here in plaintext); its agents
// get Cloud secrets as egress placeholders instead. The desktop already withholds them.
if (process.env.ORCA_IS_CLOUD === '1') {
return { ...params, keyEnv: undefined }
}
const accountEnv = withAgentKeyLaunchEnv(params.accountEnv, params.keyEnv)
const folded: T = { ...params, keyEnv: undefined, ...(accountEnv ? { accountEnv } : {}) }
return folded
@@ -52,3 +52,31 @@ describe('offerableDesktopKeys', () => {
).toEqual([{ id: 'k-0002', agent: 'gemini', envName: 'GEMINI_API_KEY', hint: '0002' }])
})
})
describe('offerableDesktopKeys: only keys a workspace gets as a stand-in', () => {
it('never offers a key the proxy cannot swap (it would sit on the VM in full)', () => {
const all = { kind: 'all' } as const
expect(
offerableDesktopKeys(
[
{
id: 'a',
agent: 'claude',
envName: 'AWS_BEARER_TOKEN_BEDROCK',
hint: 'aaaa',
hostScope: all
},
{
id: 'b',
agent: 'opencode',
envName: 'OPENROUTER_API_KEY',
hint: 'bbbb',
hostScope: all
},
{ id: 'c', agent: 'cursor', envName: 'CURSOR_API_KEY', hint: 'cccc', hostScope: all }
],
[]
).map((key) => key.envName)
).toEqual(['CURSOR_API_KEY'])
})
})
@@ -1,4 +1,5 @@
import type { AgentApiKeySummary } from '../../../../shared/agent-api-keys/agent-api-key-catalog'
import { cloudSecretSwapped } from '../../../../shared/cloud-secrets/cloud-secret-rules'
import type { CloudEffectiveSecret } from '../../../../shared/cloud-secrets/cloud-secrets-wire'
export type CloudSetupAgent = 'claude' | 'codex'
@@ -58,7 +59,13 @@ export function offerableDesktopKeys(
const seen = new Set<string>()
const offers: CloudOfferableDesktopKey[] = []
for (const key of desktopKeys) {
if (key.hostScope.kind !== 'all' || inCloud.has(key.envName) || seen.has(key.envName)) {
// Only keys a workspace gets as a stand-in; anything else would sit on the VM in full.
if (
key.hostScope.kind !== 'all' ||
!cloudSecretSwapped(key.envName) ||
inCloud.has(key.envName) ||
seen.has(key.envName)
) {
continue
}
seen.add(key.envName)
@@ -31,6 +31,11 @@ export function cloudSecretsErrorCopy(
t: Translate
): string {
switch (code) {
case 'not_swappable':
return t(
'cloudSecrets.error.notSwappable',
'A workspace would get this key in full, so Orca doesn’t save it from here. Add it in Cloud secrets if you accept that.'
)
case 'invalid_name':
return t(
'cloudSecrets.error.invalidName',
+11 -4
View File
@@ -26640,13 +26640,11 @@
"shareDetail": "Every teammate’s cloud agents use it.",
"status": {
"team": "provided by your team",
"desktopKey": "API key from Orca on this computer",
"cloudSecret": "your cloud secret",
"none": "not set up"
},
"source": {
"both": "Claude and Codex",
"desktopKey": "{{agent}} uses the API key from Orca on this computer",
"team": "Your team provides {{agent}} access",
"cloudSecret": "{{agent}} uses your cloud secret"
},
@@ -26690,7 +26688,15 @@
"discardQuestion": "Discard the key you typed?",
"discard": "Discard",
"keepEditing": "Keep editing",
"signInHandedOff": "Continues in the Agent sign-in row below."
"signInHandedOff": "Continues in the Agent sign-in row below.",
"desktopKeys": {
"title": "Keys on this computer",
"detail": "Cloud workspaces don’t get the API keys saved in Orca on this computer. Save one as a personal Cloud secret and the workspace only ever sees a stand-in for it.",
"saved": "Saved as a Cloud secret",
"saveLabel": "Save {{name}} as a personal Cloud secret",
"save": "Save as personal Cloud secret",
"retry": "Try again"
}
},
"cloudSecrets": {
"editor": {
@@ -26837,7 +26843,8 @@
"syntax": "Not a NAME=value line.",
"unterminatedQuote": "The closing quote is missing.",
"overridden": "Set again further down; the later line is used.",
"unknown": "Something went wrong in Orca Cloud. Nothing was changed. Try again."
"unknown": "Something went wrong in Orca Cloud. Nothing was changed. Try again.",
"notSwappable": "A workspace would get this key in full, so Orca doesn’t save it from here. Add it in Cloud secrets if you accept that."
},
"vm": {
"badge": "Stored on workspace",
@@ -169,3 +169,25 @@ export function isPublicHttpsUrl(value: string): boolean {
!host.startsWith('[')
)
}
/**
* Variables the Orca Cloud egress proxy swaps (cloud/apps/sandbox-control/src/provider-env.ts,
* checked equal by a control-plane test): a workspace only ever holds a stand-in for these.
*/
export const CLOUD_SWAPPED_CREDENTIALS: readonly string[] = [
'ANTHROPIC_API_KEY',
'ANTHROPIC_AUTH_TOKEN',
'AI_GATEWAY_API_KEY',
'OPENAI_API_KEY',
'GEMINI_API_KEY',
'GOOGLE_GENERATIVE_AI_API_KEY',
'CURSOR_API_KEY',
'GOOGLE_API_KEY',
'COPILOT_GITHUB_TOKEN',
'COPILOT_PROVIDER_API_KEY',
'COPILOT_PROVIDER_BEARER_TOKEN'
]
export function cloudSecretSwapped(name: string): boolean {
return CLOUD_SWAPPED_CREDENTIALS.includes(name)
}
@@ -86,6 +86,8 @@ export type CloudSecretsErrorCode =
| 'secret_limit'
| 'unauthorized'
| 'not_found'
/** Desktop only: a key the workspace would get in full (no egress stand-in), so not saved from here. */
| 'not_swappable'
| 'unavailable'
| 'timeout'
| 'not_configured'