From 88435839f798e0dccbca54ecc2f655214e5a7367 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:12:51 +0800 Subject: [PATCH 1/6] feat(ssh): add detailed SSH agent connection logging - Add logSSHKeywords helper to log SSH config with privacy masking - Enhance agent connection/communication logging with blocklogger - Use INFO level for key events (connect success/failure, signer count) - Use DEBUG level for diagnostic info (config details, key fingerprints) - Mask hostname (show first 3 and last 3 chars only) - Show only basename for identity files - Add Windows-specific hint when agent connection fails --- pkg/remote/sshclient.go | 68 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index 50a5b9dbfb..abadbcf507 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -89,6 +89,41 @@ func SimpleMessageFromPossibleConnectionError(err error) string { return err.Error() } +// logSSHKeywords logs SSH configuration in a sanitized way (DEBUG level) +func logSSHKeywords(ctx context.Context, sshKeywords *wconfig.ConnKeywords) { + blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", utilfn.SafeDeref(sshKeywords.SshUser)) + blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", maskHostName(utilfn.SafeDeref(sshKeywords.SshHostName))) + blocklogger.Debugf(ctx, "[ssh-config] Port: %s\n", utilfn.SafeDeref(sshKeywords.SshPort)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) + blocklogger.Debugf(ctx, "[ssh-config] IdentitiesOnly: %v\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile count: %d\n", len(sshKeywords.SshIdentityFile)) + // Only log file basename, not full path for privacy + for i, f := range sshKeywords.SshIdentityFile { + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, filepath.Base(f)) + } + blocklogger.Debugf(ctx, "[ssh-config] PubkeyAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPubkeyAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] PasswordAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPasswordAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] KbdInteractiveAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshKbdInteractiveAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] PreferredAuthentications: %v\n", sshKeywords.SshPreferredAuthentications) + blocklogger.Debugf(ctx, "[ssh-config] AddKeysToAgent: %v\n", utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent)) + blocklogger.Debugf(ctx, "[ssh-config] ProxyJump: %v\n", sshKeywords.SshProxyJump) + // Note: do not log PasswordSecretName value, only indicate if configured + if sshKeywords.SshPasswordSecretName != nil && *sshKeywords.SshPasswordSecretName != "" { + blocklogger.Debugf(ctx, "[ssh-config] PasswordSecretName: \n") + } +} + +// maskHostName masks hostname for privacy, showing only first 3 and last 3 characters +func maskHostName(hostname string) string { + if hostname == "" { + return "" + } + if len(hostname) <= 6 { + return "***" + } + return hostname[:3] + "***" + hostname[len(hostname)-3:] +} + // This exists to trick the ssh library into continuing to try // different public keys even when the current key cannot be // properly parsed @@ -608,6 +643,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor remoteName = chosenUser + "@" + remoteName } + // Log SSH configuration (DEBUG level) + logSSHKeywords(connCtx, sshKeywords) + var authSockSigners []ssh.Signer var agentClient agent.ExtendedAgent @@ -615,12 +653,36 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor // TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" { + blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", agentPath) conn, err := dialIdentityAgent(agentPath) if err != nil { - log.Printf("Failed to open Identity Agent Socket %q: %v", agentPath, err) + blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", agentPath, err) + if runtime.GOOS == "windows" { + blocklogger.Infof(connCtx, "[ssh-agent] hint: ensure OpenSSH Authentication Agent service is running (Get-Service ssh-agent)\n") + } } else { + blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", agentPath) agentClient = agent.NewClient(conn) - authSockSigners, _ = agentClient.Signers() + blocklogger.Debugf(connCtx, "[ssh-agent] requesting key list from agent...\n") + var signerErr error + authSockSigners, signerErr = agentClient.Signers() + if signerErr != nil { + blocklogger.Infof(connCtx, "[ssh-agent] WARNING failed to get signers from agent: %v\n", signerErr) + } else { + blocklogger.Infof(connCtx, "[ssh-agent] retrieved %d signers from agent\n", len(authSockSigners)) + // Log public key fingerprints (DEBUG level, for troubleshooting) + for i, signer := range authSockSigners { + pubKey := signer.PublicKey() + fingerprint := ssh.FingerprintSHA256(pubKey) + blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), fingerprint) + } + } + } + } else { + if agentPath == "" { + blocklogger.Debugf(connCtx, "[ssh-agent] no agent path configured\n") + } else { + blocklogger.Debugf(connCtx, "[ssh-agent] agent skipped (IdentitiesOnly=%v)\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) } } @@ -731,6 +793,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. var sshConfigKeywords *wconfig.ConnKeywords if utilfn.SafeDeref(internalSshConfigKeywords.ConnIgnoreSshConfig) { + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", opts.SSHHost) var err error sshConfigKeywords, err = findSshDefaults(opts.SSHHost) if err != nil { @@ -738,6 +801,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } } else { + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", opts.SSHHost) var err error sshConfigKeywords, err = findSshConfigKeywords(opts.SSHHost) if err != nil { From 43fb6e8d538656d1d3cec50be59d26dd13c1b085 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:54:26 +0800 Subject: [PATCH 2/6] fix(wshrpc): update StreamCancelFn signature to include context parameter --- pkg/wshrpc/wshrpctypes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 14ffb14779..d85ce20c8e 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -373,7 +373,7 @@ type RpcOpts struct { NoResponse bool `json:"noresponse,omitempty"` Route string `json:"route,omitempty"` - StreamCancelFn func() `json:"-"` // this is an *output* parameter, set by the handler + StreamCancelFn func(ctx context.Context) error `json:"-"` // this is an *output* parameter, set by the handler } const ( From 4e8f8aed65c4520c374d263957b4044b8a8eab74 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:03:41 +0800 Subject: [PATCH 3/6] feat(ssh): add privacy masking for sensitive connection log data Add comprehensive privacy masking for SSH connection logs: - Add MaskString() to mask usernames, hostnames, and connection identifiers - Add maskIdentityFile() to mask paths while preserving directory structure - Add logSSHKeywords() to log SSH config with masked sensitive values - Mask remoteName and shellPath in shellcontroller.go - Mask connection name and knownhosts address in conncontroller.go - Mask networkAddr, opts.String(), and fingerprints in sshclient.go - Fix StreamCancelFn type signature to accept context.Context --- pkg/blockcontroller/shellcontroller.go | 2 +- pkg/remote/conncontroller/conncontroller.go | 6 +- pkg/remote/sshclient.go | 111 ++++++++++++++++---- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index 5411c40404..b2ab9ff9cd 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -383,7 +383,7 @@ func (bc *ShellController) setupAndStartShellProcess(logCtx context.Context, rc if err != nil { return nil, err } - blocklogger.Infof(logCtx, "[conndebug] remoteName: %q, connType: %s, wshEnabled: %v, shell: %q, shellType: %s\n", remoteName, connUnion.ConnType, connUnion.WshEnabled, connUnion.ShellPath, connUnion.ShellType) + blocklogger.Infof(logCtx, "[conndebug] remoteName: %q, connType: %s, wshEnabled: %v, shell: %q, shellType: %s\n", remote.MaskString(remoteName), connUnion.ConnType, connUnion.WshEnabled, remote.MaskString(connUnion.ShellPath), connUnion.ShellType) var cmdStr string var cmdOpts shellexec.CommandOptsType if bc.ControllerType == BlockController_Shell { diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index b5d8779a58..aaaa69a477 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -550,7 +550,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wconfig.ConnKeyword conn.Infof(ctx, "cannot connect to %q when status is %q\n", conn.GetName(), conn.GetStatus()) return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus()) } - conn.Infof(ctx, "trying to connect to %q...\n", conn.GetName()) + conn.Infof(ctx, "trying to connect to %q...\n", remote.MaskString(conn.GetName())) conn.FireConnChangeEvent() err := conn.connectInternal(ctx, connFlags) conn.WithLock(func() { @@ -748,7 +748,7 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckRes // returns (connect-error) func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wconfig.ConnKeywords) error { - conn.Infof(ctx, "connectInternal %s\n", conn.GetName()) + conn.Infof(ctx, "connectInternal %s\n", remote.MaskString(conn.GetName())) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { conn.Infof(ctx, "ERROR ConnectToClient: %s\n", remote.SimpleMessageFromPossibleConnectionError(err)) @@ -765,7 +765,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wconfig.Con conn.waitForDisconnect() }() fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) - conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) + conn.Infof(ctx, "normalized knownhosts address: %s\n", remote.MaskString(fmtAddr)) clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) wshResult := conn.tryEnableWsh(ctx, clientDisplayName) if !wshResult.WshEnabled { diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index abadbcf507..cb0e862628 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -74,9 +74,9 @@ type ConnectionError struct { func (ce ConnectionError) Error() string { if ce.CurrentClient == nil { - return fmt.Sprintf("Connecting to %s, Error: %v", ce.NextOpts, ce.Err) + return fmt.Sprintf("Connecting to %s, Error: %v", MaskString(ce.NextOpts.String()), ce.Err) } - return fmt.Sprintf("Connecting from %v to %s (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err) + return fmt.Sprintf("Connecting from client to %s (jump number %d), Error: %v", MaskString(ce.NextOpts.String()), ce.JumpNum, ce.Err) } func SimpleMessageFromPossibleConnectionError(err error) string { @@ -91,37 +91,102 @@ func SimpleMessageFromPossibleConnectionError(err error) string { // logSSHKeywords logs SSH configuration in a sanitized way (DEBUG level) func logSSHKeywords(ctx context.Context, sshKeywords *wconfig.ConnKeywords) { - blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", utilfn.SafeDeref(sshKeywords.SshUser)) - blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", maskHostName(utilfn.SafeDeref(sshKeywords.SshHostName))) + blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", MaskString(utilfn.SafeDeref(sshKeywords.SshUser))) + blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", MaskString(utilfn.SafeDeref(sshKeywords.SshHostName))) blocklogger.Debugf(ctx, "[ssh-config] Port: %s\n", utilfn.SafeDeref(sshKeywords.SshPort)) - blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", filepath.Base(utilfn.SafeDeref(sshKeywords.SshIdentityAgent))) blocklogger.Debugf(ctx, "[ssh-config] IdentitiesOnly: %v\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) blocklogger.Debugf(ctx, "[ssh-config] IdentityFile count: %d\n", len(sshKeywords.SshIdentityFile)) - // Only log file basename, not full path for privacy + // Log masked identity file paths for privacy for i, f := range sshKeywords.SshIdentityFile { - blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, filepath.Base(f)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, maskIdentityFile(f)) } blocklogger.Debugf(ctx, "[ssh-config] PubkeyAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPubkeyAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] PasswordAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPasswordAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] KbdInteractiveAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshKbdInteractiveAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] PreferredAuthentications: %v\n", sshKeywords.SshPreferredAuthentications) blocklogger.Debugf(ctx, "[ssh-config] AddKeysToAgent: %v\n", utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent)) - blocklogger.Debugf(ctx, "[ssh-config] ProxyJump: %v\n", sshKeywords.SshProxyJump) + blocklogger.Debugf(ctx, "[ssh-config] ProxyJump count: %d\n", len(sshKeywords.SshProxyJump)) // Note: do not log PasswordSecretName value, only indicate if configured if sshKeywords.SshPasswordSecretName != nil && *sshKeywords.SshPasswordSecretName != "" { blocklogger.Debugf(ctx, "[ssh-config] PasswordSecretName: \n") } } -// maskHostName masks hostname for privacy, showing only first 3 and last 3 characters -func maskHostName(hostname string) string { - if hostname == "" { +// MaskString masks a string for privacy, showing only first 3 and last 3 characters. +// Uses rune-based slicing to properly handle multi-byte UTF-8 characters. +func MaskString(s string) string { + if s == "" { return "" } - if len(hostname) <= 6 { + runes := []rune(s) + if len(runes) <= 6 { return "***" } - return hostname[:3] + "***" + hostname[len(hostname)-3:] + return string(runes[:3]) + "***" + string(runes[len(runes)-3:]) +} + +// maskIdentityFile masks an identity file path for privacy. +// It masks the username in home directory paths (/home/user/ or C:\Users\user\) +// and masks the filename while preserving .pub suffix if present. +func maskIdentityFile(path string) string { + if path == "" { + return "" + } + + // Normalize path separators for consistent handling + normalizedPath := filepath.ToSlash(path) + + // Extract directory and filename + dir := filepath.Dir(path) + filename := filepath.Base(path) + + // Check for .pub suffix + hasPubSuffix := strings.HasSuffix(filename, ".pub") + if hasPubSuffix { + filename = strings.TrimSuffix(filename, ".pub") + } + + // Mask the filename + maskedFilename := MaskString(filename) + if hasPubSuffix { + maskedFilename += ".pub" + } + + // Mask username in home directory paths + // Unix: /home/username/... or /Users/username/... + // Windows: C:\Users\username\... (normalized to C:/Users/username/...) + maskedDir := dir + if strings.HasPrefix(normalizedPath, "/home/") { + parts := strings.SplitN(normalizedPath, "/", 4) // ["", "home", "username", "rest..."] + if len(parts) >= 3 { + maskedUsername := MaskString(parts[2]) + if len(parts) >= 4 { + maskedDir = "/home/" + maskedUsername + "/" + filepath.Dir(parts[3]) + } else { + maskedDir = "/home/" + maskedUsername + } + } + } else if strings.Contains(normalizedPath, "/Users/") { + // Handle both Unix /Users/ and Windows C:/Users/ + idx := strings.Index(normalizedPath, "/Users/") + prefix := normalizedPath[:idx] + rest := normalizedPath[idx+7:] // Skip "/Users/" + parts := strings.SplitN(rest, "/", 2) + if len(parts) >= 1 { + maskedUsername := MaskString(parts[0]) + if len(parts) >= 2 { + maskedDir = prefix + "/Users/" + maskedUsername + "/" + filepath.Dir(parts[1]) + } else { + maskedDir = prefix + "/Users/" + maskedUsername + } + } + } + + // Convert back to native path separators + maskedDir = filepath.FromSlash(maskedDir) + + return filepath.Join(maskedDir, maskedFilename) } // This exists to trick the ssh library into continuing to try @@ -653,15 +718,15 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor // TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" { - blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", agentPath) + blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", filepath.Base(agentPath)) conn, err := dialIdentityAgent(agentPath) if err != nil { - blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", agentPath, err) + blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", filepath.Base(agentPath), err) if runtime.GOOS == "windows" { blocklogger.Infof(connCtx, "[ssh-agent] hint: ensure OpenSSH Authentication Agent service is running (Get-Service ssh-agent)\n") } } else { - blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", agentPath) + blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", filepath.Base(agentPath)) agentClient = agent.NewClient(conn) blocklogger.Debugf(connCtx, "[ssh-agent] requesting key list from agent...\n") var signerErr error @@ -674,7 +739,7 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor for i, signer := range authSockSigners { pubKey := signer.PublicKey() fingerprint := ssh.FingerprintSHA256(pubKey) - blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), fingerprint) + blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), MaskString(fingerprint)) } } } @@ -750,14 +815,14 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. var err error if currentClient == nil { d := net.Dialer{Timeout: clientConfig.Timeout} - blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", MaskString(networkAddr)) clientConn, err = d.DialContext(ctx, "tcp", networkAddr) if err != nil { blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) return nil, err } } else { - blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", MaskString(networkAddr)) clientConn, err = currentClient.DialContext(ctx, "tcp", networkAddr) if err != nil { blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) @@ -769,12 +834,12 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. blocklogger.Infof(ctx, "[conndebug] ERROR ssh auth/negotiation: %s\n", SimpleMessageFromPossibleConnectionError(err)) return nil, err } - blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", MaskString(networkAddr)) return ssh.NewClient(c, chans, reqs), nil } func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wconfig.ConnKeywords) (*ssh.Client, int32, error) { - blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", opts.String(), jumpNum) + blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", MaskString(opts.String()), jumpNum) debugInfo := &ConnectionDebugInfo{ CurrentClient: currentClient, NextOpts: opts, @@ -793,7 +858,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. var sshConfigKeywords *wconfig.ConnKeywords if utilfn.SafeDeref(internalSshConfigKeywords.ConnIgnoreSshConfig) { - blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", opts.SSHHost) + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", MaskString(opts.SSHHost)) var err error sshConfigKeywords, err = findSshDefaults(opts.SSHHost) if err != nil { @@ -801,7 +866,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } } else { - blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", opts.SSHHost) + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", MaskString(opts.SSHHost)) var err error sshConfigKeywords, err = findSshConfigKeywords(opts.SSHHost) if err != nil { From 8ca09fd82cb66e69a480c050028ef70cb724c14a Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:32:01 +0800 Subject: [PATCH 4/6] Revert "fix(wshrpc): update StreamCancelFn signature to include context parameter" This reverts commit 43fb6e8d538656d1d3cec50be59d26dd13c1b085. --- pkg/wshrpc/wshrpctypes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index d85ce20c8e..14ffb14779 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -373,7 +373,7 @@ type RpcOpts struct { NoResponse bool `json:"noresponse,omitempty"` Route string `json:"route,omitempty"` - StreamCancelFn func(ctx context.Context) error `json:"-"` // this is an *output* parameter, set by the handler + StreamCancelFn func() `json:"-"` // this is an *output* parameter, set by the handler } const ( From e9999750f1b52aa381d0e1f715c6bccea66186ce Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:26:19 +0800 Subject: [PATCH 5/6] fix(ssh): resolve IdentitiesOnly agent filtering and Windows defaults This change fixes issues preventing SSH agent usage on Windows when `IdentitiesOnly` is enabled. - Fix Agent Logic: Always attempt agent connection even if `IdentitiesOnly` is set. - Fix Windows Defaults: Update `findSshDefaults` to use the correct named pipe. - Enhance Parsing: Support `authorized_keys` format for `.pub` files. --- pkg/remote/sshclient.go | 88 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index cb0e862628..a978b2ae14 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -238,6 +238,78 @@ func createPublicKeyCallback(connCtx context.Context, sshKeywords *wconfig.ConnK // require pointer to modify list in closure identityFilesPtr := &identityFiles + // If IdentitiesOnly is set, filter agent signers to only include those that match an IdentityFile + // This matches OpenSSH behavior where the agent can still be used, but only for keys explicitly listed + if utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && len(authSockSignersExt) > 0 { + var identityPubKeys []ssh.PublicKey + for _, identityFile := range identityFiles { + pubKeyData := existingKeys[identityFile] + + // 1. Try reading as OpenSSH authorized_key format (e.g., "ssh-rsa AAAA... comment") + // This is the most common format for .pub files including 1Password + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyData) + if err == nil { + identityPubKeys = append(identityPubKeys, pubKey) + blocklogger.Debugf(connCtx, "[ssh-agent] loaded public key from %s (authorized_key format)\n", maskIdentityFile(identityFile)) + continue + } + + // 2. Try reading as raw public key (binary format) + pubKey, err = ssh.ParsePublicKey(pubKeyData) + if err == nil { + identityPubKeys = append(identityPubKeys, pubKey) + blocklogger.Debugf(connCtx, "[ssh-agent] loaded public key from %s (raw format)\n", maskIdentityFile(identityFile)) + continue + } + + // 3. Try reading as unencrypted private key (to get public key) + privKey, err := ssh.ParseRawPrivateKey(pubKeyData) + if err == nil { + signer, err := ssh.NewSignerFromKey(privKey) + if err == nil { + identityPubKeys = append(identityPubKeys, signer.PublicKey()) + blocklogger.Debugf(connCtx, "[ssh-agent] loaded public key from %s (private key)\n", maskIdentityFile(identityFile)) + continue + } + } + + // 4. Handle encrypted private keys by looking for a corresponding .pub file + pubKeyPath, _ := wavebase.ExpandHomeDir(identityFile + ".pub") + if pubKeyPath != "" { + pubKeyData, err = os.ReadFile(pubKeyPath) + if err == nil { + pubKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyData) + if err == nil { + identityPubKeys = append(identityPubKeys, pubKey) + blocklogger.Debugf(connCtx, "[ssh-agent] loaded public key from %s.pub\n", maskIdentityFile(identityFile)) + continue + } + } + } + + blocklogger.Debugf(connCtx, "[ssh-agent] WARNING: could not load public key from %s\n", maskIdentityFile(identityFile)) + } + + var filtered []ssh.Signer + for _, signer := range authSockSignersExt { + matched := false + for _, pubKey := range identityPubKeys { + if bytes.Equal(signer.PublicKey().Marshal(), pubKey.Marshal()) { + matched = true + break + } + } + if matched { + filtered = append(filtered, signer) + blocklogger.Debugf(connCtx, "[ssh-agent] keeping agent key %s (matches IdentityFile)\n", MaskString(ssh.FingerprintSHA256(signer.PublicKey()))) + } else { + blocklogger.Debugf(connCtx, "[ssh-agent] skipping agent key %s (IdentitiesOnly=true, no matching IdentityFile)\n", MaskString(ssh.FingerprintSHA256(signer.PublicKey()))) + } + } + authSockSignersExt = filtered + blocklogger.Infof(connCtx, "[ssh-agent] after IdentitiesOnly filtering: %d signers remaining\n", len(authSockSignersExt)) + } + var authSockSigners []ssh.Signer authSockSigners = append(authSockSigners, authSockSignersExt...) authSockSignersPtr := &authSockSigners @@ -715,9 +787,10 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor var agentClient agent.ExtendedAgent // IdentitiesOnly indicates that only the keys listed in the identity and certificate files or passed as arguments should be used, even if there are matches in the SSH Agent, PKCS11Provider, or SecurityKeyProvider. See https://man.openbsd.org/ssh_config#IdentitiesOnly + // When IdentitiesOnly is true, we still connect to the agent but filter signers in createPublicKeyCallback // TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) - if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" { + if agentPath != "" { blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", filepath.Base(agentPath)) conn, err := dialIdentityAgent(agentPath) if err != nil { @@ -744,11 +817,7 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor } } } else { - if agentPath == "" { - blocklogger.Debugf(connCtx, "[ssh-agent] no agent path configured\n") - } else { - blocklogger.Debugf(connCtx, "[ssh-agent] agent skipped (IdentitiesOnly=%v)\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) - } + blocklogger.Debugf(connCtx, "[ssh-agent] no agent path configured\n") } var sshPassword *string @@ -1098,7 +1167,12 @@ func findSshDefaults(hostPattern string) (connKeywords *wconfig.ConnKeywords, ou sshKeywords.SshPreferredAuthentications = strings.Split(ssh_config.Default("PreferredAuthentications"), ",") sshKeywords.SshAddKeysToAgent = utilfn.Ptr(false) sshKeywords.SshIdentitiesOnly = utilfn.Ptr(false) - sshKeywords.SshIdentityAgent = utilfn.Ptr(ssh_config.Default("IdentityAgent")) + // On Windows, use the default OpenSSH named pipe; on Unix, use the SSH_AUTH_SOCK default + if runtime.GOOS == "windows" { + sshKeywords.SshIdentityAgent = utilfn.Ptr(`\\.\pipe\openssh-ssh-agent`) + } else { + sshKeywords.SshIdentityAgent = utilfn.Ptr(ssh_config.Default("IdentityAgent")) + } sshKeywords.SshProxyJump = []string{} sshKeywords.SshUserKnownHostsFile = strings.Fields(ssh_config.Default("UserKnownHostsFile")) sshKeywords.SshGlobalKnownHostsFile = strings.Fields(ssh_config.Default("GlobalKnownHostsFile")) From 3d712a2bd22294d6c3868ca75a97d585a29ef91e Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:30:58 +0800 Subject: [PATCH 6/6] fix: handle filepath.Dir returning "." in maskIdentityFile for files directly under user home --- pkg/remote/sshclient.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index a978b2ae14..bf38ef5392 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -162,7 +162,12 @@ func maskIdentityFile(path string) string { if len(parts) >= 3 { maskedUsername := MaskString(parts[2]) if len(parts) >= 4 { - maskedDir = "/home/" + maskedUsername + "/" + filepath.Dir(parts[3]) + subDir := filepath.Dir(parts[3]) + if subDir == "." { + maskedDir = "/home/" + maskedUsername + } else { + maskedDir = "/home/" + maskedUsername + "/" + subDir + } } else { maskedDir = "/home/" + maskedUsername } @@ -176,7 +181,12 @@ func maskIdentityFile(path string) string { if len(parts) >= 1 { maskedUsername := MaskString(parts[0]) if len(parts) >= 2 { - maskedDir = prefix + "/Users/" + maskedUsername + "/" + filepath.Dir(parts[1]) + subDir := filepath.Dir(parts[1]) + if subDir == "." { + maskedDir = prefix + "/Users/" + maskedUsername + } else { + maskedDir = prefix + "/Users/" + maskedUsername + "/" + subDir + } } else { maskedDir = prefix + "/Users/" + maskedUsername }