Skip to content

Commit 157a640

Browse files
authored
fix: SSH Agent auth falls back to key file when agent has no loaded identities (#729) (#741)
* fix: SSH Agent auth falls back to key file when agent has no loaded identities (#729) * fix: use URL APIs for default SSH key path construction
1 parent 1b3ffa0 commit 157a640

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Fix AI chat hanging the app during streaming, schema fetch, and conversation loading (#735)
13+
- SSH Agent auth: fall back to key file from `~/.ssh/config` or default paths when agent has no loaded identities (#729)
1314
- SSH-tunneled connections failing to reconnect after idle/sleep — health monitor now rebuilds the tunnel, OS-level TCP keepalive detects dead NAT mappings, and wake-from-sleep triggers immediate validation (#736)
1415

1516
## [0.31.4] - 2026-04-14

TablePro/Core/SSH/LibSSH2TunnelFactory.swift

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -493,15 +493,26 @@ internal enum LibSSH2TunnelFactory {
493493

494494
case .sshAgent:
495495
let socketPath = config.agentSocketPath.isEmpty ? nil : config.agentSocketPath
496-
let primary = AgentAuthenticator(socketPath: socketPath)
496+
var authenticators: [any SSHAuthenticator] = [AgentAuthenticator(socketPath: socketPath)]
497+
498+
// Fallback: try key file if agent has no loaded identities
499+
if let keyPath = resolveIdentityFile(config: config) {
500+
authenticators.append(PublicKeyAuthenticator(
501+
privateKeyPath: keyPath,
502+
passphrase: credentials.keyPassphrase
503+
))
504+
}
505+
497506
if config.totpMode != .none {
498-
let totpAuth = KeyboardInteractiveAuthenticator(
507+
authenticators.append(KeyboardInteractiveAuthenticator(
499508
password: nil,
500509
totpProvider: buildTOTPProvider(config: config, credentials: credentials)
501-
)
502-
return CompositeAuthenticator(authenticators: [primary, totpAuth])
510+
))
503511
}
504-
return primary
512+
513+
return authenticators.count == 1
514+
? authenticators[0]
515+
: CompositeAuthenticator(authenticators: authenticators)
505516

506517
case .keyboardInteractive:
507518
let totpProvider = buildTOTPProvider(config: config, credentials: credentials)
@@ -520,10 +531,47 @@ internal enum LibSSH2TunnelFactory {
520531
passphrase: nil
521532
)
522533
case .sshAgent:
523-
return AgentAuthenticator(socketPath: nil)
534+
let agent = AgentAuthenticator(socketPath: nil)
535+
if !jumpHost.privateKeyPath.isEmpty {
536+
let keyAuth = PublicKeyAuthenticator(
537+
privateKeyPath: jumpHost.privateKeyPath,
538+
passphrase: nil
539+
)
540+
return CompositeAuthenticator(authenticators: [agent, keyAuth])
541+
}
542+
return agent
524543
}
525544
}
526545

546+
/// Resolve an identity file path for agent auth fallback.
547+
/// Priority: user-configured path > ~/.ssh/config IdentityFile > default key paths.
548+
private static func resolveIdentityFile(config: SSHConfiguration) -> String? {
549+
if !config.privateKeyPath.isEmpty {
550+
return config.privateKeyPath
551+
}
552+
553+
if let entry = SSHConfigParser.findEntry(for: config.host),
554+
let identityFile = entry.identityFile,
555+
!identityFile.isEmpty {
556+
return identityFile
557+
}
558+
559+
let sshDir = FileManager.default.homeDirectoryForCurrentUser
560+
.appendingPathComponent(".ssh", isDirectory: true)
561+
let defaultPaths = [
562+
sshDir.appendingPathComponent("id_ed25519").path,
563+
sshDir.appendingPathComponent("id_rsa").path,
564+
sshDir.appendingPathComponent("id_ecdsa").path
565+
]
566+
for path in defaultPaths {
567+
if FileManager.default.isReadableFile(atPath: path) {
568+
return path
569+
}
570+
}
571+
572+
return nil
573+
}
574+
527575
private static func buildTOTPProvider(
528576
config: SSHConfiguration,
529577
credentials: SSHTunnelCredentials

0 commit comments

Comments
 (0)