Skip to content

Commit

Permalink
Merge pull request #1136 from cdoern/ssh
Browse files Browse the repository at this point in the history
add proper handling for golang ssh
  • Loading branch information
openshift-merge-robot authored Sep 26, 2022
2 parents fd97636 + 2b55d99 commit 590004b
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
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)
var keyErr *knownhosts.KeyError
// 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 590004b

Please sign in to comment.