Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

114 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Golang SSH Client.

Fast and easy golang ssh client module.

Goph is a lightweight Go SSH client focusing on simplicity!

Features ❘ Installation ❘ Get Started ❘ Usage Examples ❘ License

🀘  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

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)),
)

πŸ“‘Β  Features Examples

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"))

πŸ› οΈΒ  Utils and Helpers

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),
)

πŸ›‘οΈΒ  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_hosts file 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 raw Path and Args. Never pass untrusted input directly into commands; sanitize or use fixed argument lists.
  • Prompt the user before calling goph.AddKnownHost. A mismatched key from goph.CheckKnownHost should be treated as a potential MITM attack.

❓  FAQ

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))

🀝  Missing a Feature?

Feel free to open a new issue, or contact me.

πŸ“˜Β  License

Goph is provided under the MIT License.

About

🀘 The native golang ssh client to execute your commands over ssh connection. πŸš€πŸš€

Topics

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages