// Keywarden - Centralized SSH Key Management and Deployment // Copyright (C) 2026 Patrick Asmus (scriptos) // SPDX-License-Identifier: AGPL-3.0-or-later package deploy import ( "fmt" "net" "strings" "time" "git.techniverse.net/scriptos/keywarden/internal/database" "git.techniverse.net/scriptos/keywarden/internal/logging" "git.techniverse.net/scriptos/keywarden/internal/models" "golang.org/x/crypto/ssh" ) // Service handles deploying SSH keys to remote servers type Service struct { db *database.DB } // NewService creates a new deploy service func NewService(db *database.DB) *Service { return &Service{db: db} } // DeployKey deploys a public key to a remote server's authorized_keys func (s *Service) DeployKey(key *models.SSHKey, server *models.Server, authPrivateKey []byte) error { logging.Debug("Deploy: connecting to %s@%s:%d with key auth for key '%s'", server.Username, server.Hostname, server.Port, key.Name) // Parse the private key used for authentication signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: implement known_hosts Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("connection failed: %v", err)) return fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("session failed: %v", err)) return fmt.Errorf("failed to create session: %w", err) } defer session.Close() // Clean the public key (remove trailing newline) pubKey := strings.TrimSpace(key.PublicKey) // Append to authorized_keys cmd := fmt.Sprintf( `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '%s' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && sort -u -o ~/.ssh/authorized_keys ~/.ssh/authorized_keys`, pubKey, ) if err := session.Run(cmd); err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("command failed: %v", err)) return fmt.Errorf("failed to deploy key: %w", err) } s.logDeployment(key.ID, server.ID, "success", "key deployed successfully") return nil } // DeployKeyWithPassword deploys a public key using password authentication func (s *Service) DeployKeyWithPassword(key *models.SSHKey, server *models.Server, password string) error { logging.Debug("Deploy: connecting to %s@%s:%d with password auth for key '%s'", server.Username, server.Hostname, server.Port, key.Name) config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.Password(password), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("connection failed: %v", err)) return fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("session failed: %v", err)) return fmt.Errorf("failed to create session: %w", err) } defer session.Close() pubKey := strings.TrimSpace(key.PublicKey) cmd := fmt.Sprintf( `mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '%s' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && sort -u -o ~/.ssh/authorized_keys ~/.ssh/authorized_keys`, pubKey, ) if err := session.Run(cmd); err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("command failed: %v", err)) return fmt.Errorf("failed to deploy key: %w", err) } s.logDeployment(key.ID, server.ID, "success", "key deployed successfully") return nil } // RemoveKey removes a public key from a remote server's authorized_keys func (s *Service) RemoveKey(key *models.SSHKey, server *models.Server, authPrivateKey []byte) error { logging.Debug("Deploy: removing key '%s' from %s@%s:%d", key.Name, server.Username, server.Hostname, server.Port) signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("failed to connect to server for key removal: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for key removal: %w", err) } defer session.Close() pubKey := strings.TrimSpace(key.PublicKey) // Escape single quotes in the key for safe sed usage escapedKey := strings.ReplaceAll(pubKey, "'", "'\\''") cmd := fmt.Sprintf( `grep -v '%s' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.tmp 2>/dev/null && mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys || true`, escapedKey, ) if err := session.Run(cmd); err != nil { return fmt.Errorf("failed to remove key: %w", err) } s.logDeployment(key.ID, server.ID, "success", "key removed successfully") return nil } // DeployKeyToUser deploys a public key to a specific system user's authorized_keys. // It connects to the server as the server's admin user and manages the target systemUser. // If createUser is true, the system user will be created if it doesn't exist. // If sudo is true, a sudoers.d entry with NOPASSWD will be created. // If initialPassword is set and createUser is true, the password will be set on the system user. func (s *Service) DeployKeyToUser(key *models.SSHKey, server *models.Server, authPrivateKey []byte, systemUser string, createUser, sudo bool, initialPassword string) error { logging.Debug("Deploy: connecting to %s@%s:%d to deploy key '%s' for system user '%s' (createUser=%v, sudo=%v)", server.Username, server.Hostname, server.Port, key.Name, systemUser, createUser, sudo) signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("connection failed: %v", err)) return fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() // If createUser is true, ensure the system user exists if createUser { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for user creation: %w", err) } // Create user if not exists; use -m for home directory, -s for shell createCmd := fmt.Sprintf( `id '%s' >/dev/null 2>&1 || useradd -m -s /bin/bash '%s'`, systemUser, systemUser, ) if cerr := session.Run(createCmd); cerr != nil { session.Close() return fmt.Errorf("failed to create system user '%s': %w", systemUser, cerr) } session.Close() logging.Info("Deploy: ensured system user '%s' exists on %s", systemUser, server.Hostname) // Set initial password if provided if initialPassword != "" { pwSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for password setup: %w", err) } pwCmd := fmt.Sprintf(`echo '%s:%s' | chpasswd`, systemUser, initialPassword) if perr := pwSession.Run(pwCmd); perr != nil { pwSession.Close() logging.Warn("Deploy: failed to set initial password for user '%s' on %s: %v", systemUser, server.Hostname, perr) } else { pwSession.Close() logging.Info("Deploy: set initial password for user '%s' on %s", systemUser, server.Hostname) } } } // If sudo is true, create a sudoers.d entry with NOPASSWD if sudo { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for sudo setup: %w", err) } sudoCmd := fmt.Sprintf( `echo '%s ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/%s && chmod 440 /etc/sudoers.d/%s`, systemUser, systemUser, systemUser, ) if serr := session.Run(sudoCmd); serr != nil { session.Close() logging.Warn("Deploy: failed to add sudo for user '%s' on %s: %v", systemUser, server.Hostname, serr) } else { session.Close() logging.Info("Deploy: ensured NOPASSWD sudo for user '%s' on %s", systemUser, server.Hostname) } } // Deploy the key to the system user's authorized_keys session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for key deployment: %w", err) } defer session.Close() pubKey := strings.TrimSpace(key.PublicKey) homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } cmd := fmt.Sprintf( `mkdir -p %s/.ssh && chmod 700 %s/.ssh && echo '%s' >> %s/.ssh/authorized_keys && chmod 600 %s/.ssh/authorized_keys && chown -R '%s':'%s' %s/.ssh && sort -u -o %s/.ssh/authorized_keys %s/.ssh/authorized_keys`, homeDir, homeDir, pubKey, homeDir, homeDir, systemUser, systemUser, homeDir, homeDir, homeDir, ) if err := session.Run(cmd); err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("command failed: %v", err)) return fmt.Errorf("failed to deploy key for user '%s': %w", systemUser, err) } s.logDeployment(key.ID, server.ID, "success", fmt.Sprintf("key deployed to user '%s'", systemUser)) return nil } // DeployKeyToUserWithPassword is like DeployKeyToUser but uses password authentication func (s *Service) DeployKeyToUserWithPassword(key *models.SSHKey, server *models.Server, password string, systemUser string, createUser, sudo bool, initialPassword string) error { logging.Debug("Deploy: connecting to %s@%s:%d with password to deploy key '%s' for system user '%s'", server.Username, server.Hostname, server.Port, key.Name, systemUser) config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.Password(password), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("connection failed: %v", err)) return fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() if createUser { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for user creation: %w", err) } createCmd := fmt.Sprintf( `id '%s' >/dev/null 2>&1 || useradd -m -s /bin/bash '%s'`, systemUser, systemUser, ) if cerr := session.Run(createCmd); cerr != nil { session.Close() return fmt.Errorf("failed to create system user '%s': %w", systemUser, cerr) } session.Close() logging.Info("Deploy: ensured system user '%s' exists on %s", systemUser, server.Hostname) // Set initial password if provided if initialPassword != "" { pwSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for password setup: %w", err) } pwCmd := fmt.Sprintf(`echo '%s:%s' | chpasswd`, systemUser, initialPassword) if perr := pwSession.Run(pwCmd); perr != nil { pwSession.Close() logging.Warn("Deploy: failed to set initial password for user '%s' on %s: %v", systemUser, server.Hostname, perr) } else { pwSession.Close() logging.Info("Deploy: set initial password for user '%s' on %s", systemUser, server.Hostname) } } } if sudo { session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for sudo setup: %w", err) } sudoCmd := fmt.Sprintf( `echo '%s ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/%s && chmod 440 /etc/sudoers.d/%s`, systemUser, systemUser, systemUser, ) if serr := session.Run(sudoCmd); serr != nil { session.Close() logging.Warn("Deploy: failed to add sudo for user '%s' on %s: %v", systemUser, server.Hostname, serr) } else { session.Close() logging.Info("Deploy: ensured NOPASSWD sudo for user '%s' on %s", systemUser, server.Hostname) } } session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for key deployment: %w", err) } defer session.Close() pubKey := strings.TrimSpace(key.PublicKey) homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } cmd := fmt.Sprintf( `mkdir -p %s/.ssh && chmod 700 %s/.ssh && echo '%s' >> %s/.ssh/authorized_keys && chmod 600 %s/.ssh/authorized_keys && chown -R '%s':'%s' %s/.ssh && sort -u -o %s/.ssh/authorized_keys %s/.ssh/authorized_keys`, homeDir, homeDir, pubKey, homeDir, homeDir, systemUser, systemUser, homeDir, homeDir, homeDir, ) if err := session.Run(cmd); err != nil { s.logDeployment(key.ID, server.ID, "failed", fmt.Sprintf("command failed: %v", err)) return fmt.Errorf("failed to deploy key for user '%s': %w", systemUser, err) } s.logDeployment(key.ID, server.ID, "success", fmt.Sprintf("key deployed to user '%s'", systemUser)) return nil } // RemoveKeyFromUser removes a public key from a specific system user's authorized_keys. // It connects as the server's admin user and manages the target systemUser's keys. func (s *Service) RemoveKeyFromUser(key *models.SSHKey, server *models.Server, authPrivateKey []byte, systemUser string) error { logging.Debug("Deploy: connecting to %s@%s:%d to remove key '%s' from system user '%s'", server.Username, server.Hostname, server.Port, key.Name, systemUser) signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("failed to connect to server for key removal: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for key removal: %w", err) } defer session.Close() pubKey := strings.TrimSpace(key.PublicKey) escapedKey := strings.ReplaceAll(pubKey, "'", "'\\''") homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } cmd := fmt.Sprintf( `grep -v '%s' %s/.ssh/authorized_keys > %s/.ssh/authorized_keys.tmp 2>/dev/null && mv %s/.ssh/authorized_keys.tmp %s/.ssh/authorized_keys && chmod 600 %s/.ssh/authorized_keys && chown '%s':'%s' %s/.ssh/authorized_keys || true`, escapedKey, homeDir, homeDir, homeDir, homeDir, homeDir, systemUser, systemUser, homeDir, ) if err := session.Run(cmd); err != nil { return fmt.Errorf("failed to remove key from user '%s': %w", systemUser, err) } s.logDeployment(key.ID, server.ID, "success", fmt.Sprintf("key removed from user '%s'", systemUser)) return nil } // RemoveSystemUser removes a Linux system user from a server, including: // - Removing the SSH key from their authorized_keys // - Removing sudo rights (/etc/sudoers.d/) // - Deleting the system user account (userdel -r) func (s *Service) RemoveSystemUser(key *models.SSHKey, server *models.Server, authPrivateKey []byte, systemUser string) error { logging.Info("Deploy: removing system user '%s' from %s@%s:%d", systemUser, server.Username, server.Hostname, server.Port) signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("failed to connect to server for user removal: %w", err) } defer client.Close() // Step 1: Remove sudoers entry sudoSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for sudo removal: %w", err) } sudoCmd := fmt.Sprintf(`rm -f /etc/sudoers.d/'%s'`, systemUser) if serr := sudoSession.Run(sudoCmd); serr != nil { logging.Warn("Deploy: failed to remove sudo for user '%s' on %s: %v", systemUser, server.Hostname, serr) } else { logging.Info("Deploy: removed sudoers entry for '%s' on %s", systemUser, server.Hostname) } sudoSession.Close() // Step 2: Kill all processes of the user (so userdel doesn't fail) killSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for process kill: %w", err) } killCmd := fmt.Sprintf(`pkill -u '%s' 2>/dev/null || true`, systemUser) killSession.Run(killCmd) killSession.Close() // Small delay to let processes terminate time.Sleep(1 * time.Second) // Step 3: Delete the system user with home directory delSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for user deletion: %w", err) } defer delSession.Close() delCmd := fmt.Sprintf(`userdel -r '%s' 2>/dev/null || userdel '%s' 2>/dev/null`, systemUser, systemUser) if derr := delSession.Run(delCmd); derr != nil { return fmt.Errorf("failed to delete system user '%s': %w", systemUser, derr) } logging.Info("Deploy: successfully deleted system user '%s' from %s", systemUser, server.Hostname) s.logDeployment(key.ID, server.ID, "success", fmt.Sprintf("system user '%s' deleted", systemUser)) return nil } // DisableSystemUser locks a system user account on a server by: // - Removing the SSH key from authorized_keys // - Locking the account (usermod --lock) // - Setting the shell to /usr/sbin/nologin func (s *Service) DisableSystemUser(key *models.SSHKey, server *models.Server, authPrivateKey []byte, systemUser string) error { logging.Info("Deploy: disabling system user '%s' on %s@%s:%d", systemUser, server.Username, server.Hostname, server.Port) signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("failed to connect to server for user disable: %w", err) } defer client.Close() // Step 1: Remove SSH key from authorized_keys removeSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for key removal: %w", err) } pubKey := strings.TrimSpace(key.PublicKey) escapedKey := strings.ReplaceAll(pubKey, "'", "'\\''") homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } removeCmd := fmt.Sprintf( `grep -v '%s' %s/.ssh/authorized_keys > %s/.ssh/authorized_keys.tmp 2>/dev/null && mv %s/.ssh/authorized_keys.tmp %s/.ssh/authorized_keys && chmod 600 %s/.ssh/authorized_keys || true`, escapedKey, homeDir, homeDir, homeDir, homeDir, homeDir, ) removeSession.Run(removeCmd) removeSession.Close() // Step 2: Lock the account lockSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for user lock: %w", err) } lockCmd := fmt.Sprintf(`usermod --lock '%s' 2>/dev/null || true`, systemUser) lockSession.Run(lockCmd) lockSession.Close() // Step 3: Set shell to nologin shellSession, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session for shell change: %w", err) } defer shellSession.Close() shellCmd := fmt.Sprintf(`usermod --shell /usr/sbin/nologin '%s' 2>/dev/null || chsh -s /usr/sbin/nologin '%s' 2>/dev/null || true`, systemUser, systemUser) shellSession.Run(shellCmd) logging.Info("Deploy: successfully disabled system user '%s' on %s", systemUser, server.Hostname) s.logDeployment(key.ID, server.ID, "success", fmt.Sprintf("system user '%s' disabled (locked + nologin)", systemUser)) return nil } // TestConnection tests TCP connectivity to a server (port reachable) func (s *Service) TestConnection(hostname string, port int) error { logging.Debug("Testing TCP connection to %s:%d", hostname, port) addr := net.JoinHostPort(hostname, fmt.Sprintf("%d", port)) conn, err := net.DialTimeout("tcp", addr, 5*time.Second) if err != nil { return fmt.Errorf("cannot reach %s: %w", addr, err) } conn.Close() return nil } // TestSSHAuth tests actual SSH authentication to a server using a private key func (s *Service) TestSSHAuth(hostname string, port int, username string, privateKey []byte) error { logging.Debug("Testing SSH auth for %s@%s:%d", username, hostname, port) signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { return fmt.Errorf("failed to parse master key: %w", err) } config := &ssh.ClientConfig{ User: username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(hostname, fmt.Sprintf("%d", port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("SSH authentication failed: %w", err) } client.Close() return nil } // logDeployment records a deployment attempt func (s *Service) logDeployment(keyID, serverID int64, status, message string) { s.db.Exec( `INSERT INTO key_deployments (ssh_key_id, server_id, status, message) VALUES (?, ?, ?, ?)`, keyID, serverID, status, message, ) } // GetDeployments returns deployment history for a user's keys func (s *Service) GetDeployments(userID int64) ([]map[string]interface{}, error) { rows, err := s.db.Query( `SELECT kd.id, sk.name as key_name, srv.name as server_name, kd.status, kd.message, kd.deployed_at FROM key_deployments kd JOIN ssh_keys sk ON kd.ssh_key_id = sk.id JOIN servers srv ON kd.server_id = srv.id WHERE sk.user_id = ? ORDER BY kd.deployed_at DESC LIMIT 50`, userID, ) if err != nil { return nil, fmt.Errorf("failed to query deployments: %w", err) } defer rows.Close() var deployments []map[string]interface{} for rows.Next() { var id int64 var keyName, serverName, status, message string var deployedAt time.Time if err := rows.Scan(&id, &keyName, &serverName, &status, &message, &deployedAt); err != nil { continue } deployments = append(deployments, map[string]interface{}{ "id": id, "key_name": keyName, "server_name": serverName, "status": status, "message": message, "deployed_at": deployedAt, }) } return deployments, nil } // ReadAuthorizedKeys reads the current authorized_keys for a system user on a remote server. // Returns the list of key lines (non-empty, non-comment lines). func (s *Service) ReadAuthorizedKeys(server *models.Server, authPrivateKey []byte, systemUser string) ([]string, error) { signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return nil, fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return nil, fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } defer session.Close() homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } cmd := fmt.Sprintf(`cat %s/.ssh/authorized_keys 2>/dev/null || echo ""`, homeDir) output, err := session.Output(cmd) if err != nil { return nil, fmt.Errorf("failed to read authorized_keys: %w", err) } var keys []string for _, line := range strings.Split(string(output), "\n") { line = strings.TrimSpace(line) if line != "" && !strings.HasPrefix(line, "#") { keys = append(keys, line) } } return keys, nil } // WriteAuthorizedKeys replaces the entire authorized_keys file for a system user on a remote server // with the provided set of keys. This is the enforcement function. func (s *Service) WriteAuthorizedKeys(server *models.Server, authPrivateKey []byte, systemUser string, authorizedKeys []string) error { signer, err := ssh.ParsePrivateKey(authPrivateKey) if err != nil { return fmt.Errorf("failed to parse authentication key: %w", err) } config := &ssh.ClientConfig{ User: server.Username, Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } addr := net.JoinHostPort(server.Hostname, fmt.Sprintf("%d", server.Port)) client, err := ssh.Dial("tcp", addr, config) if err != nil { return fmt.Errorf("failed to connect to server: %w", err) } defer client.Close() session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session: %w", err) } defer session.Close() homeDir := fmt.Sprintf("/home/%s", systemUser) if systemUser == "root" { homeDir = "/root" } // Build the authorized_keys content content := strings.Join(authorizedKeys, "\n") if !strings.HasSuffix(content, "\n") { content += "\n" } // Use printf to write the content to avoid shell interpretation issues // First write to a temp file, then atomically move it escapedContent := strings.ReplaceAll(content, "'", "'\\''") cmd := fmt.Sprintf( `mkdir -p %s/.ssh && chmod 700 %s/.ssh && printf '%%s' '%s' > %s/.ssh/authorized_keys.tmp && mv %s/.ssh/authorized_keys.tmp %s/.ssh/authorized_keys && chmod 600 %s/.ssh/authorized_keys && chown '%s':'%s' %s/.ssh/authorized_keys`, homeDir, homeDir, escapedContent, homeDir, homeDir, homeDir, homeDir, systemUser, systemUser, homeDir, ) if err := session.Run(cmd); err != nil { return fmt.Errorf("failed to write authorized_keys: %w", err) } return nil }