Go version

go version go1.24.4 (Red Hat 1.24.4-1.module+el8.10.0+23323+67916f33) linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/opt/app-root/src/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/opt/app-root/src/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2709008956=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/opt/app-root/src/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/opt/app-root/src/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/golang'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/opt/app-root/src/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/usr/lib/golang/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.4 (Red Hat 1.24.4-1.module+el8.10.0+23323+67916f33)'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Compiled the below source code for FIPS compliance as

RUN CGO_ENABLED=1 GOOS=linux GOEXPERIMENT=strictfipsruntime go build -tags strictfipsruntime -buildvcs=false -o github-ssh-client .

Full source code and Dockerfile available at https://github.com/anandf/ssh-fips

package main

import (
    "crypto/fips140"
    "log"
    "os"
    "path/filepath"
)

import (
    "golang.org/x/crypto/ssh"
)

func main() {
    log.Printf("Fips enabled? %v\n", fips140.Enabled())
    homeDir, err := os.UserHomeDir()
    if err != nil {
        log.Fatalf("error when getting user's home directory: %v", err)
    }
    sshKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa")
    sshKey, err := os.ReadFile(sshKeyPath)
    if err != nil {
        log.Fatalf("error when reading private key from %s: %v", sshKeyPath, err)
    }
    sshKeySigner, err := ssh.ParsePrivateKey(sshKey)
    if err != nil {
        log.Fatalf("error when parsing private key: %v", err)
    }
    cfg := &ssh.ClientConfig{
        User:            "git",
        Auth:            []ssh.AuthMethod{ssh.PublicKeys(sshKeySigner)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    cfg.SetDefaults()

    //cfg.Ciphers = []string{"aes256-gcm@openssh.com"}
    cfg.KeyExchanges = []string{"curve25519-sha256"}
    conn, err := ssh.Dial("tcp", "github.com:22", cfg)
    if err != nil {
        log.Fatalf("Failed to dial: %v", err)
    }
    defer conn.Close()
    log.Println("SSH connection was successful")
}

What did you see happen?

When running in a cluster in FIPS enabled mode, the program crashes with the below panic and stack trace.

2025/08/18 07:15:22 Fips enabled? false
panic: curve25519: internal error: scalarBaseMult was not 32 bytes

goroutine 16 [running]:
golang.org/x/crypto/curve25519.ScalarBaseMult(0xc0001343a0, 0x6c2a50?)
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/curve25519/curve25519.go:39 +0x13b
golang.org/x/crypto/ssh.(*curve25519KeyPair).generate(0xc000134380, {0x6c4dc0?, 0x6c2a50?})
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/kex.go:457 +0x65
golang.org/x/crypto/ssh.(*curve25519sha256).Client(0xa30740?, {0x7f739b2feb18, 0xc000116120}, {0x6c4dc0, 0x6c2a50}, 0xc00006a540)
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/kex.go:468 +0x6e
golang.org/x/crypto/ssh.(*handshakeTransport).client(0xc0001084e0, {0x6c5208?, 0xa79720?}, 0xc00006a540?)
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/handshake.go:826 +0x66
golang.org/x/crypto/ssh.(*handshakeTransport).enterKeyExchange(0xc0001084e0, {0xc0000dc300, 0x2d8, 0x2d8})
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/handshake.go:716 +0x49a
golang.org/x/crypto/ssh.(*handshakeTransport).kexLoop(0xc0001084e0)
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/handshake.go:345 +0x98
created by golang.org/x/crypto/ssh.newClientTransport in goroutine 1
        /opt/app-root/src/go/pkg/mod/golang.org/x/crypto@v0.41.0/ssh/handshake.go:170 +0x256

What did you expect to see?

The key exchange algorithm curve25519 is not FIPS compliant. There should be graceful error handling that says that the selected algorithim is not FIPS compliant and if there are other available FIPS compliant algorithims available in the list, those should be prioritized for the key exchange.

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: cagedmantis

cc @drakkan @golang/security @FiloSottile

Comment From: FiloSottile

strictfipsruntime is not an upstream option, so you are using a downstream unsupported fork.

This would probably work out the same way with the supported GODEBUG=fips140=only, though.

fips140=only is known to be overly strict, and there is a discussion at #74630 on how to improve it.

In parallel, it might make sense for x/crypto/ssh to gain a FIPS mode like crypto/tls, which disables non-FIPS algorithms.

Comment From: anandf

Thanks @FiloSottile. I tried using the supported golang (docker.io/library/golang:1.24.4) and am able to reproduce this problem with runtime setting GODEBUG=fips140=only. We are working around the issue by setting GODEBUG env to fips140=on. I was wondering if there was a graceful handling of this error, instead of panic, thereby the SSH handshake can happen with a next FIPS compliant algorithm in the list of key exchange algorithm. This is the fix we have done to remove the problematic algorithm if FIPS is enabled. https://github.com/argoproj/argo-cd/pull/24086

Comment From: gopherbot

Change https://go.dev/cl/696996 mentions this issue: curve25519: include potential fips140=only error in panic message

Comment From: gopherbot

Change https://go.dev/cl/696995 mentions this issue: ssh: use curve25519.X25519 instead of curve25519.ScalarMult

Comment From: FiloSottile

No, because of how the SSH protocol works, there is no way to change the algorithms once they have been selected, so the best we can do is fail the handshake with an error instead of a panic. That's what CL 696995 does.

To avoid breaking the connection, you need to remove non-FIPS algorithms from the config. For now, you'll have to do it manually, but we will consider doing it automatically like in crypto/tls.

Comment From: anandf

Thanks @FiloSottile for your valuable inputs. I have removed the non FIPS compliant algorithm in our code when host is running in FIPS mode as part of https://github.com/argoproj/argo-cd/pull/24086.

Comment From: gopherbot

Change https://go.dev/cl/698795 mentions this issue: ssh: add support for FIPS-only mode