Skip to content

feat(ssh): support SSH agent authentication - #457

Merged
tianzhou merged 1 commit into
bytebase:mainfrom
JonLaliberte:san-antonio/dbhub-ssh-agent-patch
Oct 9, 2026
Merged

tianzhou merged 1 commit into
bytebase:mainfrom
JonLaliberte:san-antonio/dbhub-ssh-agent-patch

Conversation

@JonLaliberte

Copy link
Copy Markdown
Contributor

Closes #456

Summary

Lets SSH tunnels authenticate through an SSH agent (ssh-agent, the 1Password SSH agent, etc.), so sources no longer need ssh_password or ssh_key when the key lives in an agent.

Behaviour

  • Agent source, in order: ssh_agent (TOML, per source) or --ssh-agent, then the SSH_AUTH_SOCK environment variable. ~/ is expanded for the TOML field and the flag.
  • Auth order: the agent is offered to every host in the chain, including ssh_proxy_jump hops, after any password or key. This is ssh2's default order; configs that work today are unaffected.
  • Auth requirement: the "password or key required" checks (TOML sources, CLI/env, and the tunnel itself) now also accept an agent.
  • Missing socket: a configured socket that does not exist fails with SSH agent socket not found: <path>. A stale SSH_AUTH_SOCK is ignored with a warning and does not count as an auth method. The existence check is skipped on Windows, where agents are named pipes or Pageant.
  • Unusable keys from ~/.ssh/config: ssh2 throws on a key it cannot parse (e.g. encrypted, no passphrase) before trying other methods. A key picked up from ~/.ssh/config (IdentityFile or a default ~/.ssh/id_*) is now skipped with a warning when a password or agent is available. A key given explicitly (ssh_key, --ssh-key, SSH_KEY) is never skipped and still fails with ssh2's parse error.

Not included

  • IdentityAgent and IdentitiesOnly from ~/.ssh/config are not read.
  • No opt-out for the ambient SSH_AUTH_SOCK.

Both are raised as design questions in #456; happy to adjust either way.

Implementation notes

  • SSHTunnelConfig gains agent and privateKeyDiscovered. parseSSHConfig sets the latter for any key it resolves; a jump host's own key is always treated as discovered.
  • SourceConfig.ssh_key_discovered is an internal marker that carries that flag through the single-DSN (--ssh-host alias) path. It is stripped from TOML input.
  • connectToHost now takes one SSHHostAuth object instead of separate password/key/passphrase arguments.
  • utils.parseKey is read off ssh2's default export, because utils is not exposed as a named export of the CommonJS module under ESM.
  • No new dependencies.

Testing

  • Unit tests cover agent-only auth, precedence of ssh_agent over SSH_AUTH_SOCK, jump hosts, missing and stale sockets, the discovered-versus-explicit key skip, --ssh-agent, and TOML validation and ~ expansion. pnpm test:unit passes.
  • Manually verified an agent-only tunnel to a MySQL source through a bastion with the 1Password SSH agent on macOS, via SSH_AUTH_SOCK, on an earlier revision of this branch (before ssh_agent / --ssh-agent and the key-skip change were added).
  • Verified against real ssh2 (no mock) that an encrypted key from ~/.ssh/config falls back to the agent, an explicit one still errors, and a missing configured socket is reported.

Docs

docs/config/command-line.mdx, docs/config/toml.mdx, dbhub.toml.example, .env.example, server.json and CLAUDE.md.

Offer an SSH agent (ssh-agent, 1Password, etc.) to every host in the
tunnel chain, after any password or key. The socket comes from the
ssh_agent TOML field or --ssh-agent, falling back to SSH_AUTH_SOCK, and
makes ssh_password/ssh_key optional.

A configured socket that does not exist is an error; a stale
SSH_AUTH_SOCK is ignored. A key picked up from ~/.ssh/config that ssh2
cannot parse is skipped when a password or agent is available, while an
explicitly configured key still fails.

Closes bytebase#456

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Input-validation defects, incomplete public-key fallback, and Windows-specific test failures remain unresolved.

4 open findings
What changed in this PR

Adds SSH agent authentication for direct and proxy-jump SSH tunnels.

Changes:

  • Supports configured or ambient SSH agent sockets.
  • Skips unusable discovered keys when fallback authentication exists.
  • Adds configuration validation, tests, and documentation.
File Description
src/​utils/​ssh-tunnel.ts Resolves agents and applies authentication fallback.
src/​utils/​ssh-config-parser.ts Marks discovered private keys.
src/​utils/​__tests__/​ssh-tunnel.test.ts Tests agent authentication and fallback.
src/​utils/​__tests__/​ssh-config-parser.test.ts Tests discovered-key metadata.
src/​types/​ssh.ts Extends tunnel configuration types.
src/​types/​config.ts Adds source agent configuration.
src/​connectors/​manager.ts Passes agent settings into tunnels.
src/​connectors/​__tests__/​manager.test.ts Tests manager agent propagation.
src/​config/​toml-loader.ts Validates and expands agent paths.
src/​config/​env.ts Adds --ssh-agent handling.
src/​config/​__tests__/​toml-loader.test.ts Tests TOML agent configuration.
src/​config/​__tests__/​ssh-config-integration.test.ts Tests CLI and environment resolution.
server.json Declares SSH_AUTH_SOCK.
docs/​config/​toml.mdx Documents TOML agent support.
docs/​config/​command-line.mdx Documents CLI agent support.
dbhub.toml.example Adds an agent example.
CLAUDE.md Updates development guidance.
.env.example Documents ambient agent configuration.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/config/env.ts
Comment on lines +647 to +654
if (args["ssh-agent"]) {
config.agent = args["ssh-agent"];
// Expand ~ to home directory
if (config.agent.startsWith("~/")) {
config.agent = path.join(process.env.HOME || "", config.agent.substring(2));
}
sources.push("ssh-agent from command line");
}
Comment thread src/config/toml-loader.ts
Comment on lines +881 to +883
if (processed.ssh_agent) {
processed.ssh_agent = expandHomeDir(processed.ssh_agent);
}
expect(connectCalls[0].agent).toBe(configuredSock);
});

it('should reject a configured agent socket that does not exist', async () => {
Comment thread src/utils/ssh-tunnel.ts
Comment on lines +252 to 264
const canSkip = auth.privateKeyDiscovered && Boolean(password || agent);
const parsedKey = canSkip ? ssh2.utils.parseKey(privateKey, passphrase) : undefined;
if (parsedKey instanceof Error) {
const desc = label || `${hostInfo.host}:${hostInfo.port}`;
console.warn(
`Skipping unusable SSH private key from ~/.ssh/config for ${desc} (${parsedKey.message}).`
);
} else {
sshConfig.privateKey = privateKey;
if (passphrase) {
sshConfig.passphrase = passphrase;
}
}

@tianzhou tianzhou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. Thanks for the contribution.

I will make some minor cleanup in a followup PR.

@tianzhou
tianzhou merged commit 26638dd into bytebase:main Oct 9, 2026
3 checks passed
tianzhou added a commit that referenced this pull request Oct 9, 2026
Follow-up to #457:
- Reject a bare `--ssh-agent` via requireFlagValue instead of treating it
  as the literal path "true".
- Only tilde-expand a string `ssh_agent` in TOML so a non-string value gets
  the "invalid ssh_agent" validation error rather than a TypeError.
- Treat a discovered key with no private material (an IdentityFile pointing
  at a .pub file, as 1Password setups do) as unusable, so it no longer
  blocks password/agent fallback. Mirrors the checks ssh2 runs in connect().
- Skip the Unix-only missing-socket tests on Windows.


Claude-Session: https://claude.ai/code/session_016RxaApSn5G62Ka7HTunaxN

Co-authored-by: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support SSH agent authentication for SSH tunnels (ssh-agent, 1Password)

3 participants