SSH is a network protocol for establishing a secure shell session on distant servers. In Golang the package godoc.org/golang.org/x/crypto/ssh implements SSH client and SSH server.
In this article, we are using SSH client to run a shell command on a remote
machine. Every SSH connection requires an ssh.CleintConfig
object that
defines configuration options such as authentication.
Authentication Options
Depending on how the remote server is configure, there are two ways to authenticate:
- using a username and SSH certificate
- using a username and password credentials
If you want to authenticate with username
and password
you should create
ssh.ClientConfig
in the following way:
sshConfig := &ssh.ClientConfig{
User: "your_user_name",
Auth: []ssh.AuthMethod{
ssh.Password("your_password")
},
}
If you want to authenticate by using SSH certificate, there are two methods to obtain your ssh key:
SSH certificate file
You can parse your private key file by using ssh.ParsePrivateKey
function.
This is required by ssh.PublicKeys
auth method function that creates a ssh.AuthMethod
instance from private key.
func PublicKeyFile(file string) ssh.AuthMethod {
buffer, err := ioutil.ReadFile(file)
if err != nil {
return nil
}
key, err := ssh.ParsePrivateKey(buffer)
if err != nil {
return nil
}
return ssh.PublicKeys(key)
}
Then you should instanciate ssh.ClientConfig
:
sshConfig := &ssh.ClientConfig{
User: "your_user_name",
Auth: []ssh.AuthMethod{
PublicKeyFile("/path/to/your/pub/certificate/key")
},
}
SSH agent
SSH Agent is a program that runs during
user session in *nix
system. It stores the private keys in an encrypted form.
Because typing the passphrase can be tedious, many users would prefer to using it
to store their private keys.
You can obtain all stored keys via SSH_AUTH_SOCK
environment variable which
stores the SSH agent unix socket. We should access the keys by calling net.Dial
and then instanciate an agent client used by ssh.PublicKeysCallback
factory
auth method.
func SSHAgent() ssh.AuthMethod {
if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
}
return nil
}
Then you can use the function to instanciate the client config in the following way:
sshConfig := &ssh.ClientConfig{
User: "your_user_name",
Auth: []ssh.AuthMethod{
SSHAgent()
},
}
Note that you can add your certificate to the SSH agent by using the following command:
$ ssh-add /path/to/your/private/certificate/file
Establishing new SSH connection
After we instaciated the ssh.ClientConfig
object. We should be able to establish
a new connection to the remote server by calling ssh.Dial
:
connection, err := ssh.Dial("tcp", "host:port", sshConfig)
if err != nil {
return nil, fmt.Errorf("Failed to dial: %s", err)
}
Creating a new session
After we established the connection, we should be able to open a new session that acts as an entry point to the remote terminal. We should use the connection in the following manner:
session, err := connection.NewSession()
if err != nil {
return nil, fmt.Errorf("Failed to create session: %s", err)
}
Before we will be able to run the command on the remote machine, we should create a pseudo terminal on the remote machine. A pseudoterminal (or “pty”) is a pair of virtual character devices that provide a bidirectional communication channel.
We should create an xterm
terminal that has 80
columns and 40
rows.
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
session.Close()
return nil, fmt.Errorf("request for pseudo terminal failed: %s", err)
}
If we want to attach our os.Stdin
, os.Stdout
and os.Stderr
to remote command
we should open pipes between the local process and remote process.
Forthunatelly, ssh.Session
object provides that out of the box by invoking
session.Stdinpipe()
, session.Stdoutpipe()
and session.Stdouterr()
functions.
Then we should asyncronously copy the end of the pipes to the right file
descriptors by using go routines and os.Copy
function.
stdin, err := session.StdinPipe()
if err != nil {
return fmt.Errorf("Unable to setup stdin for session: %v", err)
}
go io.Copy(stdin, os.Stdin)
stdout, err := session.StdoutPipe()
if err != nil {
return fmt.Errorf("Unable to setup stdout for session: %v", err)
}
go io.Copy(os.Stdout, stdout)
stderr, err := session.StderrPipe()
if err != nil {
return fmt.Errorf("Unable to setup stderr for session: %v", err)
}
go io.Copy(os.Stderr, stderr)
Command execution
Then we can execute the command on the remote machine by using session.Run
function:
err = session.Run("ls -l $LC_USR_DIR")
If we want to transfer some environment variable to the remote machine, we should
use session.Setenv
function to do that.
if err := session.Setenv("LC_USR_DIR", "/usr"); err != nil {
return err
}
Note that in some cases the SSH server is configured to accepts only env
variables
with concrete suffix (such as LC_
).
You can find the sample source code here.