2022-09-14 12:46:34 -07:00
// Copyright (c) HashiCorp, Inc.
2020-10-10 16:12:04 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2020-10-10 16:12:04 -04:00
package connect
import (
2022-06-15 09:30:45 -07:00
"errors"
2020-10-10 16:12:04 -04:00
"fmt"
2022-07-20 15:17:15 -07:00
"os"
2020-10-10 16:12:04 -04:00
"strings"
2023-10-22 17:48:54 -07:00
apiproxy "github.com/hashicorp/boundary/api/proxy"
2020-10-10 16:12:04 -04:00
"github.com/hashicorp/boundary/internal/cmd/base"
2022-08-05 10:22:07 -07:00
"github.com/hashicorp/boundary/internal/target"
2020-10-10 16:12:04 -04:00
"github.com/posener/complete"
)
const (
sshSynopsis = "Authorize a session against a target and invoke an SSH client to connect"
)
func sshOptions ( c * Command , set * base . FlagSets ) {
f := set . NewFlagSet ( "SSH Options" )
f . StringVar ( & base . StringVar {
Name : "style" ,
Target : & c . flagSshStyle ,
EnvVar : "BOUNDARY_CONNECT_SSH_STYLE" ,
2022-06-15 09:30:45 -07:00
Completion : complete . PredictSet ( "ssh" , "putty" , "sshpass" ),
2020-10-10 16:12:04 -04:00
Default : "ssh" ,
Usage : `Specifies how the CLI will attempt to invoke an SSH client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "ssh" and "putty".` ,
})
2023-08-29 08:45:10 -07:00
f . StringVar ( & base . StringVar {
Name : "remote-command" ,
Target : & c . flagRemoteCommand ,
Usage : `Specifies a command that will be executed on the remote host. A complete command line may be specified as command, or it may have additional arguments. If supplied, the arguments will be appended to the command, separated by spaces.` ,
})
2020-10-10 16:12:04 -04:00
f . StringVar ( & base . StringVar {
Name : "username" ,
Target : & c . flagUsername ,
EnvVar : "BOUNDARY_CONNECT_USERNAME" ,
Completion : complete . PredictNothing ,
Usage : `Specifies the username to pass through to the client` ,
})
}
type sshFlags struct {
2023-08-29 08:45:10 -07:00
flagSshStyle string
flagRemoteCommand string
2020-10-10 16:12:04 -04:00
}
func ( s * sshFlags ) defaultExec () string {
return strings . ToLower ( s . flagSshStyle )
}
2023-10-22 17:48:54 -07:00
func ( s * sshFlags ) buildArgs ( c * Command , port , ip , _ string , creds apiproxy . Credentials ) ( args , envs [] string , retCreds apiproxy . Credentials , retErr error ) {
2022-07-20 15:17:15 -07:00
var username string
retCreds = creds
2022-06-15 09:30:45 -07:00
2022-08-05 10:22:07 -07:00
var tryConsume bool
2022-09-22 17:51:30 -07:00
switch string ( target . SubtypeFromId ( c . sessionAuthzData . GetTargetId ())) {
2022-08-05 10:22:07 -07:00
case "tcp" :
tryConsume = true
}
2022-06-15 09:30:45 -07:00
switch strings . ToLower ( s . flagSshStyle ) {
2020-10-10 16:12:04 -04:00
case "ssh" :
2022-06-15 09:30:45 -07:00
// Might want -t for ssh or -tt but seems fine without it for now...
2023-08-29 08:45:10 -07:00
args = append ( args , "-p" , port )
2022-06-15 09:30:45 -07:00
2022-09-26 14:04:15 -07:00
switch c . sessionAuthzData . GetType () {
2022-09-22 17:51:30 -07:00
case "tcp" :
// SSH detects a host key change when the localhost proxy port changes
// This uses the host ID instead of 'localhost:port'.
2022-12-05 18:08:31 +00:00
if len ( c . sessionAuthzData . GetHostId ()) > 0 {
args = append ( args , "-o" , fmt . Sprintf ( "HostKeyAlias=%s" , c . sessionAuthzData . GetHostId ()))
} else {
// In cases where the Target has no host sources and has an
// address directly attached to it, we have no Host Id. Use
// Target Id instead. Only one address can ever be present on a
// target, and no other host sources may be present at the same
// time, so this is a reasonable alternative.
args = append ( args , "-o" , fmt . Sprintf ( "HostKeyAlias=%s" , c . sessionAuthzData . GetTargetId ()))
}
2022-09-22 17:51:30 -07:00
case "ssh" :
args = append ( args , "-o" , "NoHostAuthenticationForLocalhost=yes" )
}
2022-06-15 09:30:45 -07:00
case "sshpass" :
2022-08-05 10:22:07 -07:00
if ! tryConsume {
2023-10-22 17:48:54 -07:00
return nil , nil , apiproxy . Credentials {}, errors . New ( "Credentials must be consumed when using sshpass" )
2022-08-05 10:22:07 -07:00
}
2022-07-20 15:17:15 -07:00
var password string
2023-10-22 17:48:54 -07:00
if len ( retCreds . UsernamePassword ) > 0 {
2022-07-20 15:17:15 -07:00
// For now just grab the first username password credential brokered
2022-08-08 15:15:03 -04:00
// Mark credential as consumed so that it is not printed to user
2023-10-22 17:48:54 -07:00
retCreds . UsernamePassword [ 0 ]. Consumed = true
2022-07-20 15:17:15 -07:00
2023-10-22 17:48:54 -07:00
username = retCreds . UsernamePassword [ 0 ]. Username
password = retCreds . UsernamePassword [ 0 ]. Password
2022-07-20 15:17:15 -07:00
}
if password == "" {
2023-10-22 17:48:54 -07:00
return nil , nil , apiproxy . Credentials {}, errors . New ( "Password is required when using sshpass" )
2022-06-15 09:30:45 -07:00
}
2022-07-20 15:17:15 -07:00
2022-06-15 09:30:45 -07:00
// sshpass requires that the password is passed as env-var "SSHPASS"
// when the using env-vars.
2022-07-20 15:17:15 -07:00
envs = append ( envs , fmt . Sprintf ( "SSHPASS=%s" , password ))
2022-06-15 09:30:45 -07:00
args = append ( args , "-e" , "ssh" )
2023-08-29 08:45:10 -07:00
args = append ( args , "-p" , port )
2022-06-15 09:30:45 -07:00
// sshpass cannot handle host key checking, disable localhost key verification
// to avoid error: 'SSHPASS detected host authentication prompt. Exiting.'
args = append ( args , "-o" , "NoHostAuthenticationForLocalhost=yes" )
2020-10-10 16:12:04 -04:00
case "putty" :
2023-08-29 08:45:10 -07:00
args = append ( args , "-P" , port )
2020-10-10 16:12:04 -04:00
}
2022-06-15 09:30:45 -07:00
2022-07-20 15:17:15 -07:00
// Check if we got credentials to attempt to use for ssh or putty,
2022-09-14 12:46:34 -07:00
// sshpass style has already been handled above as username password.
2022-07-20 15:17:15 -07:00
switch strings . ToLower ( s . flagSshStyle ) {
case "putty" , "ssh" :
switch {
2022-08-05 10:22:07 -07:00
// First check if we want to try and consume credentials
case ! tryConsume :
// Do nothing
// If we want to consume check if we have a private key available first
2023-10-22 17:48:54 -07:00
case len ( creds . SshPrivateKey ) > 0 :
2022-07-20 15:17:15 -07:00
// For now just grab the first ssh private key credential brokered
2023-10-22 17:48:54 -07:00
cred := retCreds . SshPrivateKey [ 0 ]
2022-08-08 15:15:03 -04:00
username = cred . Username
privateKey := cred . PrivateKey
2023-10-22 17:48:54 -07:00
cred . Consumed = true
2022-08-08 15:15:03 -04:00
if cred . Passphrase != "" {
// Don't re-print everything, but print the passphrase they'll need
2023-10-22 17:48:54 -07:00
cred . Consumed = false
delete ( cred . Raw . Credential , "username" )
delete ( cred . Raw . Credential , "private_key" )
2022-08-08 15:15:03 -04:00
}
2023-10-22 17:48:54 -07:00
retCreds . SshPrivateKey [ 0 ] = cred
2022-07-20 15:17:15 -07:00
2022-09-14 12:46:34 -07:00
pkFile , err := os . CreateTemp ( "" , "*" )
2022-07-20 15:17:15 -07:00
if err != nil {
2023-10-22 17:48:54 -07:00
return nil , nil , apiproxy . Credentials {}, fmt . Errorf ( "Error saving ssh private key to tmp file: %w" , err )
2022-07-20 15:17:15 -07:00
}
c . cleanupFuncs = append ( c . cleanupFuncs , func () error {
if err := os . Remove ( pkFile . Name ()); err != nil {
return fmt . Errorf ( "Error removing temporary ssh private key file; consider removing %s manually: %w" , pkFile . Name (), err )
}
return nil
})
2022-07-21 15:07:01 -07:00
// SSH requires the private key file to end with a newline.
2022-08-08 15:15:03 -04:00
// When ingesting an ssh_private_key from a file:// Boundary calls strings.TrimSpace
2022-07-21 15:07:01 -07:00
// which will also trim newlines.
if ! strings . HasSuffix ( privateKey , "\n" ) {
privateKey = fmt . Sprintln ( privateKey )
}
2022-07-20 15:17:15 -07:00
_ , err = pkFile . WriteString ( privateKey )
if err != nil {
2023-10-22 17:48:54 -07:00
return nil , nil , apiproxy . Credentials {}, fmt . Errorf ( "Error writing private key file to %s: %w" , pkFile . Name (), err )
2022-07-20 15:17:15 -07:00
}
if err := pkFile . Close (); err != nil {
2023-10-22 17:48:54 -07:00
return nil , nil , apiproxy . Credentials {}, fmt . Errorf ( "Error closing private key file after writing to %s: %w" , pkFile . Name (), err )
2022-07-20 15:17:15 -07:00
}
args = append ( args , "-i" , pkFile . Name ())
// Next check if we have a username password credential
2023-10-22 17:48:54 -07:00
case len ( creds . UsernamePassword ) > 0 :
2022-08-08 15:15:03 -04:00
// We cannot use the password of the credential outside of sshpass, but we
2022-07-20 15:17:15 -07:00
// can use the username.
// N.B. Do not mark credential as consumed, as user will still need enter
// the password when prompted.
if c . flagUsername == "" {
// If the user did not actively provide a username flag set the
// username to that of the first credential we got.
2023-10-22 17:48:54 -07:00
username = retCreds . UsernamePassword [ 0 ]. Username
2022-07-20 15:17:15 -07:00
}
}
}
2022-06-15 09:30:45 -07:00
switch {
2022-07-20 15:17:15 -07:00
case username != "" :
args = append ( args , "-l" , username )
2022-06-15 09:30:45 -07:00
case c . flagUsername != "" :
2020-10-10 16:12:04 -04:00
args = append ( args , "-l" , c . flagUsername )
}
2022-06-15 09:30:45 -07:00
2023-08-29 08:45:10 -07:00
// Add destination
args = append ( args , ip )
// Add optional command to run on remote host
if s . flagRemoteCommand != "" {
cmdArgs := strings . Split ( s . flagRemoteCommand , " " )
args = append ( args , cmdArgs ... )
}
2022-07-20 15:17:15 -07:00
return args , envs , retCreds , nil
2020-10-10 16:12:04 -04:00
}