
本文介绍如何使用 Go 标准生态的 golang.org/x/crypto/ssh 包,在命令行工具中安全、可靠地执行远程 Shell 命令,支持单跳直连与双跳(Bastion)场景,并提供可落地的代码示例与关键注意事项。
本文介绍如何使用 go 标准生态的 `golang.org/x/crypto/ssh` 包,在命令行工具中安全、可靠地执行远程 shell 命令,支持单跳直连与双跳(bastion)场景,并提供可落地的代码示例与关键注意事项。
在构建运维类 Go CLI 工具时,常需通过 SSH 远程执行命令——例如部署服务、采集日志或触发健康检查。Go 官方维护的 golang.org/x/crypto/ssh 包提供了完整的 SSH 客户端能力,无需依赖外部二进制(如 ssh 命令),即可实现纯 Go 的 SSH 连接与会话管理。
基础单跳远程命令执行
以下是一个健壮、可复用的 remoteRun 函数,支持密钥认证、标准输出捕获及错误处理:
package main
import (
"bytes"
"fmt"
"io"
"net"
"os"
"golang.org/x/crypto/ssh"
)
// remoteRun 在目标主机上执行单条 shell 命令,返回 stdout 内容与错误
// 示例调用:output, err := remoteRun("ubuntu", "192.168.1.100", privateKeyPEM, "uptime")
func remoteRun(user, addr, privateKeyPEM, cmd string) (string, error) {
key, err := ssh.ParsePrivateKey([]byte(privateKeyPEM))
if err != nil {
return "", fmt.Errorf("解析私钥失败: %w", err)
}
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(key)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // ⚠️ 仅用于测试!生产环境请使用 ssh.FixedHostKey 或 ssh.RejectKnownHosts
Timeout: 10 * time.Second,
}
client, err := ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
if err != nil {
return "", fmt.Errorf("SSH 连接失败 (%s): %w", addr, err)
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return "", fmt.Errorf("创建会话失败: %w", err)
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = os.Stderr // 可选:将 stderr 输出到本地终端便于调试
if err := session.Run(cmd); err != nil {
return "", fmt.Errorf("命令执行失败 (%q): %w", cmd, err)
}
return stdoutBuf.String(), nil
}✅ 关键说明:
- ssh.InsecureIgnoreHostKey() 仅适用于开发/测试环境;生产中务必验证服务器公钥(推荐使用 ssh.FixedHostKey 或集成 known_hosts 文件);
- 私钥应从安全来源加载(如文件、Vault、OS 密钥环),避免硬编码;
- session.Run() 适合执行简单、无交互的命令;若需交互式会话(如 sudo -i),应使用 session.Shell() + session.StdinPipe()/StdoutPipe()。
支持双跳(Bastion)的链式 SSH
当目标机器位于内网、需经跳板机(Bastion)访问时,Go 并不原生支持“SSH 隧道链式跳转”,但可通过嵌套 SSH 客户端连接实现:先连 Bastion,再在其上建立新 SSH 连接至目标主机。
// bastionRun 先连接 Bastion,再通过其执行目标主机上的命令(模拟 ProxyJump)
func bastionRun(
bastionUser, bastionAddr, bastionKeyPEM string,
targetUser, targetAddr, targetKeyPEM, cmd string,
) (string, error) {
// Step 1: 连接 Bastion
bastionClient, err := dialSSH(bastionUser, bastionAddr, bastionKeyPEM)
if err != nil {
return "", err
}
defer bastionClient.Close()
// Step 2: 在 Bastion 上启动远程命令(使用 ssh 命令调用目标主机)
// 注意:要求 Bastion 上已预置 targetKeyPEM(如 ~/.ssh/id_rsa_target),且目标主机公钥已加入 known_hosts
sshCmd := fmt.Sprintf(
`ssh -o StrictHostKeyChecking=no -i /home/%s/.ssh/id_rsa_target %s@%s %s`,
bastionUser, targetUser, targetAddr, cmd,
)
session, err := bastionClient.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var stdoutBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = os.Stderr
if err := session.Run(sshCmd); err != nil {
return "", fmt.Errorf("Bastion 上执行跳转命令失败: %w", err)
}
return stdoutBuf.String(), nil
}
func dialSSH(user, addr, keyPEM string) (*ssh.Client, error) {
key, err := ssh.ParsePrivateKey([]byte(keyPEM))
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(key)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
return ssh.Dial("tcp", net.JoinHostPort(addr, "22"), config)
}? 安全提示:
- Bastion 场景下,建议将目标主机私钥以加密方式存于 Bastion,并通过 ssh-agent 转发(启用 ForwardAgent: true)而非明文传输;
- 更优方案是使用 OpenSSH 的 ProxyJump(需系统 ssh 命令可用),或借助 golang.org/x/crypto/ssh 实现 TCP 端口转发(client.ListenTCP()),构建透明隧道——这属于进阶用法,需额外处理连接生命周期与超时。
总结与最佳实践
- ✅ 优先使用密钥认证,避免密码硬编码;私钥读取应结合 io/fs(Go 1.16+)或安全凭据管理器;
- ✅ 始终设置 Timeout 和 KeepAlive,防止网络异常导致 CLI 挂起;
- ✅ 区分 Run / Shell / Start:Run 适合脚本化命令;Shell 适合交互;Start + Wait 适合异步控制;
- ❌ 禁用 InsecureIgnoreHostKey 上线:生产环境必须校验主机指纹,可结合 ssh.ParseKnownHosts() 解析 ~/.ssh/known_hosts;
- ? 依赖管理:确保 go.mod 中包含 require golang.org/x/crypto v0.25.0(或更高稳定版)。
通过以上方法,你可快速构建企业级 Go CLI 工具,无缝支持云环境中的直连与跳板机场景,兼具安全性、可维护性与跨平台能力。


















