Features β Installation β Get Started β Usage Examples β License
- 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.
go get github.com/melbahja/goph/v2Run 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.
Expand each group then expand individual examples.
Password Authentication
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("your_password"),
)Key File Authentication
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", ""),
)Protected Private Key (with Passphrase)
client, err := goph.New("root", "192.1.1.3",
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
)Raw Private Key (from bytes)
privateKey, _ := os.ReadFile("/home/user/.ssh/id_rsa")
client, err := goph.New("root", "192.1.1.3",
goph.WithKey(privateKey, "passphrase"),
)SSH Agent (auto-detect)
if goph.HasAgent() {
client, err := goph.New("root", "192.1.1.3",
goph.WithDefaultAgent(),
)
}Custom Agent Socket Path
client, err := goph.New("root", "192.1.1.3",
goph.WithAgentSocket("/run/user/1000/keyring/ssh"),
)Custom Agent Connection (net.Conn)
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),
)Keyboard-Interactive (password prompt)
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)
}),
)Keyboard-Interactive OTP / 2FA
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)
}),
)Multiple Auth Methods
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("try_password_first"),
goph.WithKeyFile("/home/user/.ssh/id_rsa", "passphrase"),
goph.WithDefaultAgent(),
)Custom Signer (any ssh.Signer)
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),
)Custom Auth Method (ssh.AuthMethod)
client, err := goph.New("root", "192.1.1.3",
goph.WithAuth(ssh.Password("pass")),
goph.WithAuth(ssh.PublicKeysCallback(myCustomCallback)),
)Custom Port and Timeout
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithPort(2222),
goph.WithTimeout(10*time.Second),
)Known Hosts Verification (Default)
// 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"),
)Disable Host Key Verification (Insecure)
client, err := goph.New("root", "192.1.1.3",
goph.WithPassword("pass"),
goph.WithInsecureIgnoreHostKey(),
)Custom Host Key Callback
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),
)Banner Callback
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
}),
)Connect Through SOCKS5 Proxy
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:proxypass@127.0.0.1:1080"),
)Connect Through Jump Host (Bastion)
// 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),
)Proxy + Jump Host Together
// 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),
)Run a Command
out, err := client.Run("ls /tmp/")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))Run with Timeout (Context)
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")Cancel from Another Goroutine
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)
}Custom Cancel Function (override SIGINT)
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)
}Using Cmd for Fine Control
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")Upload File
err := client.Upload("/path/to/local/file", "/path/to/remote/file")Download File
err := client.Download("/path/to/remote/file", "/path/to/local/file")SFTP File Operations
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 (streaming from io.Reader)
// 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()Execute a Script File
// 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"))Override Config Before Dial
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
}),
)Low-Level: Custom ssh.ClientConfig (Dial)
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()Reusable Dialer (Multiple Connections)
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"))Check if Host is Known
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
}Add Host to Known Hosts
err := goph.AddKnownHost("myhost", remoteAddr, publicKey, "")Parse Private Key File
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),
)Parse Raw Key (from bytes)
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),
)Check SSH Agent Availability
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")
}Default Known Hosts Path and Ensure
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),
)- 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.
Expand each question to see the answer.
Why is there no Dir field like os/exec.Cmd?
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")How do I run sudo commands?
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()Does goph work on Windows?
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))Feel free to open a new issue, or contact me.
Goph is provided under the MIT License.