diff --git a/cmd/minikube/cmd/service.go b/cmd/minikube/cmd/service.go index 02c3c808e4a5..dcb5af9903dc 100644 --- a/cmd/minikube/cmd/service.go +++ b/cmd/minikube/cmd/service.go @@ -46,6 +46,7 @@ import ( "k8s.io/minikube/pkg/minikube/style" "k8s.io/minikube/pkg/minikube/tunnel/kic" pkgnetwork "k8s.io/minikube/pkg/network" + "k8s.io/minikube/pkg/util/sshkeys" ) const defaultServiceFormatTemplate = "http://{{.IP}}:{{.Port}}" @@ -210,7 +211,10 @@ func startKicServiceTunnel(services service.URLs, configName, driverName string) exit.Error(reason.DrvPortForward, "error getting ssh port", err) } sshPort := strconv.Itoa(port) - sshKey := filepath.Join(localpath.MiniPath(), "machines", configName, "id_rsa") + machineDir := filepath.Join(localpath.MiniPath(), "machines", configName) + primary := filepath.Join(machineDir, sshkeys.Ed25519KeyName) + fallback := filepath.Join(machineDir, sshkeys.RSAKeyName) + sshKey := sshkeys.ResolveKeyPath(primary, fallback) serviceTunnel := kic.NewServiceTunnel(sshPort, sshKey, clientset.CoreV1(), serviceURLMode) urls, err := serviceTunnel.Start(svc.Name, namespace) diff --git a/cmd/minikube/cmd/ssh-host.go b/cmd/minikube/cmd/ssh-host.go index 97984a7e13b0..f24a2733d6e8 100644 --- a/cmd/minikube/cmd/ssh-host.go +++ b/cmd/minikube/cmd/ssh-host.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/spf13/cobra" "k8s.io/client-go/util/homedir" @@ -69,9 +70,13 @@ func appendKnownHelper(nodeName string, appendKnown bool) { } } - scanArgs := []string{"-t", "rsa"} + scanArgs := []string{"-t", "ed25519"} keys, err := machine.RunSSHHostCommand(co.API, *co.Config, *n, "ssh-keyscan", scanArgs) + if err != nil || strings.TrimSpace(keys) == "" { + scanArgs = []string{"-t", "rsa"} + keys, err = machine.RunSSHHostCommand(co.API, *co.Config, *n, "ssh-keyscan", scanArgs) + } if err != nil { // This is typically due to a non-zero exit code, so no need for flourish. out.ErrLn("ssh-keyscan: %v", err) diff --git a/cmd/minikube/cmd/ssh-key.go b/cmd/minikube/cmd/ssh-key.go index d7e389890de5..da339064f3e6 100644 --- a/cmd/minikube/cmd/ssh-key.go +++ b/cmd/minikube/cmd/ssh-key.go @@ -28,6 +28,7 @@ import ( "k8s.io/minikube/pkg/minikube/node" "k8s.io/minikube/pkg/minikube/out" "k8s.io/minikube/pkg/minikube/reason" + "k8s.io/minikube/pkg/util/sshkeys" ) // sshKeyCmd represents the sshKey command @@ -43,7 +44,10 @@ var sshKeyCmd = &cobra.Command{ exit.Error(reason.GuestNodeRetrieve, "retrieving node", err) } - out.Ln(filepath.Join(localpath.MiniPath(), "machines", config.MachineName(*cc, *n), "id_rsa")) + machineDir := filepath.Join(localpath.MiniPath(), "machines", config.MachineName(*cc, *n)) + primary := filepath.Join(machineDir, sshkeys.Ed25519KeyName) + fallback := filepath.Join(machineDir, sshkeys.RSAKeyName) + out.Ln(sshkeys.ResolveKeyPath(primary, fallback)) }, } diff --git a/cmd/minikube/cmd/tunnel.go b/cmd/minikube/cmd/tunnel.go index 7e9f6f246d9e..ca9a202edb82 100644 --- a/cmd/minikube/cmd/tunnel.go +++ b/cmd/minikube/cmd/tunnel.go @@ -43,6 +43,7 @@ import ( "k8s.io/minikube/pkg/minikube/tunnel" "k8s.io/minikube/pkg/minikube/tunnel/kic" pkgnetwork "k8s.io/minikube/pkg/network" + "k8s.io/minikube/pkg/util/sshkeys" ) var cleanup bool @@ -104,7 +105,10 @@ var tunnelCmd = &cobra.Command{ exit.Error(reason.DrvPortForward, "error getting ssh port", err) } sshPort := strconv.Itoa(port) - sshKey := filepath.Join(localpath.MiniPath(), "machines", cname, "id_rsa") + machineDir := filepath.Join(localpath.MiniPath(), "machines", cname) + primary := filepath.Join(machineDir, sshkeys.Ed25519KeyName) + fallback := filepath.Join(machineDir, sshkeys.RSAKeyName) + sshKey := sshkeys.ResolveKeyPath(primary, fallback) outputTunnelStarted() kicSSHTunnel := kic.NewSSHTunnel(ctx, sshPort, sshKey, bindAddress, clientset.CoreV1(), clientset.NetworkingV1()) diff --git a/deploy/iso/minikube-iso/board/minikube/aarch64/rootfs-overlay/etc/ssh/sshd_config b/deploy/iso/minikube-iso/board/minikube/aarch64/rootfs-overlay/etc/ssh/sshd_config index b7ab4a59a66a..d5ece8f30c2a 100644 --- a/deploy/iso/minikube-iso/board/minikube/aarch64/rootfs-overlay/etc/ssh/sshd_config +++ b/deploy/iso/minikube-iso/board/minikube/aarch64/rootfs-overlay/etc/ssh/sshd_config @@ -117,8 +117,3 @@ Subsystem sftp /usr/libexec/sftp-server # AllowTcpForwarding no # PermitTTY no # ForceCommand cvs server - -# Temporarily accept ssh-rsa algorithm for openssh >= 8.8, -# until most ssh clients could deprecate ssh-rsa. -HostkeyAlgorithms +ssh-rsa -PubkeyAcceptedAlgorithms +ssh-rsa diff --git a/deploy/iso/minikube-iso/board/minikube/x86_64/rootfs-overlay/etc/ssh/sshd_config b/deploy/iso/minikube-iso/board/minikube/x86_64/rootfs-overlay/etc/ssh/sshd_config index b7ab4a59a66a..d5ece8f30c2a 100644 --- a/deploy/iso/minikube-iso/board/minikube/x86_64/rootfs-overlay/etc/ssh/sshd_config +++ b/deploy/iso/minikube-iso/board/minikube/x86_64/rootfs-overlay/etc/ssh/sshd_config @@ -117,8 +117,3 @@ Subsystem sftp /usr/libexec/sftp-server # AllowTcpForwarding no # PermitTTY no # ForceCommand cvs server - -# Temporarily accept ssh-rsa algorithm for openssh >= 8.8, -# until most ssh clients could deprecate ssh-rsa. -HostkeyAlgorithms +ssh-rsa -PubkeyAcceptedAlgorithms +ssh-rsa diff --git a/deploy/kicbase/Dockerfile b/deploy/kicbase/Dockerfile index 93a9f958af0b..f5706bbb75bc 100644 --- a/deploy/kicbase/Dockerfile +++ b/deploy/kicbase/Dockerfile @@ -139,12 +139,8 @@ RUN clean-install \ openssh-server \ dnsutils -# Add support for rsa1 in sshd -# modern debian-based OSs dont support rsa1 by default, so we need to enable it to support older ssh clients -# TODO: remove after https://github.com/kubernetes/minikube/issues/21543 is solved +# Ensure key-based SSH auth is enabled and password auth disabled RUN cat <> /etc/ssh/sshd_config -PubkeyAcceptedAlgorithms +ssh-rsa -HostkeyAlgorithms +ssh-rsa PubkeyAuthentication yes PasswordAuthentication no EOF diff --git a/go.mod b/go.mod index ff3a0d0db11a..e8cb013a02ec 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-getter v1.8.0 github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 github.com/hooklift/iso9660 v1.0.0 github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 github.com/johanneswuerbach/nfsexports v0.0.0-20200318065542-c48c3734757f @@ -167,7 +168,6 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 // indirect github.com/hooklift/assert v0.0.0-20170704181755-9d1defd6d214 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/hack/prow/minitest/deployer/docker_deployer.go b/hack/prow/minitest/deployer/docker_deployer.go index a33813792fce..c160a8678355 100644 --- a/hack/prow/minitest/deployer/docker_deployer.go +++ b/hack/prow/minitest/deployer/docker_deployer.go @@ -18,14 +18,14 @@ package deployer import ( "context" + "crypto/ed25519" "crypto/rand" - "crypto/rsa" - "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io" "os" + "path/filepath" "strconv" "github.com/docker/go-connections/nat" @@ -33,7 +33,7 @@ import ( "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "github.com/phayes/freeport" - gossh "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh" "k8s.io/klog/v2" ) @@ -252,54 +252,20 @@ func (m *MiniTestDockerDeployer) sshSetUp() error { m.sshTempDir = sshTempDir klog.Info("Created temp dir for ssh keys: ", sshTempDir) - // create private key file - sshPrivateKeyFile, err := os.CreateTemp(sshTempDir, "id_rsa") - if err != nil { - return fmt.Errorf("failed to create temp file for private key: %v", err) - } - if sshPrivateKeyFile.Chmod(0600) != nil { - return fmt.Errorf("failed to chmod private key file: %v", err) + keyPath := filepath.Join(sshTempDir, "id_ed25519") + pubKeyPath := keyPath + ".pub" + if err := generateEd25519KeyPair(keyPath, pubKeyPath); err != nil { + return fmt.Errorf("failed to generate ssh key pair: %v", err) } - m.sshPrivateKeyFile = sshPrivateKeyFile.Name() + m.sshPrivateKeyFile = keyPath + m.sshPublicKeyFile = pubKeyPath klog.Info("Created temp file for private key: ", m.sshPrivateKeyFile) - - // create public key file - sshPublicKeyFile, err := os.CreateTemp(sshTempDir, "id_rsa.pub") - if err != nil { - return fmt.Errorf("failed to create temp file for public key: %v", err) - } - if sshPublicKeyFile.Chmod(0644) != nil { - return fmt.Errorf("failed to chmod public key file: %v", err) - } - m.sshPublicKeyFile = sshPublicKeyFile.Name() klog.Info("Created temp file for public key: ", m.sshPublicKeyFile) - // generate private key and convert to PEM format - privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + pubBytes, err := os.ReadFile(m.sshPublicKeyFile) if err != nil { - return fmt.Errorf("failed to generate private key: %v", err) - } - privDER := x509.MarshalPKCS1PrivateKey(privateKey) - privBlock := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: privDER, - } - - if err := pem.Encode(sshPrivateKeyFile, privBlock); err != nil { - sshPrivateKeyFile.Close() - return fmt.Errorf("failed to write private key to file: %v", err) + return fmt.Errorf("failed to read public key file: %v", err) } - - pub, err := gossh.NewPublicKey(&privateKey.PublicKey) - if err != nil { - return fmt.Errorf("failed to generate public key: %v", err) - } - pubBytes := gossh.MarshalAuthorizedKey(pub) - - if _, err := sshPublicKeyFile.Write(pubBytes); err != nil { - return fmt.Errorf("failed to write public key to file: %v", err) - } - sshPublicKeyFile.Close() m.sshPublicKeyContent = string(pubBytes) klog.Infof("Generated ssh public key:%s ", m.sshPublicKeyContent) return nil @@ -312,3 +278,43 @@ func (m *MiniTestDockerDeployer) sshAdditionalArgs() []string { func (m *MiniTestDockerDeployer) scpAdditionalArgs() []string { return []string{"-i", m.sshPrivateKeyFile, "-P", m.sshPort} } + +func generateEd25519KeyPair(privateKeyPath, publicKeyPath string) error { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("generate ed25519 key: %w", err) + } + + block, err := ssh.MarshalPrivateKey(privateKey, "") + if err != nil { + return fmt.Errorf("marshal private key: %w", err) + } + if err := writeKeyFile(privateKeyPath, pem.EncodeToMemory(block), 0600); err != nil { + return fmt.Errorf("write private key: %w", err) + } + + sshPublicKey, err := ssh.NewPublicKey(publicKey) + if err != nil { + return fmt.Errorf("convert public key: %w", err) + } + if err := writeKeyFile(publicKeyPath, ssh.MarshalAuthorizedKey(sshPublicKey), 0644); err != nil { + return fmt.Errorf("write public key: %w", err) + } + + return nil +} + +func writeKeyFile(path string, data []byte, mode os.FileMode) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + return os.Chmod(path, mode) +} diff --git a/hack/prow/minitest/deployer/util.go b/hack/prow/minitest/deployer/util.go index 61de5a224e49..cce9afa10929 100644 --- a/hack/prow/minitest/deployer/util.go +++ b/hack/prow/minitest/deployer/util.go @@ -27,7 +27,7 @@ import ( "k8s.io/klog/v2" ) -const ssh = "ssh" +const sshBinary = "ssh" const rsync = "rsync" func executeLocalCommand(ctx context.Context, name string, args ...string) error { @@ -66,7 +66,7 @@ func sshConnectionCheck(ctx context.Context, user string, addr string, sshArgume } func executeRsyncSSHCommand(ctx context.Context, sshArguments []string, src string, dst string, rsyncArgs []string) error { - sshArgs := []string{ssh, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"} + sshArgs := []string{sshBinary, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"} sshArgs = append(sshArgs, sshArguments...) allArgs := []string{"-e", strings.Join(sshArgs, " "), "-avz"} diff --git a/pkg/drivers/common/common.go b/pkg/drivers/common/common.go index 0885b8bd42cc..732e737f354a 100644 --- a/pkg/drivers/common/common.go +++ b/pkg/drivers/common/common.go @@ -31,12 +31,12 @@ import ( "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnflag" "github.com/docker/machine/libmachine/mcnutils" - "github.com/docker/machine/libmachine/ssh" "github.com/pkg/errors" "k8s.io/klog/v2" "k8s.io/minikube/pkg/minikube/run" "k8s.io/minikube/pkg/util" + "k8s.io/minikube/pkg/util/sshkeys" ) // LeasesPath is the path to dhcpd leases @@ -157,7 +157,7 @@ func MakeDiskImage(d *drivers.BaseDriver, boot2dockerURL string, diskSize int) e keyPath := d.GetSSHKeyPath() klog.Infof("Creating ssh key: %s...", keyPath) - if err := ssh.GenerateSSHKey(keyPath); err != nil { + if err := sshkeys.GenerateSSHKey(keyPath); err != nil { return errors.Wrap(err, "generate ssh key") } diff --git a/pkg/drivers/kic/kic.go b/pkg/drivers/kic/kic.go index f8e35de5f51d..c37f121a6912 100644 --- a/pkg/drivers/kic/kic.go +++ b/pkg/drivers/kic/kic.go @@ -28,7 +28,6 @@ import ( "time" "github.com/docker/machine/libmachine/drivers" - "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" "github.com/pkg/errors" "k8s.io/klog/v2" @@ -47,6 +46,7 @@ import ( "k8s.io/minikube/pkg/minikube/style" "k8s.io/minikube/pkg/minikube/sysinit" "k8s.io/minikube/pkg/util/retry" + "k8s.io/minikube/pkg/util/sshkeys" ) // Driver represents a kic driver https://minikube.sigs.k8s.io/docs/reference/drivers/docker @@ -223,7 +223,7 @@ func (d *Driver) Create() error { func (d *Driver) prepareSSH() error { keyPath := d.GetSSHKeyPath() klog.Infof("Creating ssh key for kic: %s...", keyPath) - if err := ssh.GenerateSSHKey(keyPath); err != nil { + if err := sshkeys.GenerateSSHKey(keyPath); err != nil { return errors.Wrap(err, "generate ssh key") } @@ -324,7 +324,9 @@ func (d *Driver) GetSSHUsername() string { // GetSSHKeyPath returns the ssh key path func (d *Driver) GetSSHKeyPath() string { if d.SSHKeyPath == "" { - d.SSHKeyPath = d.ResolveStorePath("id_rsa") + primary := d.ResolveStorePath(sshkeys.Ed25519KeyName) + fallback := d.ResolveStorePath(sshkeys.RSAKeyName) + d.SSHKeyPath = sshkeys.ResolveKeyPath(primary, fallback) } return d.SSHKeyPath } diff --git a/pkg/drivers/krunkit/krunkit.go b/pkg/drivers/krunkit/krunkit.go index 0d4d9055b373..6dd0a8e6751c 100644 --- a/pkg/drivers/krunkit/krunkit.go +++ b/pkg/drivers/krunkit/krunkit.go @@ -36,7 +36,6 @@ import ( "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnutils" - "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" "github.com/pkg/errors" @@ -51,6 +50,7 @@ import ( "k8s.io/minikube/pkg/minikube/reason" "k8s.io/minikube/pkg/minikube/run" "k8s.io/minikube/pkg/minikube/style" + "k8s.io/minikube/pkg/util/sshkeys" ) const ( @@ -112,7 +112,9 @@ func (d *Driver) GetSSHHostname() (string, error) { } func (d *Driver) GetSSHKeyPath() string { - return d.ResolveStorePath("id_rsa") + primary := d.ResolveStorePath(sshkeys.Ed25519KeyName) + fallback := d.ResolveStorePath(sshkeys.RSAKeyName) + return sshkeys.ResolveKeyPath(primary, fallback) } func (d *Driver) GetSSHPort() (int, error) { @@ -172,7 +174,7 @@ func (d *Driver) Create() error { } log.Info("Creating SSH key...") - if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + if err := sshkeys.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { return err } diff --git a/pkg/drivers/qemu/qemu.go b/pkg/drivers/qemu/qemu.go index f787de6de341..90be3910bb84 100644 --- a/pkg/drivers/qemu/qemu.go +++ b/pkg/drivers/qemu/qemu.go @@ -36,7 +36,6 @@ import ( "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnutils" - "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" "github.com/pkg/errors" @@ -51,6 +50,7 @@ import ( "k8s.io/minikube/pkg/minikube/style" "k8s.io/minikube/pkg/network" "k8s.io/minikube/pkg/util/retry" + "k8s.io/minikube/pkg/util/sshkeys" ) const ( @@ -108,7 +108,9 @@ func (d *Driver) GetSSHHostname() (string, error) { } func (d *Driver) GetSSHKeyPath() string { - return d.ResolveStorePath("id_rsa") + primary := d.ResolveStorePath(sshkeys.Ed25519KeyName) + fallback := d.ResolveStorePath(sshkeys.RSAKeyName) + return sshkeys.ResolveKeyPath(primary, fallback) } func (d *Driver) GetSSHPort() (int, error) { @@ -271,7 +273,7 @@ func (d *Driver) Create() error { } log.Info("Creating SSH key...") - if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + if err := sshkeys.GenerateSSHKey(d.sshKeyPath()); err != nil { return err } @@ -673,7 +675,9 @@ func (d *Driver) Upgrade() error { func (d *Driver) sshKeyPath() string { machineDir := filepath.Join(d.StorePath, "machines", d.GetMachineName()) - return filepath.Join(machineDir, "id_rsa") + primary := filepath.Join(machineDir, sshkeys.Ed25519KeyName) + fallback := filepath.Join(machineDir, sshkeys.RSAKeyName) + return sshkeys.ResolveKeyPath(primary, fallback) } func (d *Driver) publicSSHKeyPath() string { diff --git a/pkg/drivers/vfkit/vfkit.go b/pkg/drivers/vfkit/vfkit.go index 499037965054..c764ebefae76 100644 --- a/pkg/drivers/vfkit/vfkit.go +++ b/pkg/drivers/vfkit/vfkit.go @@ -38,7 +38,6 @@ import ( "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnutils" - "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" "github.com/pkg/errors" @@ -54,6 +53,7 @@ import ( "k8s.io/minikube/pkg/minikube/reason" "k8s.io/minikube/pkg/minikube/run" "k8s.io/minikube/pkg/minikube/style" + "k8s.io/minikube/pkg/util/sshkeys" ) const ( @@ -110,7 +110,9 @@ func (d *Driver) GetSSHHostname() (string, error) { } func (d *Driver) GetSSHKeyPath() string { - return d.ResolveStorePath("id_rsa") + primary := d.ResolveStorePath(sshkeys.Ed25519KeyName) + fallback := d.ResolveStorePath(sshkeys.RSAKeyName) + return sshkeys.ResolveKeyPath(primary, fallback) } func (d *Driver) GetSSHPort() (int, error) { @@ -203,7 +205,7 @@ func (d *Driver) Create() error { } log.Info("Creating SSH key...") - if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + if err := sshkeys.GenerateSSHKey(d.sshKeyPath()); err != nil { return err } @@ -526,7 +528,9 @@ func (d *Driver) Upgrade() error { func (d *Driver) sshKeyPath() string { machineDir := filepath.Join(d.StorePath, "machines", d.GetMachineName()) - return filepath.Join(machineDir, "id_rsa") + primary := filepath.Join(machineDir, sshkeys.Ed25519KeyName) + fallback := filepath.Join(machineDir, sshkeys.RSAKeyName) + return sshkeys.ResolveKeyPath(primary, fallback) } func (d *Driver) publicSSHKeyPath() string { diff --git a/pkg/minikube/machine/client_test.go b/pkg/minikube/machine/client_test.go index a164f6922c66..b68b4137c73b 100644 --- a/pkg/minikube/machine/client_test.go +++ b/pkg/minikube/machine/client_test.go @@ -37,7 +37,7 @@ const vboxConfig = ` "MachineName": "minikube", "SSHUser": "docker", "SSHPort": 33627, - "SSHKeyPath": "/home/sundarp/.minikube/machines/minikube/id_rsa", + "SSHKeyPath": "/home/sundarp/.minikube/machines/minikube/id_ed25519", "StorePath": "/home/sundarp/.minikube", "SwarmMaster": false, "SwarmHost": "", diff --git a/pkg/minikube/reason/known_issues.go b/pkg/minikube/reason/known_issues.go index dc878352efe4..aa7753a0b80c 100644 --- a/pkg/minikube/reason/known_issues.go +++ b/pkg/minikube/reason/known_issues.go @@ -1059,7 +1059,7 @@ var guestIssues = []match{ Advice: "minikube is missing files relating to your guest environment. This can be fixed by running 'minikube delete'", Issues: []int{9130}, }, - Regexp: re(`id_rsa: no such file or directory`), + Regexp: re(`id_(ed25519|rsa): no such file or directory`), }, { Kind: Kind{ diff --git a/pkg/minikube/sshutil/sshutil.go b/pkg/minikube/sshutil/sshutil.go index 2ef0454a101f..b13f1ccc873e 100644 --- a/pkg/minikube/sshutil/sshutil.go +++ b/pkg/minikube/sshutil/sshutil.go @@ -33,6 +33,7 @@ import ( "k8s.io/klog/v2" "k8s.io/minikube/pkg/util/retry" + "k8s.io/minikube/pkg/util/sshkeys" ) // NewSSHClient returns an SSH client object for running commands. @@ -42,7 +43,9 @@ func NewSSHClient(d drivers.Driver) (*ssh.Client, error) { return nil, errors.Wrap(err, "Error creating new ssh host from driver") } - defaultKeyPath := filepath.Join(homedir.HomeDir(), ".ssh", "id_rsa") + primary := filepath.Join(homedir.HomeDir(), ".ssh", sshkeys.Ed25519KeyName) + fallback := filepath.Join(homedir.HomeDir(), ".ssh", sshkeys.RSAKeyName) + defaultKeyPath := sshkeys.ResolveKeyPath(primary, fallback) auth := &machinessh.Auth{} if h.SSHKeyPath != "" { auth.Keys = []string{h.SSHKeyPath} diff --git a/pkg/util/sshkeys/sshkeys.go b/pkg/util/sshkeys/sshkeys.go new file mode 100644 index 000000000000..e6d677ee2c05 --- /dev/null +++ b/pkg/util/sshkeys/sshkeys.go @@ -0,0 +1,111 @@ +/* +Copyright 2025 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sshkeys + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/pem" + "fmt" + "os" + "runtime" + + "github.com/hectane/go-acl" + "golang.org/x/crypto/ssh" +) + +const ( + Ed25519KeyName = "id_ed25519" + RSAKeyName = "id_rsa" +) + +// ResolveKeyPath returns the first existing key path, or primary when neither exists. +func ResolveKeyPath(primary, fallback string) string { + if fileExists(primary) { + return primary + } + if fallback != "" && fileExists(fallback) { + return fallback + } + return primary +} + +// GenerateSSHKey creates an ed25519 SSH keypair at the given private key path. +func GenerateSSHKey(path string) error { + if _, err := os.Stat(path); err == nil { + return nil + } else if !os.IsNotExist(err) { + return fmt.Errorf("desired directory for SSH keys does not exist: %w", err) + } + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return fmt.Errorf("error generating key pair: %w", err) + } + + block, err := ssh.MarshalPrivateKey(priv, "") + if err != nil { + return fmt.Errorf("error marshaling private key: %w", err) + } + if err := writeKeyFile(path, pem.EncodeToMemory(block)); err != nil { + return fmt.Errorf("error writing private key: %w", err) + } + + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + return fmt.Errorf("error converting public key: %w", err) + } + if err := writeKeyFile(path+".pub", ssh.MarshalAuthorizedKey(sshPub)); err != nil { + return fmt.Errorf("error writing public key: %w", err) + } + + return nil +} + +func writeKeyFile(path string, data []byte) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + if err != nil { + return err + } + if _, err := f.Write(data); err != nil { + f.Close() + return err + } + if err := f.Close(); err != nil { + return err + } + return chmodFile(path, 0600) +} + +func chmodFile(path string, mode os.FileMode) error { + switch runtime.GOOS { + case "darwin", "freebsd", "linux", "openbsd": + return os.Chmod(path, mode) + case "windows": + return acl.Chmod(path, mode) + default: + return os.Chmod(path, mode) + } +} + +func fileExists(path string) bool { + if path == "" { + return false + } + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} diff --git a/site/content/en/docs/handbook/accessing.md b/site/content/en/docs/handbook/accessing.md index df5b1e6dd3ec..36ae44b726f4 100644 --- a/site/content/en/docs/handbook/accessing.md +++ b/site/content/en/docs/handbook/accessing.md @@ -81,7 +81,7 @@ Services of type `NodePort` can be exposed via the `minikube service