Be explicit about ssh configs suitable only for localhost

... and warn loudly against generalization.

Should not change behavior.

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This commit is contained in:
Miloslav Trmač
2025-05-15 23:04:49 +02:00
parent 5fef6b714d
commit 265ca77276
10 changed files with 45 additions and 30 deletions

View File

@ -95,7 +95,7 @@ func cp(_ *cobra.Command, args []string) error {
cpOpts.SrcPath = srcPath cpOpts.SrcPath = srcPath
cpOpts.DestPath = destPath cpOpts.DestPath = destPath
err = secureCopy(&cpOpts) err = localhostSSHCopy(&cpOpts)
if err != nil { if err != nil {
return fmt.Errorf("copy failed: %s", err.Error()) return fmt.Errorf("copy failed: %s", err.Error())
} }
@ -105,7 +105,8 @@ func cp(_ *cobra.Command, args []string) error {
return nil return nil
} }
func secureCopy(opts *cpOptions) error { // localhostSSHCopy uses scp to copy files from/to a localhost machine using ssh.
func localhostSSHCopy(opts *cpOptions) error {
srcPath := opts.SrcPath srcPath := opts.SrcPath
destPath := opts.DestPath destPath := opts.DestPath
sshConfig := opts.Machine.SSH sshConfig := opts.Machine.SSH
@ -123,7 +124,7 @@ func secureCopy(opts *cpOptions) error {
} }
args := []string{"-r", "-i", sshConfig.IdentityPath, "-P", strconv.Itoa(sshConfig.Port)} args := []string{"-r", "-i", sshConfig.IdentityPath, "-P", strconv.Itoa(sshConfig.Port)}
args = append(args, machine.CommonSSHArgs()...) args = append(args, machine.LocalhostSSHArgs()...) // Warning: This MUST NOT be generalized to allow communication over untrusted networks.
args = append(args, []string{srcPath, destPath}...) args = append(args, []string{srcPath, destPath}...)
cmd := exec.Command("scp", args...) cmd := exec.Command("scp", args...)

View File

@ -115,6 +115,6 @@ func ssh(cmd *cobra.Command, args []string) error {
} }
} }
err = machine.CommonSSHShell(sshOpts.Username, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, sshOpts.Args) err = machine.LocalhostSSHShell(sshOpts.Username, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, sshOpts.Args)
return utils.HandleOSExecError(err) return utils.HandleOSExecError(err)
} }

View File

@ -64,7 +64,7 @@ func startShares(mc *vmconfigs.MachineConfig) error {
} }
args = append(args, "machine", "client9p", fmt.Sprintf("%d", *mount.VSockNumber), strconv.Quote(mount.Target)) args = append(args, "machine", "client9p", fmt.Sprintf("%d", *mount.VSockNumber), strconv.Quote(mount.Target))
if err := machine.CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil { if err := machine.LocalhostSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil {
return err return err
} }
} }

View File

@ -24,7 +24,7 @@ type MachineOS struct {
func (m *MachineOS) Apply(image string, opts ApplyOptions) error { func (m *MachineOS) Apply(image string, opts ApplyOptions) error {
args := []string{"podman", "machine", "os", "apply", image} args := []string{"podman", "machine", "os", "apply", image}
if err := machine.CommonSSH(m.VM.SSH.RemoteUsername, m.VM.SSH.IdentityPath, m.VMName, m.VM.SSH.Port, args); err != nil { if err := machine.LocalhostSSH(m.VM.SSH.RemoteUsername, m.VM.SSH.IdentityPath, m.VMName, m.VM.SSH.Port, args); err != nil {
return err return err
} }

View File

@ -52,6 +52,6 @@ func getProxyScript(isWSL bool) io.Reader {
} }
func ApplyProxies(mc *vmconfigs.MachineConfig) error { func ApplyProxies(mc *vmconfigs.MachineConfig) error {
return machine.CommonSSHWithStdin("root", mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"/usr/bin/bash"}, return machine.LocalhostSSHWithStdin("root", mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"/usr/bin/bash"},
getProxyScript(mc.WSLHypervisor != nil)) getProxyScript(mc.WSLHypervisor != nil))
} }

View File

@ -353,7 +353,7 @@ func (q *QEMUStubber) MountVolumesToVM(mc *vmconfigs.MachineConfig, quiet bool)
if !strings.HasPrefix(mount.Target, "/home") && !strings.HasPrefix(mount.Target, "/mnt") { if !strings.HasPrefix(mount.Target, "/home") && !strings.HasPrefix(mount.Target, "/mnt") {
args = append(args, ";", "sudo", "chattr", "+i", "/", ";") args = append(args, ";", "sudo", "chattr", "+i", "/", ";")
} }
err := machine.CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args) err := machine.LocalhostSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args)
if err != nil { if err != nil {
return err return err
} }
@ -368,7 +368,7 @@ func (q *QEMUStubber) MountVolumesToVM(mc *vmconfigs.MachineConfig, quiet bool)
mountFlags += ",ro" mountFlags += ",ro"
} }
mountOptions = append(mountOptions, "-o", mountFlags) mountOptions = append(mountOptions, "-o", mountFlags)
err = machine.CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, append([]string{"sudo", "mount"}, mountOptions...)) err = machine.LocalhostSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, append([]string{"sudo", "mount"}, mountOptions...))
if err != nil { if err != nil {
return err return err
} }

View File

@ -609,7 +609,7 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe
} }
if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil && mc.IsFirstBoot() { if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil && mc.IsFirstBoot() {
if err := machine.CommonSSHSilent(mc.Ansible.User, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"ansible-playbook", mc.Ansible.PlaybookPath}); err != nil { if err := machine.LocalhostSSHSilent(mc.Ansible.User, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"ansible-playbook", mc.Ansible.PlaybookPath}); err != nil {
logrus.Error(err) logrus.Error(err)
} }
} }

View File

@ -150,7 +150,7 @@ func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backo
// CoreOS users have reported the same observation but // CoreOS users have reported the same observation but
// the underlying source of the issue remains unknown. // the underlying source of the issue remains unknown.
if sshError = machine.CommonSSHSilent(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil { if sshError = machine.LocalhostSSHSilent(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil {
logrus.Debugf("SSH readiness check for machine failed: %v", sshError) logrus.Debugf("SSH readiness check for machine failed: %v", sshError)
continue continue
} }

View File

@ -13,27 +13,27 @@ import (
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
// CommonSSH is a common function for ssh'ing to a podman machine using system-connections // LocalhostSSH is a common function for ssh'ing to a podman machine using system-connections
// and a port // and a port
// TODO This should probably be taught about an machineconfig to reduce input // TODO This should probably be taught about an machineconfig to reduce input
func CommonSSH(username, identityPath, name string, sshPort int, inputArgs []string) error { func LocalhostSSH(username, identityPath, name string, sshPort int, inputArgs []string) error {
return commonBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, os.Stdin) return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, os.Stdin)
} }
func CommonSSHShell(username, identityPath, name string, sshPort int, inputArgs []string) error { func LocalhostSSHShell(username, identityPath, name string, sshPort int, inputArgs []string) error {
return commonNativeSSH(username, identityPath, name, sshPort, inputArgs, os.Stdin) return localhostNativeSSH(username, identityPath, name, sshPort, inputArgs, os.Stdin)
} }
func CommonSSHSilent(username, identityPath, name string, sshPort int, inputArgs []string) error { func LocalhostSSHSilent(username, identityPath, name string, sshPort int, inputArgs []string) error {
return commonBuiltinSSH(username, identityPath, name, sshPort, inputArgs, false, nil) return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, false, nil)
} }
func CommonSSHWithStdin(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader) error { func LocalhostSSHWithStdin(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader) error {
return commonBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, stdin) return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, stdin)
} }
func commonBuiltinSSH(username, identityPath, name string, sshPort int, inputArgs []string, passOutput bool, stdin io.Reader) error { func localhostBuiltinSSH(username, identityPath, name string, sshPort int, inputArgs []string, passOutput bool, stdin io.Reader) error {
config, err := createConfig(username, identityPath) config, err := createLocalhostConfig(username, identityPath) // WARNING: This MUST NOT be generalized to allow communication over untrusted networks.
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,10 @@ func runSessionWithDebug(session *ssh.Session, cmd string) error {
return session.Wait() return session.Wait()
} }
func createConfig(user string, identityPath string) (*ssh.ClientConfig, error) { // createLocalhostConfig returns a *ssh.ClientConfig for authenticating a user using a private key
//
// WARNING: This MUST NOT be used to communicate over untrusted networks.
func createLocalhostConfig(user string, identityPath string) (*ssh.ClientConfig, error) {
key, err := os.ReadFile(identityPath) key, err := os.ReadFile(identityPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -103,18 +106,23 @@ func createConfig(user string, identityPath string) (*ssh.ClientConfig, error) {
} }
return &ssh.ClientConfig{ return &ssh.ClientConfig{
// Not specifying ciphers / MACs seems to allow fairly weak ciphers. This config is restricted
// to connecting to localhost: where we rely on the kernels process isolation, not primarily on cryptography.
User: user, User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
// This config is restricted to connecting to localhost (and to a VM we manage),
// we rely on the kernels process isolation, not on cryptography,
// This would be UNACCEPTABLE for most other uses.
HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, nil }, nil
} }
func commonNativeSSH(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader) error { func localhostNativeSSH(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader) error {
sshDestination := username + "@localhost" sshDestination := username + "@localhost"
port := strconv.Itoa(sshPort) port := strconv.Itoa(sshPort)
interactive := true interactive := true
args := append([]string{"-i", identityPath, "-p", port, sshDestination}, CommonSSHArgs()...) args := append([]string{"-i", identityPath, "-p", port, sshDestination}, LocalhostSSHArgs()...) // WARNING: This MUST NOT be generalized to allow communication over untrusted networks.
if len(inputArgs) > 0 { if len(inputArgs) > 0 {
interactive = false interactive = false
args = append(args, inputArgs...) args = append(args, inputArgs...)
@ -134,7 +142,13 @@ func commonNativeSSH(username, identityPath, name string, sshPort int, inputArgs
return cmd.Run() return cmd.Run()
} }
func CommonSSHArgs() []string { // LocalhostSSHArgs returns OpenSSH command-line options for connecting with no host key identity checks.
//
// WARNING: This MUST NOT be used to communicate over untrusted networks.
func LocalhostSSHArgs() []string {
// This config is restricted to connecting to localhost (and to a VM we manage),
// we rely on the kernels process isolation, not on cryptography,
// This would be UNACCEPTABLE for most other uses.
return []string{ return []string{
"-o", "IdentitiesOnly=yes", "-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",

View File

@ -14,13 +14,13 @@ func UpdatePodmanDockerSockService(mc *vmconfigs.MachineConfig) error {
content := ignition.GetPodmanDockerTmpConfig(mc.HostUser.UID, mc.HostUser.Rootful, false) content := ignition.GetPodmanDockerTmpConfig(mc.HostUser.UID, mc.HostUser.Rootful, false)
command := fmt.Sprintf("'echo %q > %s'", content, ignition.PodmanDockerTmpConfPath) command := fmt.Sprintf("'echo %q > %s'", content, ignition.PodmanDockerTmpConfPath)
args := []string{"sudo", "bash", "-c", command} args := []string{"sudo", "bash", "-c", command}
if err := CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil { if err := LocalhostSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil {
logrus.Warnf("Could not update internal docker sock config") logrus.Warnf("Could not update internal docker sock config")
return err return err
} }
args = []string{"sudo", "systemd-tmpfiles", "--create", "--prefix=/run/docker.sock"} args = []string{"sudo", "systemd-tmpfiles", "--create", "--prefix=/run/docker.sock"}
if err := CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil { if err := LocalhostSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, args); err != nil {
logrus.Warnf("Could not create internal docker sock") logrus.Warnf("Could not create internal docker sock")
return err return err
} }