Skip to content

Commit

Permalink
SSH Config support (#29)
Browse files Browse the repository at this point in the history
* initial changes for ssh config support

* support tilde homedir, process made up default identity file correctly

* use passphrase with ssh config identity file

* add support for custom authentication agent and support identity agent in config parsing

* docs and typos

* fix docs and agent socket ~ processing
  • Loading branch information
raydent committed Jan 19, 2024
1 parent 0a77bf2 commit 83fb016
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 54 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func main() {

creds := dcreds.NewSimpleCredentials(
dcreds.WithUsername(dcreds.GetLogin()),
dcreds.WithSSHAgent(), // try pubkey auth using agent
dcreds.WithSSHAgentSocket(dcreds.GetDefaultAgentSocket()), // try pubkey auth using agent
dcreds.WithPassword(dcreds.Secret(password)), // and password
dcreds.WithLogger(logger),
)
Expand Down
53 changes: 48 additions & 5 deletions cmd/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ func main() {
devType := flag.String("devtype", "", fmt.Sprintf("Device type from dev-conf file or from predifined: %s", dt))
login := flag.String("login", "", "Login")
password := flag.String("password", "", "Password")
useSSHConfig := flag.Bool("use-ssh-config", false, "Use default ssh config")
sshConfigPassphrase := flag.String("ssh-config-passphrase", "", "Passphrase for ssh config's identity file")
debug := flag.Bool("debug", false, "Set debug log level")
test := flag.Bool("test", false, "Run tests on config")
jsonOut := flag.Bool("json", false, "Output in JSON")
Expand Down Expand Up @@ -120,7 +122,10 @@ func main() {
panic("empty command")
}
commands := strings.Split(*command, "\n")
creds := buildCreds(*login, *password, logger)
creds, err := buildCreds(*login, *password, *hostname, *sshConfigPassphrase, *useSSHConfig, logger)
if err != nil {
panic(err)
}
sshOpts := []ssh.StreamerOption{ssh.WithLogger(logger)}
if port != nil {
sshOpts = append(sshOpts, ssh.WithPort(*port))
Expand Down Expand Up @@ -187,22 +192,60 @@ func formatJSON(command []string, inputs []cmd.CmdRes) (string, error) {
return string(res), err
}

func buildCreds(login, password string, logger *zap.Logger) gcred.Credentials {
func buildCreds(login, password, host, sshConfigPassphrase string, useSSHConfig bool, logger *zap.Logger) (gcred.Credentials, error) {
if len(login) == 0 {
newLogin := gcred.GetLogin()
login = newLogin
}

if useSSHConfig {
return buildCredsFromSSHConfig(login, password, host, sshConfigPassphrase, logger)
}
return buildBasicCreds(login, password, logger), nil
}

func buildBasicCreds(login, password string, logger *zap.Logger) gcred.Credentials {
opts := []gcred.CredentialsOption{
gcred.WithUsername(login),
gcred.WithSSHAgentSocket(gcred.GetDefaultAgentSocket()),
gcred.WithLogger(logger),
}
if len(password) > 0 {
opts = append(opts, gcred.WithPassword(gcred.Secret(password)))
}
return gcred.NewSimpleCredentials(opts...)
}

func buildCredsFromSSHConfig(login, password, host, sshConfigPassphrase string, logger *zap.Logger) (gcred.Credentials, error) {
privateKeys, err := gcred.GetPrivateKeysFromConfig(host)
if err != nil {
return nil, err
}
configLogin := gcred.GetUsernameFromConfig(host)
if configLogin != "" {
login = configLogin
}
agentSocket, err := gcred.GetAgentSocketFromConfig(host)
if err != nil {
return nil, err
}

opts := []gcred.CredentialsOption{
gcred.WithUsername(login),
gcred.WithSSHAgent(),
gcred.WithLogger(logger),
gcred.WithSSHAgentSocket(agentSocket),
}
if len(password) > 0 {
opts = append(opts, gcred.WithPassword(gcred.Secret(password)))
}
creds := gcred.NewSimpleCredentials(opts...)
return creds
if len(privateKeys) > 0 {
opts = append(opts, gcred.WithPrivateKeys(privateKeys))
}
if len(sshConfigPassphrase) > 0 {
opts = append(opts, gcred.WithPassphrase(gcred.Secret(sshConfigPassphrase)))
}

return gcred.NewSimpleCredentials(opts...), nil
}

func exec(ctx context.Context, dev device.Device, commands []string, cmdopts []cmd.CmdOption, logger *zap.Logger) ([]cmd.CmdRes, error) {
Expand Down
2 changes: 1 addition & 1 deletion docs/basic_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func main() {

creds := dcreds.NewSimpleCredentials(
dcreds.WithUsername(dcreds.GetLogin()),
dcreds.WithSSHAgent(), // try pubkey auth using agent
dcreds.WithSSHAgentSocket(dcreds.GetDefaultAgentSocket()), // try pubkey auth using agent
dcreds.WithPassword(dcreds.Secret(password)), // and password
dcreds.WithLogger(logger),
)
Expand Down
4 changes: 4 additions & 0 deletions docs/basic_usage_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,8 @@ Usage of cli:
Password
-port int
Port (default 22)
-use-ssh-config
Use default ssh config ($HOME/.ssh/config, falling back to /etc/ssh/ssh_config) to search for options for provided hostname. Supported keywords: User, IdentityAgent, ForwardAgent, IdentityFile. If option is specified in config, it will override options from other sources (e.g. User will override -login if specified)
-ssh-config-passphrase string
Passphrase for IdentityFiles specified in ssh config.
```
2 changes: 1 addition & 1 deletion examples/exec_with_question/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func main() {
creds := credentials.NewSimpleCredentials(
credentials.WithUsername(credentials.GetLogin()),
credentials.WithPassword(credentials.Secret("mypassword")),
credentials.WithSSHAgent(),
credentials.WithSSHAgentSocket(credentials.GetDefaultAgentSocket()),
credentials.WithLogger(logger),
)

Expand Down
2 changes: 1 addition & 1 deletion examples/simple_exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func main() {
creds := credentials.NewSimpleCredentials(
credentials.WithUsername(*login),
credentials.WithPassword(credentials.Secret(*password)),
credentials.WithSSHAgent(),
credentials.WithSSHAgentSocket(credentials.GetDefaultAgentSocket()),
credentials.WithLogger(logger),
)

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/net v0.16.0 // indirect
golang.org/x/sys v0.13.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand All @@ -39,6 +41,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
Expand All @@ -50,6 +54,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
Expand Down
117 changes: 97 additions & 20 deletions pkg/credentials/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,43 @@ Package credentials describes credentials.
package credentials

import (
"errors"
"fmt"
"io/fs"
"os"
"os/user"

"github.com/kevinburke/ssh_config"
"github.com/mitchellh/go-homedir"
"go.uber.org/zap"
)

type Credentials interface {
GetUsername() (string, error)
GetPasswords() []Secret
GetPrivateKey() []byte
GetPrivateKeys() [][]byte
GetPassphrase() Secret
AgentEnabled() bool
GetAgentSocket() string
}

type SimpleCredentials struct {
username string
passwords []Secret
privKey []byte
passphrase Secret
agent bool
logger *zap.Logger
username string
passwords []Secret
privKeys [][]byte
passphrase Secret
agentSocket string
logger *zap.Logger
}

type CredentialsOption func(*SimpleCredentials)

func NewSimpleCredentials(opts ...CredentialsOption) *SimpleCredentials {
cred := &SimpleCredentials{
username: "",
passwords: []Secret{},
passphrase: "",
agent: false,
logger: zap.NewNop(),
username: "",
passwords: []Secret{},
passphrase: "",
agentSocket: "",
logger: zap.NewNop(),
}
for _, opt := range opts {
opt(cred)
Expand Down Expand Up @@ -70,7 +74,13 @@ func WithLogger(logger *zap.Logger) CredentialsOption {

func WithPrivateKey(key []byte) CredentialsOption {
return func(h *SimpleCredentials) {
h.privKey = key
h.privKeys = [][]byte{key}
}
}

func WithPrivateKeys(key [][]byte) CredentialsOption {
return func(h *SimpleCredentials) {
h.privKeys = key
}
}

Expand All @@ -80,9 +90,9 @@ func WithPassphrase(passphrase Secret) CredentialsOption {
}
}

func WithSSHAgent() CredentialsOption {
func WithSSHAgentSocket(agentSocket string) CredentialsOption {
return func(h *SimpleCredentials) {
h.agent = true
h.agentSocket = agentSocket
}
}

Expand All @@ -105,14 +115,20 @@ func (m SimpleCredentials) GetPassphrase() Secret {
return m.passphrase
}

func (m SimpleCredentials) GetPrivateKey() []byte {
return m.privKey
func (m SimpleCredentials) GetPrivateKeys() [][]byte {
return m.privKeys
}

func (m SimpleCredentials) AgentEnabled() bool {
return m.agent
func (m SimpleCredentials) GetAgentSocket() string {
return m.agentSocket
}

// GetDefaultAgentSocket returns default ssh authentication agent socket (read from SSH_AUTH_SOCK env)
func GetDefaultAgentSocket() string {
return os.Getenv("SSH_AUTH_SOCK")
}

// GetLogin tries to get sudo user from env, falling back to current user
func GetLogin() string {
login := os.Getenv("SUDO_USER")
if login == "" {
Expand All @@ -138,3 +154,64 @@ func (Secret) MarshalText() ([]byte, error) {
func (m Secret) Value() string {
return string(m)
}

// GetUsernameFromConfig extracts User keyword value for given host from default ssh config.
func GetUsernameFromConfig(host string) string {
return ssh_config.Get(host, "User")
}

// GetAgentSocketFromConfig computes SSH authentication agent socket path using default ssh config's IdentityAgent and ForwardAgent keywords.
// IdentityAgent value supports tilde syntax, but it doesn't support %d, %h, %l and %r.
func GetAgentSocketFromConfig(host string) (string, error) {
// todo:
// IdentityAgent and IdentityFile accept the tokens %%, %d, %h, %l, %r, and %u.
ia := ssh_config.Get(host, "IdentityAgent")
expandedIa, err := homedir.Expand(ia)
if err != nil {
return "", err
}
if expandedIa == "none" {
return "", nil
}
if expandedIa == "SSH_AUTH_SOCK" {
return GetDefaultAgentSocket(), nil
}
if expandedIa == "" && ssh_config.Get(host, "ForwardAgent") == "yes" {
return GetDefaultAgentSocket(), nil
}

return expandedIa, nil
}

// GetPrivateKeysFromConfig tries to extract PrivateKeys from default config's IdentityFiles specifed for provided host.
// IdentityFile value supports tilde syntax, but it doesn't support %d, %u, %l, %h and %r.
func GetPrivateKeysFromConfig(host string) ([][]byte, error) {
identityFiles := ssh_config.GetAll(host, "IdentityFile")
privKeys := make([][]byte, 0, len(identityFiles))
for _, v := range identityFiles {
// todo:
// IdentityAgent and IdentityFile accept the tokens %%, %d, %h, %l, %r, and %u.
expandedPath, err := homedir.Expand(v)
if err != nil {
return nil, fmt.Errorf("failed to expand path of identity file %s: %w", v, err)
}
content, err := os.ReadFile(expandedPath)
if err != nil && isSSHConfigMadeUpDefaultFileError(identityFiles, err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read identity file %s: %w", v, err)
}
privKeys = append(privKeys, content)
}
return privKeys, nil
}

// isSSHConfigMadeUpDefaultFileError checks if given error occured because
// ssh_config lib made up a default value for IdentityFile due to it's absence in config
func isSSHConfigMadeUpDefaultFileError(identityFiles []string, err error) bool {
if len(identityFiles) == 1 && identityFiles[0] == "~/.ssh/identity" && errors.Is(err, fs.ErrNotExist) {
return true
}
return false
}
8 changes: 4 additions & 4 deletions pkg/device/ros/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,16 @@ func (m rosUsernameWrapper) GetPasswords() []credentials.Secret {
return m.creds.GetPasswords()
}

func (m rosUsernameWrapper) GetPrivateKey() []byte {
return m.creds.GetPrivateKey()
func (m rosUsernameWrapper) GetPrivateKeys() [][]byte {
return m.creds.GetPrivateKeys()
}

func (m rosUsernameWrapper) GetPassphrase() credentials.Secret {
return m.creds.GetPassphrase()
}

func (m rosUsernameWrapper) AgentEnabled() bool {
return m.creds.AgentEnabled()
func (m rosUsernameWrapper) GetAgentSocket() string {
return m.creds.GetAgentSocket()
}

func newRosUsernameWrapper(creds credentials.Credentials) rosUsernameWrapper {
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func BuildCreds(login, password string, enableAgent bool, logger *zap.Logger) cr
opts = append(opts, credentials.WithLogger(logger))
}
if enableAgent {
opts = append(opts, credentials.WithSSHAgent())
opts = append(opts, credentials.WithSSHAgentSocket(credentials.GetDefaultAgentSocket()))
}
if len(password) > 0 {
opts = append(opts, credentials.WithPassword(credentials.Secret(password)))
Expand Down
Loading

0 comments on commit 83fb016

Please sign in to comment.