From cbca6253282cc76be74b3005da80b63de94a8180 Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Tue, 2 Jun 2020 11:46:24 -0700 Subject: V2 Add support for ssh authentication methods * podman --remote ssh://:@: * podman --remote ssh://:@: \ --identity --passphrase * ssh-add podman --remote ssh://@ * Fix `podman help` to run even if podman missing components * Prompt for passphrase on stdin IFF key is protected and passphrase not given via any other configuration * cobra flags do not support optional value flags therefore refactored --remote to be a boolean and --url will now contain the URI to Podman service Signed-off-by: Jhon Honce --- pkg/bindings/bindings.go | 43 ++++++++++++++++++++ pkg/bindings/connection.go | 80 +++++++++++++++++++++++++++++++------- pkg/domain/entities/engine.go | 6 ++- pkg/domain/infra/runtime_abi.go | 4 +- pkg/domain/infra/runtime_tunnel.go | 4 +- 5 files changed, 117 insertions(+), 20 deletions(-) (limited to 'pkg') diff --git a/pkg/bindings/bindings.go b/pkg/bindings/bindings.go index 7e2a444bd..da47ea713 100644 --- a/pkg/bindings/bindings.go +++ b/pkg/bindings/bindings.go @@ -9,7 +9,13 @@ package bindings import ( + "errors" + "fmt" + "io" + "os" + "github.com/blang/semver" + "golang.org/x/crypto/ssh/terminal" ) var ( @@ -25,3 +31,40 @@ var ( // _*YES*- podman will fail to run if this value is wrong APIVersion = semver.MustParse("1.0.0") ) + +// readPassword prompts for a secret and returns value input by user from stdin +// Unlike terminal.ReadPassword(), $(echo $SECRET | podman...) is supported. +// Additionally, all input after `/n` is queued to podman command. +func readPassword(prompt string) (pw []byte, err error) { + fd := int(os.Stdin.Fd()) + if terminal.IsTerminal(fd) { + fmt.Fprint(os.Stderr, prompt) + pw, err = terminal.ReadPassword(fd) + fmt.Fprintln(os.Stderr) + return + } + + var b [1]byte + for { + n, err := os.Stdin.Read(b[:]) + // terminal.ReadPassword discards any '\r', so we do the same + if n > 0 && b[0] != '\r' { + if b[0] == '\n' { + return pw, nil + } + pw = append(pw, b[0]) + // limit size, so that a wrong input won't fill up the memory + if len(pw) > 1024 { + err = errors.New("password too long, 1024 byte limit") + } + } + if err != nil { + // terminal.ReadPassword accepts EOF-terminated passwords + // if non-empty, so we do the same + if err == io.EOF && len(pw) > 0 { + err = nil + } + return pw, err + } + } +} diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index c26093a7f..b130b9598 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -13,6 +13,7 @@ import ( "path/filepath" "strconv" "strings" + "sync" "time" "github.com/blang/semver" @@ -20,6 +21,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" "k8s.io/client-go/util/homedir" ) @@ -29,6 +31,8 @@ var ( Host: "d", Path: "/v" + APIVersion.String() + "/libpod", } + passPhrase []byte + phraseSync sync.Once ) type APIResponse struct { @@ -61,6 +65,10 @@ func JoinURL(elements ...string) string { return "/" + strings.Join(elements, "/") } +func NewConnection(ctx context.Context, uri string) (context.Context, error) { + return NewConnectionWithIdentity(ctx, uri, "") +} + // NewConnection takes a URI as a string and returns a context with the // Connection embedded as a value. This context needs to be passed to each // endpoint to work correctly. @@ -69,23 +77,28 @@ func JoinURL(elements ...string) string { // For example tcp://localhost: // or unix:///run/podman/podman.sock // or ssh://@[:port]/run/podman/podman.sock?secure=True -func NewConnection(ctx context.Context, uri string, identity ...string) (context.Context, error) { +func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase string, identities ...string) (context.Context, error) { var ( err error secure bool ) - if v, found := os.LookupEnv("PODMAN_HOST"); found { + if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" { uri = v } - if v, found := os.LookupEnv("PODMAN_SSHKEY"); found { - identity = []string{v} + if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identities) == 0 { + identities = append(identities, v) + } + + if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found && passPhrase == "" { + passPhrase = v } _url, err := url.Parse(uri) if err != nil { - return nil, errors.Wrapf(err, "Value of PODMAN_HOST is not a valid url: %s", uri) + return nil, errors.Wrapf(err, "Value of CONTAINER_HOST is not a valid url: %s", uri) } + // TODO Fill in missing defaults for _url... // Now we setup the http Client to use the connection above var connection Connection @@ -95,7 +108,7 @@ func NewConnection(ctx context.Context, uri string, identity ...string) (context if err != nil { secure = false } - connection, err = sshClient(_url, identity[0], secure) + connection, err = sshClient(_url, secure, passPhrase, identities...) case "unix": if !strings.HasPrefix(uri, "unix:///") { // autofix unix://path_element vs unix:///path_element @@ -172,10 +185,31 @@ func pingNewConnection(ctx context.Context) error { return errors.Errorf("ping response was %q", response.StatusCode) } -func sshClient(_url *url.URL, identity string, secure bool) (Connection, error) { - auth, err := publicKey(identity) - if err != nil { - return Connection{}, errors.Wrapf(err, "Failed to parse identity %s: %v\n", _url.String(), identity) +func sshClient(_url *url.URL, secure bool, passPhrase string, identities ...string) (Connection, error) { + var authMethods []ssh.AuthMethod + + for _, i := range identities { + auth, err := publicKey(i, []byte(passPhrase)) + if err != nil { + fmt.Fprint(os.Stderr, errors.Wrapf(err, "failed to parse identity %q", i).Error()+"\n") + continue + } + authMethods = append(authMethods, auth) + } + + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { + logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) + + c, err := net.Dial("unix", sock) + if err != nil { + return Connection{}, err + } + a := agent.NewClient(c) + authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers)) + } + + if pw, found := _url.User.Password(); found { + authMethods = append(authMethods, ssh.Password(pw)) } callback := ssh.InsecureIgnoreHostKey() @@ -195,7 +229,7 @@ func sshClient(_url *url.URL, identity string, secure bool) (Connection, error) net.JoinHostPort(_url.Hostname(), port), &ssh.ClientConfig{ User: _url.User.Username(), - Auth: []ssh.AuthMethod{auth}, + Auth: authMethods, HostKeyCallback: callback, HostKeyAlgorithms: []string{ ssh.KeyAlgoRSA, @@ -307,7 +341,7 @@ func (h *APIResponse) IsServerError() bool { return h.Response.StatusCode/100 == 5 } -func publicKey(path string) (ssh.AuthMethod, error) { +func publicKey(path string, passphrase []byte) (ssh.AuthMethod, error) { key, err := ioutil.ReadFile(path) if err != nil { return nil, err @@ -315,12 +349,30 @@ func publicKey(path string) (ssh.AuthMethod, error) { signer, err := ssh.ParsePrivateKey(key) if err != nil { - return nil, err + if _, ok := err.(*ssh.PassphraseMissingError); !ok { + return nil, err + } + if len(passphrase) == 0 { + phraseSync.Do(promptPassphrase) + passphrase = passPhrase + } + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphrase) + if err != nil { + return nil, err + } } - return ssh.PublicKeys(signer), nil } +func promptPassphrase() { + phrase, err := readPassword("Key Passphrase: ") + if err != nil { + passPhrase = []byte{} + return + } + passPhrase = phrase +} + func hostKey(host string) ssh.PublicKey { // parse OpenSSH known_hosts file // ssh or use ssh-keyscan to get initial key diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index db58befa5..b2bef0eea 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -43,14 +43,16 @@ type PodmanConfig struct { EngineMode EngineMode // ABI or Tunneling mode Identities []string // ssh identities for connecting to server MaxWorks int // maximum number of parallel threads + PassPhrase string // ssh passphrase for identity for connecting to server RegistriesConf string // allows for specifying a custom registries.conf + Remote bool // Connection to Podman API Service will use RESTful API RuntimePath string // --runtime flag will set Engine.RuntimePath + Span opentracing.Span // tracing object SpanCloser io.Closer // Close() for tracing object SpanCtx context.Context // context to use when tracing - Span opentracing.Span // tracing object Syslog bool // write to StdOut and Syslog, not supported when tunneling Trace bool // Hidden: Trace execution - Uri string // URI to API Service + Uri string // URI to RESTful API Service Runroot string StorageDriver string diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index 67c1cd534..d860a8115 100644 --- a/pkg/domain/infra/runtime_abi.go +++ b/pkg/domain/infra/runtime_abi.go @@ -20,7 +20,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine, r, err := NewLibpodRuntime(facts.FlagSet, facts) return r, err case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.Uri, facts.PassPhrase, facts.Identities...) return &tunnel.ContainerEngine{ClientCxt: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) @@ -33,7 +33,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error) r, err := NewLibpodImageRuntime(facts.FlagSet, facts) return r, err case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.Uri, facts.PassPhrase, facts.Identities...) return &tunnel.ImageEngine{ClientCxt: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) diff --git a/pkg/domain/infra/runtime_tunnel.go b/pkg/domain/infra/runtime_tunnel.go index 752218aaf..70e4d37ca 100644 --- a/pkg/domain/infra/runtime_tunnel.go +++ b/pkg/domain/infra/runtime_tunnel.go @@ -16,7 +16,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine, case entities.ABIMode: return nil, fmt.Errorf("direct runtime not supported") case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.Uri, facts.PassPhrase, facts.Identities...) return &tunnel.ContainerEngine{ClientCxt: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) @@ -28,7 +28,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error) case entities.ABIMode: return nil, fmt.Errorf("direct image runtime not supported") case entities.TunnelMode: - ctx, err := bindings.NewConnection(context.Background(), facts.Uri, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.Uri, facts.PassPhrase, facts.Identities...) return &tunnel.ImageEngine{ClientCxt: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) -- cgit v1.2.3-54-g00ecf