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:

  1. Load private key from file
  2. Connect to server
  3. Offer public key for authentication
  4. Sign challenge with private key
  5. Send signature to server

Server Side:

  1. Receive connection
  2. Check authorized_keys file
  3. Find matching public key
  4. Send challenge to client
  5. Verify signature
  6. 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 keys
  • UserKnownHostsFile: Custom known_hosts file
  • PasswordAuthentication=no: Disable password auth
  • PubkeyAuthentication=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

  1. Use ed25519 keys (modern, secure, fast)
  2. Generate unique keys per purpose
  3. Rotate keys every 6-12 months
  4. Use passphrases for high-security environments
  5. Never share private keys

Deployment

  1. Test keys on non-production first
  2. Maintain backup authentication method
  3. Document key deployments
  4. Automate key distribution
  5. Regular key audits

Security

  1. Disable password authentication
  2. Use key-based auth exclusively
  3. Regular security updates
  4. Monitor SSH logs
  5. Implement fail2ban or similar

Operations

  1. Use connection timeouts
  2. Implement retry logic
  3. Log all SSH operations
  4. Monitor transfer performance
  5. Regular connectivity tests

See Also