README
¶
ssh-client-go
A unified SSH client library for Go that provides seamless key discovery and signing operations across ssh-agent and filesystem keys.
Features
- ✅ Unified Key Discovery: Discovers keys from both ssh-agent and filesystem (
~/.ssh/id_*) - ✅ Passphrase Support: Interactive passphrase prompts for encrypted private keys
- ✅ Standard SSH Behavior: Follows OpenSSH client key discovery and iteration patterns
- ✅ Custom Key Paths: Support for non-standard key locations
- ✅ Hardware Keys: Works with hardware-backed SSH keys (PKCS#11, PIV cards)
- ✅ Flexible Configuration: Environment variables and programmatic configuration
- ✅ Comprehensive Error Handling: Detailed error messages for debugging
Installation
go get github.com/nikogura/ssh-client-go/pkg/sshclient
Quick Start
package main
import (
"fmt"
"log"
"github.com/nikogura/ssh-client-go/pkg/sshclient"
)
func main() {
// Create client with default config (agent + filesystem keys)
config := &sshclient.Config{
UseAgent: true, // Try ssh-agent first
IdentitiesOnly: false, // Also try filesystem keys
}
client, err := sshclient.NewUnifiedSSHClient(config)
if err != nil {
log.Fatal(err)
}
// Discover all available keys
keys, err := client.GetKeys()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found %d SSH keys\n", len(keys))
for _, key := range keys {
fmt.Printf(" - %s (%s)\n", key.Fingerprint, key.Source)
}
// Sign data with the first key
data := []byte("hello world")
signature, pubKey, err := client.SignWithKey(keys[0], data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Signed with key: %s\n", key.Fingerprint)
}
Configuration
Using Environment Variables
config := &sshclient.Config{
UseAgent: os.Getenv("SSH_USE_AGENT") != "false",
IdentitiesOnly: os.Getenv("SSH_IDENTITIES_ONLY") == "true",
}
// Parse custom key paths
if keyPaths := os.Getenv("SSH_KEY_PATHS"); keyPaths != "" {
config.SSHKeyPaths = strings.Split(keyPaths, ":")
}
Environment Variables
SSH_KEY_PATHS- Colon-separated list of custom SSH key pathsSSH_USE_AGENT- Use ssh-agent for key discovery (default: true)SSH_IDENTITIES_ONLY- Only use keys specified via SSHKeyPaths (default: false)
Programmatic Configuration
config := &sshclient.Config{
SSHKeyPaths: []string{"/custom/path/id_rsa", "/custom/path/id_ed25519"},
UseAgent: true,
IdentitiesOnly: false,
}
Key Discovery Behavior
The library follows standard SSH client behavior for key discovery:
-
Agent Keys (if
UseAgentis true and agent available):- Queries ssh-agent for all loaded keys
- Returns agent keys if found and no specific paths requested
-
Filesystem Keys (if agent keys not found or specific paths requested):
- Scans standard SSH locations:
~/.ssh/id_rsa,id_ecdsa,id_ed25519, etc. - Or uses custom paths from
SSHKeyPaths - Prompts for passphrases on encrypted keys (up to 3 attempts)
- Scans standard SSH locations:
-
Combined Results:
- Returns all discovered keys for iteration
- Caller can try each key until one succeeds (standard SSH behavior)
API Reference
Types
type Config struct {
SSHKeyPaths []string // Custom SSH key paths
UseAgent bool // Whether to use ssh-agent
IdentitiesOnly bool // Only use specified keys
}
type SSHKey struct {
PublicKey ssh.PublicKey
Blob []byte // Public key bytes
Comment string
Signer ssh.Signer // For filesystem keys
AgentKey *agent.Key // For agent keys
Source string // "agent" or filesystem path
Fingerprint string // SHA256 fingerprint
}
type UnifiedSSHClient struct {
// ... internal fields
}
Functions
NewUnifiedSSHClient(config *Config) (*UnifiedSSHClient, error)
Creates a new unified SSH client with the given configuration.
(c *UnifiedSSHClient) GetKeys() ([]*SSHKey, error)
Discovers and returns all available SSH keys from agent and/or filesystem.
(c *UnifiedSSHClient) SignWithKey(key *SSHKey, data []byte) (*ssh.Signature, ssh.PublicKey, error)
Signs data with the specified SSH key.
FormatPublicKey(key *SSHKey) string
Formats an SSH public key in authorized_keys format.
Advanced Usage
Custom Key Paths
config := &sshclient.Config{
SSHKeyPaths: []string{
"/path/to/custom/id_rsa",
"/path/to/custom/id_ed25519",
},
UseAgent: false, // Skip agent
IdentitiesOnly: true, // Only use specified keys
}
Multi-Key Authentication
client, _ := sshclient.NewUnifiedSSHClient(config)
keys, _ := client.GetKeys()
// Try each key until one succeeds
for _, key := range keys {
signature, pubKey, err := client.SignWithKey(key, data)
if err == nil {
fmt.Printf("Authenticated with key: %s\n", key.Fingerprint)
break
}
fmt.Printf("Key %s failed: %v\n", key.Fingerprint, err)
}
Agent-Only Keys
config := &sshclient.Config{
UseAgent: true,
IdentitiesOnly: true, // Skip filesystem even if agent fails
}
Error Handling
The library uses sentinel errors for common cases:
keys, err := client.GetKeys()
if errors.Is(err, sshclient.ErrKeyNotFound) {
// Specific key path not found (non-fatal for discovery)
}
Testing
# Run unit tests
make test-unit
# Run integration tests (requires ssh-agent)
make test-integration
# Run all tests
make test
# Run linter
make lint
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Inspired by kubectl-ssh-oidc
- Follows OpenSSH client behavior for key discovery and iteration
- Uses golang.org/x/crypto/ssh for SSH operations
Related Projects
- kubectl-ssh-oidc - Kubernetes authentication via SSH-signed OIDC tokens
- vlogin - HashiCorp Vault authentication via SSH keys
Made with ❤️ for the Go community