cli, etcd, lib, setup: Support ssh hostkey logic
This makes it easy to pass in the expected key so that we never have to guess and risk MITM attacks.
This commit is contained in:
@@ -63,6 +63,11 @@ type DeployArgs struct {
|
||||
// setup for things to work.
|
||||
SSHURL string `arg:"--ssh-url" help:"transport the etcd client connection over SSH to this server"`
|
||||
|
||||
// SSHHostKey is the key part (which is already base64 encoded) from a
|
||||
// known_hosts file, representing the host we're connecting to. If this
|
||||
// is specified, then it overrides looking for it in the URL.
|
||||
SSHHostKey string `arg:"--ssh-hostkey" help:"use this ssh known hosts key when connecting over SSH"`
|
||||
|
||||
Seeds []string `arg:"--seeds,separate,env:MGMT_SEEDS" help:"default etcd client endpoints"`
|
||||
Noop bool `arg:"--noop" help:"globally force all resources into no-op mode"`
|
||||
Sema int `arg:"--sema" default:"-1" help:"globally add a semaphore to all resources with this lock count"`
|
||||
@@ -211,6 +216,7 @@ func (obj *DeployArgs) Run(ctx context.Context, data *cliUtil.Data) (bool, error
|
||||
if obj.SSHURL != "" { // alternate world implementation over SSH
|
||||
world = &etcdSSH.World{
|
||||
URL: obj.SSHURL,
|
||||
HostKey: obj.SSHHostKey,
|
||||
Seeds: obj.Seeds,
|
||||
NS: lib.NS,
|
||||
//MetadataPrefix: lib.MetadataPrefix,
|
||||
|
||||
@@ -165,6 +165,8 @@ type SetupPkgArgs struct {
|
||||
type SetupSvcArgs struct {
|
||||
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
|
||||
SSHURL string `arg:"--ssh-url" help:"transport the etcd client connection over SSH to this server"`
|
||||
SSHHostKey string `arg:"--ssh-hostkey" help:"use this ssh known hosts key when connecting over SSH"`
|
||||
|
||||
Seeds []string `arg:"--seeds,separate,env:MGMT_SEEDS" help:"default etcd client endpoints"`
|
||||
NoServer bool `arg:"--no-server" help:"do not start embedded etcd server (do not promote from client to peer)"`
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ package ssh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -55,6 +56,7 @@ import (
|
||||
const (
|
||||
defaultUser = "root"
|
||||
defaultSSHPort uint16 = 22
|
||||
defaultSSHHostKeyFieldName = "hostkey" // querystring field name
|
||||
defaultEtcdPort uint16 = 2379 // TODO: get this from etcd pkg
|
||||
defaultIDRsaPath = "~/.ssh/id_rsa"
|
||||
defaultIDEd25519Path = "~/.ssh/id_ed25519"
|
||||
@@ -65,9 +67,21 @@ const (
|
||||
type World struct {
|
||||
// URL is the ssh server to connect to. Use the format, james@server:22
|
||||
// or similar. From there, we connect to each of the etcd Seeds, so the
|
||||
// ip's should be relative to this server.
|
||||
// ip's should be relative to this server. If you pass in a ?hostkey=
|
||||
// query string parameter, you can specify a base64, known_hosts key to
|
||||
// use for confirmation that you're connecting to the right host.
|
||||
// Without this, it will look in your ~/.ssh/known_hosts file which may
|
||||
// not necessarily exist yet, and without it connection is impossible.
|
||||
// You can find the key by running the ssh-keyscan command. It can also
|
||||
// be read from the HostKey parameter, which avoids you needing to
|
||||
// urlencode it here.
|
||||
URL string
|
||||
|
||||
// HostKey is the key part (which is already base64 encoded) from a
|
||||
// known_hosts file, representing the host we're connecting to. If this
|
||||
// is specified, then it overrides looking for it in the URL.
|
||||
HostKey string
|
||||
|
||||
// SSHID is the path to the ~/.ssh/id_rsa or ~/.ssh/id_ed25519 to use
|
||||
// for auth. If you omit this then this will look for your private key
|
||||
// in both of those default paths. If you specific a specific path, then
|
||||
@@ -126,21 +140,60 @@ func (obj *World) sshKeyAuth(sshID string) (ssh.AuthMethod, error) {
|
||||
return ssh.PublicKeys(signer), nil
|
||||
}
|
||||
|
||||
// knownHostsKey takes a known_hosts key entry (just the base64 key part) and
|
||||
// turns it into the ssh.PublicKey needed for hostKeyCallback. This excerpt was
|
||||
// taken from: x/crypto/ssh:keys.go:func parseAuthorizedKey
|
||||
func (obj *World) knownHostsKey(hostkey string) (ssh.PublicKey, error) {
|
||||
key := make([]byte, base64.StdEncoding.DecodedLen(len(hostkey)))
|
||||
n, err := base64.StdEncoding.Decode(key, []byte(hostkey))
|
||||
if err != nil {
|
||||
// Make it easier to spot this common error...
|
||||
s := err.Error()
|
||||
m := "illegal base64 data at input byte "
|
||||
if strings.HasPrefix(s, m) {
|
||||
if d, e := strconv.Atoi(s[len(m):]); e == nil {
|
||||
obj.init.Logf("error: %v", err)
|
||||
obj.init.Logf("host key: %s", hostkey)
|
||||
obj.init.Logf("location: %s^", strings.Repeat(" ", d))
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
key = key[:n]
|
||||
return ssh.ParsePublicKey(key)
|
||||
}
|
||||
|
||||
// hostKeyCallback is a helper function to get the ssh callback function needed.
|
||||
func (obj *World) hostKeyCallback() (ssh.HostKeyCallback, error) {
|
||||
// func (obj *World) hostKeyCallback() (ssh.HostKeyCallback, error) {
|
||||
func (obj *World) hostKeyCallback(hostkey ssh.PublicKey) ssh.HostKeyCallback {
|
||||
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
|
||||
// First try our one known key if it exists.
|
||||
if hostkey != nil {
|
||||
fn := ssh.FixedHostKey(hostkey)
|
||||
if fn(hostname, remote, key) == nil {
|
||||
return nil // found it!
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider allowing a user-specified path in the future
|
||||
s := defaultKnownHostsPath // "~/.ssh/known_hosts"
|
||||
|
||||
// expand strings of the form: ~james/.ssh/known_hosts
|
||||
p, err := util.ExpandHome(s)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "can't find home directory")
|
||||
return errwrap.Wrapf(err, "can't find home directory for known_hosts file")
|
||||
}
|
||||
if p == "" {
|
||||
return nil, fmt.Errorf("empty path specified")
|
||||
return fmt.Errorf("empty known_hosts path specified")
|
||||
}
|
||||
|
||||
return knownhosts.New(p)
|
||||
fn, err := knownhosts.New(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(hostname, remote, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect runs first.
|
||||
@@ -198,6 +251,20 @@ func (obj *World) Connect(ctx context.Context, init *engine.WorldInit) error {
|
||||
port = s
|
||||
}
|
||||
|
||||
// TODO: Should we read out a list of these, one for each key type?
|
||||
base64Key := u.Query().Get(defaultSSHHostKeyFieldName) // urlencode me!
|
||||
if obj.HostKey != "" { // override
|
||||
base64Key = obj.HostKey
|
||||
}
|
||||
var pubKey ssh.PublicKey // known hosts key
|
||||
if base64Key != "" {
|
||||
k, err := obj.knownHostsKey(base64Key)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "invalid known_hosts key")
|
||||
}
|
||||
pubKey = k
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", hostname, port)
|
||||
|
||||
auths := []ssh.AuthMethod{}
|
||||
@@ -228,17 +295,12 @@ func (obj *World) Connect(ctx context.Context, init *engine.WorldInit) error {
|
||||
return fmt.Errorf("no auth options available")
|
||||
}
|
||||
|
||||
hostKeyCallback, err := obj.hostKeyCallback()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// SSH connection configuration
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: auths,
|
||||
//HostKeyCallback: ssh.InsecureIgnoreHostKey(), // testing
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
HostKeyCallback: obj.hostKeyCallback(pubKey),
|
||||
}
|
||||
|
||||
obj.init.Logf("ssh: %s@%s", user, addr)
|
||||
|
||||
@@ -157,6 +157,11 @@ type Config struct {
|
||||
// setup for things to work.
|
||||
SSHURL string `arg:"--ssh-url" help:"transport the etcd client connection over SSH to this server"`
|
||||
|
||||
// SSHHostKey is the key part (which is already base64 encoded) from a
|
||||
// known_hosts file, representing the host we're connecting to. If this
|
||||
// is specified, then it overrides looking for it in the URL.
|
||||
SSHHostKey string `arg:"--ssh-hostkey" help:"use this ssh known hosts key when connecting over SSH"`
|
||||
|
||||
// Seeds are the list of default etcd client endpoints. If empty, it
|
||||
// will startup a new server.
|
||||
Seeds []string `arg:"--seeds,separate,env:MGMT_SEEDS" help:"default etcd client endpoints"`
|
||||
@@ -620,6 +625,7 @@ func (obj *Main) Run() error {
|
||||
if obj.SSHURL != "" { // alternate world implementation over SSH
|
||||
world = &etcdSSH.World{
|
||||
URL: obj.SSHURL,
|
||||
HostKey: obj.SSHHostKey,
|
||||
Seeds: obj.Seeds,
|
||||
NS: NS,
|
||||
MetadataPrefix: MetadataPrefix,
|
||||
|
||||
@@ -115,6 +115,11 @@ func (obj *Svc) Run(ctx context.Context) error {
|
||||
argv = append(argv, fmt.Sprintf("--ssh-url=%s", s))
|
||||
}
|
||||
|
||||
if s := obj.SetupSvcArgs.SSHHostKey; s != "" {
|
||||
// TODO: validate ssh url? Should be user@server:port
|
||||
argv = append(argv, fmt.Sprintf("--ssh-hostkey=%s", s))
|
||||
}
|
||||
|
||||
for _, seed := range obj.SetupSvcArgs.Seeds {
|
||||
// TODO: validate each seed?
|
||||
s := fmt.Sprintf("--seeds=%s", seed)
|
||||
|
||||
Reference in New Issue
Block a user