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 --- 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 +++++++++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 132 deletions(-) create mode 100644 pkg/terminal/util.go (limited to 'pkg') 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