diff --git a/builtin/logical/ssh/path_creds_create.go b/builtin/logical/ssh/path_creds_create.go index a43a84ddd7..c50e9e2e46 100644 --- a/builtin/logical/ssh/path_creds_create.go +++ b/builtin/logical/ssh/path_creds_create.go @@ -119,6 +119,9 @@ func (b *backend) pathCredsCreateWrite( result = b.Secret(SecretOTPType).Response(map[string]interface{}{ "key_type": role.KeyType, "key": otp, + "username": username, + "ip": ip, + "port": role.Port, }, map[string]interface{}{ "otp": otp, }) @@ -130,6 +133,9 @@ func (b *backend) pathCredsCreateWrite( result = b.Secret(SecretDynamicKeyType).Response(map[string]interface{}{ "key": dynamicPrivateKey, "key_type": role.KeyType, + "username": username, + "ip": ip, + "port": role.Port, }, map[string]interface{}{ "admin_user": role.AdminUser, "username": username, diff --git a/builtin/logical/ssh/path_roles.go b/builtin/logical/ssh/path_roles.go index 9a3d49eb3c..ec7ad920cb 100644 --- a/builtin/logical/ssh/path_roles.go +++ b/builtin/logical/ssh/path_roles.go @@ -72,7 +72,10 @@ func pathRoles(b *backend) *framework.Path { Type: framework.TypeInt, Description: ` [Optional for both types] - Port number for SSH connection. Default is '22'.`, + Port number for SSH connection. Default is '22'. Port number does not + play any role in creation of OTP. For 'otp' type, this is just a way + to inform client about the port number to use. Port number will be + returned to client by Vault server along with OTP.`, }, "key_type": &framework.FieldSchema{ Type: framework.TypeString, diff --git a/command/ssh.go b/command/ssh.go index 0bc6314a0c..559d453265 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "os/user" + "strconv" "strings" "github.com/hashicorp/vault/builtin/logical/ssh" @@ -19,13 +20,15 @@ type SSHCommand struct { } func (c *SSHCommand) Run(args []string) int { - var role, port, mountPoint string + var portNum int + var role, mountPoint, format string var noExec bool var sshCmdArgs []string var sshDynamicKeyFileName string flags := c.Meta.FlagSet("ssh", FlagSetDefault) + flags.StringVar(&format, "format", "table", "") flags.StringVar(&role, "role", "", "") - flags.StringVar(&port, "port", "22", "") + flags.IntVar(&portNum, "port", 22, "") flags.StringVar(&mountPoint, "mount-point", "ssh", "") flags.BoolVar(&noExec, "no-exec", false, "") @@ -39,15 +42,23 @@ func (c *SSHCommand) Run(args []string) int { return 2 } + port := strconv.Itoa(portNum) + client, err := c.Client() if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) return 2 } + // split the parameter username@ip input := strings.Split(args[0], "@") var username string var ipAddr 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. if len(input) == 1 { u, err := user.Current() if err != nil { @@ -63,18 +74,28 @@ func (c *SSHCommand) Run(args []string) int { return 2 } + // Resolving domain names to IP address on the client side. + // Vault only deals with IP addressess. ip, err := net.ResolveIPAddr("ip", ipAddr) if err != nil { c.Ui.Error(fmt.Sprintf("Error resolving IP Address: %s", err)) return 2 } + // 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. if role == "" { role, err = c.defaultRole(mountPoint, ip.String()) if err != nil { - c.Ui.Error(fmt.Sprintf("Error setting default role: %s", err)) + c.Ui.Error(fmt.Sprintf("Error choosing role: %s", 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: %s", role)) } @@ -89,9 +110,9 @@ func (c *SSHCommand) Run(args []string) int { return 2 } + // if no-exec was chosen, just print out the secret and return. if noExec { - c.Ui.Output(fmt.Sprintf("IP:%s\nUsername: %s\nKey:%s\n", ip.String(), username, keySecret.Data["key"])) - return 0 + return OutputSecret(c.Ui, format, keySecret) } if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic { @@ -100,11 +121,16 @@ func (c *SSHCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Invalid key")) return 2 } - sshDynamicKeyFileName = fmt.Sprintf("vault_temp_file_%s_%s", username, ip.String()) + sshDynamicKeyFileName = fmt.Sprintf("vault_ssh_%s_%s", username, ip.String()) err = ioutil.WriteFile(sshDynamicKeyFileName, []byte(sshDynamicKey), 0600) sshCmdArgs = append(sshCmdArgs, []string{"-i", sshDynamicKeyFileName}...) } else if keySecret.Data["key_type"].(string) == ssh.KeyTypeOTP { + // 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 { sshCmdArgs = append(sshCmdArgs, []string{"-p", string(keySecret.Data["key"].(string)), "ssh", "-p", port}...) @@ -118,9 +144,8 @@ func (c *SSHCommand) Run(args []string) int { } return 0 } - c.Ui.Output(fmt.Sprintf("OTP for the session is %s\n[Note: Install 'sshpass' to automate typing in OTP]\n", string(keySecret.Data["key"].(string)))) - } else { - c.Ui.Error("Error creating key") + c.Ui.Output("OTP for the session is" + string(keySecret.Data["key"].(string))) + c.Ui.Output("[Note: Install 'sshpass' to automate typing in OTP]") } sshCmdArgs = append(sshCmdArgs, []string{"-p", port}...) sshCmdArgs = append(sshCmdArgs, args...) @@ -129,18 +154,28 @@ func (c *SSHCommand) Run(args []string) int { sshCmd.Stdin = os.Stdin sshCmd.Stdout = os.Stdout + // Running the command as a separate command. The reason for using exec.Command instead + // of using crypto/ssh package is that, this way, user can have the same feeling of + // connecting to remote hosts with the ssh command. Package crypto/ssh did not have a way + // to establish an independent session like this. err = sshCmd.Run() if err != nil { c.Ui.Error(fmt.Sprintf("Error while running ssh command:%s", err)) } + // Delete the temporary key file generated by the command. if keySecret.Data["key_type"].(string) == ssh.KeyTypeDynamic { - err = os.Remove(sshDynamicKeyFileName) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error deleting key file: %s", err)) - } + // Ignoring the error from the below call since it is not a security + // issue if the deletion of file is not successful. User is authorized + // to have this secret. + os.Remove(sshDynamicKeyFileName) } + // If the session established was longer than the lease expiry, the secret + // might have been revoked already. If not, then revoke it. Since the key + // file is deleted and since user doesn't know the credential anymore, there + // is not point in Vault maintaining this secret anymore. Everytime the command + // is run, a fresh credential is generated anyways. err = client.Sys().Revoke(keySecret.LeaseID) if err != nil { c.Ui.Error(fmt.Sprintf("Error revoking the key: %s", err)) @@ -181,7 +216,10 @@ func (c *SSHCommand) defaultRole(mountPoint, ip string) (string, error) { roleNames += item.(string) + ", " } roleNames = strings.TrimRight(roleNames, ", ") - return "", fmt.Errorf("IP %s has multiple roles.\nSelect a role using '-role' option.\nPossible roles: [%s]\nNote that all roles may not be permitted, based on ACLs.", ip, roleNames) + return "", fmt.Errorf("Roles:[%s]"+` + 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) } } @@ -225,6 +263,11 @@ SSH Options: -mount-point Mount point of SSH backend. If the backend is mounted at 'ssh', which is the default as well, this parameter can be skipped. + + -format If no-exec option is enabled, then the credentials will be + printed out and SSH connection will not be established. The + format of the output can be 'json' or 'table'. JSON output + is useful when writing scripts. Default is 'table'. ` return strings.TrimSpace(helpText) }