Skip to content

[bug]: SSH agent auth always fails when ~/.ssh/config has IdentitiesOnly yes (IdentityFilteredAgent is rejected by ssh2's isAgent check) #2901

Description

@stephenwilley

Bug Description

When an SSH connection is set to agent authentication and the user's ~/.ssh/config resolution for that host includes IdentitiesOnly yes together with an IdentityFile, the connection always fails with All configured authentication methods failed even though the agent is reachable and holds exactly the right key.

Root cause (in apps/emdash-desktop/src/main/core/ssh/connect/ssh-connect-auth.ts): when the IdentitiesOnly branch fires, buildAuthConfig passes config.agent = new IdentityFilteredAgent(...). That class is declared as

class IdentityFilteredAgent implements BaseAgent {

implements is type-level only - there's no prototype link to ssh2's BaseAgent. ssh2's client config validation accepts an agent only as a string path or via isAgent(val), which is literally val instanceof BaseAgent (ssh2 lib/client.js ~L219–224, lib/agent.js isAgent). The instanceof check fails, so ssh2 silently sets this.config.agent = undefined and connects with no auth methods at all: it sends the none probe, receives USERAUTH_FAILURE (publickey,password), and disconnects without ever offering a key.

The identity-filtering logic itself is correct. Verified getIdentities filters to exactly the expected key when called directly. The only problem is that ssh2 never accepts the wrapper object.

Suggested fix: make the class extends BaseAgent, which needs three small edits as below (tested locally: with this change ssh2 accepts the filtered agent and auth succeeds):

 import ssh2, {
-  type BaseAgent,
+  BaseAgent,
   type ConnectConfig,
   ...
 } from 'ssh2';

-class IdentityFilteredAgent implements BaseAgent {
+class IdentityFilteredAgent extends BaseAgent {
   readonly kind = 'identity-filtered-agent';
   declare getStream?: BaseAgent['getStream'];

   constructor(
     readonly socketPath: string,
     private readonly agent: BaseAgent,
     private readonly allowedKeys: ParsedKey[]
   ) {
+    super();
     if (agent.getStream) {
       this.getStream = agent.getStream.bind(agent);
     }
   }

Note this failure mode is easy to hit: IdentitiesOnly yes under Host * was part of an ssh-config hardening pattern suggested to me, and any user who has it gets a guaranteed auth failure in agent mode.

Steps to Reproduce

  1. Have a working ssh-agent with a key that authenticates to some host.

  2. In ~/.ssh/config, have the host match a block containing both directives, e.g.:

    Host *
        IdentityFile ~/.ssh/id_ed25519
        IdentitiesOnly yes
    
  3. In Emdash, create an SSH connection to that host with authentication = SSH agent.

  4. Connect (or use Test connection).

Removing IdentitiesOnly yes (or overriding it to no for that host) makes the same connection succeed immediately. That's the workaround I'm using.

Actual vs Expected Behavior

Actual: connection fails with All configured authentication methods failed. The ssh2 debug log shows the handshake completing and then no publickey attempt at all:

Outbound: Sending USERAUTH_REQUEST (none)
Inbound: Received USERAUTH_FAILURE (publickey,password)
Client: none auth failed
Outbound: Sending DISCONNECT (11)

Expected: the agent key matching the IdentityFile is offered and authentication succeeds.

Verified directly against ssh2 1.17.0: passing a wrapper object of this shape as agent fails with the same error; passing the raw socket path to the identical connect() call succeeds. Happy to share the standalone repro script if useful.

emdash Version

1.1.39

Operating System

macOS 26.5.2

Additional Context

  • The bundled ssh2 is 1.17.0; its isAgent() is val instanceof BaseAgent, so any agent object that merely has the right methods, without actually inheriting from BaseAgent, is silently discarded during Client.connect config normalization. No error is surfaced, which is why the resulting message points users at their credentials instead of the real cause.
  • Possibly related history: [bug]: SSH Host IdentityAgent not being loaded when selecting SSH Agent Authentication #1102 (IdentityAgent support in agent mode), which this filtering code appears to be part of.
  • Happy to open a PR for the extends BaseAgent fix if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions