Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const resolveProductionSshConnectConfig = createSshConnectConfigResolver(
spawnProxyJump,
createAgent,
env: process.env,
platform: process.platform,
});

export type { SshConnectInput, SshConnectResult };
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
resolveSshConnectConfig,
type SshConnectDeps,
} from './resolve-ssh-connect-config';
import { WINDOWS_OPENSSH_AGENT_PIPE } from './ssh-agent-socket';

function baseConfig(partial: Partial<SshConfig> = {}): SshConfig {
return {
Expand Down Expand Up @@ -66,6 +67,7 @@ function deps(overrides: Partial<SshConnectDeps> = {}): SshConnectDeps {
getStream: (callback) => callback(undefined, new PassThrough()),
}) satisfies BaseAgent,
env: { SSH_AUTH_SOCK: '/tmp/default-agent.sock' },
platform: 'linux',
...overrides,
};
}
Expand Down Expand Up @@ -243,6 +245,129 @@ describe('resolveSshConnectConfig', () => {
});
});

it('falls back to the Windows OpenSSH agent pipe for agent auth when no socket is configured', async () => {
await expect(
resolveSshConnectConfig(
{
kind: 'transient',
config: baseConfig({ sshConfigAlias: 'corp-dev', authType: 'agent' }),
},
deps({
env: {},
platform: 'win32',
resolveSshConfig: async () => ({
hostname: 'dev.internal',
user: 'alice',
port: 22,
identityFile: [],
identityAgent: undefined,
identityAgentDisabled: false,
identitiesOnly: false,
proxyCommand: undefined,
proxyJump: undefined,
forwardAgent: false,
}),
})
)
).resolves.toMatchObject({
config: { agent: WINDOWS_OPENSSH_AGENT_PIPE },
});
});

it('falls back to the Windows OpenSSH agent pipe when SSH_AUTH_SOCK is missing or empty', async () => {
for (const env of [{}, { SSH_AUTH_SOCK: '' }]) {
await expect(
resolveSshConnectConfig(
{
kind: 'transient',
config: baseConfig({ sshConfigAlias: 'corp-dev', authType: 'agent' }),
},
deps({
env,
platform: 'win32',
resolveSshConfig: async () => ({
hostname: 'dev.internal',
user: 'alice',
port: 22,
identityFile: [],
identityAgent: 'SSH_AUTH_SOCK',
identityAgentDisabled: false,
identitiesOnly: false,
proxyCommand: undefined,
proxyJump: undefined,
forwardAgent: false,
}),
})
)
).resolves.toMatchObject({
config: { agent: WINDOWS_OPENSSH_AGENT_PIPE },
});
}
});

it('does not override IdentityAgent none with the Windows OpenSSH agent pipe', async () => {
await expect(
resolveSshConnectConfig(
{
kind: 'transient',
config: baseConfig({ sshConfigAlias: 'corp-dev', authType: 'agent' }),
},
deps({
env: {},
platform: 'win32',
resolveSshConfig: async () => ({
hostname: 'dev.internal',
user: 'alice',
port: 22,
identityFile: [],
identityAgent: undefined,
identityAgentDisabled: true,
identitiesOnly: false,
proxyCommand: undefined,
proxyJump: undefined,
forwardAgent: false,
}),
})
)
).rejects.toThrow('SSH agent is disabled');
});

it('falls back to the Windows OpenSSH agent pipe for ForwardAgent yes when no socket is configured', async () => {
await expect(
resolveSshConnectConfig(
{
kind: 'transient',
config: {
...baseConfig({ sshConfigAlias: 'corp-dev', authType: 'password' }),
password: 'pw',
},
},
deps({
env: {},
platform: 'win32',
resolveSshConfig: async () => ({
hostname: 'dev.internal',
user: 'alice',
port: 22,
identityFile: [],
identityAgent: undefined,
identityAgentDisabled: false,
identitiesOnly: false,
proxyCommand: undefined,
proxyJump: undefined,
forwardAgent: true,
}),
})
)
).resolves.toMatchObject({
config: {
password: 'pw',
agentForward: true,
agent: WINDOWS_OPENSSH_AGENT_PIPE,
},
});
});

it('limits alias-backed agent auth to IdentityFile keys when IdentitiesOnly is enabled', async () => {
const readFiles: string[] = [];
const allowedKey = parseFixturePublicKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface SshConnectDeps {
) => Omit<TransportResult, 'process'>;
createAgent: (socketPath: string) => BaseAgent;
env: Record<string, string | undefined>;
platform: NodeJS.Platform;
}

function defaultDeps(): SshConnectDeps {
Expand All @@ -68,6 +69,7 @@ function defaultDeps(): SshConnectDeps {
defaultSpawnProxyJump(jumpSpec, destHost, destPort),
createAgent,
env: process.env,
platform: process.platform,
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const WINDOWS_OPENSSH_AGENT_PIPE = '\\\\.\\pipe\\openssh-ssh-agent';

export function defaultSshAgentSocket(
env: Record<string, string | undefined>,
platform: NodeJS.Platform
): string | undefined {
return env.SSH_AUTH_SOCK || (platform === 'win32' ? WINDOWS_OPENSSH_AGENT_PIPE : undefined);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type ResolvedSshConfig,
} from '../config/resolve-ssh-config';
import type { SshConnectDeps, SshConnectInput } from './resolve-ssh-connect-config';
import { defaultSshAgentSocket } from './ssh-agent-socket';

const { utils } = ssh2;

Expand Down Expand Up @@ -149,7 +150,7 @@ export async function buildAuthConfig(
? agentSocket.path
: agentSocket.kind === 'disabled'
? undefined
: deps.env.SSH_AUTH_SOCK;
: defaultSshAgentSocket(deps.env, deps.platform);
if (agentSocket.kind === 'disabled') {
throw new Error(`SSH agent is disabled by SSH config for connection '${base.name}'`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type ResolvedSshConfig,
} from '../config/resolve-ssh-config';
import type { SshConnectDeps } from './resolve-ssh-connect-config';
import { defaultSshAgentSocket } from './ssh-agent-socket';
import type { AuthResult } from './ssh-connect-auth';

function expandForwardAgentValue(
Expand All @@ -32,7 +33,10 @@ function agentForForwarding(resolved: ResolvedSshConfig | undefined, deps: SshCo
const agentSocket = resolved
? resolveAgentSocketFromResolved(resolved, deps.env)
: ({ kind: 'unset' } satisfies ResolvedAgentSocket);
const agent = agentSocket.kind === 'socket' ? agentSocket.path : deps.env.SSH_AUTH_SOCK;
const agent =
agentSocket.kind === 'socket'
? agentSocket.path
: defaultSshAgentSocket(deps.env, deps.platform);
Comment thread
janburzinski marked this conversation as resolved.
if (!agent) {
throw new Error('Agent forwarding was requested, but no SSH agent socket is available');
}
Expand Down
Loading