knownhosts: enhanced Golang SSH known_hosts management
This repo is brought to you by Skeema, a declarative pure-SQL schema management system for MySQL and MariaDB. Our premium products include extensive SSH tunnel functionality, which internally makes use of this package.
Go provides excellent functionality for OpenSSH known_hosts files in its
external package golang.org/x/crypto/ssh/knownhosts.
However, that package is somewhat low-level, making it difficult to implement full known_hosts management similar to command-line ssh's behavior for StrictHostKeyChecking=no configuration.
This repo (github.com/skeema/knownhosts) is a thin wrapper package around golang.org/x/crypto/ssh/knownhosts, adding functions which provide the following functionality:
- Look up known_hosts public keys for any given host
- Auto-populate ssh.ClientConfig.HostKeyAlgorithms easily based on known_hosts
- Write new known_hosts entries to an io.Writer
- Determine if an ssh.HostKeyCallback's error corresponds to a host whose key has changed (indicating potential MitM attack) vs a host that just isn't known yet
How host key lookup works
Although golang.org/x/crypto/ssh/knownhosts doesn't directly expose a way to query its known_host map, we use a subtle trick to do so: invoke the HostKeyCallback with a valid host but a bogus key. The resulting KeyError allows us to determine which public keys are actually present for that host.
By using this technique, github.com/skeema/knownhosts doesn't need to duplicate or re-implement any of the actual known_hosts management from golang.org/x/crypto/ssh/knownhosts.
Populating ssh.ClientConfig.HostKeyAlgorithms based on known_hosts
Hosts often have multiple public keys, each of a different type (algorithm). This can be problematic in golang.org/x/crypto/ssh/knownhosts: if a host's first public key is not in known_hosts, but a key of a different type is, the HostKeyCallback returns an error. The solution is to populate ssh.ClientConfig.HostKeyAlgorithms based on the algorithms of the known_hosts entries for that host, but
golang.org/x/crypto/ssh/knownhosts
does not provide an obvious way to do so.
This package uses its host key lookup trick in order to make ssh.ClientConfig.HostKeyAlgorithms easy to populate:
import (
"golang.org/x/crypto/ssh"
"github.com/skeema/knownhosts"
)
func sshConfigForHost(hostWithPort string) (*ssh.ClientConfig, error) {
kh, err := knownhosts.New("/home/myuser/.ssh/known_hosts")
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: kh.HostKeyCallback(), // or, equivalently, use ssh.HostKeyCallback(kh)
HostKeyAlgorithms: kh.HostKeyAlgorithms(hostWithPort),
}
return config, nil
}
Writing new known_hosts entries
If you wish to mimic the behavior of OpenSSH's StrictHostKeyChecking=no or StrictHostKeyChecking=ask, this package provides a few functions to simplify this task. For example:
sshHost := "yourserver.com:22"
khPath := "/home/myuser/.ssh/known_hosts"
kh, err := knownhosts.New(khPath)
if err != nil {
log.Fatal("Failed to read known_hosts: ", err)
}
// Create a custom permissive hostkey callback which still errors on hosts
// with changed keys, but allows unknown hosts and adds them to known_hosts
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := kh(hostname, remote, key)
if knownhosts.IsHostKeyChanged(err) {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname)
} else if knownhosts.IsHostUnknown(err) {
f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600)
if ferr == nil {
defer f.Close()
ferr = knownhosts.WriteKnownHost(f, hostname, remote, key)
}
if ferr == nil {
log.Printf("Added host %s to known_hosts\n", hostname)
} else {
log.Printf("Failed to add host %s to known_hosts: %v\n", hostname, ferr)
}
return nil // permit previously-unknown hosts (warning: may be insecure)
}
return err
})
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}