Repository: lima-vm/lima

Issue:
Automatically detect if GSSAPIAuthentication is available or not
### Description

Some hosts (e.g. NixOS) don't provide an `ssh` with `GSSAPIAuthentication` available.  (And setting `GSSAPIAuthentication=no` will return an error/warning)

Lima should detect if it is available before trying to use it. See: https://github.com/lima-vm/lima/issues/3151#issuecomment-2638389458 


Discussion:
@Horiodino: /assign@kachick: I'm not familiar with the issue assignment workflow in this repository. My pull request is currently under review, so please ensure there are no conflicts.
I apologize for overlooking any recent updates to this issue. I'm surprised we're addressing the same issue simultaneously.
I'm perfectly fine if someone wants to take over and progress this issue. If a different approach is chosen, feel free to close my pull request.@jandubois: @kachick Sorry, my mistake. I did mix up the user handles; my intention was to assign the issue to the person who had already created the PR, but got it wrong. I thought the request for assignment was from you.

Solution patch:
diff --git a/go.mod b/go.mod
index 8134023f..14a53959 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/foxcpp/go-mockdns v1.1.0
 	github.com/goccy/go-yaml v1.18.0
 	github.com/google/go-cmp v0.7.0
-	github.com/google/yamlfmt v0.17.0
+	github.com/google/yamlfmt v0.17.1
 	github.com/invopop/jsonschema v0.13.0
 	github.com/lima-vm/go-qcow2reader v0.6.0
 	github.com/lima-vm/sshocker v0.3.8 // gomodjail:unconfined
@@ -49,9 +49,9 @@ require (
 	google.golang.org/protobuf v1.36.6 // gomodjail:unconfined
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gotest.tools/v3 v3.5.2
-	k8s.io/api v0.32.5
-	k8s.io/apimachinery v0.32.5
-	k8s.io/client-go v0.32.5
+	k8s.io/api v0.32.6
+	k8s.io/apimachinery v0.32.6
+	k8s.io/client-go v0.32.6
 )
 
 require (
diff --git a/go.sum b/go.sum
index 152136a8..fb72ed4d 100644
--- a/go.sum
+++ b/go.sum
@@ -123,8 +123,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/yamlfmt v0.17.0 h1:/tdp01rIlvLz3LgJ2NtMLnqgAadZm33P7GcPU680b+w=
-github.com/google/yamlfmt v0.17.0/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
+github.com/google/yamlfmt v0.17.1 h1:NgKi21+MUVntxhPhIsstylN6QNCCfDv3wKNKwlm/3Hs=
+github.com/google/yamlfmt v0.17.1/go.mod h1:gs0UEklJOYkUJ+OOCG0hg9n+DzucKDPlJElTUasVNK8=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
@@ -430,12 +430,12 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
-k8s.io/api v0.32.5 h1:uqjjsYo1kTJr5NIcoIaP9F+TgXgADH7nKQx91FDAhtk=
-k8s.io/api v0.32.5/go.mod h1:bXXFU3fGCZ/eFMZvfHZC69PeGbXEL4zzjuPVzOxHF64=
-k8s.io/apimachinery v0.32.5 h1:6We3aJ6crC0ap8EhsEXcgX3LpI6SEjubpiOMXLROwPM=
-k8s.io/apimachinery v0.32.5/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.5 h1:huFmQMzgWu0z4kbWsuZci+Gt4Fo72I4CcrvhToZ/Qp0=
-k8s.io/client-go v0.32.5/go.mod h1:Qchw6f9WIVrur7DKojAHpRgGLcANT0RLIvF39Jz58xA=
+k8s.io/api v0.32.6 h1:UiBAMRzTP24Tz9UT1uhhmAv1auGTT9PT/npywSk9JrU=
+k8s.io/api v0.32.6/go.mod h1:+iFCyQN34v2rsL53iQEN9lYE03mFdgPvgSXvATIDteg=
+k8s.io/apimachinery v0.32.6 h1:odtEUjg7OT3132sBFsFn4Arj4Gd+BplYekmLQP8L3ak=
+k8s.io/apimachinery v0.32.6/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.6 h1:Q+O+Sd9LKKFnsGZNVX2q1RDILYRpQZX+ea2RoIgjKlM=
+k8s.io/client-go v0.32.6/go.mod h1:yqL9XJ2cTXy3WdJwdeyob3O6xiLwWrh9DP7SeszniW0=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 750383e2..8298882c 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -219,7 +219,7 @@ func Validate(y *LimaYAML, warn bool) error {
 		case ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeData, ProvisionModeDependency, ProvisionModeAnsible:
 		default:
 			return fmt.Errorf("field `provision[%d].mode` must one of %q, %q, %q, %q, %q, or %q",
-				i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeDependency, ProvisionModeAnsible, ProvisionModeAnsible)
+				i, ProvisionModeSystem, ProvisionModeUser, ProvisionModeBoot, ProvisionModeData, ProvisionModeDependency, ProvisionModeAnsible)
 		}
 		if p.Mode != ProvisionModeDependency && p.SkipDefaultDependencyResolution != nil {
 			return fmt.Errorf("field `provision[%d].mode` cannot set skipDefaultDependencyResolution, only valid on scripts of type %q",
diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go
index 81e81e37..fa5370a4 100644
--- a/pkg/sshutil/sshutil.go
+++ b/pkg/sshutil/sshutil.go
@@ -154,13 +154,22 @@ func DefaultPubKeys(loadDotSSH bool) ([]PubKey, error) {
 	return res, nil
 }
 
+type openSSHInfo struct {
+	// Version is set to the version of OpenSSH, or semver.New("0.0.0") if the version cannot be determined.
+	Version semver.Version
+
+	// Some distributions omit this feature by default, for example, Alpine, NixOS.
+	GSSAPISupported bool
+}
+
 var sshInfo struct {
 	sync.Once
 	// aesAccelerated is set to true when AES acceleration is available.
 	// Available on almost all modern Intel/AMD processors.
 	aesAccelerated bool
-	// openSSHVersion is set to the version of OpenSSH, or semver.New("0.0.0") if the version cannot be determined.
-	openSSHVersion semver.Version
+
+	// OpenSSH executable information for the version and supported options.
+	openSSH openSSHInfo
 }
 
 // CommonOpts returns ssh option key-value pairs like {"IdentityFile=/path/to/id_foo"}.
@@ -226,7 +235,6 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
 		"StrictHostKeyChecking=no",
 		"UserKnownHostsFile=/dev/null",
 		"NoHostAuthenticationForLocalhost=yes",
-		"GSSAPIAuthentication=no",
 		"PreferredAuthentications=publickey",
 		"Compression=no",
 		"BatchMode=yes",
@@ -235,11 +243,15 @@ func CommonOpts(sshPath string, useDotSSH bool) ([]string, error) {
 
 	sshInfo.Do(func() {
 		sshInfo.aesAccelerated = detectAESAcceleration()
-		sshInfo.openSSHVersion = DetectOpenSSHVersion(sshPath)
+		sshInfo.openSSH = detectOpenSSHInfo(sshPath)
 	})
 
+	if sshInfo.openSSH.GSSAPISupported {
+		opts = append(opts, "GSSAPIAuthentication=no")
+	}
+
 	// Only OpenSSH version 8.1 and later support adding ciphers to the front of the default set
-	if !sshInfo.openSSHVersion.LessThan(*semver.New("8.1.0")) {
+	if !sshInfo.openSSH.Version.LessThan(*semver.New("8.1.0")) {
 		// By default, `ssh` choose chacha20-poly1305@openssh.com, even when AES accelerator is available.
 		// (OpenSSH_8.1p1, macOS 11.6, MacBookPro 2020, Core i7-1068NG7)
 		//
@@ -321,7 +333,7 @@ func SSHArgsFromOpts(opts []string) []string {
 }
 
 func ParseOpenSSHVersion(version []byte) *semver.Version {
-	regex := regexp.MustCompile(`^OpenSSH_(\d+\.\d+)(?:p(\d+))?\b`)
+	regex := regexp.MustCompile(`(?m)^OpenSSH_(\d+\.\d+)(?:p(\d+))?\b`)
 	matches := regex.FindSubmatch(version)
 	if len(matches) == 3 {
 		if len(matches[2]) == 0 {
@@ -332,6 +344,10 @@ func ParseOpenSSHVersion(version []byte) *semver.Version {
 	return &semver.Version{}
 }
 
+func parseOpenSSHGSSAPISupported(version string) bool {
+	return !strings.Contains(version, `Unsupported option "gssapiauthentication"`)
+}
+
 // sshExecutable beyond path also records size and mtime, in the case of ssh upgrades.
 type sshExecutable struct {
 	Path    string
@@ -340,14 +356,14 @@ type sshExecutable struct {
 }
 
 var (
-	// sshVersions caches the parsed version of each ssh executable, if it is needed again.
-	sshVersions   = map[sshExecutable]*semver.Version{}
-	sshVersionsRW sync.RWMutex
+	// openSSHInfos caches the parsed version and supported options of each ssh executable, if it is needed again.
+	openSSHInfos   = map[sshExecutable]*openSSHInfo{}
+	openSSHInfosRW sync.RWMutex
 )
 
-func DetectOpenSSHVersion(ssh string) semver.Version {
+func detectOpenSSHInfo(ssh string) openSSHInfo {
 	var (
-		v      semver.Version
+		info   openSSHInfo
 		exe    sshExecutable
 		stderr bytes.Buffer
 	)
@@ -357,25 +373,33 @@ func DetectOpenSSHVersion(ssh string) semver.Version {
 	} else {
 		st, _ := os.Stat(path)
 		exe = sshExecutable{Path: path, Size: st.Size(), ModTime: st.ModTime()}
-		sshVersionsRW.RLock()
-		ver := sshVersions[exe]
-		sshVersionsRW.RUnlock()
-		if ver != nil {
-			return *ver
+		openSSHInfosRW.RLock()
+		info := openSSHInfos[exe]
+		openSSHInfosRW.RUnlock()
+		if info != nil {
+			return *info
 		}
 	}
-	cmd := exec.Command(path, "-V")
+	// -V should be last
+	cmd := exec.Command(path, "-o", "GSSAPIAuthentication=no", "-V")
 	cmd.Stderr = &stderr
 	if err := cmd.Run(); err != nil {
 		logrus.Warnf("failed to run %v: stderr=%q", cmd.Args, stderr.String())
 	} else {
-		v = *ParseOpenSSHVersion(stderr.Bytes())
-		logrus.Debugf("OpenSSH version %s detected", v)
-		sshVersionsRW.Lock()
-		sshVersions[exe] = &v
-		sshVersionsRW.Unlock()
+		info = openSSHInfo{
+			Version:         *ParseOpenSSHVersion(stderr.Bytes()),
+			GSSAPISupported: parseOpenSSHGSSAPISupported(stderr.String()),
+		}
+		logrus.Debugf("OpenSSH version %s detected, is GSSAPI supported: %t", info.Version, info.GSSAPISupported)
+		openSSHInfosRW.Lock()
+		openSSHInfos[exe] = &info
+		openSSHInfosRW.Unlock()
 	}
-	return v
+	return info
+}
+
+func DetectOpenSSHVersion(ssh string) semver.Version {
+	return detectOpenSSHInfo(ssh).Version
 }
 
 // detectValidPublicKey returns whether content represent a public key.



Test patch:
diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go
index ed223382..d23db802 100644
--- a/pkg/limayaml/validate_test.go
+++ b/pkg/limayaml/validate_test.go
@@ -40,6 +40,37 @@ func TestValidateProbes(t *testing.T) {
 	assert.Error(t, err, "field `probe[0].file.digest` support is not yet implemented")
 }
 
+func TestValidateProvisionMode(t *testing.T) {
+	images := `images: [{location: /}]`
+	provisionBoot := `provision: [{mode: boot, script: "touch /tmp/param-$PARAM_BOOT"}]`
+	y, err := Load([]byte(provisionBoot+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.NilError(t, err)
+
+	provisionUser := `provision: [{mode: user, script: "touch /tmp/param-$PARAM_USER"}]`
+	y, err = Load([]byte(provisionUser+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.NilError(t, err)
+
+	provisionDependency := `provision: [{mode: ansible, script: "touch /tmp/param-$PARAM_DEPENDENCY"}]`
+	y, err = Load([]byte(provisionDependency+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.NilError(t, err)
+
+	provisionInvalid := `provision: [{mode: invalid}]`
+	y, err = Load([]byte(provisionInvalid+"\n"+images), "lima.yaml")
+	assert.NilError(t, err)
+
+	err = Validate(y, false)
+	assert.Error(t, err, "field `provision[0].mode` must one of \"system\", \"user\", \"boot\", \"data\", \"dependency\", or \"ansible\"")
+}
+
 func TestValidateProvisionData(t *testing.T) {
 	images := `images: [{location: /}]`
 	validData := `provision: [{mode: data, path: /tmp, content: hello}]`
diff --git a/pkg/sshutil/sshutil_test.go b/pkg/sshutil/sshutil_test.go
index 972c3037..4b8b81cc 100644
--- a/pkg/sshutil/sshutil_test.go
+++ b/pkg/sshutil/sshutil_test.go
@@ -29,6 +29,16 @@ func TestParseOpenSSHVersion(t *testing.T) {
 
 	// OpenBSD 5.8
 	assert.Check(t, ParseOpenSSHVersion([]byte("OpenSSH_7.0, LibreSSL")).Equal(*semver.New("7.0.0")))
+
+	// NixOS 25.05
+	assert.Check(t, ParseOpenSSHVersion([]byte(`command-line line 0: Unsupported option "gssapiauthentication"
+OpenSSH_10.0p2, OpenSSL 3.4.1 11 Feb 2025`)).Equal(*semver.New("10.0.2")))
+}
+
+func TestParseOpenSSHGSSAPISupported(t *testing.T) {
+	assert.Check(t, parseOpenSSHGSSAPISupported("OpenSSH_8.4p1 Ubuntu"))
+	assert.Check(t, !parseOpenSSHGSSAPISupported(`command-line line 0: Unsupported option "gssapiauthentication"
+OpenSSH_10.0p2, OpenSSL 3.4.1 11 Feb 2025`))
 }
 
 func Test_detectValidPublicKey(t *testing.T) {