
本文介绍使用 Go 标准库 os/exec 直接将本地 SSH 公钥写入远程服务器 ~/.ssh/authorized_keys 的正确方法,避免 shell 字符串拼接风险,兼容 macOS/Linux 环境,且不依赖 ssh-copy-id。
本文介绍使用 go 标准库 `os/exec` 直接将本地 ssh 公钥写入远程服务器 `~/.ssh/authorized_keys` 的正确方法,避免 shell 字符串拼接风险,兼容 macos/linux 环境,且不依赖 `ssh-copy-id`。
在 Go 中通过命令行方式(如 cat key.pub | ssh user@host 'cat >> ~/.ssh/authorized_keys')实现公钥分发看似直观,但原代码存在多个关键问题:
- ❌ 错误调用 exec.Command(identity, address):将两个独立命令(cat ... 和 ssh ...)作为参数传给单个 exec.Command,实际会尝试执行名为 cat /home/foo/.ssh/foobar.pub 的二进制文件(显然不存在);
- ❌ 未正确构建管道逻辑:Go 的 exec.Command 不会自动解析 shell 管道符 |,需显式设置 Stdin;
- ❌ 路径硬编码与拼接风险:~/.shh/authorized_keys(注意 typo:shh 应为 ssh)和字符串格式化易引入路径注入或权限错误;
- ❌ 忽略 SSH 连接前置条件:如目标用户 .ssh 目录权限(应为 700)、authorized_keys 文件权限(推荐 600),否则 OpenSSH 会拒绝加载。
✅ 正确做法是:用 Go 原生读取公钥文件内容,并将其作为 stdin 流式传递给 ssh 进程。这样既规避了 shell 解析的不确定性,又提升安全性与可移植性。以下是完整、健壮的实现:
import (
"io"
"log"
"os"
"os/exec"
"path/filepath"
)
func copySSHPubKeyToServer(username, hostname, keyName string) error {
// 1. 构建公钥文件路径(使用 filepath.Join 避免路径拼接漏洞)
homeDir := os.Getenv("HOME")
if homeDir == "" {
return fmt.Errorf("HOME environment variable not set")
}
keyPath := filepath.Join(homeDir, ".ssh", keyName+".pub")
// 2. 打开公钥文件
keyFile, err := os.Open(keyPath)
if err != nil {
return fmt.Errorf("failed to open public key %s: %w", keyPath, err)
}
defer keyFile.Close()
// 3. 构建 ssh 命令:注意目标路径必须为 ~/.ssh/authorized_keys(修正 typo)
// 使用绝对路径更可靠(如 $HOME/.ssh/authorized_keys),但 ~ 在 ssh 上下文中通常可被服务端 shell 展开
sshTarget := username + "@" + hostname
cmd := exec.Command("ssh", sshTarget, "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys")
// 4. 将公钥文件设为命令标准输入
cmd.Stdin = keyFile
// 5. 捕获输出以便调试
output, err := cmd.CombinedOutput()
if err != nil {
log.Printf("SSH command failed (output: %s)", string(output))
return fmt.Errorf("failed to copy SSH key to %s: %w", sshTarget, err)
}
log.Printf("Successfully added %s.pub to %s's authorized_keys", keyName, sshTarget)
return nil
}? 关键注意事项:
- 权限自动化:命令中内联执行 mkdir -p ~/.ssh && chmod 700 ~/.ssh 确保目录存在且权限合规,防止因权限错误导致 authorized_keys 被忽略;
- 错误处理增强:使用 CombinedOutput() 同时捕获 stdout 和 stderr,便于定位连接失败、认证拒绝或磁盘满等具体原因;
- 资源管理:务必 defer keyFile.Close() 防止文件句柄泄漏;
- 跨平台兼容性:该方案在 macOS 和 Linux 上均有效,无需 ssh-copy-id;Windows 用户需确保已安装 OpenSSH 客户端(Windows 10/11 自带);
- 安全加固建议:生产环境应验证目标主机指纹(通过 ssh-keyscan 预先获取并比对),或启用 StrictHostKeyChecking=yes 配合已知 hosts 文件,防范中间人攻击。
总结:与其在 Go 中模拟 shell 管道,不如利用语言原生 I/O 能力,将文件流直接注入进程 stdin。这不仅代码更简洁、逻辑更清晰,也从根本上规避了命令注入、路径遍历和 shell 兼容性等常见陷阱。


















