Skip to content

Commit

Permalink
Merge pull request containers#6469 from jwhonce/wip/auth
Browse files Browse the repository at this point in the history
V2 Add support for ssh authentication methods
  • Loading branch information
openshift-merge-robot authored Jun 3, 2020
2 parents df0141d + cbca625 commit cbfb498
Show file tree
Hide file tree
Showing 14 changed files with 1,874 additions and 30 deletions.
17 changes: 15 additions & 2 deletions cmd/podman/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {

cfg := registry.PodmanConfig()

// Help is a special case, no need for more setup
if cmd.Name() == "help" {
return nil
}

// Prep the engines
if _, err := registry.NewImageEngine(cmd, args); err != nil {
return err
Expand Down Expand Up @@ -150,6 +155,11 @@ func persistentPostRunE(cmd *cobra.Command, args []string) error {
// TODO: Remove trace statement in podman V2.1
logrus.Debugf("Called %s.PersistentPostRunE(%s)", cmd.Name(), strings.Join(os.Args, " "))

// Help is a special case, no need for more cleanup
if cmd.Name() == "help" {
return nil
}

cfg := registry.PodmanConfig()
if cmd.Flag("cpu-profile").Changed {
pprof.StopCPUProfile()
Expand Down Expand Up @@ -191,8 +201,11 @@ func loggingHook() {

func rootFlags(opts *entities.PodmanConfig, flags *pflag.FlagSet) {
// V2 flags
flags.StringVarP(&opts.Uri, "remote", "r", registry.DefaultAPIAddress(), "URL to access Podman service")
flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file")
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")

cfg := opts.Config
flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")")
Expand Down
14 changes: 10 additions & 4 deletions docs/source/markdown/podman.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ Podman and libpod currently support an additional `precreate` state which is cal
**--identity**=*path*
Path to SSH identity file

**--passphrase**=*secret*
pass phrase for SSH identity file

**--log-level**=*level*

Log messages above specified level: debug, info, warn, error (default), fatal or panic (default: "error")
Expand All @@ -73,18 +76,21 @@ When namespace is set, created containers and pods will join the given namespace
**--network-cmd-path**=*path*
Path to the command binary to use for setting up a network. It is currently only used for setting up a slirp4netns network. If "" is used then the binary is looked up using the $PATH environment variable.

**--remote**, **-r**=*url*
URL to access Podman service (default "unix:/run/user/3267/podman/podman.sock")
**--remote**, **-r**
Access Podman service will be remote

**--url**=*value*
URL to access Podman service (default from `containers.conf`, rootless "unix://run/user/$UID/podman/podman.sock" or as root "unix://run/podman/podman.sock).

**--root**=*value*

Storage root dir in which data, including images, is stored (default: "/var/lib/containers/storage" for UID 0, "$HOME/.local/share/containers/storage" for other users).
Default root dir is configured in `/etc/containers/storage.conf`.
Default root dir configured in `/etc/containers/storage.conf`.

**--runroot**=*value*

Storage state directory where all state information is stored (default: "/var/run/containers/storage" for UID 0, "/var/run/user/$UID/run" for other users).
Default state dir is configured in `/etc/containers/storage.conf`.
Default state dir configured in `/etc/containers/storage.conf`.

**--runtime**=*value*

Expand Down
43 changes: 43 additions & 0 deletions pkg/bindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
package bindings

import (
"errors"
"fmt"
"io"
"os"

"github.com/blang/semver"
"golang.org/x/crypto/ssh/terminal"
)

var (
Expand All @@ -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 `<secret>/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
}
}
}
80 changes: 66 additions & 14 deletions pkg/bindings/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"time"

"github.com/blang/semver"
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"
)

Expand All @@ -29,6 +31,8 @@ var (
Host: "d",
Path: "/v" + APIVersion.String() + "/libpod",
}
passPhrase []byte
phraseSync sync.Once
)

type APIResponse struct {
Expand Down Expand Up @@ -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.
Expand All @@ -69,23 +77,28 @@ func JoinURL(elements ...string) string {
// For example tcp://localhost:<port>
// or unix:///run/podman/podman.sock
// or ssh://<user>@<host>[: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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -307,20 +341,38 @@ 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
}

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
Expand Down
6 changes: 4 additions & 2 deletions pkg/domain/entities/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/domain/infra/runtime_abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions pkg/domain/infra/runtime_tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/libpod_suite_remote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ func SkipIfRootlessV2() {

// Podman is the exec call to podman on the filesystem
func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration {
var remoteArgs = []string{"--remote", p.RemoteSocket}
var remoteArgs = []string{"--remote", "--url", p.RemoteSocket}
remoteArgs = append(remoteArgs, args...)
podmanSession := p.PodmanBase(remoteArgs, false, false)
return &PodmanSessionIntegration{podmanSession}
}

// PodmanExtraFiles is the exec call to podman on the filesystem and passes down extra files
func (p *PodmanTestIntegration) PodmanExtraFiles(args []string, extraFiles []*os.File) *PodmanSessionIntegration {
var remoteArgs = []string{"--remote", p.RemoteSocket}
var remoteArgs = []string{"--remote", "--url", p.RemoteSocket}
remoteArgs = append(remoteArgs, args...)
podmanSession := p.PodmanAsUserBase(remoteArgs, 0, 0, "", nil, false, false, extraFiles)
return &PodmanSessionIntegration{podmanSession}
}

// PodmanNoCache calls podman with out adding the imagecache
func (p *PodmanTestIntegration) PodmanNoCache(args []string) *PodmanSessionIntegration {
var remoteArgs = []string{"--remote", p.RemoteSocket}
var remoteArgs = []string{"--remote", "--url", p.RemoteSocket}
remoteArgs = append(remoteArgs, args...)
podmanSession := p.PodmanBase(remoteArgs, false, true)
return &PodmanSessionIntegration{podmanSession}
Expand Down
2 changes: 1 addition & 1 deletion test/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (p *PodmanTest) PodmanAsUserBase(args []string, uid, gid uint32, cwd string
podmanBinary = p.RemotePodmanBinary
}
if p.RemoteTest {
podmanOptions = append([]string{"--remote", p.RemoteSocket}, podmanOptions...)
podmanOptions = append([]string{"--remote", "--url", p.RemoteSocket}, podmanOptions...)
}
if env == nil {
fmt.Printf("Running: %s %s\n", podmanBinary, strings.Join(podmanOptions, " "))
Expand Down
Loading

0 comments on commit cbfb498

Please sign in to comment.