Skip to content

Commit

Permalink
Discover keypaths from ssh_config, update agent key selection logic (#73
Browse files Browse the repository at this point in the history
)

* Using an explicit path to a private key 

If the key is not passphrase protected, use it as-is

If it is password protected, search for a public key in <path>.pub. If the file is found, look for the key in agent. (there's no way to check if an encrypted private key is known by the agent without decrypting it with a passphrase. Tools like ssh-keygen will also look for the .pub in similar cases)

If there's no agent or the key is not found in it, use PasswordCallback if implemented, otherwise fail with an error.

* Using an explicit path to a public key (new!)

Now you can also give a path to a public key -- if agent is available and a private key for that public key is available on the agent, use it from the agent

* Getting keypath from ssh_config (new!)

If you give an empty/null keypath, ssh_config is queried for an IdentityFile (can be multiple). If it fails, fall back to using a list of hardcoded defaults (was just ~/.ssh/id_rsa, now it is [~/.ssh/identity, ~/.ssh/id_rsa, ~/.ssh/id_dsa]).

* Auth method caching

If the same keypath is used for multiple hosts, it will only be loaded once, so you don't have to enter passphrases multiple times for the same keys.

* Agent fallback

The whole list of keys from agent will only be used when the keypath wasn't explicitly set for the host either in KeyPath: or in ssh_config.

* Integration test suite

Added a simplistic integration test suite using footloose and cmd/rigtest.

Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
  • Loading branch information
kke committed Nov 10, 2022
1 parent b5ca9e0 commit 51844ce
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 114 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: install test dependencies
run: |
sudo apt-get update
sudo apt-get install expect
- uses: actions/checkout@v2

- name: Set up Go
Expand All @@ -32,3 +37,7 @@ jobs:

- name: Test
run: go test -v ./...

- name: Run integration tests
run: make -C test test

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test/rigtest
test/footloose.yaml
test/Library
test/.ssh
163 changes: 163 additions & 0 deletions cmd/rigtest/rigtest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package main

import (
"flag"
"fmt"
goos "os"
"strconv"
"strings"
"time"

"github.com/k0sproject/rig"
"github.com/k0sproject/rig/exec"
"github.com/k0sproject/rig/os"
"github.com/k0sproject/rig/os/registry"
_ "github.com/k0sproject/rig/os/support"
"github.com/kevinburke/ssh_config"
)

type configurer interface {
WriteFile(os.Host, string, string, string) error
LineIntoFile(os.Host, string, string, string) error
ReadFile(os.Host, string) (string, error)
FileExist(os.Host, string) bool
DeleteFile(os.Host, string) error
Stat(os.Host, string, ...exec.Option) (*os.FileInfo, error)
}

// Host is a host that utilizes rig for connections
type Host struct {
rig.Connection

Configurer configurer
}

// LoadOS is a function that assigns a OS support package to the host and
// typecasts it to a suitable interface
func (h *Host) LoadOS() error {
bf, err := registry.GetOSModuleBuilder(*h.OSVersion)
if err != nil {
return err
}

h.Configurer = bf().(configurer)

return nil
}

func main() {
dh := flag.String("host", "127.0.0.1", "target host [+ :port], can give multiple comma separated")
usr := flag.String("user", "root", "user name")
kp := flag.String("keypath", "", "keypath")
pc := flag.Bool("askpass", false, "ask passwords")

fn := fmt.Sprintf("test_%s.txt", time.Now().Format("20060102150405"))

flag.Parse()

if *dh == "" {
println("see -help")
goos.Exit(1)
}

if configPath := goos.Getenv("SSH_CONFIG"); configPath != "" {
f, err := goos.Open(configPath)
if err != nil {
panic(err)
}
cfg, err := ssh_config.Decode(f)
if err != nil {
panic(err)
}
rig.SSHConfigGetAll = func(dst, key string) []string {
res, err := cfg.GetAll(dst, key)
if err != nil {
return nil
}
return res
}
}

var passfunc func() (string, error)
if *pc {
passfunc = func() (string, error) {
var pass string
fmt.Print("Password: ")
fmt.Scanln(&pass)
return pass, nil
}
}

var hosts []Host

for _, address := range strings.Split(*dh, ",") {
port := 22
if addr, portstr, ok := strings.Cut(address, ":"); ok {
address = addr
p, err := strconv.Atoi(portstr)
if err != nil {
panic("invalid port " + portstr)
}
port = p
}

h := Host{
Connection: rig.Connection{
SSH: &rig.SSH{
Address: address,
Port: port,
User: *usr,
KeyPath: kp,
PasswordCallback: passfunc,
},
},
}
hosts = append(hosts, h)
}

for _, h := range hosts {
if err := h.Connect(); err != nil {
panic(err)
}

if err := h.LoadOS(); err != nil {
panic(err)
}

if err := h.Configurer.WriteFile(h, fn, "test\ntest2\ntest3", "0644"); err != nil {
panic(err)
}

if err := h.Configurer.LineIntoFile(h, fn, "test2", "test4"); err != nil {
panic(err)
}

if !h.Configurer.FileExist(h, fn) {
panic("file does not exist")
}

row, err := h.Configurer.ReadFile(h, fn)
if err != nil {
panic(err)
}
if row != "test\ntest4\ntest3" {
panic("file content is not correct")
}

stat, err := h.Configurer.Stat(h, fn)
if err != nil {
panic(err)
}
if !strings.HasSuffix(stat.FName, fn) {
panic("file stat is not correct")
}

if err := h.Configurer.DeleteFile(h, fn); err != nil {
panic(err)
}

if h.Configurer.FileExist(h, fn) {
panic("file still exists")
}
}
}
18 changes: 5 additions & 13 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@ func (c *Connection) SetDefaults() {
if c.client == nil {
c.client = defaultClient()
}
_ = defaults.Set(c.client)
}

_ = defaults.Set(c.client)
}

// Protocol returns the connection protocol name
Expand Down Expand Up @@ -133,16 +132,11 @@ func (c *Connection) IsConnected() bool {
// String returns a printable representation of the connection, which will look
// like: `[ssh] address:port`
func (c Connection) String() string {
client := c.client
if client == nil {
client = c.configuredClient()
_ = defaults.Set(c)
}
if client == nil {
client = defaultClient()
if c.client == nil {
return fmt.Sprintf("[%s] %s", c.Protocol(), c.Address())
}

return client.String()
return c.client.String()
}

// IsWindows returns true on windows hosts
Expand Down Expand Up @@ -311,9 +305,7 @@ func (c *Connection) configuredClient() client {
}

func defaultClient() client {
c := &Localhost{Enabled: true}
_ = defaults.Set(c)
return c
return &Localhost{Enabled: true}
}

// GroupParams separates exec.Options from other sprintf templating args
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/davidmz/go-pageant v1.0.2
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kevinburke/ssh_config v1.2.0
github.com/masterzen/winrm v0.0.0-20220917170901-b07f6cb0598d
github.com/mitchellh/go-homedir v1.1.0
github.com/stretchr/testify v1.8.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
11 changes: 11 additions & 0 deletions log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "fmt"

// Logger interface should be implemented by the logging library you wish to use
type Logger interface {
Tracef(string, ...interface{})
Debugf(string, ...interface{})
Infof(string, ...interface{})
Errorf(string, ...interface{})
Expand All @@ -12,6 +13,11 @@ type Logger interface {
// Log can be assigned a proper logger, such as logrus configured to your liking.
var Log Logger

// Tracef logs a trace level log message
func Tracef(t string, args ...interface{}) {
Log.Debugf(t, args...)
}

// Debugf logs a debug level log message
func Debugf(t string, args ...interface{}) {
Log.Debugf(t, args...)
Expand All @@ -32,6 +38,11 @@ type StdLog struct {
Logger
}

// Debugf prints a debug level log message
func (l *StdLog) Tracef(t string, args ...interface{}) {
fmt.Println("TRACE", fmt.Sprintf(t, args...))
}

// Debugf prints a debug level log message
func (l *StdLog) Debugf(t string, args ...interface{}) {
fmt.Println("DEBUG", fmt.Sprintf(t, args...))
Expand Down
Loading

0 comments on commit 51844ce

Please sign in to comment.