Skip to content

sshutil: suppress warnings when SSH lacks GSSAPI #3637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 47 additions & 23 deletions pkg/sshutil/sshutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}.
Expand Down Expand Up @@ -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",
Expand All @@ -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 [email protected], even when AES accelerator is available.
// (OpenSSH_8.1p1, macOS 11.6, MacBookPro 2020, Core i7-1068NG7)
//
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
)
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions pkg/sshutil/sshutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading