Golang – How to write ssh.HostKeyCallback

ssh.InsecureIgnoreHostKey is lazy and seems popular?

I have seen many tutorials and some codes in github that ignore host key checking, this is not recommended as you need to ensure everytime you connect to the known ssh server is the actual server that serves your purpose, if host key checking is ignore then any server that has the same FQDN or IP address can impersonate the actual ssh server.

For proper host key call back handling I recommend you study the code https://github.com/melbahja/goph, read the hosts.go and client.go. The author of goph – Mr. Mohamed El Bahja – has taken care of all scenarios in host key checking i.e. ignore host key checking scenario, host key mistmatch scenario and host key does not exist scenario, and finally how to add host key when key does not exist.

To start to learn how to use ssh package refer to this tutorial, this tutorial presents how to use key authentication but unfortunately ignores host key checking, this tutorial will be perfect if all scenarios of the host key checking are explained. Using go routine to run io.Copy to copy the os.Stdout to the session stdout does not work consistently, so sometime I will see my output in stdout sometime not, the best solution is to assign os.Stdout to the session.Stdout, this goes the same for Stderr.

Another post from stackoverflow enforces the host key checking, if key does not exist warn the user and present the pubkey string to user, a portion of my code is from the post, read the solution posted by colm.anseo which I am appalled the correct solution only gets one vote.

The only best solution in stackoverflow post but only gets one vote.

Observe the source code for ssh.InsecureIgnoreHostKey(), the function simply returns a nil which has no host key checking handling, while ignoring host key checking is convenient and easy to make ssh client works it is bad as it creates a habit of simply making things worked instead of making things secured and worked.

By seeing ssh.InsecureIgnoreHostKey() got 16 votes which is more than the correct way of handling, and by searching the net for tutorials on how to connect ssh I inevitably will see ssh.InsecureIgnoreKey() almost dampens my desire to make the callback right, as examples seem to be scarce…

HostKeyCallBack

The HostKeyCallBack documentation explains that this function type is used for handling host key checking, if error is nil connection proceeds if there is error then connection stops.

This post records with reference to goph‘s code on how to write a proper host key checking call back that handles add host key if host is not in known_hosts file, if host key mismatches the connection drops and if host key exists the connection proceeds.

Known Hosts

Within the golang.org/x/crypto/ssh there is a knownhosts directory this directory contains functions that helps in serializing the pubkey with the host ip to the known_hosts file, and also help to generate a callback with the New function.

Normalize normalizes the host in known_hosts format.

Line helps to append the pub key of host to known_hosts file.

The packages that need to call for ssh are:

“golang.org/x/crypto/ssh”

“golang.org/x/crypto/ssh/knownhosts”

The knownhosts package can also parse known_hosts that has HashKnownHosts set to yes. This is how HashKnownHosts set to yes looks like:

The host is hashed.

networkbit.ch has a gethostkey function code which will only work if the host is plaintext hence it will not work if the host is hashed in known_hosts file, with knownhosts package this problem can be ignored.

Generic error handling function

This function takes in an error as argument and gives a fatal log, this function is use for generic error handling however if more handling is required then this function is not used.

func errCallBack(e error) {
	if e != nil {
		log.Fatal(e)
	}
}

Create known_hosts file function

This code helps to create known_hosts if does not exist, I am using the os package which has an OpenFile function, this function accepts 3 arguments, the file path is the first, second is the flag you can have multiple flags in this argument example os.O_CREATE|os.O_APPEND which creates the file if not exists, if exists append the file, the last argument is the permission.

func createKnownHosts() {
	f, fErr := os.OpenFile(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"), os.O_CREATE, 0600)
	if fErr != nil {
		log.Fatal(fErr)
	}
	f.Close()
}

Check for host in known_hosts file function

This function creates a know_hosts callback function with the New function, this callback function can be used to check if the host exists in the known_hosts file.

I am using the default path of where known_hosts file normally exists which is $HOME/.ssh/known_hosts. To get the path I am using filepath package to use its Join method, and also the os package to use its Getenv method.

It is require to use the knownhosts package.

func checkKnownHosts() ssh.HostKeyCallback {
	createKnownHosts()
	kh, e := knownhosts.New(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts"))
	errCallBack(e)
	return kh
}

Add host key function

This function adds the host key to known_hosts file by using Normalize and Line functions of knownhosts package.

The function takes in hostname, remote server’s ip address and public key as argument, these arguments are feed in by using ssh.HostKeyCallback

func addHostKey(host string, remote net.Addr, pubKey ssh.PublicKey) error {
	// add host key if host is not found in known_hosts, error object is return, if nil then connection proceeds,
	// if not nil then connection stops.
	khFilePath := filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")

	f, fErr := os.OpenFile(khFilePath, os.O_APPEND|os.O_WRONLY, 0600)
	if fErr != nil {
		return fErr
	}
	defer f.Close()

	knownHosts := knownhosts.Normalize(remote.String())
	_, fileErr := f.WriteString(knownhosts.Line([]string{knownHosts}, pubKey))
	return fileErr
}

Key Error

KeyError is returned when either the host key is not found or key is mismatched.

This KeyError type is used to compare whether it is host not found or key mismatch by using the len function.

SSH client function

To get the ssh client, you will need to first use ssh.ClientConfig. The ssh.ClientConfig is where the HostKeyCallback has to be defined.

Using knownhosts.New, a callback is created, this callback is then used to check if there is key error, if there is key error check if the keyError Want slice is empty or not, if it is empty then no host key found, if it is not empty then it is mismatched.

func (c *sshConfig) getSSHClient() {
	// Reference: https://github.com/melbahja/goph/blob/master/client.go
	// Reference: https://github.com/melbahja/goph/blob/master/hosts.go
	// Study the client.go and hosts.go to understand how to write host key call back.
	var (
		e      error
		keyErr *knownhosts.KeyError
	)
	config := &ssh.ClientConfig{
		User: c.username,
		Auth: []ssh.AuthMethod{
			ssh.Password(c.password),
		},
		HostKeyCallback: ssh.HostKeyCallback(func(host string, remote net.Addr, pubKey ssh.PublicKey) error {
			kh := checkKnownHosts()
			hErr := kh(host, remote, pubKey)
			// Reference: https://blog.golang.org/go1.13-errors
			// To understand what errors.As is.
			if errors.As(hErr, &keyErr) && len(keyErr.Want) > 0 {
				// Reference: https://www.godoc.org/golang.org/x/crypto/ssh/knownhosts#KeyError
				// if keyErr.Want slice is empty then host is unknown, if keyErr.Want is not empty
				// and if host is known then there is key mismatch the connection is then rejected.
				log.Printf("WARNING: %v is not a key of %s, either a MiTM attack or %s has reconfigured the host pub key.", hostKeyString(pubKey), host, host)
				return keyErr
			} else if errors.As(hErr, &keyErr) && len(keyErr.Want) == 0 {
				// host key not found in known_hosts then give a warning and continue to connect.
				log.Printf("WARNING: %s is not trusted, adding this key: %q to known_hosts file.", host, hostKeyString(pubKey))
				return addHostKey(host, remote, pubKey)
			}
			log.Printf("Pub key exists for %s.", host)
			return nil
		}),
	}

	c.client, e = ssh.Dial("tcp", c.svrAddr, config)
	errCallBack(e)
}

Host key not exists scenario

A new connection to host, but the host is not found in known_hosts file.

A new entry is appended in known_hosts file

Host key exists scenario

Host key exists in known_hosts file, there will be no warning and the connection is successful.

Key Mismatch scenario

To demonstrate this scenario the existing public keys will be deleted and recreate with dpkg-reconfigure openssh-server command. Because the new host key in ssh server is different from the key in known_hosts a warning is displayed and connection drops.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s