From 7377e578a9402a416579be92cee44c3b3786f81a Mon Sep 17 00:00:00 2001 From: Jhon Honce Date: Thu, 4 Jun 2020 11:51:29 -0700 Subject: V2 podman system connection * Implement command * Refactor podman-remote to pull from containers.conf by default * podman-remote defaults to --remote being true * Write podman-system-connection.1.md Signed-off-by: Jhon Honce --- cmd/podman/main.go | 2 +- cmd/podman/registry/config.go | 3 +- cmd/podman/registry/config_tunnel.go | 7 + cmd/podman/root.go | 17 +- cmd/podman/system/connection.go | 221 ++++++++++++++++++--- docs/source/markdown/podman-system-connection.1.md | 37 ++++ docs/source/markdown/podman-system.1.md | 19 +- docs/source/markdown/podman.1.md | 6 +- pkg/bindings/bindings.go | 43 ---- pkg/bindings/connection.go | 98 ++------- pkg/domain/entities/engine.go | 3 +- pkg/domain/infra/runtime_abi.go | 4 +- pkg/domain/infra/runtime_tunnel.go | 4 +- pkg/terminal/util.go | 133 +++++++++++++ 14 files changed, 411 insertions(+), 186 deletions(-) create mode 100644 docs/source/markdown/podman-system-connection.1.md create mode 100644 pkg/terminal/util.go diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 76ec7bc8e..f502e7a67 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -35,7 +35,7 @@ func main() { _, found := c.Command.Annotations[registry.ParentNSRequired] if rootless.IsRootless() && found { c.Command.RunE = func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("cannot `%s` in rootless mode", cmd.CommandPath()) + return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath()) } } diff --git a/cmd/podman/registry/config.go b/cmd/podman/registry/config.go index 49d5bca74..a67568d73 100644 --- a/cmd/podman/registry/config.go +++ b/cmd/podman/registry/config.go @@ -68,7 +68,6 @@ func newPodmanConfig() { } } - // FIXME: for rootless, add flag to get the path to override configuration cfg, err := config.NewConfig("") if err != nil { fmt.Fprint(os.Stderr, "Failed to obtain podman configuration: "+err.Error()) @@ -83,7 +82,7 @@ func newPodmanConfig() { podmanOptions = entities.PodmanConfig{Config: cfg, EngineMode: mode} } -// SetXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set. +// setXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set. // containers/image uses XDG_RUNTIME_DIR to locate the auth file, XDG_CONFIG_HOME is // use for the libpod.conf configuration file. func setXdgDirs() error { diff --git a/cmd/podman/registry/config_tunnel.go b/cmd/podman/registry/config_tunnel.go index bb3da947e..4f9f51163 100644 --- a/cmd/podman/registry/config_tunnel.go +++ b/cmd/podman/registry/config_tunnel.go @@ -2,6 +2,13 @@ package registry +import ( + "os" +) + func init() { abiSupport = false + + // Enforce that podman-remote == podman --remote + os.Args = append(os.Args, "--remote") } diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 4f834e87d..25e53cbee 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -8,6 +8,7 @@ import ( "runtime/pprof" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" @@ -103,13 +104,13 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { // TODO: Remove trace statement in podman V2.1 logrus.Debugf("Called %s.PersistentPreRunE(%s)", cmd.Name(), strings.Join(os.Args, " ")) - cfg := registry.PodmanConfig() - // Help is a special case, no need for more setup if cmd.Name() == "help" { return nil } + cfg := registry.PodmanConfig() + // Prep the engines if _, err := registry.NewImageEngine(cmd, args); err != nil { return err @@ -211,10 +212,14 @@ func loggingHook() { func rootFlags(opts *entities.PodmanConfig, flags *pflag.FlagSet) { // V2 flags flags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)") - // TODO Read uri from containers.config when available - flags.StringVar(&opts.URI, "url", registry.DefaultAPIAddress(), "URL to access Podman service (CONTAINER_HOST)") - flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file, (CONTAINER_SSHKEY)") - flags.StringVar(&opts.PassPhrase, "passphrase", "", "passphrase for identity file (not secure, CONTAINER_PASSPHRASE), ssh-agent always supported") + + custom, _ := config.ReadCustomConfig() + defaultURI := custom.Engine.RemoteURI + if defaultURI == "" { + defaultURI = registry.DefaultAPIAddress() + } + flags.StringVar(&opts.URI, "url", defaultURI, "URL to access Podman service (CONTAINER_HOST)") + flags.StringVar(&opts.Identity, "identity", custom.Engine.RemoteIdentity, "path to SSH identity file, (CONTAINER_SSHKEY)") cfg := opts.Config flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") diff --git a/cmd/podman/system/connection.go b/cmd/podman/system/connection.go index 3af905fad..d8c709d6e 100644 --- a/cmd/podman/system/connection.go +++ b/cmd/podman/system/connection.go @@ -1,54 +1,209 @@ -// +build !remote - package system import ( + "bytes" + "fmt" + "net" + "net/url" + "os" + "os/user" + "regexp" + + "github.com/containers/common/pkg/config" "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/terminal" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) +const schemaPattern = "^[A-Za-z][A-Za-z0-9+.-]*:" + var ( - connectionDescription = `TBD -` - connectionCommand = &cobra.Command{ - Use: "connection", - //Args: validate.NoArgs, - Long: connectionDescription, - Short: "Add remote ssh connection", - RunE: connection, - Example: `podman system connection server.foobar.com -podman system connection --identity ~/.ssh/dev_rsa --default root@server.foobar.com:222`, + // Skip creating engines since this command will obtain connection information to engine + noOp = func(cmd *cobra.Command, args []string) error { + return nil + } + connectionCmd = &cobra.Command{ + Use: "connection [flags] destination", + Args: cobra.ExactArgs(1), + Long: `Store ssh destination information in podman configuration. + "destination" is of the form [user@]hostname or + an URI of the form ssh://[user@]hostname[:port] +`, + Short: "Record remote ssh destination", + PersistentPreRunE: noOp, + PersistentPostRunE: noOp, + TraverseChildren: false, + RunE: connection, + Example: `podman system connection server.fubar.com + podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222 + podman system connection --identity ~/.ssh/dev_rsa -port 22 root@server.fubar.com`, } -) -var connectionOptions = struct { - Alias string - Default bool - Identity string - SocketPath string - User string -}{} + cOpts = struct { + Identity string + Port int + UDSPath string + }{} +) func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ - Mode: []entities.EngineMode{entities.ABIMode}, - Command: connectionCommand, + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: connectionCmd, Parent: systemCmd, }) - flags := connectionCommand.Flags() - flags.StringVar(&connectionOptions.Alias, "alias", "", "alias name for connection") - flags.BoolVar(&connectionOptions.Default, "default", false, "set as the default connection") - flags.StringVar(&connectionOptions.Identity, "identity", "", "path to ssh identity file") - //flags.StringVar(&connectionOptions.User, "user", "", "remote username") - flags.StringVar(&connectionOptions.SocketPath, "socket-path", "", "path to podman socket on remote host") + + flags := connectionCmd.Flags() + flags.StringVar(&cOpts.Identity, "identity", "", "path to ssh identity file") + flags.IntVarP(&cOpts.Port, "port", "p", 22, "port number for destination") + flags.StringVar(&cOpts.UDSPath, "socket-path", "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)") } func connection(cmd *cobra.Command, args []string) error { - // if no user is provided, assume local user name - // if no socket is provided, then do an ssh to look for it - // default connection, if exists, is then assumed with podman remote - // if no identity exists, should we be prompting for password? + // Default to ssh: schema if none given + dest := []byte(args[0]) + if match, err := regexp.Match(schemaPattern, dest); err != nil { + return errors.Wrapf(err, "internal regex error %q", schemaPattern) + } else if !match { + dest = append([]byte("ssh://"), dest...) + } + + uri, err := url.Parse(string(dest)) + if err != nil { + return errors.Wrapf(err, "failed to parse %q", string(dest)) + } + + if uri.User.Username() == "" { + if uri.User, err = getUserInfo(uri); err != nil { + return err + } + } + + if cmd.Flag("socket-path").Changed { + uri.Path = cmd.Flag("socket-path").Value.String() + } + + if cmd.Flag("port").Changed { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").Value.String()) + } + + if uri.Port() == "" { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue) + } + + if uri.Path == "" { + if uri.Path, err = getUDS(cmd, uri); err != nil { + return errors.Wrapf(err, "failed to connect to %q", uri.String()) + } + } + + custom, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if cmd.Flag("identity").Changed { + custom.Engine.RemoteIdentity = cOpts.Identity + } + + custom.Engine.RemoteURI = uri.String() + return custom.Write() +} + +func getUserInfo(uri *url.URL) (*url.Userinfo, error) { + var ( + usr *user.User + err error + ) + if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found { + usr, err = user.LookupId(u) + if err != nil { + return nil, errors.Wrapf(err, "failed to find user %q", u) + } + } else { + usr, err = user.Current() + if err != nil { + return nil, errors.Wrapf(err, "failed to obtain current user") + } + } + + pw, set := uri.User.Password() + if set { + return url.UserPassword(usr.Username, pw), nil + } + return url.User(usr.Username), nil +} + +func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { + var authMethods []ssh.AuthMethod + passwd, set := uri.User.Password() + if set { + authMethods = append(authMethods, ssh.Password(passwd)) + } + + ident := cmd.Flag("identity") + if ident.Changed { + auth, err := terminal.PublicKey(ident.Value.String(), []byte(passwd)) + if err != nil { + return "", errors.Wrapf(err, "Failed to read identity %q", ident.Value.String()) + } + 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 "", err + } + a := agent.NewClient(c) + authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers)) + } - return nil + config := &ssh.ClientConfig{ + User: uri.User.Username(), + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + dial, err := ssh.Dial("tcp", uri.Host, config) + if err != nil { + return "", errors.Wrapf(err, "failed to connect to %q", uri.Host) + } + defer dial.Close() + + session, err := dial.NewSession() + if err != nil { + return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) + } + defer session.Close() + + // Override podman binary for testing etc + podman := "podman" + if v, found := os.LookupEnv("PODMAN_BINARY"); found { + podman = v + } + run := podman + " info --format=json" + + var buffer bytes.Buffer + session.Stdout = &buffer + if err := session.Run(run); err != nil { + return "", errors.Wrapf(err, "failed to run %q", run) + } + + var info define.Info + if err := json.Unmarshal(buffer.Bytes(), &info); err != nil { + return "", errors.Wrapf(err, "failed to parse 'podman info' results") + } + + if info.Host.RemoteSocket == nil || !info.Host.RemoteSocket.Exists { + return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host) + } + return info.Host.RemoteSocket.Path, nil } diff --git a/docs/source/markdown/podman-system-connection.1.md b/docs/source/markdown/podman-system-connection.1.md new file mode 100644 index 000000000..ed73980d6 --- /dev/null +++ b/docs/source/markdown/podman-system-connection.1.md @@ -0,0 +1,37 @@ +% podman-system-connection(1) + +## NAME +podman\-system\-connection - Record ssh destination for remote podman service + +## SYNOPSIS +**podman system connection** [*options*] [*ssh destination*] + +## DESCRIPTION +Record ssh destination for remote podman service(s). The ssh destination is given as one of: + - [user@]hostname[:port] + - ssh://[user@]hostname[:port] + +The user will be prompted for the remote ssh login password or key file pass phrase as required. `ssh-agent` is supported if it is running. + +## OPTIONS + +**-p**, **--port**=*port* + +Port for ssh destination. The default value is `22`. + +**--socket-path**=*path* + +Path to podman service unix domain socket on the ssh destination host + +## EXAMPLE +``` +$ podman system connection podman.fubar.com + +$ podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222 + +``` +## SEE ALSO +podman-system(1) , containers.conf(5) , connections.conf(5) + +## HISTORY +June 2020, Originally compiled by Jhon Honce (jhonce at redhat dot com) diff --git a/docs/source/markdown/podman-system.1.md b/docs/source/markdown/podman-system.1.md index 5f163c6f0..1f19fd0b6 100644 --- a/docs/source/markdown/podman-system.1.md +++ b/docs/source/markdown/podman-system.1.md @@ -11,15 +11,16 @@ The system command allows you to manage the podman systems ## COMMANDS -| Command | Man Page | Description | -| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | -| df | [podman-system-df(1)](podman-system-df.1.md) | Show podman disk usage. | -| info | [podman-system-info(1)](podman-info.1.md) | Displays Podman related system information. | -| migrate | [podman-system-migrate(1)](podman-system-migrate.1.md)| Migrate existing containers to a new podman version. | -| prune | [podman-system-prune(1)](podman-system-prune.1.md) | Remove all unused container, image and volume data. | -| renumber | [podman-system-renumber(1)](podman-system-renumber.1.md)| Migrate lock numbers to handle a change in maximum number of locks. | -| reset | [podman-system-reset(1)](podman-system-reset.1.md) | Reset storage back to initial state. | -| service | [podman-service(1)](podman-system-service.1.md) | Run an API service | +| Command | Man Page | Description | +| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | +| df | [podman-system-df(1)](podman-system-df.1.md) | Show podman disk usage. | +| connection | [podman-system-connection(1)](podman-system-connection.1.md) | Record ssh destination for remote podman service. | +| info | [podman-system-info(1)](podman-info.1.md) | Displays Podman related system information. | +| migrate | [podman-system-migrate(1)](podman-system-migrate.1.md) | Migrate existing containers to a new podman version. | +| prune | [podman-system-prune(1)](podman-system-prune.1.md) | Remove all unused container, image and volume data. | +| renumber | [podman-system-renumber(1)](podman-system-renumber.1.md) | Migrate lock numbers to handle a change in maximum number of locks. | +| reset | [podman-system-reset(1)](podman-system-reset.1.md) | Reset storage back to initial state. | +| service | [podman-service(1)](podman-system-service.1.md) | Run an API service | ## SEE ALSO diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 2f338452c..ce02ef3a7 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -59,10 +59,10 @@ Podman and libpod currently support an additional `precreate` state which is cal **WARNING**: the `precreate` hook lets you do powerful things, such as adding additional mounts to the runtime configuration. That power also makes it easy to break things. Before reporting libpod errors, try running your container with `precreate` hooks disabled to see if the problem is due to one of your hooks. **--identity**=*path* -Path to SSH identity file -**--passphrase**=*secret* -pass phrase for SSH identity file +Path to ssh identity file. If the identity file has been encrypted, podman prompts the user for the passphrase. +If no identity file is provided and no user is given, podman defaults to the user running the podman command. +Podman prompts for the login password on the remote server. **--log-level**=*level* diff --git a/pkg/bindings/bindings.go b/pkg/bindings/bindings.go index 94f7a45d0..ae5610b0f 100644 --- a/pkg/bindings/bindings.go +++ b/pkg/bindings/bindings.go @@ -8,13 +8,7 @@ package bindings import ( - "errors" - "fmt" - "io" - "os" - "github.com/blang/semver" - "golang.org/x/crypto/ssh/terminal" ) var ( @@ -30,40 +24,3 @@ var ( // APIVersion - 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 a9c61e5ae..584aa55c1 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -1,28 +1,24 @@ package bindings import ( - "bufio" "context" "fmt" "io" - "io/ioutil" "net" "net/http" "net/url" "os" - "path/filepath" "strconv" "strings" - "sync" "time" "github.com/blang/semver" + "github.com/containers/libpod/pkg/terminal" jsoniter "github.com/json-iterator/go" "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" ) var ( @@ -31,8 +27,6 @@ var ( Host: "d", Path: "/v" + APIVersion.String() + "/libpod", } - passPhrase []byte - phraseSync sync.Once ) type APIResponse struct { @@ -77,7 +71,7 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) { // For example tcp://localhost: // or unix:///run/podman/podman.sock // or ssh://@[:port]/run/podman/podman.sock?secure=True -func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase string, identities ...string) (context.Context, error) { +func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) { var ( err error secure bool @@ -86,11 +80,12 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin uri = v } - if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identities) == 0 { - identities = append(identities, v) + if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identity) == 0 { + identity = v } - if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found && passPhrase == "" { + passPhrase := "" + if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found { passPhrase = v } @@ -98,7 +93,6 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin if err != nil { 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 @@ -108,7 +102,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin if err != nil { secure = false } - connection, err = sshClient(_url, secure, passPhrase, identities...) + connection, err = sshClient(_url, secure, passPhrase, identity) case "unix": if !strings.HasPrefix(uri, "unix:///") { // autofix unix://path_element vs unix:///path_element @@ -122,7 +116,7 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, passPhrase strin } connection = tcpClient(_url) default: - return nil, errors.Errorf("'%s' is not a supported schema", _url.Scheme) + return nil, errors.Errorf("unable to create connection. %q is not a supported schema", _url.Scheme) } if err != nil { return nil, errors.Wrapf(err, "Failed to create %sClient", _url.Scheme) @@ -185,16 +179,14 @@ func pingNewConnection(ctx context.Context) error { return errors.Errorf("ping response was %q", response.StatusCode) } -func sshClient(_url *url.URL, secure bool, passPhrase string, identities ...string) (Connection, error) { +func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) { 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) + auth, err := terminal.PublicKey(identity, []byte(passPhrase)) + if err != nil { + return Connection{}, errors.Wrapf(err, "failed to parse identity %q", identity) } + logrus.Debugf("public key signer enabled for identity %q", identity) + 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) @@ -213,7 +205,7 @@ func sshClient(_url *url.URL, secure bool, passPhrase string, identities ...stri callback := ssh.InsecureIgnoreHostKey() if secure { - key := hostKey(_url.Hostname()) + key := terminal.HostKey(_url.Hostname()) if key != nil { callback = ssh.FixedHostKey(key) } @@ -339,63 +331,3 @@ func (h *APIResponse) IsClientError() bool { func (h *APIResponse) IsServerError() bool { return h.Response.StatusCode/100 == 5 } - -func publicKey(path string, passphrase []byte) (ssh.AuthMethod, error) { - key, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - signer, err := ssh.ParsePrivateKey(key) - if err != nil { - 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 - knownHosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts") - fd, err := os.Open(knownHosts) - if err != nil { - logrus.Error(err) - return nil - } - - scanner := bufio.NewScanner(fd) - for scanner.Scan() { - _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) - if err != nil { - logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text()) - continue - } - - for _, h := range hosts { - if h == host { - return key - } - } - } - - return nil -} diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index 1f056bad7..6776d09e9 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -41,9 +41,8 @@ type PodmanConfig struct { ConmonPath string // --conmon flag will set Engine.ConmonPath CPUProfile string // Hidden: Should CPU profile be taken EngineMode EngineMode // ABI or Tunneling mode - Identities []string // ssh identities for connecting to server + Identity string // ssh identity 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 diff --git a/pkg/domain/infra/runtime_abi.go b/pkg/domain/infra/runtime_abi.go index 60d0c6e86..3b344cb08 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.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) 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.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) 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 24a93b888..039a8339b 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.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) 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.NewConnectionWithIdentity(context.Background(), facts.URI, facts.PassPhrase, facts.Identities...) + ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) return &tunnel.ImageEngine{ClientCxt: ctx}, err } return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) diff --git a/pkg/terminal/util.go b/pkg/terminal/util.go new file mode 100644 index 000000000..ab3dc54e4 --- /dev/null +++ b/pkg/terminal/util.go @@ -0,0 +1,133 @@ +package terminal + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sync" + + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/terminal" + "k8s.io/client-go/util/homedir" +) + +var ( + passPhrase []byte + phraseSync sync.Once + password []byte + passwordSync sync.Once +) + +// 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 + } + } +} + +func PublicKey(path string, passphrase []byte) (ssh.AuthMethod, error) { + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + if _, ok := err.(*ssh.PassphraseMissingError); !ok { + return nil, err + } + if len(passphrase) == 0 { + passphrase = ReadPassphrase() + } + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, passphrase) + if err != nil { + return nil, err + } + } + return ssh.PublicKeys(signer), nil +} + +func ReadPassphrase() []byte { + phraseSync.Do(func() { + secret, err := ReadPassword("Key Passphrase: ") + if err != nil { + secret = []byte{} + } + passPhrase = secret + }) + return passPhrase +} + +func ReadLogin() []byte { + passwordSync.Do(func() { + secret, err := ReadPassword("Login password: ") + if err != nil { + secret = []byte{} + } + password = secret + }) + return password +} + +func HostKey(host string) ssh.PublicKey { + // parse OpenSSH known_hosts file + // ssh or use ssh-keyscan to get initial key + knownHosts := filepath.Join(homedir.HomeDir(), ".ssh", "known_hosts") + fd, err := os.Open(knownHosts) + if err != nil { + logrus.Error(err) + return nil + } + + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) + if err != nil { + logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text()) + continue + } + + for _, h := range hosts { + if h == host { + return key + } + } + } + + return nil +} -- cgit v1.2.3-54-g00ecf