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.
|
// setup for things to work.
|
||||||
SSHURL string `arg:"--ssh-url" help:"transport the etcd client connection over SSH to this server"`
|
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"`
|
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"`
|
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"`
|
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
|
if obj.SSHURL != "" { // alternate world implementation over SSH
|
||||||
world = &etcdSSH.World{
|
world = &etcdSSH.World{
|
||||||
URL: obj.SSHURL,
|
URL: obj.SSHURL,
|
||||||
|
HostKey: obj.SSHHostKey,
|
||||||
Seeds: obj.Seeds,
|
Seeds: obj.Seeds,
|
||||||
NS: lib.NS,
|
NS: lib.NS,
|
||||||
//MetadataPrefix: lib.MetadataPrefix,
|
//MetadataPrefix: lib.MetadataPrefix,
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ type SetupPkgArgs struct {
|
|||||||
type SetupSvcArgs struct {
|
type SetupSvcArgs struct {
|
||||||
BinaryPath string `arg:"--binary-path" help:"path to the binary"`
|
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"`
|
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"`
|
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)"`
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -55,6 +56,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
defaultUser = "root"
|
defaultUser = "root"
|
||||||
defaultSSHPort uint16 = 22
|
defaultSSHPort uint16 = 22
|
||||||
|
defaultSSHHostKeyFieldName = "hostkey" // querystring field name
|
||||||
defaultEtcdPort uint16 = 2379 // TODO: get this from etcd pkg
|
defaultEtcdPort uint16 = 2379 // TODO: get this from etcd pkg
|
||||||
defaultIDRsaPath = "~/.ssh/id_rsa"
|
defaultIDRsaPath = "~/.ssh/id_rsa"
|
||||||
defaultIDEd25519Path = "~/.ssh/id_ed25519"
|
defaultIDEd25519Path = "~/.ssh/id_ed25519"
|
||||||
@@ -65,9 +67,21 @@ const (
|
|||||||
type World struct {
|
type World struct {
|
||||||
// URL is the ssh server to connect to. Use the format, james@server:22
|
// 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
|
// 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
|
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
|
// 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
|
// 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
|
// 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
|
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.
|
// 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
|
// TODO: consider allowing a user-specified path in the future
|
||||||
s := defaultKnownHostsPath // "~/.ssh/known_hosts"
|
s := defaultKnownHostsPath // "~/.ssh/known_hosts"
|
||||||
|
|
||||||
// expand strings of the form: ~james/.ssh/known_hosts
|
// expand strings of the form: ~james/.ssh/known_hosts
|
||||||
p, err := util.ExpandHome(s)
|
p, err := util.ExpandHome(s)
|
||||||
if err != nil {
|
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 == "" {
|
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.
|
// Connect runs first.
|
||||||
@@ -198,6 +251,20 @@ func (obj *World) Connect(ctx context.Context, init *engine.WorldInit) error {
|
|||||||
port = s
|
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)
|
addr := fmt.Sprintf("%s:%s", hostname, port)
|
||||||
|
|
||||||
auths := []ssh.AuthMethod{}
|
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")
|
return fmt.Errorf("no auth options available")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostKeyCallback, err := obj.hostKeyCallback()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSH connection configuration
|
// SSH connection configuration
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig := &ssh.ClientConfig{
|
||||||
User: user,
|
User: user,
|
||||||
Auth: auths,
|
Auth: auths,
|
||||||
//HostKeyCallback: ssh.InsecureIgnoreHostKey(), // testing
|
//HostKeyCallback: ssh.InsecureIgnoreHostKey(), // testing
|
||||||
HostKeyCallback: hostKeyCallback,
|
HostKeyCallback: obj.hostKeyCallback(pubKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.init.Logf("ssh: %s@%s", user, addr)
|
obj.init.Logf("ssh: %s@%s", user, addr)
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ type Config struct {
|
|||||||
// setup for things to work.
|
// setup for things to work.
|
||||||
SSHURL string `arg:"--ssh-url" help:"transport the etcd client connection over SSH to this server"`
|
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
|
// Seeds are the list of default etcd client endpoints. If empty, it
|
||||||
// will startup a new server.
|
// will startup a new server.
|
||||||
Seeds []string `arg:"--seeds,separate,env:MGMT_SEEDS" help:"default etcd client endpoints"`
|
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
|
if obj.SSHURL != "" { // alternate world implementation over SSH
|
||||||
world = &etcdSSH.World{
|
world = &etcdSSH.World{
|
||||||
URL: obj.SSHURL,
|
URL: obj.SSHURL,
|
||||||
|
HostKey: obj.SSHHostKey,
|
||||||
Seeds: obj.Seeds,
|
Seeds: obj.Seeds,
|
||||||
NS: NS,
|
NS: NS,
|
||||||
MetadataPrefix: MetadataPrefix,
|
MetadataPrefix: MetadataPrefix,
|
||||||
|
|||||||
@@ -115,6 +115,11 @@ func (obj *Svc) Run(ctx context.Context) error {
|
|||||||
argv = append(argv, fmt.Sprintf("--ssh-url=%s", s))
|
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 {
|
for _, seed := range obj.SetupSvcArgs.Seeds {
|
||||||
// TODO: validate each seed?
|
// TODO: validate each seed?
|
||||||
s := fmt.Sprintf("--seeds=%s", seed)
|
s := fmt.Sprintf("--seeds=%s", seed)
|
||||||
|
|||||||
Reference in New Issue
Block a user