默认开启 known_hosts 校验,连新主机会因主机密钥未知而失败,需先加入 known_hosts 或显式关闭校验;导入路径带 /v2,写成不带版本号的旧路径会拉不到对应版本;用私钥认证时若私钥有 passphrase,必须作为第二个参数传给 WithKeyFile,否则连接失败

轻量 Go SSH 客户端库,用简单 API 在代码里远程执行命令和传文件
用 Go 写的原生 SSH 客户端库,能在代码里连远程主机执行命令、传文件,省去自己封装 SSH 协议的麻烦。
核心能力
- 用 goph.New 加选项函数一行建立 SSH 连接
- 默认校验 known_hosts,支持密码、私钥、passphrase 和 ssh agent
- 支持 SOCKS5 代理与跳板机(jump host)多级连接
- 通过 SFTP 上传下载文件并做 Open/Create/Chmod 等操作
- 支持 context.Context 取消远程命令,可自定义取消信号
容易踩的坑
- v2.0.1v2最新正式版2026-07-15 · 2026-07-15 18:40 UTC
Goph v2
Goph v2 brings Functional options API, context-aware commands, SOCKS5 proxy, jump hosts, lazy agent sockets, and many more features.
Breaking Changes
-
Removed
goph.Authtype andgoph.Configstruct.- Replaced by functional options passed directly to
goph.New.
- Replaced by functional options passed directly to
-
Removed constructors:
NewUnknown,NewConn.- Use
goph.New(user, addr, opts...)withWithInsecureIgnoreHostKey()where needed.
- Use
-
Removed auth constructor functions:
goph.Password(),goph.Key(),goph.UseAgent().- Replaced by
WithPassword,WithKeyFile,WithDefaultAgent,WithAgentSocketetc.
- Replaced by
-
Client.Configfield removed.*ssh.ClientConfigis now ephemeral: built inNew, passed to options and dial helpers, then discarded. It is no longer stored on the connectedClient.
-
Renamed key auth options:
WithKey(file, passphrase)=>WithKeyFile(file, passphrase)WithRawKey(pem, passphrase)=>WithKey(pem, passphrase)
-
Renamed key parsing helpers:
GetSigner(file, passphrase)=>ParseKeyFile(file, passphrase)GetSignerForRawKey(key, passphrase)=>ParseKey(key, passphrase)
New Features
-
Functional options API.
WithPassword,WithKeyFile,WithKey,WithAgent,WithAgentSocket,WithDefaultAgent,WithSigner,WithAuthWithPort,WithTimeout,WithKnownHosts,WithInsecureIgnoreHostKey,WithHostKeyCallback,WithBannerCallback,WithConfigWithProxy,WithJump
-
Dialerfor reusable connections.d := goph.NewDialer(opts...)thend.New(user, addr)for multiple hosts with shared defaults.
-
SOCKS5 proxy authentication.
WithProxycan accepts URLs with embedded credentials:socks5://user:pass@host:port.
-
Context-aware command cancellation.
RunContextandCommandContextacceptcontext.Context.- Cancellation sends
ssh.SIGINTto the remote session.
-
goph.Dial.goph.Dial(c *Client, config *ssh.ClientConfig) errorfor manualClientconstruction and customssh.ClientConfig.Clientnow storesUser;Dialdefaultsconfig.UsertoClient.Useror the current OS user when empty.- Direct TCP dials now respect
config.Timeout.
-
Security fixes.
WithPasswordno longer includes a keyboard-interactive fallback. Added standaloneWithKeyboardInteractive(handler)that gives callers full control over each server prompt (password, OTP, 2FA, etc.).Cmd.Stringwarning now clearly states thatPathandArgsare not shell-escaped and that callers must sanitize untrusted input.
-
Known-hosts helpers.
KnownHostsnow documents that the file must exist.- New
EnsureKnownHostscreates the file (and parent directory) if missing. DefaultKnownHostsusesEnsureKnownHostsso first-time users no longer hit a file-read error when~/.ssh/known_hostsis absent.
Pull Request by @melbahja in https://github.com/melbahja/goph/pull/78 Full Changelog https://github.com/melbahja/goph/compare/v1.5.0...v2.0.1
-
- v1.5.22026-07-15 · 2026-07-15 18:03 UTC
查看更新说明
- Security update for crypto pkg for goph v1
Full Changelog: https://github.com/melbahja/goph/compare/v1.5.1...v1.5.2
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
🤘 Features
- Easy to use and simple API
- Supports known hosts by default.
- Supports connections with passwords.
- Supports connections with private keys.
- Supports connections with protected private keys with passphrase.
- Supports multiple auth methods in a single connection.
- Supports upload files from local to remote.
- Supports download files from remote to local.
- Supports connections with ssh agent.
- Supports connections with custom signers.
- Supports adding new hosts to known_hosts file.
- Supports host key callback check from default known_hosts file.
- Supports file operations like:
Open, Create, Chmod...via SFTP. - Supports context.Context for command cancellation.
- Supports SOCKS5 proxy for connecting through intermediaries.
- Supports proxy jump for connecting through jump hosts.
🚀 Installation
go get github.com/melbahja/goph/v2
📄 Get Started
Run a command via ssh can be simple as this example:
package main
import (
"log"
"fmt"
"github.com/melbahja/goph/v2"
)
func main() {
// Start new ssh connection with private key.
client, err := goph.New("root", "192.1.1.3", goph.WithPassword("try_password_first"))
if err != nil {
log.Fatal(err)
}
// Defer closing the network connection.
defer client.Close()
// Execute your command.
out, err := client.Run("ls /tmp/")
if err != nil {
log.Fatal(err)
}
// Get your output as []byte.
fmt.Println(string(out))
}
Docs are available in Go Docs.
📚 Usage Examples
Expand each group then expand individual examples.
🔐 Authentication Examples
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("your_password"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
goph.WithKey(privateKey, "passphrase"),
)
if goph.HasAgent() {
client, err := goph.New("root", "192.1.1.3",
goph.WithDefaultAgent(),
)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)
conn, err := net.Dial("unix", "/custom/agent.sock")
if err != nil {
log.Fatal(err)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithAgent(conn),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
if strings.Contains(strings.ToLower(question), "password") {
return "your_password", nil
}
return "", fmt.Errorf("unexpected prompt: %s", question)
}),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyboardInteractive(func(user, instruction, question string, echo bool) (string, error) {
if strings.Contains(strings.ToLower(question), "otp") ||
strings.Contains(strings.ToLower(question), "verification code") {
fmt.Print(question, " ")
var code string
fmt.Scanln(&code)
return code, nil
}
return "", fmt.Errorf("unexpected prompt: %s", question)
}),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("try_password_first"),
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
goph.WithDefaultAgent(),
)
signer, err := ssh.NewSignerFromKey(myPrivateKey) // or from any source
if err != nil {
log.Fatal(err)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithSigner(signer),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithAuth(ssh.Password("pass")),
goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)
📡 Features Examples
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithPort(2222),
goph.WithTimeout(10*time.Second),
)
// Known hosts verification is ON by default via ~/.ssh/known_hosts
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
)
// Or specify a custom known_hosts file:
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithKnownHosts("/path/to/known_hosts"),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithInsecureIgnoreHostKey(),
)
hostKeyCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
// Check the key against a database or prompt the user
return nil
}
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithHostKeyCallback(hostKeyCallback),
)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithBannerCallback(func(message string) error {
log.Printf("SSH Banner: %s", message)
return nil
}),
)
client, err := goph.New("root", "target-host",
goph.WithPassword("pass"),
goph.WithProxy("socks5://127.0.0.1:1080"),
)
// With proxy authentication:
client, err = goph.New("root", "target-host",
goph.WithPassword("pass"),
goph.WithProxy("socks5://proxyuser:[email protected]:1080"),
)
// First connect to the jump host
jump, err := goph.New("jumpuser", "bastion.example.com",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
goph.WithPort(2222),
)
if err != nil {
log.Fatal(err)
}
defer jump.Close()
// Then tunnel through it to the target
client, err := goph.New("root", "internal-host",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
goph.WithJump(jump),
)
// Put the proxy on the JUMP client, then pass the jump to the target
jump, err := goph.New("jumpuser", "bastion.example.com",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
goph.WithProxy("socks5://127.0.0.1:1080"),
)
if err != nil {
log.Fatal(err)
}
defer jump.Close()
client, err := goph.New("root", "internal-host",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
goph.WithJump(jump),
)
out, err := client.Run("ls /tmp/")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Sends SIGINT and returns error after 1 second
out, err := client.RunContext(ctx, "sleep 5")
ctx, cancel := context.WithCancel(context.Background())
cmd, err := client.CommandContext(ctx, "sleep", "60")
if err != nil {
log.Fatal(err)
}
// Cancel from another goroutine
go func() {
time.Sleep(5 * time.Second)
cancel()
}()
if err := cmd.Run(); err != nil {
// context canceled
fmt.Println("Command stopped:", err)
}
cmd, err := client.CommandContext(ctx, "sleep", "30")
if err != nil {
log.Fatal(err)
}
// Send SIGKILL instead of the default SIGINT on cancellation
cmd.Cancel = func() error {
return cmd.Signal(ssh.SIGKILL)
}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
cmd, err := client.Command("ls", "-alh", "/tmp")
if err != nil {
log.Fatal(err)
}
// Set env vars (server must accept them)
cmd.Env = []string{"MY_VAR=MYVALUE"}
// Run (CombinedOutput, Output, Start, Wait also available)
err = cmd.Run()
// With context:
cmd, err = client.CommandContext(ctx, "ls", "-alh", "/tmp")
err := client.Upload("/path/to/local/file", "/path/to/remote/file")
err := client.Download("/path/to/remote/file", "/path/to/local/file")
sftp, err := client.NewSftp()
if err != nil {
log.Fatal(err)
}
defer sftp.Close()
// Create a file
file, err := sftp.Create("/tmp/remote_file")
if err != nil {
log.Fatal(err)
}
defer file.Close()
file.Write([]byte("Hello world"))
// Open existing file
f, _ := sftp.Open("/etc/hostname")
// ... read from f
// Other methods: MkdirAll, Remove, Rename, ReadDir, Stat, etc.
// Execute a script from any io.Reader (e.g. a string, a file handle, an HTTP body)
cmd, err := client.Script(ctx, strings.NewReader("echo hello"))
if err != nil {
log.Fatal(err)
}
out, err := cmd.CombinedOutput()
// Read a local script file into memory and execute it on the remote host
cmd, err := client.ScriptFile(ctx, "/path/to/local/script.sh")
if err != nil {
log.Fatal(err)
}
// Override the interpreter for a PHP script:
cmd, err = client.ScriptFile(ctx, "/path/to/script.php", goph.WithPath("/usr/bin/php"))
// Override the shell path (default: /bin/sh)
cmd, err = client.ScriptFile(ctx, "/path/to/local/script.sh", goph.WithPath("/bin/zsh"))
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithConfig(func(config *ssh.ClientConfig) error {
config.Timeout = 30 * time.Second
config.ClientVersion = "SSH-2.0-MyApp"
return nil
}),
)
c := &goph.Client{
User: "root",
Addr: "192.1.1.3",
Port: 22,
}
config := &ssh.ClientConfig{
Auth: []ssh.AuthMethod{ssh.Password("pass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
if err := goph.Dial(c, config); err != nil {
log.Fatal(err)
}
defer c.Close()
d := goph.NewDialer(
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
goph.WithPort(2222),
)
client1, _ := d.New("root", "host1")
client2, _ := d.New("root", "host2", goph.WithPassword("fallback"))
🛠️ Utils and Helpers
found, err := goph.CheckKnownHost("myhost", remoteAddr, publicKey, "")
if err != nil {
// key mismatch! possible MITM attack!
log.Fatal(err)
}
if !found {
// host is new, ask user to trust it
}
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")
signer, err := goph.ParseKeyFile("/home/user/.ssh/id_rsa", "passphrase")
if err != nil {
log.Fatal(err)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithSigner(signer),
)
keyBytes, _ := os.ReadFile("/home/user/.ssh/id_rsa")
signer, err := goph.ParseKey(keyBytes, "passphrase")
if err != nil {
log.Fatal(err)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithSigner(signer),
)
if goph.HasAgent() {
fmt.Println("SSH agent is available at", os.Getenv("SSH_AUTH_SOCK"))
} else {
fmt.Println("SSH agent not running or SSH_AUTH_SOCK not set")
}
path, err := goph.DefaultKnownHostsPath()
if err != nil {
log.Fatal(err)
}
fmt.Println("Known hosts file:", path)
// EnsureKnownHosts returns a callback and creates the file (and ~/.ssh/)
// if it does not exist yet — useful when writing first-time tools.
cb, err := goph.EnsureKnownHosts(filepath.Join(os.Getenv("HOME"), ".ssh", "my_hosts"))
if err != nil {
log.Fatal(err)
}
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithHostKeyCallback(cb),
)
🛡️ Security Notes
- Known-hosts verification is enabled by default. Do not use
goph.WithInsecureIgnoreHostKey()in production; it makes you vulnerable to MITM attacks. - A missing
~/.ssh/known_hostsfile is created automatically as an empty file, but unknown host keys are still rejected until you explicitly trust them. - Command arguments are not shell-escaped.
Cmd.String()returns rawPathandArgs. Never pass untrusted input directly into commands; sanitize or use fixed argument lists. - Prompt the user before calling
goph.AddKnownHost. A mismatched key fromgoph.CheckKnownHostshould be treated as a potential MITM attack.
❓ FAQ
Expand each question to see the answer.
SSH exec requests do not support setting a working directory in the protocol. The server runs the command in the user's default shell context (typically their home directory). To run a command in a specific directory, prefix it:
out, err := client.Run("cd /var/log && ls -la")
Either connect as root, or use sudo -S and feed the password through stdin. Be aware that sudo may require a TTY, which goph does not provide by default:
cmd, err := client.Command("sudo", "-S", "systemctl", "restart", "nginx")
if err != nil {
log.Fatal(err)
}
cmd.Stdin = strings.NewReader("your_sudo_password\n")
out, err := cmd.CombinedOutput()
Yes. goph is pure Go and works on Windows, Linux, and macOS. For Windows-specific key providers (CNG/CAPI, Pageant, named-pipe OpenSSH agent), build or obtain an ssh.Signer and use WithSigner:
signer, err := ssh.NewSignerFromKey(myPrivateKey)
client, err := goph.New("user", "host", goph.WithSigner(signer))
🤝 Missing a Feature?
Feel free to open a new issue, or contact me.
📘 License
Goph is provided under the MIT License.