Skip to content

Commit

Permalink
podman machine and podman-remote need some softer handling when it co…
Browse files Browse the repository at this point in the history
…mes to key verification

this ensures that podman machine will still work (until we want to make this mandatory). I made the call back function more verbose so we know what is happening from now on.

Signed-off-by: Charlie Doern <cdoern@redhat.com>
  • Loading branch information
cdoern committed Sep 19, 2022
1 parent 3f02448 commit 7231b94
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 30 deletions.
19 changes: 11 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,9 @@ type Destination struct {

// Identity file with ssh key, optional
Identity string `toml:"identity,omitempty"`

// isMachine describes if the remote destination is a machine.
IsMachine bool `toml:"is_machine,omitempty"`
}

// NewConfig creates a new Config. It starts with an empty config and, if
Expand Down Expand Up @@ -1235,32 +1238,32 @@ func Reload() (*Config, error) {
return defConfig()
}

func (c *Config) ActiveDestination() (uri, identity string, err error) {
func (c *Config) ActiveDestination() (uri, identity string, machine bool, err error) {
if uri, found := os.LookupEnv("CONTAINER_HOST"); found {
if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found {
identity = v
}
return uri, identity, nil
return uri, identity, false, nil
}
connEnv := os.Getenv("CONTAINER_CONNECTION")
switch {
case connEnv != "":
d, found := c.Engine.ServiceDestinations[connEnv]
if !found {
return "", "", fmt.Errorf("environment variable CONTAINER_CONNECTION=%q service destination not found", connEnv)
return "", "", false, fmt.Errorf("environment variable CONTAINER_CONNECTION=%q service destination not found", connEnv)
}
return d.URI, d.Identity, nil
return d.URI, d.Identity, d.IsMachine, nil

case c.Engine.ActiveService != "":
d, found := c.Engine.ServiceDestinations[c.Engine.ActiveService]
if !found {
return "", "", fmt.Errorf("%q service destination not found", c.Engine.ActiveService)
return "", "", false, fmt.Errorf("%q service destination not found", c.Engine.ActiveService)
}
return d.URI, d.Identity, nil
return d.URI, d.Identity, d.IsMachine, nil
case c.Engine.RemoteURI != "":
return c.Engine.RemoteURI, c.Engine.RemoteIdentity, nil
return c.Engine.RemoteURI, c.Engine.RemoteIdentity, false, nil
}
return "", "", errors.New("no service destination configured")
return "", "", false, errors.New("no service destination configured")
}

var (
Expand Down
12 changes: 8 additions & 4 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,10 @@ image_copy_tmp_dir="storage"`
cfg, err = ReadCustomConfig()
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

u, i, err := cfg.ActiveDestination()
u, i, m, err := cfg.ActiveDestination()
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

gomega.Expect(m).To(gomega.Equal(false))
gomega.Expect(u).To(gomega.Equal("https://qa/run/podman/podman.sock"))
gomega.Expect(i).To(gomega.Equal("/.ssh/id_rsa"))
})
Expand Down Expand Up @@ -620,14 +621,15 @@ image_copy_tmp_dir="storage"`
oldContainerConnection, hostEnvSet := os.LookupEnv("CONTAINER_CONNECTION")
os.Setenv("CONTAINER_CONNECTION", "QB")

u, i, err := cfg.ActiveDestination()
u, i, m, err := cfg.ActiveDestination()
// Undo that
if hostEnvSet {
os.Setenv("CONTAINER_CONNECTION", oldContainerConnection)
} else {
os.Unsetenv("CONTAINER_CONNECTION")
}
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(m).To(gomega.Equal(false))

gomega.Expect(u).To(gomega.Equal("https://qb/run/podman/podman.sock"))
gomega.Expect(i).To(gomega.Equal("/.ssh/qb_id_rsa"))
Expand Down Expand Up @@ -660,7 +662,8 @@ image_copy_tmp_dir="storage"`
os.Setenv("CONTAINER_HOST", "foo.bar")
os.Setenv("CONTAINER_SSHKEY", "/.ssh/newid_rsa")

u, i, err := cfg.ActiveDestination()
u, i, m, err := cfg.ActiveDestination()

// Undo that
if hostEnvSet {
os.Setenv("CONTAINER_HOST", oldContainerHost)
Expand All @@ -675,6 +678,7 @@ image_copy_tmp_dir="storage"`
}

gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
gomega.Expect(m).To(gomega.Equal(false))

gomega.Expect(u).To(gomega.Equal("foo.bar"))
gomega.Expect(i).To(gomega.Equal("/.ssh/newid_rsa"))
Expand All @@ -684,7 +688,7 @@ image_copy_tmp_dir="storage"`
cfg, err := ReadCustomConfig()
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())

_, _, err = cfg.ActiveDestination()
_, _, _, err = cfg.ActiveDestination()
gomega.Expect(err).Should(gomega.HaveOccurred())
})

Expand Down
83 changes: 73 additions & 10 deletions pkg/ssh/connection_golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ssh
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -70,7 +71,7 @@ func golangConnectionDial(options ConnectionDialOptions) (*ConnectionDialReport,
if err != nil {
return nil, err
}
cfg, err := ValidateAndConfigure(uri, options.Identity)
cfg, err := ValidateAndConfigure(uri, options.Identity, options.InsecureIsMachineConnection)
if err != nil {
return nil, err
}
Expand All @@ -84,12 +85,15 @@ func golangConnectionDial(options ConnectionDialOptions) (*ConnectionDialReport,
}

func golangConnectionExec(options ConnectionExecOptions) (*ConnectionExecReport, error) {
if !strings.HasPrefix(options.Host, "ssh://") {
options.Host = "ssh://" + options.Host
}
_, uri, err := Validate(options.User, options.Host, options.Port, options.Identity)
if err != nil {
return nil, err
}

cfg, err := ValidateAndConfigure(uri, options.Identity)
cfg, err := ValidateAndConfigure(uri, options.Identity, false)
if err != nil {
return nil, err
}
Expand All @@ -111,11 +115,15 @@ func golangConnectionScp(options ConnectionScpOptions) (*ConnectionScpReport, er
return nil, err
}

// removed for parsing
if !strings.HasPrefix(host, "ssh://") {
host = "ssh://" + host
}
_, uri, err := Validate(options.User, host, options.Port, options.Identity)
if err != nil {
return nil, err
}
cfg, err := ValidateAndConfigure(uri, options.Identity)
cfg, err := ValidateAndConfigure(uri, options.Identity, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -209,7 +217,7 @@ func GetUserInfo(uri *url.URL) (*url.Userinfo, error) {
// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid
// iden iden can be blank to mean no identity key
// once the function validates the information it creates and returns an ssh.ClientConfig.
func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) {
func ValidateAndConfigure(uri *url.URL, iden string, insecureIsMachineConnection bool) (*ssh.ClientConfig, error) {
var signers []ssh.Signer
passwd, passwdSet := uri.User.Password()
if iden != "" { // iden might be blank if coming from image scp or if no validation is needed
Expand Down Expand Up @@ -272,23 +280,61 @@ func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error)
if err != nil {
return nil, err
}
keyFilePath := filepath.Join(homedir.Get(), ".ssh", "known_hosts")
known, err := knownhosts.New(keyFilePath)
if err != nil {
return nil, fmt.Errorf("creating host key callback function for %s: %w", keyFilePath, err)

var callback ssh.HostKeyCallback
var keyErr *knownhosts.KeyError
if insecureIsMachineConnection {
callback = ssh.InsecureIgnoreHostKey()
} else {
callback = ssh.HostKeyCallback(func(host string, remote net.Addr, pubKey ssh.PublicKey) error {
keyFilePath := filepath.Join(homedir.Get(), ".ssh", "known_hosts")
known, err := knownhosts.New(keyFilePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
logrus.Warn("please create a known_hosts file. The next time this host is connected to, podman will add it to known_hosts")
return nil
}
return err
}
// we need to check if there is an error from reading known hosts for this public key and if there is an error, what is it, and why is it happening?
// if it is a key mismatch we want to error since we know the host using another key
// however, if it is a general error not because of a known key, we want to add our key to the known_hosts file
hErr := known(host, remote, pubKey)
// if keyErr.Want is not empty, we are receiving a different key meaning the host is known but we are using the wrong key
as := errors.As(hErr, &keyErr)
switch {
case as && len(keyErr.Want) > 0:
logrus.Warnf("ssh host key mismatch for host %s, got key %s of type %s", host, ssh.FingerprintSHA256(pubKey), pubKey.Type())
return keyErr
// if keyErr.Want is empty that just means we do not know this host yet, add it.
case as && len(keyErr.Want) == 0:
// write to known_hosts
err := addKnownHostsEntry(host, pubKey)
if err != nil {
if os.IsNotExist(err) {
logrus.Warn("podman will soon require a known_hosts file to function properly.")
return nil
}
return err
}
case hErr != nil:
return hErr
}
return nil
})
}

cfg := &ssh.ClientConfig{
User: uri.User.Username(),
Auth: authMethods,
HostKeyCallback: known,
HostKeyCallback: callback,
Timeout: tick,
}
return cfg, nil
}

func getUDS(uri *url.URL, iden string) (string, error) {
cfg, err := ValidateAndConfigure(uri, iden)
cfg, err := ValidateAndConfigure(uri, iden, false)
if err != nil {
return "", fmt.Errorf("failed to validate: %w", err)
}
Expand Down Expand Up @@ -324,3 +370,20 @@ func getUDS(uri *url.URL, iden string) (string, error) {
}
return info.Host.RemoteSocket.Path, nil
}

// addKnownHostsEntry adds (host, pubKey) to user’s known_hosts.
func addKnownHostsEntry(host string, pubKey ssh.PublicKey) error {
hd := homedir.Get()
known := filepath.Join(hd, ".ssh", "known_hosts")
f, err := os.OpenFile(known, os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer f.Close()
l := knownhosts.Line([]string{host}, pubKey)
if _, err = f.WriteString("\n" + l + "\n"); err != nil {
return err
}
logrus.Infof("key %s added to %s", ssh.FingerprintSHA256(pubKey), known)
return nil
}
13 changes: 7 additions & 6 deletions pkg/ssh/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ type ConnectionCreateOptions struct {
}

type ConnectionDialOptions struct {
Host string
Identity string
User *url.Userinfo
Port int
Auth []string
Timeout time.Duration
Host string
Identity string
User *url.Userinfo
Port int
Auth []string
Timeout time.Duration
InsecureIsMachineConnection bool
}

type ConnectionDialReport struct {
Expand Down
5 changes: 3 additions & 2 deletions pkg/ssh/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func Validate(user *url.Userinfo, path string, port int, identity string) (*conf
if strings.Contains(path, "/run") {
sock = strings.Split(path, "/run")[1]
}
// url.Parse NEEDS ssh://, if this ever fails or returns some nonsense, that is why.
uri, err := url.Parse(path)
if err != nil {
return nil, nil, err
Expand All @@ -33,9 +34,9 @@ func Validate(user *url.Userinfo, path string, port int, identity string) (*conf

if uri.Port() == "" {
if port != 0 {
uri.Host = net.JoinHostPort(uri.Hostname(), strconv.Itoa(port))
uri.Host = net.JoinHostPort(uri.Host, strconv.Itoa(port))
} else {
uri.Host = net.JoinHostPort(uri.Hostname(), "22")
uri.Host = net.JoinHostPort(uri.Host, "22")
}
}

Expand Down

0 comments on commit 7231b94

Please sign in to comment.