From abd9674d400e1198544c53f311f6967a02541660 Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Sat, 4 Apr 2026 20:27:16 -0700 Subject: [PATCH 1/2] fix: skip SSH agent when no keys loaded to prevent auth failure Go's x/crypto/ssh library fails the entire publickey auth chain when PublicKeysCallback returns zero signers from an empty agent. This breaks SSH for macOS users where SSH_AUTH_SOCK is always set but no keys are explicitly loaded in the agent. Now checks agent.Signers() before adding the agent as an auth method. Also closes the agent connection immediately after use (fixes FD leak from #10) and removes em dash from comment. --- internal/hetzner/client.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/hetzner/client.go b/internal/hetzner/client.go index f8ae2ca..8ba1f17 100644 --- a/internal/hetzner/client.go +++ b/internal/hetzner/client.go @@ -287,15 +287,22 @@ func SSHConnect(ip string) (*ssh.Client, error) { var diagErrors []string // Try SSH agent first (handles passphrase-protected keys) - var agentConn net.Conn if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { conn, err := net.Dial("unix", sock) if err != nil { diagErrors = append(diagErrors, fmt.Sprintf("SSH agent dial failed: %v", err)) } else { - agentConn = conn agentClient := agent.NewClient(conn) - authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) + signers, err := agentClient.Signers() + if err != nil { + diagErrors = append(diagErrors, fmt.Sprintf("SSH agent signers failed: %v", err)) + conn.Close() + } else if len(signers) == 0 { + conn.Close() + } else { + authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) + conn.Close() + } } } @@ -314,7 +321,7 @@ func SSHConnect(ip string) (*ssh.Client, error) { if err != nil { var passErr *ssh.PassphraseMissingError if errors.As(err, &passErr) { - continue // passphrase-protected, skip — agent handles these + continue // passphrase-protected, skip - agent handles these } diagErrors = append(diagErrors, fmt.Sprintf("failed to parse %s: %v", keyPath, err)) continue @@ -340,9 +347,6 @@ func SSHConnect(ip string) (*ssh.Client, error) { client, err := ssh.Dial("tcp", ip+":22", config) if err != nil { - if agentConn != nil { - agentConn.Close() - } return nil, err } return client, nil From 3bf24bbc1ec6249aad7163fc876816e8dd305ab5 Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Sat, 4 Apr 2026 20:31:15 -0700 Subject: [PATCH 2/2] fix: keep agent connection open through ssh.Dial for lazy callback PublicKeysCallback calls Signers() lazily during the SSH handshake, not when the auth method is added. The previous commit closed the agent connection too early, which would break agent auth for users with keys loaded. Now keeps the connection open through Dial and closes it after, whether success or failure. --- internal/hetzner/client.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/hetzner/client.go b/internal/hetzner/client.go index 8ba1f17..6d8560e 100644 --- a/internal/hetzner/client.go +++ b/internal/hetzner/client.go @@ -286,7 +286,10 @@ func SSHConnect(ip string) (*ssh.Client, error) { var authMethods []ssh.AuthMethod var diagErrors []string - // Try SSH agent first (handles passphrase-protected keys) + // Try SSH agent first (handles passphrase-protected keys). + // The agent connection must stay open through ssh.Dial because + // PublicKeysCallback calls Signers() lazily during the handshake. + var agentConn net.Conn if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" { conn, err := net.Dial("unix", sock) if err != nil { @@ -300,8 +303,8 @@ func SSHConnect(ip string) (*ssh.Client, error) { } else if len(signers) == 0 { conn.Close() } else { + agentConn = conn authMethods = append(authMethods, ssh.PublicKeysCallback(agentClient.Signers)) - conn.Close() } } } @@ -346,6 +349,9 @@ func SSHConnect(ip string) (*ssh.Client, error) { } client, err := ssh.Dial("tcp", ip+":22", config) + if agentConn != nil { + agentConn.Close() + } if err != nil { return nil, err }