mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-07 07:07:05 +02:00
This is implementing the same fix that was added for the CA mode for vault ssh in https://github.com/hashicorp/vault/pull/3922 Using the IP address caused `Host` entries in the ssh_config to not match anymore meaning you would need to hardcode all of your IP addresses in your ssh config instead of using DNS to connect to hosts
760 lines
22 KiB
Go
760 lines
22 KiB
Go
package command
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/builtin/logical/ssh"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/pkg/errors"
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
var _ cli.Command = (*SSHCommand)(nil)
|
|
var _ cli.CommandAutocomplete = (*SSHCommand)(nil)
|
|
|
|
type SSHCommand struct {
|
|
*BaseCommand
|
|
|
|
// Common SSH options
|
|
flagMode string
|
|
flagRole string
|
|
flagNoExec bool
|
|
flagMountPoint string
|
|
flagStrictHostKeyChecking string
|
|
flagUserKnownHostsFile string
|
|
|
|
// SSH CA Mode options
|
|
flagPublicKeyPath string
|
|
flagPrivateKeyPath string
|
|
flagHostKeyMountPoint string
|
|
flagHostKeyHostnames string
|
|
flagValidPrincipals string
|
|
}
|
|
|
|
func (c *SSHCommand) Synopsis() string {
|
|
return "Initiate an SSH session"
|
|
}
|
|
|
|
func (c *SSHCommand) Help() string {
|
|
helpText := `
|
|
Usage: vault ssh [options] username@ip [ssh options]
|
|
|
|
Establishes an SSH connection with the target machine.
|
|
|
|
This command uses one of the SSH secrets engines to authenticate and
|
|
automatically establish an SSH connection to a host. This operation requires
|
|
that the SSH secrets engine 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
|
|
|
|
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
|
|
|
|
For the full list of options and arguments, please see the documentation.
|
|
|
|
` + c.Flags().Help()
|
|
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *SSHCommand) Flags() *FlagSets {
|
|
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
|
|
|
f := set.NewFlagSet("SSH Options")
|
|
|
|
// TODO: doc field?
|
|
|
|
// General
|
|
f.StringVar(&StringVar{
|
|
Name: "mode",
|
|
Target: &c.flagMode,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictSet("ca", "dynamic", "otp"),
|
|
Usage: "Name of the authentication mode (ca, dynamic, otp).",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "role",
|
|
Target: &c.flagRole,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "Name of the role to use to generate the key.",
|
|
})
|
|
|
|
f.BoolVar(&BoolVar{
|
|
Name: "no-exec",
|
|
Target: &c.flagNoExec,
|
|
Default: false,
|
|
EnvVar: "",
|
|
Completion: complete.PredictNothing,
|
|
Usage: "Print the generated credentials, but do not establish a " +
|
|
"connection.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "mount-point",
|
|
Target: &c.flagMountPoint,
|
|
Default: "ssh/",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "Mount point to the SSH secrets engine.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "strict-host-key-checking",
|
|
Target: &c.flagStrictHostKeyChecking,
|
|
Default: "ask",
|
|
EnvVar: "VAULT_SSH_STRICT_HOST_KEY_CHECKING",
|
|
Completion: complete.PredictSet("ask", "no", "yes"),
|
|
Usage: "Value to use for the SSH configuration option " +
|
|
"\"StrictHostKeyChecking\".",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "user-known-hosts-file",
|
|
Target: &c.flagUserKnownHostsFile,
|
|
Default: "~/.ssh/known_hosts",
|
|
EnvVar: "VAULT_SSH_USER_KNOWN_HOSTS_FILE",
|
|
Completion: complete.PredictFiles("*"),
|
|
Usage: "Value to use for the SSH configuration option " +
|
|
"\"UserKnownHostsFile\".",
|
|
})
|
|
|
|
// SSH CA
|
|
f = set.NewFlagSet("CA Mode Options")
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "public-key-path",
|
|
Target: &c.flagPublicKeyPath,
|
|
Default: "~/.ssh/id_rsa.pub",
|
|
EnvVar: "",
|
|
Completion: complete.PredictFiles("*"),
|
|
Usage: "Path to the SSH public key to send to Vault for signing.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "private-key-path",
|
|
Target: &c.flagPrivateKeyPath,
|
|
Default: "~/.ssh/id_rsa",
|
|
EnvVar: "",
|
|
Completion: complete.PredictFiles("*"),
|
|
Usage: "Path to the SSH private key to use for authentication. This must " +
|
|
"be the corresponding private key to -public-key-path.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "host-key-mount-point",
|
|
Target: &c.flagHostKeyMountPoint,
|
|
Default: "",
|
|
EnvVar: "VAULT_SSH_HOST_KEY_MOUNT_POINT",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "Mount point to the SSH secrets engine 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 to verify the " +
|
|
"SSH connection's host keys against the provided CA. By default, host " +
|
|
"keys are validated against the user's local \"known_hosts\" file. " +
|
|
"This flag forces strict key host checking and ignores a custom user " +
|
|
"known hosts file.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "host-key-hostnames",
|
|
Target: &c.flagHostKeyHostnames,
|
|
Default: "*",
|
|
EnvVar: "VAULT_SSH_HOST_KEY_HOSTNAMES",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "List of hostnames to delegate for the CA. The default value " +
|
|
"allows all domains and IPs. This is specified as a comma-separated " +
|
|
"list of values.",
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "valid-principals",
|
|
Target: &c.flagValidPrincipals,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Completion: complete.PredictAnything,
|
|
Usage: "List of valid principal names to include in the generated " +
|
|
"user certificate. This is specified as a comma-separated list of values.",
|
|
})
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *SSHCommand) AutocompleteArgs() complete.Predictor {
|
|
return nil
|
|
}
|
|
|
|
func (c *SSHCommand) AutocompleteFlags() complete.Flags {
|
|
return c.Flags().Completions()
|
|
}
|
|
|
|
// Structure to hold the fields returned when asked for a credential from SSH
|
|
// secrets engine.
|
|
type SSHCredentialResp struct {
|
|
KeyType string `mapstructure:"key_type"`
|
|
Key string `mapstructure:"key"`
|
|
Username string `mapstructure:"username"`
|
|
IP string `mapstructure:"ip"`
|
|
Port string `mapstructure:"port"`
|
|
}
|
|
|
|
func (c *SSHCommand) Run(args []string) int {
|
|
f := c.Flags()
|
|
|
|
if err := f.Parse(args); err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
// Use homedir to expand any relative paths such as ~/.ssh
|
|
c.flagUserKnownHostsFile = expandPath(c.flagUserKnownHostsFile)
|
|
c.flagPublicKeyPath = expandPath(c.flagPublicKeyPath)
|
|
c.flagPrivateKeyPath = expandPath(c.flagPrivateKeyPath)
|
|
|
|
args = f.Args()
|
|
if len(args) < 1 {
|
|
c.UI.Error(fmt.Sprintf("Not enough arguments, (expected 1-n, got %d)", len(args)))
|
|
return 1
|
|
}
|
|
|
|
// Extract the username and IP.
|
|
username, hostname, ip, err := c.userHostAndIP(args[0])
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error parsing user and IP: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// The rest of the args are ssh args
|
|
sshArgs := []string{}
|
|
if len(args) > 1 {
|
|
sshArgs = args[1:]
|
|
}
|
|
|
|
// Set the client in the command
|
|
_, err = c.Client()
|
|
if err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 1
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// TODO: remove in 0.9.0, convert to validation error
|
|
if c.flagRole == "" {
|
|
c.UI.Warn(wrapAtLength(
|
|
"WARNING: No -role specified. Use -role to tell Vault which ssh role " +
|
|
"to use for authentication. In the future, you will need to tell " +
|
|
"Vault which role to use. For now, Vault will attempt to guess based " +
|
|
"on the API response. This will be removed in the Vault 0.11 (or " +
|
|
"later."))
|
|
|
|
role, err := c.defaultRole(c.flagMountPoint, ip)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error choosing role: %v", err))
|
|
return 1
|
|
}
|
|
// 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.
|
|
c.UI.Output(fmt.Sprintf("Vault SSH: Role: %q", role))
|
|
c.flagRole = role
|
|
}
|
|
|
|
// 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.flagMode == "" {
|
|
c.UI.Warn(wrapAtLength(
|
|
"WARNING: No -mode specified. Use -mode to tell Vault which ssh " +
|
|
"authentication mode to use. In the future, you will need to tell " +
|
|
"Vault which mode to use. For now, Vault will attempt to guess based " +
|
|
"on the API response. This guess involves creating a temporary " +
|
|
"credential, reading its type, and then revoking it. To reduce the " +
|
|
"number of API calls and surface area, specify -mode directly. This " +
|
|
"will be removed in Vault 0.11 (or later)."))
|
|
secret, cred, err := c.generateCredential(username, ip)
|
|
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.flagMode = ssh.KeyTypeCA
|
|
} else {
|
|
c.UI.Error(fmt.Sprintf("Error getting credential: %s", err))
|
|
return 1
|
|
}
|
|
} else {
|
|
c.flagMode = cred.KeyType
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
}
|
|
|
|
switch strings.ToLower(c.flagMode) {
|
|
case ssh.KeyTypeCA:
|
|
return c.handleTypeCA(username, hostname, ip, sshArgs)
|
|
case ssh.KeyTypeOTP:
|
|
return c.handleTypeOTP(username, hostname, ip, sshArgs)
|
|
case ssh.KeyTypeDynamic:
|
|
return c.handleTypeDynamic(username, ip, sshArgs)
|
|
default:
|
|
c.UI.Error(fmt.Sprintf("Unknown SSH mode: %s", c.flagMode))
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// handleTypeCA is used to handle SSH logins using the "CA" key type.
|
|
func (c *SSHCommand) handleTypeCA(username, hostname, ip string, sshArgs []string) int {
|
|
// Read the key from disk
|
|
publicKey, err := ioutil.ReadFile(c.flagPublicKeyPath)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to read public key %s: %s",
|
|
c.flagPublicKeyPath, err))
|
|
return 1
|
|
}
|
|
|
|
sshClient := c.client.SSHWithMountPoint(c.flagMountPoint)
|
|
|
|
var principals = username
|
|
if c.flagValidPrincipals != "" {
|
|
principals = c.flagValidPrincipals
|
|
}
|
|
|
|
// Attempt to sign the public key
|
|
secret, err := sshClient.SignKey(c.flagRole, 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": principals,
|
|
"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 {
|
|
c.UI.Error(fmt.Sprintf("failed to sign public key %s: %s",
|
|
c.flagPublicKeyPath, err))
|
|
return 2
|
|
}
|
|
if secret == nil || secret.Data == nil {
|
|
c.UI.Error("missing signed key")
|
|
return 2
|
|
}
|
|
|
|
// Handle no-exec
|
|
if c.flagNoExec {
|
|
if c.flagField != "" {
|
|
return PrintRawField(c.UI, secret, c.flagField)
|
|
}
|
|
return OutputSecret(c.UI, secret)
|
|
}
|
|
|
|
// Extract public key
|
|
key, ok := secret.Data["signed_key"].(string)
|
|
if !ok || key == "" {
|
|
c.UI.Error("signed key is empty")
|
|
return 2
|
|
}
|
|
|
|
// Capture the current value - this could be overwritten later if the user
|
|
// enabled host key signing verification.
|
|
userKnownHostsFile := c.flagUserKnownHostsFile
|
|
strictHostKeyChecking := c.flagStrictHostKeyChecking
|
|
|
|
// 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.flagHostKeyMountPoint != "" {
|
|
secret, err := c.client.Logical().Read(c.flagHostKeyMountPoint + "/config/ca")
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to get host signing key: %s", err))
|
|
return 2
|
|
}
|
|
if secret == nil || secret.Data == nil {
|
|
c.UI.Error("missing host signing key")
|
|
return 2
|
|
}
|
|
publicKey, ok := secret.Data["public_key"].(string)
|
|
if !ok || publicKey == "" {
|
|
c.UI.Error("host signing key is empty")
|
|
return 2
|
|
}
|
|
|
|
// Write the known_hosts file
|
|
name := fmt.Sprintf("vault_ssh_ca_known_hosts_%s_%s", username, ip)
|
|
data := fmt.Sprintf("@cert-authority %s %s", c.flagHostKeyHostnames, publicKey)
|
|
knownHosts, err, closer := c.writeTemporaryFile(name, []byte(data), 0644)
|
|
defer closer()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to write host public key: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Update the variables
|
|
userKnownHostsFile = knownHosts
|
|
strictHostKeyChecking = "yes"
|
|
}
|
|
|
|
// Write the signed public key to disk
|
|
name := fmt.Sprintf("vault_ssh_ca_%s_%s", username, ip)
|
|
signedPublicKeyPath, err, closer := c.writeTemporaryKey(name, []byte(key))
|
|
defer closer()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to write signed public key: %s", err))
|
|
return 2
|
|
}
|
|
|
|
args := append([]string{
|
|
"-i", c.flagPrivateKeyPath,
|
|
"-i", signedPublicKeyPath,
|
|
"-o UserKnownHostsFile=" + userKnownHostsFile,
|
|
"-o StrictHostKeyChecking=" + strictHostKeyChecking,
|
|
username + "@" + hostname,
|
|
}, sshArgs...)
|
|
|
|
cmd := exec.Command("ssh", args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
exitCode := 2
|
|
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
if exitError.Success() {
|
|
return 0
|
|
}
|
|
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
|
exitCode = ws.ExitStatus()
|
|
}
|
|
}
|
|
|
|
c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
|
|
return exitCode
|
|
}
|
|
|
|
// There is no secret to revoke, since it's a certificate signing
|
|
return 0
|
|
}
|
|
|
|
// handleTypeOTP is used to handle SSH logins using the "otp" key type.
|
|
func (c *SSHCommand) handleTypeOTP(username, hostname string, ip string, sshArgs []string) int {
|
|
secret, cred, err := c.generateCredential(username, ip)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err))
|
|
return 2
|
|
}
|
|
|
|
// Handle no-exec
|
|
if c.flagNoExec {
|
|
if c.flagField != "" {
|
|
return PrintRawField(c.UI, secret, c.flagField)
|
|
}
|
|
return OutputSecret(c.UI, secret)
|
|
}
|
|
|
|
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(wrapAtLength(
|
|
"Vault could not locate \"sshpass\". The OTP code for the session is " +
|
|
"displayed below. Enter this code in the SSH password prompt. If you " +
|
|
"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.flagUserKnownHostsFile,
|
|
"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
|
|
"-p", cred.Port,
|
|
username + "@" + hostname,
|
|
}, sshArgs...)
|
|
cmd = exec.Command("ssh", args...)
|
|
} else {
|
|
args := append([]string{
|
|
"-e", // Read password for SSHPASS environment variable
|
|
"ssh",
|
|
"-o UserKnownHostsFile=" + c.flagUserKnownHostsFile,
|
|
"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
|
|
"-p", cred.Port,
|
|
username + "@" + hostname,
|
|
}, 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
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
exitCode := 2
|
|
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
if exitError.Success() {
|
|
return 0
|
|
}
|
|
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
|
exitCode = ws.ExitStatus()
|
|
}
|
|
}
|
|
|
|
c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
|
|
return exitCode
|
|
}
|
|
|
|
// Revoke the key if it's longer than expected
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err))
|
|
return 2
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// handleTypeDynamic is used to handle SSH logins using the "dyanmic" key type.
|
|
func (c *SSHCommand) handleTypeDynamic(username, ip string, sshArgs []string) int {
|
|
// Generate the credential
|
|
secret, cred, err := c.generateCredential(username, ip)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err))
|
|
return 2
|
|
}
|
|
|
|
// Handle no-exec
|
|
if c.flagNoExec {
|
|
if c.flagField != "" {
|
|
return PrintRawField(c.UI, secret, c.flagField)
|
|
}
|
|
return OutputSecret(c.UI, secret)
|
|
}
|
|
|
|
// Write the dynamic key to disk
|
|
name := fmt.Sprintf("vault_ssh_dynamic_%s_%s", username, ip)
|
|
keyPath, err, closer := c.writeTemporaryKey(name, []byte(cred.Key))
|
|
defer closer()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to write dynamic key: %s", err))
|
|
return 1
|
|
}
|
|
|
|
args := append([]string{
|
|
"-i", keyPath,
|
|
"-o UserKnownHostsFile=" + c.flagUserKnownHostsFile,
|
|
"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
|
|
"-p", cred.Port,
|
|
username + "@" + ip,
|
|
}, sshArgs...)
|
|
|
|
cmd := exec.Command("ssh", args...)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
exitCode := 2
|
|
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
if exitError.Success() {
|
|
return 0
|
|
}
|
|
if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
|
exitCode = ws.ExitStatus()
|
|
}
|
|
}
|
|
|
|
c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
|
|
return exitCode
|
|
}
|
|
|
|
// Revoke the key if it's longer than expected
|
|
if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
|
|
c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err))
|
|
return 2
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// generateCredential generates a credential for the given role and returns the
|
|
// decoded secret data.
|
|
func (c *SSHCommand) generateCredential(username, ip string) (*api.Secret, *SSHCredentialResp, error) {
|
|
sshClient := c.client.SSHWithMountPoint(c.flagMountPoint)
|
|
|
|
// Attempt to generate the credential.
|
|
secret, err := sshClient.Credential(c.flagRole, map[string]interface{}{
|
|
"username": username,
|
|
"ip": ip,
|
|
})
|
|
if err != nil {
|
|
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()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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) {
|
|
// default closer to prevent panic
|
|
closer := func() error { return nil }
|
|
|
|
f, err := ioutil.TempFile("", name)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "creating temporary file"), closer
|
|
}
|
|
|
|
closer = func() error { return os.Remove(f.Name()) }
|
|
|
|
if err := ioutil.WriteFile(f.Name(), data, perms); err != nil {
|
|
return "", errors.Wrap(err, "writing temporary key"), closer
|
|
}
|
|
|
|
return f.Name(), nil, closer
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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.
|
|
func (c *SSHCommand) defaultRole(mountPoint, ip string) (string, error) {
|
|
data := map[string]interface{}{
|
|
"ip": ip,
|
|
}
|
|
secret, err := c.client.Logical().Write(mountPoint+"/lookup", data)
|
|
if err != nil {
|
|
return "", errwrap.Wrapf(fmt.Sprintf("error finding roles for IP %q: {{err}}", ip), err)
|
|
}
|
|
if secret == nil || secret.Data == nil {
|
|
return "", errwrap.Wrapf(fmt.Sprintf("error finding roles for IP %q: {{err}}", ip), err)
|
|
}
|
|
|
|
if secret.Data["roles"] == nil {
|
|
return "", fmt.Errorf("no matching roles found for IP %q", ip)
|
|
}
|
|
|
|
if len(secret.Data["roles"].([]interface{})) == 1 {
|
|
return secret.Data["roles"].([]interface{})[0].(string), nil
|
|
} else {
|
|
var roleNames string
|
|
for _, item := range secret.Data["roles"].([]interface{}) {
|
|
roleNames += item.(string) + ", "
|
|
}
|
|
roleNames = strings.TrimRight(roleNames, ", ")
|
|
return "", fmt.Errorf("Roles: %q. "+`
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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) userHostAndIP(s string) (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, address, ip, nil
|
|
}
|