SSH Operations Architecture
Overview
SSH (Secure Shell) forms the security foundation of cpm, providing encrypted communication, key-based authentication, and secure file transfer capabilities. This document details the SSH architecture, key management, authentication flows, and security best practices implemented in cpm.
SSH Technology Stack
SSH Library: golang.org/x/crypto/ssh
Key Algorithm: ed25519 (primary), RSA (supported)
External Tools: OpenSSH client, rsync
Key Management Architecture
Key Generation
cpm generates ed25519 SSH key pairs with secure permissions:
┌──────────────────────────────────────────┐
│ Key Generation Process │
│ │
│ 1. Validate key name │
│ 2. Check for existing key │
│ 3. Generate ed25519 private key │
│ 4. Derive public key │
│ 5. Save private key (0600 permissions) │
│ 6. Save public key (0644 permissions) │
│ 7. Register in database │
│ 8. Calculate fingerprint │
└──────────────────────────────────────────┘
Implementation: internal/ssh/keygen.go
func GenerateED25519KeyPair(name string) (*KeyPair, error) {
// Generate private key
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
// Encode private key in OpenSSH format
privKeyBytes := ssh.MarshalPrivateKey(privKey, "")
// Encode public key
sshPubKey, err := ssh.NewPublicKey(pubKey)
if err != nil {
return nil, err
}
pubKeyBytes := ssh.MarshalAuthorizedKey(sshPubKey)
// Save keys with proper permissions
privKeyPath := filepath.Join(keysDir, name)
pubKeyPath := filepath.Join(keysDir, name+".pub")
// Write private key (0600)
if err := os.WriteFile(privKeyPath, privKeyBytes, 0600); err != nil {
return nil, err
}
// Write public key (0644)
if err := os.WriteFile(pubKeyPath, pubKeyBytes, 0644); err != nil {
return nil, err
}
return &KeyPair{
Name: name,
PrivateKeyPath: privKeyPath,
PublicKeyPath: pubKeyPath,
}, nil
}
Key Storage
~/.cpm/keys/
├── main (private key, 0600)
├── main.pub (public key, 0644)
├── backup (private key, 0600)
├── backup.pub (public key, 0644)
└── production (private key, 0600)
production.pub (public key, 0644)
Key Database Registration
INSERT INTO ssh_keys (name, public_key, private_key_path, created_at)
VALUES ('main', 'ssh-ed25519 AAAA...', '/home/user/.cpm/keys/main', CURRENT_TIMESTAMP);
Key Deployment
Push Operation
Deploy public key to remote server's authorized_keys file:
┌─────────────────────────────────────────────┐
│ Key Deployment Flow │
│ │
│ Client Remote Server │
│ │ │ │
│ │ 1. Read public key │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 2. SSH connect │ │
│ │ (existing auth) │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 3. Create ~/.ssh/ │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 4. Check existing keys │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 5. Append to │ │
│ │ authorized_keys │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 6. Set permissions │ │
│ │ (0600) │ │
│ │────────────────────────►│ │
│ │ │ │
│ │ 7. Verify deployment │ │
│ │◄────────────────────────│ │
└─────────────────────────────────────────────┘
Implementation: internal/ssh/transfer.go
func PushPublicKey(keyName, server string) error {
// Read public key
pubKey, err := readPublicKey(keyName)
if err != nil {
return err
}
// Parse server (user@host)
user, host, port, err := parseServer(server)
if err != nil {
return err
}
// Connect via SSH
conn, err := sshConnect(user, host, port)
if err != nil {
return err
}
defer conn.Close()
// Execute remote commands
commands := []string{
"mkdir -p ~/.ssh",
"chmod 700 ~/.ssh",
fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", pubKey),
"chmod 600 ~/.ssh/authorized_keys",
}
for _, cmd := range commands {
if err := execRemote(conn, cmd); err != nil {
return err
}
}
return nil
}
Authentication Flow
SSH Connection Process
┌────────────────────────────────────────────────┐
│ SSH Authentication Flow │
│ │
│ Client Server │
│ │ │ │
│ │ 1. Connect (TCP) │ │
│ │─────────────────────────►│ │
│ │ │ │
│ │ 2. Protocol negotiation │ │
│ │◄────────────────────────►│ │
│ │ │ │
│ │ 3. Key exchange │ │
│ │◄────────────────────────►│ │
│ │ │ │
│ │ 4. Authentication request│ │
│ │ (publickey) │ │
│ │─────────────────────────►│ │
│ │ │ │
│ │ 5. Sign challenge with │ │
│ │ private key │ │
│ │─────────────────────────►│ │
│ │ │ │
│ │ 6. Verify signature with │ │
│ │ public key │ │
│ │◄─────────────────────────│ │
│ │ │ │
│ │ 7. Establish session │ │
│ │◄────────────────────────►│ │
└────────────────────────────────────────────────┘
Public Key Authentication
Client Side:
- Load private key from file
- Connect to server
- Offer public key for authentication
- Sign challenge with private key
- Send signature to server
Server Side:
- Receive connection
- Check authorized_keys file
- Find matching public key
- Send challenge to client
- Verify signature
- Grant or deny access
File Transfer Operations
rsync over SSH
Primary transfer method for repository synchronization:
┌──────────────────────────────────────────┐
│ rsync Transfer Flow │
│ │
│ rsync -avz --delete -e ssh │
│ source destination │
│ │
│ 1. Establish SSH connection │
│ 2. Compare file checksums │
│ 3. Transfer only differences │
│ 4. Verify integrity │
│ 5. Update timestamps │
│ 6. Delete removed files (--delete) │
└──────────────────────────────────────────┘
Command Structure:
rsync -avz --delete \
-e "ssh -i ~/.cpm/keys/main -o StrictHostKeyChecking=no" \
/local/repo.git \
user@host:/remote/repo.git
Options:
-a: Archive mode (preserves permissions, times, etc.)-v: Verbose output-z: Compression during transfer--delete: Remove files not in source-e: Specify SSH command and options
Fallback: tar + scp
If rsync unavailable, fallback to tar compression with scp:
# Create tarball
tar czf - /local/repo.git | \
ssh -i ~/.cpm/keys/main user@host \
"cd /remote && tar xzf -"
Security Implementation
Key Security
Private Key Protection:
- Stored with 0600 permissions (owner read/write only)
- Never transmitted over network
- Not readable by other users
- Regular rotation recommended
Public Key Distribution:
- Stored with 0644 permissions (world-readable)
- Safe to share and distribute
- Registered in server authorized_keys
- Multiple servers supported
SSH Options
Security Hardening:
ssh -o StrictHostKeyChecking=ask \
-o UserKnownHostsFile=~/.cpm/known_hosts \
-o PasswordAuthentication=no \
-o PubkeyAuthentication=yes \
-i ~/.cpm/keys/main \
user@host
Options Explained:
StrictHostKeyChecking=ask: Verify host keysUserKnownHostsFile: Custom known_hosts filePasswordAuthentication=no: Disable password authPubkeyAuthentication=yes: Enable key auth-i: Specify private key
Connection Validation
func validateSSHConnection(server *Server) error {
// Load private key
key, err := loadPrivateKey(server.SSHKeyPath)
if err != nil {
return err
}
// Configure SSH client
config := &ssh.ClientConfig{
User: server.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Production: use known_hosts
Timeout: 10 * time.Second,
}
// Test connection
addr := fmt.Sprintf("%s:%d", server.Host, server.Port)
conn, err := ssh.Dial("tcp", addr, config)
if err != nil {
return err
}
defer conn.Close()
return nil
}
Error Handling
Common SSH Errors
| Error | Cause | Resolution |
|---|---|---|
Permission denied (publickey) |
Key not in authorized_keys | Deploy key with cpm ssh-key push |
Connection refused |
SSH server not running | Start SSH server on remote |
Connection timeout |
Network/firewall issue | Check connectivity, firewall rules |
Host key verification failed |
Host key changed | Update known_hosts file |
Could not resolve hostname |
DNS/hostname issue | Verify hostname/IP address |
Error Recovery
func connectWithRetry(server *Server, maxRetries int) (*ssh.Client, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
conn, err := sshConnect(server)
if err == nil {
return conn, nil
}
lastErr = err
// Exponential backoff
time.Sleep(time.Duration(1<<i) * time.Second)
}
return nil, fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}
Performance Optimization
Connection Pooling
Reuse SSH connections for multiple operations:
type ConnectionPool struct {
connections map[string]*ssh.Client
mu sync.RWMutex
}
func (p *ConnectionPool) Get(server string) (*ssh.Client, error) {
p.mu.RLock()
if conn, ok := p.connections[server]; ok {
p.mu.RUnlock()
return conn, nil
}
p.mu.RUnlock()
// Create new connection
conn, err := sshConnect(server)
if err != nil {
return nil, err
}
p.mu.Lock()
p.connections[server] = conn
p.mu.Unlock()
return conn, nil
}
Compression
Enable compression for transfers:
# SSH compression
ssh -C -i key user@host
# rsync compression
rsync -z source dest
Best Practices
Key Management
- Use ed25519 keys (modern, secure, fast)
- Generate unique keys per purpose
- Rotate keys every 6-12 months
- Use passphrases for high-security environments
- Never share private keys
Deployment
- Test keys on non-production first
- Maintain backup authentication method
- Document key deployments
- Automate key distribution
- Regular key audits
Security
- Disable password authentication
- Use key-based auth exclusively
- Regular security updates
- Monitor SSH logs
- Implement fail2ban or similar
Operations
- Use connection timeouts
- Implement retry logic
- Log all SSH operations
- Monitor transfer performance
- Regular connectivity tests