podman machine ssh handling

add the key used in newly initialized machines to the user's known_hosts file. This ensures that golang will be able to ssh into the machine using
podman-remote. Also, remove the /dev/null redirection for podman machine ssh's known_hosts file.

resolves #15347

Signed-off-by: Charlie Doern <cdoern@redhat.com>
Signed-off-by: cdoern <cbdoer23@g.holycross.edu>
This commit is contained in:
Charlie Doern
2022-08-23 11:04:54 -04:00
committed by cdoern
parent 5fc6d95a94
commit 2e4e1bb97c
18 changed files with 134 additions and 60 deletions

View File

@ -101,7 +101,7 @@ func remoteConnectionUsername() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
dest, _, err := cfg.ActiveDestination() dest, _, _, err := cfg.ActiveDestination()
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -176,7 +176,7 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error {
setupConnection := func() error { setupConnection := func() error {
var err error var err error
cfg.URI, cfg.Identity, err = cfg.ActiveDestination() cfg.URI, cfg.Identity, cfg.MachineMode, err = cfg.ActiveDestination()
if err != nil { if err != nil {
return fmt.Errorf("failed to resolve active destination: %w", err) return fmt.Errorf("failed to resolve active destination: %w", err)
} }
@ -368,10 +368,13 @@ func loggingHook() {
func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) {
cfg := opts.Config cfg := opts.Config
srv, uri, ident := resolveDestination() srv, uri, ident, machine := resolveDestination()
lFlags := cmd.Flags() lFlags := cmd.Flags()
// non configurable option to help ssh dialing
opts.MachineMode = machine
sshFlagName := "ssh" sshFlagName := "ssh"
lFlags.StringVar(&opts.SSHMode, sshFlagName, string(ssh.GolangMode), "define the ssh mode") lFlags.StringVar(&opts.SSHMode, sshFlagName, string(ssh.GolangMode), "define the ssh mode")
_ = cmd.RegisterFlagCompletionFunc(sshFlagName, common.AutocompleteSSH) _ = cmd.RegisterFlagCompletionFunc(sshFlagName, common.AutocompleteSSH)
@ -513,26 +516,26 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) {
} }
} }
func resolveDestination() (string, string, string) { func resolveDestination() (string, string, string, bool) {
if uri, found := os.LookupEnv("CONTAINER_HOST"); found { if uri, found := os.LookupEnv("CONTAINER_HOST"); found {
var ident string var ident string
if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found { if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found {
ident = v ident = v
} }
return "", uri, ident return "", uri, ident, false
} }
cfg, err := config.ReadCustomConfig() cfg, err := config.ReadCustomConfig()
if err != nil { if err != nil {
logrus.Warning(fmt.Errorf("unable to read local containers.conf: %w", err)) logrus.Warning(fmt.Errorf("unable to read local containers.conf: %w", err))
return "", registry.DefaultAPIAddress(), "" return "", registry.DefaultAPIAddress(), "", false
} }
uri, ident, err := cfg.ActiveDestination() uri, ident, machine, err := cfg.ActiveDestination()
if err != nil { if err != nil {
return "", registry.DefaultAPIAddress(), "" return "", registry.DefaultAPIAddress(), "", false
} }
return cfg.Engine.ActiveService, uri, ident return cfg.Engine.ActiveService, uri, ident, machine
} }
func formatError(err error) string { func formatError(err error) string {

View File

@ -105,8 +105,9 @@ func inspect(cmd *cobra.Command, args []string) error {
r := namedDestination{ r := namedDestination{
Name: k, Name: k,
Destination: config.Destination{ Destination: config.Destination{
Identity: v.Identity, Identity: v.Identity,
URI: v.URI, URI: v.URI,
IsMachine: v.IsMachine,
}, },
Default: def, Default: def,
} }

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/containernetworking/cni v1.1.2 github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.1.1 github.com/containernetworking/plugins v1.1.1
github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36 github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36
github.com/containers/common v0.49.2-0.20220920205255-8062f81c5497 github.com/containers/common v0.49.2-0.20220926195839-590004b80685
github.com/containers/conmon v2.0.20+incompatible github.com/containers/conmon v2.0.20+incompatible
github.com/containers/image/v5 v5.22.1-0.20220919112403-fe51f7ffca50 github.com/containers/image/v5 v5.22.1-0.20220919112403-fe51f7ffca50
github.com/containers/ocicrypt v1.1.5 github.com/containers/ocicrypt v1.1.5

3
go.sum
View File

@ -419,8 +419,9 @@ github.com/containernetworking/plugins v1.1.1 h1:+AGfFigZ5TiQH00vhR8qPeSatj53eNG
github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8= github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8=
github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36 h1:LTSEbPUbs0slJSJ+IH6atAjYDe0IDzA0sPgBLjT1yAo= github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36 h1:LTSEbPUbs0slJSJ+IH6atAjYDe0IDzA0sPgBLjT1yAo=
github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36/go.mod h1:cY3pGPyMmrNp/sEDK8ESoBOf4hoNovptZSI0oyo8eQM= github.com/containers/buildah v1.27.1-0.20220921131114-d3064796af36/go.mod h1:cY3pGPyMmrNp/sEDK8ESoBOf4hoNovptZSI0oyo8eQM=
github.com/containers/common v0.49.2-0.20220920205255-8062f81c5497 h1:LB9SxcAglqSAHiiHGacN1Abi0ZL9haJpQ1numVlqtxM=
github.com/containers/common v0.49.2-0.20220920205255-8062f81c5497/go.mod h1:ZnhOPR/07UOkfIg5bezUpBilGjxEUdaeoUpu7gRBGc0= github.com/containers/common v0.49.2-0.20220920205255-8062f81c5497/go.mod h1:ZnhOPR/07UOkfIg5bezUpBilGjxEUdaeoUpu7gRBGc0=
github.com/containers/common v0.49.2-0.20220926195839-590004b80685 h1:rutCgIKcew85mTUO0JTnh7XDXQfaTz/qQ3HyQHb0jZE=
github.com/containers/common v0.49.2-0.20220926195839-590004b80685/go.mod h1:ZnhOPR/07UOkfIg5bezUpBilGjxEUdaeoUpu7gRBGc0=
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg= github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I= github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
github.com/containers/image/v5 v5.22.1-0.20220907162003-651744379993/go.mod h1:/Ruurd87C6Ap45t1PWNOD8+SGwiZbk79XCgs1iUTvYA= github.com/containers/image/v5 v5.22.1-0.20220907162003-651744379993/go.mod h1:/Ruurd87C6Ap45t1PWNOD8+SGwiZbk79XCgs1iUTvYA=

View File

@ -59,7 +59,7 @@ func JoinURL(elements ...string) string {
// NewConnection creates a new service connection without an identity // NewConnection creates a new service connection without an identity
func NewConnection(ctx context.Context, uri string) (context.Context, error) { func NewConnection(ctx context.Context, uri string) (context.Context, error) {
return NewConnectionWithIdentity(ctx, uri, "") return NewConnectionWithIdentity(ctx, uri, "", false)
} }
// NewConnectionWithIdentity takes a URI as a string and returns a context with the // NewConnectionWithIdentity takes a URI as a string and returns a context with the
@ -70,7 +70,7 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) {
// For example tcp://localhost:<port> // For example tcp://localhost:<port>
// or unix:///run/podman/podman.sock // or unix:///run/podman/podman.sock
// or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True // or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True
func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) { func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, machine bool) (context.Context, error) {
var ( var (
err error err error
) )
@ -96,10 +96,11 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string)
return nil, err return nil, err
} }
conn, err := ssh.Dial(&ssh.ConnectionDialOptions{ conn, err := ssh.Dial(&ssh.ConnectionDialOptions{
Host: uri, Host: uri,
Identity: identity, Identity: identity,
User: _url.User, User: _url.User,
Port: port, Port: port,
InsecureIsMachineConnection: machine,
}, "golang") }, "golang")
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -54,4 +54,5 @@ type PodmanConfig struct {
StorageDriver string StorageDriver string
StorageOpts []string StorageOpts []string
SSHMode string SSHMode string
MachineMode bool
} }

View File

@ -21,7 +21,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine,
r, err := NewLibpodRuntime(facts.FlagSet, facts) r, err := NewLibpodRuntime(facts.FlagSet, facts)
return r, err return r, err
case entities.TunnelMode: case entities.TunnelMode:
ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity, facts.MachineMode)
return &tunnel.ContainerEngine{ClientCtx: ctx}, err return &tunnel.ContainerEngine{ClientCtx: ctx}, err
} }
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
@ -35,7 +35,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error)
return r, err return r, err
case entities.TunnelMode: case entities.TunnelMode:
// TODO: look at me! // TODO: look at me!
ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity) ctx, err := bindings.NewConnectionWithIdentity(context.Background(), facts.URI, facts.Identity, facts.MachineMode)
return &tunnel.ImageEngine{ClientCtx: ctx}, err return &tunnel.ImageEngine{ClientCtx: ctx}, err
} }
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)

View File

@ -18,12 +18,12 @@ var (
connection *context.Context connection *context.Context
) )
func newConnection(uri string, identity string) (context.Context, error) { func newConnection(uri string, identity string, machine bool) (context.Context, error) {
connectionMutex.Lock() connectionMutex.Lock()
defer connectionMutex.Unlock() defer connectionMutex.Unlock()
if connection == nil { if connection == nil {
ctx, err := bindings.NewConnectionWithIdentity(context.Background(), uri, identity) ctx, err := bindings.NewConnectionWithIdentity(context.Background(), uri, identity, machine)
if err != nil { if err != nil {
return ctx, err return ctx, err
} }
@ -37,7 +37,7 @@ func NewContainerEngine(facts *entities.PodmanConfig) (entities.ContainerEngine,
case entities.ABIMode: case entities.ABIMode:
return nil, fmt.Errorf("direct runtime not supported") return nil, fmt.Errorf("direct runtime not supported")
case entities.TunnelMode: case entities.TunnelMode:
ctx, err := newConnection(facts.URI, facts.Identity) ctx, err := newConnection(facts.URI, facts.Identity, facts.MachineMode)
return &tunnel.ContainerEngine{ClientCtx: ctx}, err return &tunnel.ContainerEngine{ClientCtx: ctx}, err
} }
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)
@ -49,7 +49,7 @@ func NewImageEngine(facts *entities.PodmanConfig) (entities.ImageEngine, error)
case entities.ABIMode: case entities.ABIMode:
return nil, fmt.Errorf("direct image runtime not supported") return nil, fmt.Errorf("direct image runtime not supported")
case entities.TunnelMode: case entities.TunnelMode:
ctx, err := newConnection(facts.URI, facts.Identity) ctx, err := newConnection(facts.URI, facts.Identity, facts.MachineMode)
return &tunnel.ImageEngine{ClientCtx: ctx}, err return &tunnel.ImageEngine{ClientCtx: ctx}, err
} }
return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode) return nil, fmt.Errorf("runtime mode '%v' is not supported", facts.EngineMode)

View File

@ -25,7 +25,8 @@ func AddConnection(uri fmt.Stringer, name, identity string, isDefault bool) erro
cfg.Engine.ActiveService = name cfg.Engine.ActiveService = name
} }
dst := config.Destination{ dst := config.Destination{
URI: uri.String(), URI: uri.String(),
IsMachine: true,
} }
dst.Identity = identity dst.Identity = identity
if cfg.Engine.ServiceDestinations == nil { if cfg.Engine.ServiceDestinations == nil {

View File

@ -1,8 +1,6 @@
package e2e_test package e2e_test
import ( import (
"os"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec" . "github.com/onsi/gomega/gexec"
@ -24,10 +22,6 @@ var _ = Describe("run basic podman commands", func() {
It("Basic ops", func() { It("Basic ops", func() {
// golangci-lint has trouble with actually skipping tests marked Skip // golangci-lint has trouble with actually skipping tests marked Skip
// so skip it on cirrus envs and where CIRRUS_CI isn't set. // so skip it on cirrus envs and where CIRRUS_CI isn't set.
if os.Getenv("CIRRUS_CI") != "false" {
Skip("FIXME: #15347 - ssh know hosts broken - fails on PR runs and on x86_64")
}
name := randomString() name := randomString()
i := new(initMachine) i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run()

View File

@ -405,6 +405,7 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
WritePath: v.getIgnitionFile(), WritePath: v.getIgnitionFile(),
UID: v.UID, UID: v.UID,
} }
err = machine.NewIgnitionFile(ign) err = machine.NewIgnitionFile(ign)
return err == nil, err return err == nil, err
} }
@ -1033,7 +1034,7 @@ func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error {
sshDestination := username + "@localhost" sshDestination := username + "@localhost"
port := strconv.Itoa(v.Port) port := strconv.Itoa(v.Port)
args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile=/dev/null", args := []string{"-i", v.IdentityPath, "-p", port, sshDestination,
"-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="}
if len(opts.Args) > 0 { if len(opts.Args) > 0 {
args = append(args, opts.Args...) args = append(args, opts.Args...)

View File

@ -63,6 +63,9 @@ func toPlatformString(os, arch, variant string) string {
// * 2) a bool indicating whether architecture, os or variant were set (some callers need that to decide whether they need to throw an error) // * 2) a bool indicating whether architecture, os or variant were set (some callers need that to decide whether they need to throw an error)
// * 3) a fatal error that occurred prior to check for matches (e.g., storage errors etc.) // * 3) a fatal error that occurred prior to check for matches (e.g., storage errors etc.)
func (i *Image) matchesPlatform(ctx context.Context, os, arch, variant string) (error, bool, error) { func (i *Image) matchesPlatform(ctx context.Context, os, arch, variant string) (error, bool, error) {
if err := i.isCorrupted(""); err != nil {
return err, false, nil
}
inspectInfo, err := i.inspectInfo(ctx) inspectInfo, err := i.inspectInfo(ctx)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("inspecting image: %w", err) return nil, false, fmt.Errorf("inspecting image: %w", err)

View File

@ -613,6 +613,9 @@ type Destination struct {
// Identity file with ssh key, optional // Identity file with ssh key, optional
Identity string `toml:"identity,omitempty"` 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 // NewConfig creates a new Config. It starts with an empty config and, if
@ -1235,32 +1238,32 @@ func Reload() (*Config, error) {
return defConfig() 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 uri, found := os.LookupEnv("CONTAINER_HOST"); found {
if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found { if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found {
identity = v identity = v
} }
return uri, identity, nil return uri, identity, false, nil
} }
connEnv := os.Getenv("CONTAINER_CONNECTION") connEnv := os.Getenv("CONTAINER_CONNECTION")
switch { switch {
case connEnv != "": case connEnv != "":
d, found := c.Engine.ServiceDestinations[connEnv] d, found := c.Engine.ServiceDestinations[connEnv]
if !found { 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 != "": case c.Engine.ActiveService != "":
d, found := c.Engine.ServiceDestinations[c.Engine.ActiveService] d, found := c.Engine.ServiceDestinations[c.Engine.ActiveService]
if !found { 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 != "": 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 ( var (

View File

@ -3,6 +3,7 @@ package ssh
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -70,7 +71,7 @@ func golangConnectionDial(options ConnectionDialOptions) (*ConnectionDialReport,
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := ValidateAndConfigure(uri, options.Identity) cfg, err := ValidateAndConfigure(uri, options.Identity, options.InsecureIsMachineConnection)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -84,12 +85,15 @@ func golangConnectionDial(options ConnectionDialOptions) (*ConnectionDialReport,
} }
func golangConnectionExec(options ConnectionExecOptions) (*ConnectionExecReport, error) { 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) _, uri, err := Validate(options.User, options.Host, options.Port, options.Identity)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := ValidateAndConfigure(uri, options.Identity) cfg, err := ValidateAndConfigure(uri, options.Identity, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,11 +115,15 @@ func golangConnectionScp(options ConnectionScpOptions) (*ConnectionScpReport, er
return nil, err return nil, err
} }
// removed for parsing
if !strings.HasPrefix(host, "ssh://") {
host = "ssh://" + host
}
_, uri, err := Validate(options.User, host, options.Port, options.Identity) _, uri, err := Validate(options.User, host, options.Port, options.Identity)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := ValidateAndConfigure(uri, options.Identity) cfg, err := ValidateAndConfigure(uri, options.Identity, false)
if err != nil { if err != nil {
return nil, err 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 // 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 // iden iden can be blank to mean no identity key
// once the function validates the information it creates and returns an ssh.ClientConfig. // 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 var signers []ssh.Signer
passwd, passwdSet := uri.User.Password() passwd, passwdSet := uri.User.Password()
if iden != "" { // iden might be blank if coming from image scp or if no validation is needed 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 { if err != nil {
return nil, err return nil, err
} }
keyFilePath := filepath.Join(homedir.Get(), ".ssh", "known_hosts")
known, err := knownhosts.New(keyFilePath) var callback ssh.HostKeyCallback
if err != nil { if insecureIsMachineConnection {
return nil, fmt.Errorf("creating host key callback function for %s: %w", keyFilePath, err) 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{ cfg := &ssh.ClientConfig{
User: uri.User.Username(), User: uri.User.Username(),
Auth: authMethods, Auth: authMethods,
HostKeyCallback: known, HostKeyCallback: callback,
Timeout: tick, Timeout: tick,
} }
return cfg, nil return cfg, nil
} }
func getUDS(uri *url.URL, iden string) (string, error) { func getUDS(uri *url.URL, iden string) (string, error) {
cfg, err := ValidateAndConfigure(uri, iden) cfg, err := ValidateAndConfigure(uri, iden, false)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to validate: %w", err) 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 return info.Host.RemoteSocket.Path, nil
} }
// addKnownHostsEntry adds (host, pubKey) to users 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
}

View File

@ -27,12 +27,13 @@ type ConnectionCreateOptions struct {
} }
type ConnectionDialOptions struct { type ConnectionDialOptions struct {
Host string Host string
Identity string Identity string
User *url.Userinfo User *url.Userinfo
Port int Port int
Auth []string Auth []string
Timeout time.Duration Timeout time.Duration
InsecureIsMachineConnection bool
} }
type ConnectionDialReport struct { type ConnectionDialReport struct {

View File

@ -21,6 +21,7 @@ func Validate(user *url.Userinfo, path string, port int, identity string) (*conf
if strings.Contains(path, "/run") { if strings.Contains(path, "/run") {
sock = strings.Split(path, "/run")[1] 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) uri, err := url.Parse(path)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -33,9 +34,9 @@ func Validate(user *url.Userinfo, path string, port int, identity string) (*conf
if uri.Port() == "" { if uri.Port() == "" {
if port != 0 { if port != 0 {
uri.Host = net.JoinHostPort(uri.Hostname(), strconv.Itoa(port)) uri.Host = net.JoinHostPort(uri.Host, strconv.Itoa(port))
} else { } else {
uri.Host = net.JoinHostPort(uri.Hostname(), "22") uri.Host = net.JoinHostPort(uri.Host, "22")
} }
} }

2
vendor/modules.txt vendored
View File

@ -120,7 +120,7 @@ github.com/containers/buildah/pkg/rusage
github.com/containers/buildah/pkg/sshagent github.com/containers/buildah/pkg/sshagent
github.com/containers/buildah/pkg/util github.com/containers/buildah/pkg/util
github.com/containers/buildah/util github.com/containers/buildah/util
# github.com/containers/common v0.49.2-0.20220920205255-8062f81c5497 # github.com/containers/common v0.49.2-0.20220926195839-590004b80685
## explicit; go 1.17 ## explicit; go 1.17
github.com/containers/common/libimage github.com/containers/common/libimage
github.com/containers/common/libimage/define github.com/containers/common/libimage/define