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 7, 2022
1 parent 0fe9ce2 commit 7fd7c2c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 32 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 whenever podman runs in a podman machine environment.
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
76 changes: 62 additions & 14 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 @@ -89,7 +90,7 @@ func golangConnectionExec(options ConnectionExecOptions) (*ConnectionExecReport,
return nil, err
}

cfg, err := ValidateAndConfigure(uri, options.Identity)
cfg, err := ValidateAndConfigure(uri, options.Identity, false)
if err != nil {
return nil, err
}
Expand All @@ -115,7 +116,7 @@ func golangConnectionScp(options ConnectionScpOptions) (*ConnectionScpReport, er
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 +210,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 +273,53 @@ 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 keyErr *knownhosts.KeyError
cfg := &ssh.ClientConfig{
User: uri.User.Username(),
Auth: authMethods,
HostKeyCallback: known,
Timeout: tick,
User: uri.User.Username(),
Auth: authMethods,
HostKeyCallback: 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 os.IsNotExist(err) && insecureIsMachineConnection {
logrus.Warn("please create a known_hosts file and recreate the machine to add the public key entry")
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 mismatchm 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 ot add our key ot the known_hosts file
hErr := known(host, remote, pubKey)
// if keyErr.Want is not empty, we are recieving a different key meaning the host is known but we are using the wrong key
if errors.As(hErr, &keyErr) && 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.
} else if errors.As(hErr, &keyErr) && len(keyErr.Want) == 0 {
// write to known_hosts
err = writeKnownHosts(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
}
logrus.Infof("key %s added to %s", ssh.FingerprintSHA256(pubKey), known)
} else if hErr != nil {
return hErr
}
return nil
}),
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 +355,20 @@ func getUDS(uri *url.URL, iden string) (string, error) {
}
return info.Host.RemoteSocket.Path, nil
}

// writeKnownHosts writes to the given users' known_hosts file
func writeKnownHosts(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()
// normalize the line, then add it to known_hosts
l := knownhosts.Line([]string{host}, pubKey)
if _, err = f.WriteString("\n" + l); err != nil {
return err
}
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

0 comments on commit 7fd7c2c

Please sign in to comment.