diff --git a/pkg/config/config.go b/pkg/config/config.go index 858f961b6..cde7cec53 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 @@ -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 ( diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 9a13981b1..923beaf30 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -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")) }) @@ -620,7 +621,7 @@ 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) @@ -628,6 +629,7 @@ image_copy_tmp_dir="storage"` 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")) @@ -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) @@ -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")) @@ -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()) }) diff --git a/pkg/ssh/connection_golang.go b/pkg/ssh/connection_golang.go index a5c1be89c..8ec3c45ed 100644 --- a/pkg/ssh/connection_golang.go +++ b/pkg/ssh/connection_golang.go @@ -3,6 +3,7 @@ package ssh import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net" @@ -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 } @@ -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 } @@ -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 } @@ -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 @@ -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) } @@ -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 +} diff --git a/pkg/ssh/types.go b/pkg/ssh/types.go index f22b5fba9..16512c43f 100644 --- a/pkg/ssh/types.go +++ b/pkg/ssh/types.go @@ -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 { diff --git a/pkg/ssh/utils.go b/pkg/ssh/utils.go index c15745015..b05105d9c 100644 --- a/pkg/ssh/utils.go +++ b/pkg/ssh/utils.go @@ -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 @@ -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") } }