2015-06-17 16:39:49 +00:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2016-05-25 19:02:31 +00:00
|
|
|
"encoding/json"
|
2015-06-17 16:39:49 +00:00
|
|
|
"fmt"
|
2015-06-18 00:33:03 +00:00
|
|
|
"io/ioutil"
|
2015-06-24 22:13:12 +00:00
|
|
|
"net"
|
2015-06-18 00:33:03 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
2015-07-29 18:21:36 +00:00
|
|
|
"os/user"
|
2015-06-17 16:39:49 +00:00
|
|
|
"strings"
|
2015-07-02 23:00:14 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
"github.com/hashicorp/vault/api"
|
2015-07-23 21:20:28 +00:00
|
|
|
"github.com/hashicorp/vault/builtin/logical/ssh"
|
2016-04-01 17:16:05 +00:00
|
|
|
"github.com/hashicorp/vault/meta"
|
2017-08-17 02:01:24 +00:00
|
|
|
homedir "github.com/mitchellh/go-homedir"
|
2015-08-18 01:22:03 +00:00
|
|
|
"github.com/mitchellh/mapstructure"
|
2017-08-17 02:01:24 +00:00
|
|
|
"github.com/pkg/errors"
|
2015-06-17 16:39:49 +00:00
|
|
|
)
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// SSHCommand is a Command that establishes a SSH connection with target by
|
|
|
|
// generating a dynamic key
|
2015-07-01 15:58:49 +00:00
|
|
|
type SSHCommand struct {
|
2016-04-01 17:16:05 +00:00
|
|
|
meta.Meta
|
2017-08-17 02:01:24 +00:00
|
|
|
|
|
|
|
// API
|
|
|
|
client *api.Client
|
|
|
|
sshClient *api.SSH
|
|
|
|
|
|
|
|
// Common options
|
|
|
|
mode string
|
|
|
|
noExec bool
|
|
|
|
format string
|
|
|
|
mountPoint string
|
|
|
|
role string
|
|
|
|
username string
|
|
|
|
ip string
|
|
|
|
sshArgs []string
|
|
|
|
|
|
|
|
// Key options
|
|
|
|
strictHostKeyChecking string
|
|
|
|
userKnownHostsFile string
|
|
|
|
|
|
|
|
// SSH CA backend specific options
|
2017-08-17 23:12:58 +00:00
|
|
|
publicKeyPath string
|
|
|
|
privateKeyPath string
|
|
|
|
hostKeyMountPoint string
|
|
|
|
hostKeyHostnames string
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
|
|
|
|
2015-08-18 01:22:03 +00:00
|
|
|
// Structure to hold the fields returned when asked for a credential from SSHh backend.
|
|
|
|
type SSHCredentialResp struct {
|
|
|
|
KeyType string `mapstructure:"key_type"`
|
|
|
|
Key string `mapstructure:"key"`
|
|
|
|
Username string `mapstructure:"username"`
|
|
|
|
IP string `mapstructure:"ip"`
|
2016-05-25 19:02:31 +00:00
|
|
|
Port string `mapstructure:"port"`
|
2015-08-18 01:22:03 +00:00
|
|
|
}
|
|
|
|
|
2015-07-01 15:58:49 +00:00
|
|
|
func (c *SSHCommand) Run(args []string) int {
|
2017-08-17 02:01:24 +00:00
|
|
|
|
2016-04-01 17:16:05 +00:00
|
|
|
flags := c.Meta.FlagSet("ssh", meta.FlagSetDefault)
|
2015-07-29 18:21:36 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
envOrDefault := func(key string, def string) string {
|
|
|
|
if k := os.Getenv(key); k != "" {
|
|
|
|
return k
|
|
|
|
}
|
|
|
|
return def
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
2016-06-01 03:31:53 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
expandPath := func(p string) string {
|
|
|
|
e, err := homedir.Expand(p)
|
|
|
|
if err != nil {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
return e
|
2016-06-01 03:31:53 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Common options
|
|
|
|
flags.StringVar(&c.mode, "mode", "", "")
|
|
|
|
flags.BoolVar(&c.noExec, "no-exec", false, "")
|
|
|
|
flags.StringVar(&c.format, "format", "table", "")
|
|
|
|
flags.StringVar(&c.mountPoint, "mount-point", "ssh", "")
|
|
|
|
flags.StringVar(&c.role, "role", "", "")
|
|
|
|
|
|
|
|
// Key options
|
|
|
|
flags.StringVar(&c.strictHostKeyChecking, "strict-host-key-checking",
|
|
|
|
envOrDefault("VAULT_SSH_STRICT_HOST_KEY_CHECKING", "ask"), "")
|
|
|
|
flags.StringVar(&c.userKnownHostsFile, "user-known-hosts-file",
|
|
|
|
envOrDefault("VAULT_SSH_USER_KNOWN_HOSTS_FILE", expandPath("~/.ssh/known_hosts")), "")
|
|
|
|
|
|
|
|
// CA-specific options
|
|
|
|
flags.StringVar(&c.publicKeyPath, "public-key-path",
|
|
|
|
expandPath("~/.ssh/id_rsa.pub"), "")
|
|
|
|
flags.StringVar(&c.privateKeyPath, "private-key-path",
|
|
|
|
expandPath("~/.ssh/id_rsa"), "")
|
2017-08-17 23:12:58 +00:00
|
|
|
flags.StringVar(&c.hostKeyMountPoint, "host-key-mount-point", "", "")
|
|
|
|
flags.StringVar(&c.hostKeyHostnames, "host-key-hostnames", "*", "")
|
2017-08-17 02:01:24 +00:00
|
|
|
|
|
|
|
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
|
|
|
if err := flags.Parse(args); err != nil {
|
|
|
|
return 1
|
2016-06-01 03:31:53 +00:00
|
|
|
}
|
|
|
|
|
2015-06-26 18:08:03 +00:00
|
|
|
args = flags.Args()
|
|
|
|
if len(args) < 1 {
|
|
|
|
c.Ui.Error("ssh expects at least one argument")
|
2015-08-19 02:00:27 +00:00
|
|
|
return 1
|
2015-06-26 18:08:03 +00:00
|
|
|
}
|
2015-06-30 02:00:08 +00:00
|
|
|
|
2015-06-17 16:39:49 +00:00
|
|
|
client, err := c.Client()
|
|
|
|
if err != nil {
|
2016-10-18 16:46:54 +00:00
|
|
|
c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err))
|
2015-08-19 02:00:27 +00:00
|
|
|
return 1
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
c.client = client
|
|
|
|
c.sshClient = client.SSHWithMountPoint(c.mountPoint)
|
2015-07-27 20:42:03 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Extract the username and IP.
|
|
|
|
c.username, c.ip, err = c.userAndIP(args[0])
|
|
|
|
if err != nil {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error parsing user and IP: %s", err))
|
2015-08-19 02:00:27 +00:00
|
|
|
return 1
|
2015-07-27 20:42:03 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// The rest of the args are ssh args
|
|
|
|
if len(args) > 1 {
|
|
|
|
c.sshArgs = args[1:]
|
2015-06-30 02:00:08 +00:00
|
|
|
}
|
|
|
|
|
2015-08-13 23:55:47 +00:00
|
|
|
// Credentials are generated only against a registered role. If user
|
|
|
|
// does not specify a role with the SSH command, then lookup API is used
|
|
|
|
// to fetch all the roles with which this IP is associated. If there is
|
|
|
|
// only one role associated with it, use it to establish the connection.
|
2017-08-17 02:01:24 +00:00
|
|
|
//
|
|
|
|
// TODO: remove in 0.9.0, convert to validation error
|
|
|
|
if c.role == "" {
|
|
|
|
c.Ui.Warn("" +
|
|
|
|
"WARNING: No -role specified. Use -role to tell Vault which ssh role\n" +
|
|
|
|
"to use for authentication. In the future, you will need to tell Vault\n" +
|
|
|
|
"which role to use. For now, Vault will attempt to guess based on a\n" +
|
|
|
|
"the API response.")
|
|
|
|
|
|
|
|
role, err := c.defaultRole(c.mountPoint, c.ip)
|
2015-06-30 02:00:08 +00:00
|
|
|
if err != nil {
|
2016-10-18 16:46:54 +00:00
|
|
|
c.Ui.Error(fmt.Sprintf("Error choosing role: %v", err))
|
2015-06-30 02:00:08 +00:00
|
|
|
return 1
|
|
|
|
}
|
2015-08-13 23:55:47 +00:00
|
|
|
// Print the default role chosen so that user knows the role name
|
|
|
|
// if something doesn't work. If the role chosen is not allowed to
|
|
|
|
// be used by the user (ACL enforcement), then user should see an
|
|
|
|
// error message accordingly.
|
2016-10-18 16:46:54 +00:00
|
|
|
c.Ui.Output(fmt.Sprintf("Vault SSH: Role: %q", role))
|
2017-08-17 02:01:24 +00:00
|
|
|
c.role = role
|
2015-06-30 02:00:08 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// If no mode was given, perform the old-school lookup. Keep this now for
|
|
|
|
// backwards-compatability, but print a warning.
|
|
|
|
//
|
|
|
|
// TODO: remove in 0.9.0, convert to validation error
|
|
|
|
if c.mode == "" {
|
|
|
|
c.Ui.Warn("" +
|
|
|
|
"WARNING: No -mode specified. Use -mode to tell Vault which ssh\n" +
|
|
|
|
"authentication mode to use. In the future, you will need to tell\n" +
|
|
|
|
"Vault which mode to use. For now, Vault will attempt to guess based\n" +
|
2017-08-17 22:35:55 +00:00
|
|
|
"on the API response. This guess involves creating a temporary\n" +
|
|
|
|
"credential, reading its type, and then revoking it. To reduce the\n" +
|
|
|
|
"number of API calls and surface area, specify -mode directly.")
|
|
|
|
secret, cred, err := c.generateCredential()
|
2017-08-17 02:01:24 +00:00
|
|
|
if err != nil {
|
|
|
|
// This is _very_ hacky, but is the only sane backwards-compatible way
|
|
|
|
// to do this. If the error is "key type unknown", we just assume the
|
|
|
|
// type is "ca". In the future, mode will be required as an option.
|
|
|
|
if strings.Contains(err.Error(), "key type unknown") {
|
|
|
|
c.mode = ssh.KeyTypeCA
|
|
|
|
} else {
|
|
|
|
c.Ui.Error(fmt.Sprintf("Error getting credential: %s", err))
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.mode = cred.KeyType
|
|
|
|
}
|
2017-08-17 22:35:55 +00:00
|
|
|
|
|
|
|
// Revoke the secret, since the child functions will generate their own
|
|
|
|
// credential. Users wishing to avoid this should specify -mode.
|
|
|
|
if secret != nil {
|
|
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
|
|
c.Ui.Warn(fmt.Sprintf("Failed to revoke temporary key: %s", err))
|
|
|
|
}
|
|
|
|
}
|
2015-06-24 22:13:12 +00:00
|
|
|
}
|
2015-07-27 20:42:03 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
switch strings.ToLower(c.mode) {
|
|
|
|
case ssh.KeyTypeCA:
|
|
|
|
if err := c.handleTypeCA(); err != nil {
|
|
|
|
c.Ui.Error(err.Error())
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
case ssh.KeyTypeOTP:
|
|
|
|
if err := c.handleTypeOTP(); err != nil {
|
|
|
|
c.Ui.Error(err.Error())
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
case ssh.KeyTypeDynamic:
|
|
|
|
if err := c.handleTypeDynamic(); err != nil {
|
|
|
|
c.Ui.Error(err.Error())
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
c.Ui.Error(fmt.Sprintf("Unknown SSH mode: %s", c.mode))
|
2015-08-19 02:00:27 +00:00
|
|
|
return 1
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
2015-06-30 02:00:08 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleTypeCA is used to handle SSH logins using the "CA" key type.
|
|
|
|
func (c *SSHCommand) handleTypeCA() error {
|
|
|
|
// Read the key from disk
|
|
|
|
publicKey, err := ioutil.ReadFile(c.publicKeyPath)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to read public key")
|
2015-07-29 18:21:36 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Attempt to sign the public key
|
|
|
|
secret, err := c.sshClient.SignKey(c.role, map[string]interface{}{
|
|
|
|
// WARNING: publicKey is []byte, which is b64 encoded on JSON upload. We
|
|
|
|
// have to convert it to a string. SV lost many hours to this...
|
|
|
|
"public_key": string(publicKey),
|
|
|
|
"valid_principals": c.username,
|
|
|
|
"cert_type": "user",
|
|
|
|
|
|
|
|
// TODO: let the user configure these. In the interim, if users want to
|
|
|
|
// customize these values, they can produce the key themselves.
|
|
|
|
"extensions": map[string]string{
|
|
|
|
"permit-X11-forwarding": "",
|
|
|
|
"permit-agent-forwarding": "",
|
|
|
|
"permit-port-forwarding": "",
|
|
|
|
"permit-pty": "",
|
|
|
|
"permit-user-rc": "",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to sign public key")
|
2016-05-25 21:26:32 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
if secret == nil || secret.Data == nil {
|
2017-08-17 23:12:58 +00:00
|
|
|
return fmt.Errorf("client signing returned empty credentials")
|
2015-08-18 01:22:03 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Handle no-exec
|
|
|
|
if c.noExec {
|
|
|
|
// This is hacky, but OutputSecret returns an int, not an error :(
|
|
|
|
if i := OutputSecret(c.Ui, c.format, secret); i != 0 {
|
|
|
|
return fmt.Errorf("an error occurred outputting the secret")
|
2016-10-18 16:46:54 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract public key
|
|
|
|
key, ok := secret.Data["signed_key"].(string)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("missing signed key")
|
|
|
|
}
|
2016-10-18 16:46:54 +00:00
|
|
|
|
2017-08-17 23:12:58 +00:00
|
|
|
// Capture the current value - this could be overwritten later if the user
|
|
|
|
// enabled host key signing verification.
|
|
|
|
userKnownHostsFile := c.userKnownHostsFile
|
|
|
|
strictHostKeyChecking := c.strictHostKeyChecking
|
|
|
|
|
|
|
|
// Handle host key signing verification. If the user specified a mount point,
|
|
|
|
// download the public key, trust it with the given domains, and use that
|
|
|
|
// instead of the user's regular known_hosts file.
|
|
|
|
if c.hostKeyMountPoint != "" {
|
|
|
|
secret, err := c.client.Logical().Read(c.hostKeyMountPoint + "/config/ca")
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to get host signing key")
|
|
|
|
}
|
|
|
|
if secret == nil || secret.Data == nil {
|
|
|
|
return fmt.Errorf("missing host signing key")
|
|
|
|
}
|
|
|
|
publicKey, ok := secret.Data["public_key"].(string)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("host signing key is empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the known_hosts file
|
|
|
|
name := fmt.Sprintf("vault_ssh_ca_known_hosts_%s_%s", c.username, c.ip)
|
|
|
|
data := fmt.Sprintf("@cert-authority %s %s", c.hostKeyHostnames, publicKey)
|
|
|
|
knownHosts, err, closer := c.writeTemporaryFile(name, []byte(data), 0644)
|
|
|
|
defer closer()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to write host public key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the variables
|
|
|
|
userKnownHostsFile = knownHosts
|
|
|
|
strictHostKeyChecking = "yes"
|
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Write the signed public key to disk
|
|
|
|
name := fmt.Sprintf("vault_ssh_ca_%s_%s", c.username, c.ip)
|
|
|
|
signedPublicKeyPath, err, closer := c.writeTemporaryKey(name, []byte(key))
|
|
|
|
defer closer()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to write signed public key")
|
|
|
|
}
|
2016-10-18 16:46:54 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
args := append([]string{
|
|
|
|
"-i", c.privateKeyPath,
|
|
|
|
"-i", signedPublicKeyPath,
|
2017-08-17 23:12:58 +00:00
|
|
|
"-o UserKnownHostsFile=" + userKnownHostsFile,
|
|
|
|
"-o StrictHostKeyChecking=" + strictHostKeyChecking,
|
2017-08-17 02:01:24 +00:00
|
|
|
c.username + "@" + c.ip,
|
|
|
|
}, c.sshArgs...)
|
|
|
|
|
|
|
|
cmd := exec.Command("ssh", args...)
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to run ssh command")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Revoke the key if it's longer than expected
|
|
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to revoke key")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleTypeOTP is used to handle SSH logins using the "otp" key type.
|
|
|
|
func (c *SSHCommand) handleTypeOTP() error {
|
|
|
|
secret, cred, err := c.generateCredential()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to generate credential")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle no-exec
|
|
|
|
if c.noExec {
|
|
|
|
// This is hacky, but OutputSecret returns an int, not an error :(
|
|
|
|
if i := OutputSecret(c.Ui, c.format, secret); i != 0 {
|
|
|
|
return fmt.Errorf("an error occurred outputting the secret")
|
2016-10-18 16:46:54 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var cmd *exec.Cmd
|
|
|
|
|
|
|
|
// Check if the application 'sshpass' is installed in the client machine.
|
|
|
|
// If it is then, use it to automate typing in OTP to the prompt. Unfortunately,
|
|
|
|
// it was not possible to automate it without a third-party application, with
|
|
|
|
// only the Go libraries.
|
|
|
|
// Feel free to try and remove this dependency.
|
|
|
|
sshpassPath, err := exec.LookPath("sshpass")
|
|
|
|
if err != nil {
|
|
|
|
c.Ui.Warn("" +
|
|
|
|
"Vault could not locate sshpass. The OTP code for the session will be\n" +
|
|
|
|
"displayed below. Enter this code in the SSH password prompt. If you\n" +
|
|
|
|
"install sshpass, Vault can automatically perform this step for you.")
|
|
|
|
c.Ui.Output("OTP for the session is " + cred.Key)
|
|
|
|
|
|
|
|
args := append([]string{
|
|
|
|
"-o UserKnownHostsFile=" + c.userKnownHostsFile,
|
|
|
|
"-o StrictHostKeyChecking=" + c.strictHostKeyChecking,
|
|
|
|
"-p", cred.Port,
|
|
|
|
c.username + "@" + c.ip,
|
|
|
|
}, c.sshArgs...)
|
|
|
|
cmd = exec.Command("ssh", args...)
|
|
|
|
} else {
|
|
|
|
args := append([]string{
|
|
|
|
"-e", // Read password for SSHPASS environment variable
|
|
|
|
"ssh",
|
|
|
|
"-o UserKnownHostsFile=" + c.userKnownHostsFile,
|
|
|
|
"-o StrictHostKeyChecking=" + c.strictHostKeyChecking,
|
|
|
|
"-p", cred.Port,
|
|
|
|
c.username + "@" + c.ip,
|
|
|
|
}, c.sshArgs...)
|
|
|
|
cmd = exec.Command(sshpassPath, args...)
|
|
|
|
env := os.Environ()
|
|
|
|
env = append(env, fmt.Sprintf("SSHPASS=%s", string(cred.Key)))
|
|
|
|
cmd.Env = env
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to run ssh command")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Revoke the key if it's longer than expected
|
|
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to revoke key")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleTypeDynamic is used to handle SSH logins using the "dyanmic" key type.
|
|
|
|
func (c *SSHCommand) handleTypeDynamic() error {
|
|
|
|
// Generate the credential
|
|
|
|
secret, cred, err := c.generateCredential()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to generate credential")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle no-exec
|
|
|
|
if c.noExec {
|
|
|
|
// This is hacky, but OutputSecret returns an int, not an error :(
|
|
|
|
if i := OutputSecret(c.Ui, c.format, secret); i != 0 {
|
|
|
|
return fmt.Errorf("an error occurred outputting the secret")
|
2015-08-06 21:00:50 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
return nil
|
2015-07-23 21:20:28 +00:00
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
|
|
|
|
// Write the dynamic key to disk
|
|
|
|
name := fmt.Sprintf("vault_ssh_dynamic_%s_%s", c.username, c.ip)
|
|
|
|
keyPath, err, closer := c.writeTemporaryKey(name, []byte(cred.Key))
|
|
|
|
defer closer()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to save dyanmic key")
|
2016-08-01 18:53:00 +00:00
|
|
|
}
|
2015-07-10 15:56:14 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
args := append([]string{
|
|
|
|
"-i", keyPath,
|
|
|
|
"-o UserKnownHostsFile=" + c.userKnownHostsFile,
|
|
|
|
"-o StrictHostKeyChecking=" + c.strictHostKeyChecking,
|
|
|
|
"-p", cred.Port,
|
|
|
|
c.username + "@" + c.ip,
|
|
|
|
}, c.sshArgs...)
|
|
|
|
|
|
|
|
cmd := exec.Command("ssh", args...)
|
|
|
|
cmd.Stdin = os.Stdin
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
err = cmd.Run()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to run ssh command")
|
|
|
|
}
|
2015-07-10 15:56:14 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Revoke the key if it's longer than expected
|
|
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
|
|
return errors.Wrap(err, "failed to revoke key")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateCredential generates a credential for the given role and returns the
|
|
|
|
// decoded secret data.
|
|
|
|
func (c *SSHCommand) generateCredential() (*api.Secret, *SSHCredentialResp, error) {
|
|
|
|
// Attempt to generate the credential.
|
|
|
|
secret, err := c.sshClient.Credential(c.role, map[string]interface{}{
|
|
|
|
"username": c.username,
|
|
|
|
"ip": c.ip,
|
|
|
|
})
|
2015-06-18 00:33:03 +00:00
|
|
|
if err != nil {
|
2017-08-17 02:01:24 +00:00
|
|
|
return nil, nil, errors.Wrap(err, "failed to get credentials")
|
|
|
|
}
|
|
|
|
if secret == nil || secret.Data == nil {
|
|
|
|
return nil, nil, fmt.Errorf("vault returned empty credentials")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Port comes back as a json.Number which mapstructure doesn't like, so
|
|
|
|
// convert it
|
|
|
|
if d, ok := secret.Data["port"].(json.Number); ok {
|
|
|
|
secret.Data["port"] = d.String()
|
2015-07-06 15:05:02 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// Use mapstructure to decode the response
|
|
|
|
var resp SSHCredentialResp
|
|
|
|
if err := mapstructure.Decode(secret.Data, &resp); err != nil {
|
|
|
|
return nil, nil, errors.Wrap(err, "failed to decode credential")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for an empty key response
|
|
|
|
if len(resp.Key) == 0 {
|
|
|
|
return nil, nil, fmt.Errorf("vault returned an invalid key")
|
|
|
|
}
|
|
|
|
|
|
|
|
return secret, &resp, nil
|
|
|
|
}
|
|
|
|
|
2017-08-17 23:12:58 +00:00
|
|
|
// writeTemporaryFile writes a file to a temp location with the given data and
|
|
|
|
// file permissions.
|
|
|
|
func (c *SSHCommand) writeTemporaryFile(name string, data []byte, perms os.FileMode) (string, error, func() error) {
|
2017-08-17 02:01:24 +00:00
|
|
|
// default closer to prevent panic
|
|
|
|
closer := func() error { return nil }
|
|
|
|
|
|
|
|
f, err := ioutil.TempFile("", name)
|
2015-07-06 15:05:02 +00:00
|
|
|
if err != nil {
|
2017-08-17 02:01:24 +00:00
|
|
|
return "", errors.Wrap(err, "creating temporary file"), closer
|
2015-06-18 00:33:03 +00:00
|
|
|
}
|
2015-07-02 23:00:14 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
closer = func() error { return os.Remove(f.Name()) }
|
|
|
|
|
2017-08-17 23:12:58 +00:00
|
|
|
if err := ioutil.WriteFile(f.Name(), data, perms); err != nil {
|
2017-08-17 02:01:24 +00:00
|
|
|
return "", errors.Wrap(err, "writing temporary key"), closer
|
|
|
|
}
|
|
|
|
|
|
|
|
return f.Name(), nil, closer
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 23:12:58 +00:00
|
|
|
// writeTemporaryKey writes the key to a temporary file and returns the path.
|
|
|
|
// The caller should defer the closer to cleanup the key.
|
|
|
|
func (c *SSHCommand) writeTemporaryKey(name string, data []byte) (string, error, func() error) {
|
|
|
|
return c.writeTemporaryFile(name, data, 0600)
|
|
|
|
}
|
|
|
|
|
2015-07-29 18:21:36 +00:00
|
|
|
// If user did not provide the role with which SSH connection has
|
|
|
|
// to be established and if there is only one role associated with
|
|
|
|
// the IP, it is used by default.
|
2015-08-12 17:30:50 +00:00
|
|
|
func (c *SSHCommand) defaultRole(mountPoint, ip string) (string, error) {
|
2015-07-02 23:00:14 +00:00
|
|
|
data := map[string]interface{}{
|
|
|
|
"ip": ip,
|
|
|
|
}
|
2015-07-29 18:21:36 +00:00
|
|
|
client, err := c.Client()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2015-08-12 17:30:50 +00:00
|
|
|
secret, err := client.Logical().Write(mountPoint+"/lookup", data)
|
2015-07-02 23:00:14 +00:00
|
|
|
if err != nil {
|
2016-10-18 16:46:54 +00:00
|
|
|
return "", fmt.Errorf("Error finding roles for IP %q: %q", ip, err)
|
2015-07-02 23:00:14 +00:00
|
|
|
|
|
|
|
}
|
2017-08-17 02:01:24 +00:00
|
|
|
if secret == nil || secret.Data == nil {
|
2016-10-18 16:46:54 +00:00
|
|
|
return "", fmt.Errorf("Error finding roles for IP %q: %q", ip, err)
|
2015-07-02 23:00:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if secret.Data["roles"] == nil {
|
2016-10-18 16:46:54 +00:00
|
|
|
return "", fmt.Errorf("No matching roles found for IP %q", ip)
|
2015-07-02 23:00:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(secret.Data["roles"].([]interface{})) == 1 {
|
|
|
|
return secret.Data["roles"].([]interface{})[0].(string), nil
|
|
|
|
} else {
|
2015-07-29 18:21:36 +00:00
|
|
|
var roleNames string
|
|
|
|
for _, item := range secret.Data["roles"].([]interface{}) {
|
|
|
|
roleNames += item.(string) + ", "
|
|
|
|
}
|
|
|
|
roleNames = strings.TrimRight(roleNames, ", ")
|
2016-10-18 16:46:54 +00:00
|
|
|
return "", fmt.Errorf("Roles:%q. "+`
|
2015-08-13 23:55:47 +00:00
|
|
|
Multiple roles are registered for this IP.
|
|
|
|
Select a role using '-role' option.
|
|
|
|
Note that all roles may not be permitted, based on ACLs.`, roleNames)
|
2015-07-02 23:00:14 +00:00
|
|
|
}
|
2015-06-24 22:13:12 +00:00
|
|
|
}
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
// userAndIP takes an argument in the format foo@1.2.3.4 and separates the IP
|
|
|
|
// and user parts, returning any errors.
|
|
|
|
func (c *SSHCommand) userAndIP(s string) (string, string, error) {
|
|
|
|
// split the parameter username@ip
|
|
|
|
input := strings.Split(s, "@")
|
|
|
|
var username, address string
|
|
|
|
|
|
|
|
// If only IP is mentioned and username is skipped, assume username to
|
|
|
|
// be the current username. Vault SSH role's default username could have
|
|
|
|
// been used, but in order to retain the consistency with SSH command,
|
|
|
|
// current username is employed.
|
|
|
|
switch len(input) {
|
|
|
|
case 1:
|
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
return "", "", errors.Wrap(err, "failed to fetch current user")
|
|
|
|
}
|
|
|
|
username, address = u.Username, input[0]
|
|
|
|
case 2:
|
|
|
|
username, address = input[0], input[1]
|
|
|
|
default:
|
|
|
|
return "", "", fmt.Errorf("invalid arguments: %q", s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolving domain names to IP address on the client side.
|
|
|
|
// Vault only deals with IP addresses.
|
|
|
|
ipAddr, err := net.ResolveIPAddr("ip", address)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", errors.Wrap(err, "failed to resolve IP address")
|
|
|
|
}
|
|
|
|
ip := ipAddr.String()
|
|
|
|
|
|
|
|
return username, ip, nil
|
|
|
|
}
|
|
|
|
|
2015-07-01 15:58:49 +00:00
|
|
|
func (c *SSHCommand) Synopsis() string {
|
2017-04-04 16:22:14 +00:00
|
|
|
return "Initiate an SSH session"
|
2015-06-17 16:39:49 +00:00
|
|
|
}
|
|
|
|
|
2015-07-01 15:58:49 +00:00
|
|
|
func (c *SSHCommand) Help() string {
|
2015-06-17 16:39:49 +00:00
|
|
|
helpText := `
|
2017-08-17 02:01:24 +00:00
|
|
|
Usage: vault ssh [options] username@ip [ssh options]
|
2015-07-02 21:23:09 +00:00
|
|
|
|
2017-04-04 16:22:14 +00:00
|
|
|
Establishes an SSH connection with the target machine.
|
2015-07-02 21:23:09 +00:00
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
This command uses one of the SSH authentication backends to authenticate and
|
|
|
|
automatically establish an SSH connection to a host. This operation requires
|
|
|
|
that the SSH backend is mounted and configured.
|
|
|
|
|
|
|
|
SSH using the OTP mode (requires sshpass for full automation):
|
|
|
|
|
|
|
|
$ vault ssh -mode=otp -role=my-role user@1.2.3.4
|
|
|
|
|
|
|
|
SSH using the CA mode:
|
|
|
|
|
|
|
|
$ vault ssh -mode=ca -role=my-role user@1.2.3.4
|
2015-07-02 21:23:09 +00:00
|
|
|
|
2017-08-17 23:12:58 +00:00
|
|
|
SSH using CA mode with host key verification:
|
|
|
|
|
|
|
|
$ vault ssh \
|
|
|
|
-mode=ca \
|
|
|
|
-role=my-role \
|
|
|
|
-host-key-mount-point=host-signer \
|
|
|
|
-host-key-hostnames=example.com \
|
|
|
|
user@example.com
|
|
|
|
|
2017-08-17 02:01:24 +00:00
|
|
|
For the full list of options and arguments, please see the documentation.
|
2015-07-29 18:21:36 +00:00
|
|
|
|
2015-07-02 21:23:09 +00:00
|
|
|
General Options:
|
2016-04-01 20:50:12 +00:00
|
|
|
` + meta.GeneralOptionsUsage() + `
|
2015-07-02 21:23:09 +00:00
|
|
|
SSH Options:
|
|
|
|
|
2017-08-16 01:21:30 +00:00
|
|
|
-role Role to be used to create the key. Each IP is associated with
|
|
|
|
a role. To see the associated roles with IP, use "lookup"
|
|
|
|
endpoint. If you are certain that there is only one role
|
|
|
|
associated with the IP, you can skip mentioning the role. It
|
|
|
|
will be chosen by default. If there are no roles associated
|
|
|
|
with the IP, register the CIDR block of that IP using the
|
|
|
|
"roles/" endpoint.
|
|
|
|
|
|
|
|
-no-exec Shows the credentials but does not establish connection.
|
|
|
|
|
|
|
|
-mount-point Mount point of SSH backend. If the backend is mounted at
|
|
|
|
"ssh" (default), this parameter can be skipped.
|
|
|
|
|
|
|
|
-format If the "no-exec" option is enabled, the credentials will be
|
|
|
|
printed out and SSH connection will not be established. The
|
|
|
|
format of the output can be "json" or "table" (default).
|
|
|
|
|
|
|
|
-strict-host-key-checking This option corresponds to "StrictHostKeyChecking"
|
|
|
|
of SSH configuration. If "sshpass" is employed to enable
|
|
|
|
automated login, then if host key is not "known" to the
|
|
|
|
client, "vault ssh" command will fail. Set this option to
|
|
|
|
"no" to bypass the host key checking. Defaults to "ask".
|
|
|
|
Can also be specified with the
|
|
|
|
"VAULT_SSH_STRICT_HOST_KEY_CHECKING" environment variable.
|
|
|
|
|
|
|
|
-user-known-hosts-file This option corresponds to "UserKnownHostsFile" of
|
|
|
|
SSH configuration. Assigns the file to use for storing the
|
|
|
|
host keys. If this option is set to "/dev/null" along with
|
|
|
|
"-strict-host-key-checking=no", both warnings and host key
|
|
|
|
checking can be avoided while establishing the connection.
|
|
|
|
Defaults to "~/.ssh/known_hosts". Can also be specified with
|
|
|
|
"VAULT_SSH_USER_KNOWN_HOSTS_FILE" environment variable.
|
2017-08-17 23:12:58 +00:00
|
|
|
|
|
|
|
CA Mode Options:
|
|
|
|
|
|
|
|
- public-key-path=<path>
|
|
|
|
The path to the public key to send to Vault for signing. The default value
|
|
|
|
is ~/.ssh/id_rsa.pub.
|
|
|
|
|
|
|
|
- private-key-path=<path>
|
|
|
|
The path to the private key to use for authentication. This must be the
|
|
|
|
corresponding private key to -public-key-path. The default value is
|
|
|
|
~/.ssh/id_rsa.
|
|
|
|
|
|
|
|
- host-key-mount-point=<string>
|
|
|
|
The mount point to the SSH backend where host keys are signed. When given
|
|
|
|
a value, Vault will generate a custom known_hosts file with delegation to
|
|
|
|
the CA at the provided mount point and verify the SSH connection's host
|
|
|
|
keys against the provided CA. By default, this command uses the users's
|
|
|
|
existing known_hosts file. When this flag is set, this command will force
|
|
|
|
strict host key checking and will override any values provided for a
|
|
|
|
custom -user-known-hosts-file.
|
|
|
|
|
|
|
|
- host-key-hostnames=<string>
|
|
|
|
The list of hostnames to delegate for this certificate authority. By
|
|
|
|
default, this is "*", which allows all domains and IPs. To restrict
|
|
|
|
validation to a series of hostnames, specify them as comma-separated
|
|
|
|
values here.
|
2015-07-02 21:23:09 +00:00
|
|
|
`
|
2015-06-17 16:39:49 +00:00
|
|
|
return strings.TrimSpace(helpText)
|
|
|
|
}
|