Ideally, you want to build software once and run it anywhere. With FIPS and Go that can be tricky because in upstream, FIPS is baked in at build time. Using the Red Hat golang toolchain you can produce binaries that detect the FIPS mode of the underlying host, and automatically run in the right mode.

Go 1.26 introduced crypto/fips140 — a new standard library package giving Go programs first-class awareness of FIPS 140-3 mode. Red Hat has been shipping a patched Go toolchain in their UBI images for longer than that, with OpenSSL-backed crypto that integrates with the Linux kernel’s FIPS flag.

But how do these two mechanisms interact? And which build combinations actually give you automatic, kernel-aware FIPS — meaning FIPS active on a FIPS-enabled host and transparent everywhere else, from the same binary?

We ran a systematic test across all meaningful build combinations on both a FIPS-enabled RHEL 10 host and a standard non-FIPS Linux host. This post shares what we found. The test program and Makefile are available at github.com/Jamstah/fips-go.

TL;DR

This blog post is all based on Go 1.26. In Go 1.27, Red Hat are no longer supporting use of openssl as the crypto for their Go toolset, see Migration to Upstream FIPS certified cryptography.

To build a Go binary that automatically uses FIPS on FIPS-enabled hosts and runs normally everywhere else, pick one:

Option A — OpenSSL backend (simplest, no runtime config):

The platform openssl is used at runtime, so your runtime OS needs to include openssl and support FIPs.

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
RUN go build -o /myapp .          # no GOFIPS140, no no_openssl

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

Option B — Go native FIPS module (fips140.Enabled() works correctly):

You have to build on the Red Hat toolset but you can run on any OS because native Go FIPS is used. You need to set the GODEBUG option at runtime for automatic FIPS enablement.

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
RUN go build -tags no_openssl -o /myapp .

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENV GODEBUG=fips140=auto
ENTRYPOINT ["/usr/local/bin/myapp"]

Option C — Go native FIPS module and config using go.mod:

You need to build on the Red Hat toolset and you can run anywhere without runtime configuration because the default GODEBUG fips140 setting is linked in to the binary.

Add a godebug directive to go.mod:

godebug fips140=auto

Then build with no_openssl on the Red Hat toolset:

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
RUN go build -tags no_openssl -o /myapp .

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

Background: Two independent FIPS mechanisms

Before looking at results, it helps to understand that there are two completely separate ways Go can enforce FIPS on RHEL/UBI. They do not communicate with each other.

1. The Go native FIPS module (GOFIPS140)

GOFIPS140 is a build-time environment variable consumed by cmd/go. When set to anything other than off, it compiles the FIPS 140-3 cryptographic module into the binary and bakes a GODEBUG=fips140=on default into the binary at link time. This default is not a runtime environment variable — it is embedded in the binary and takes effect on every host, unconditionally.

The crypto/fips140.Enabled() function introduced in Go 1.26 reflects this mechanism.

2. The Red Hat OpenSSL backend

The Red Hat ubi10/go-toolset image replaces Go’s standard crypto with an OpenSSL-backed implementation by default (GOEXPERIMENT=opensslcrypto). OpenSSL reads /proc/sys/crypto/fips_enabled at process startup and enforces FIPS when the kernel flag is set — completely independently of GOFIPS140.

crypto/fips140.Enabled() is blind to this mechanism. A binary built with GOFIPS140=off on a FIPS-enabled RHEL host will report fips140.Enabled() = false while OpenSSL is actively enforcing FIPS.

Why you can’t combine them

Combining GOFIPS140=latest (or any non-off value) with the OpenSSL backend panics at startup:

panic: opensslcrypto: GOLANG_FIPS and GODEBUG=fips140 are mutually exclusive

The Red Hat toolchain deliberately prevents both paths from being active simultaneously. The no_openssl build tag disables the OpenSSL backend and switches to the pure-Go FIPS module.


The test

We built a small Go program that:

  1. Prints build metadata from the binary’s embedded build info (Go version, GOFIPS140, build tags) so each run is self-describing
  2. Reports crypto/fips140.Enabled()
  3. Attempts an HTTPS connection to 3des.badssl.com, a host that only negotiates the non-FIPS 3DES cipher suite. The client is configured to offer only that suite (TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, TLS 1.2 max, InsecureSkipVerify since cert validation is not what we’re testing). If anything has removed 3DES from the stack, the handshake fails — no runtime flags needed.

We tested eleven combinations: eight distinct image builds, plus three of those re-run with GODEBUG=fips140=auto injected at runtime.


Results

The full matrix

Target GODEBUG at runtime fips140.Enabled() 3DES on FIPS host 3DES on non-FIPS host FIPS mechanism
golang:1.26, GOFIPS140=off false succeeded succeeded none
golang:1.26, GOFIPS140 unset false succeeded succeeded none (unset == off on upstream)
golang:1.26, GOFIPS140=latest true blocked blocked Go native, unconditional
ubi10/go-toolset, GOFIPS140=off false blocked succeeded OpenSSL reads kernel flag
ubi10/go-toolset, GOFIPS140 unset false blocked succeeded same as above
ubi10/go-toolset, GOFIPS140=off, no_openssl false succeeded succeeded none
ubi10/go-toolset, GOFIPS140 unset, no_openssl false succeeded succeeded none
ubi10/go-toolset, GOFIPS140=latest, no_openssl true blocked blocked Go native, unconditional
ubi10/go-toolset, GOFIPS140=off, no_openssl fips140=auto false/true blocked succeeded Go native, kernel-aware
ubi10/go-toolset, GOFIPS140 unset, no_openssl fips140=auto false/true blocked succeeded Go native, kernel-aware
ubi10/go-toolset, GOFIPS140=latest, no_openssl fips140=auto false/true blocked succeeded auto overrides link-time on

Key observations

crypto/fips140.Enabled() only reflects the Go native FIPS module. When the OpenSSL backend is enforcing FIPS, fips140.Enabled() returns false. You cannot use this API to check whether FIPS is active in an OpenSSL-backed binary.

GOFIPS140=latest is unconditional. The FIPS module is active on every host, FIPS-enabled or not. There is no “activate only if the kernel has FIPS enabled” behaviour in the upstream toolchain.

GODEBUG=fips140=auto overrides the link-time default in both directions. On a GOFIPS140=latest binary running on a non-FIPS host, setting GODEBUG=fips140=auto at runtime disables FIPS — 3DES succeeded and fips140.Enabled() returned false.


The two approaches that give you automatic FIPS

If you want a binary that is FIPS-compliant on a FIPS host and runs normally elsewhere — without changing the binary between environments — there are two approaches that work today.

Approach 1: Build with the Red Hat toolset, keep the OpenSSL backend

This is the simplest approach and the default when using ubi10/go-toolset.

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
# Do NOT set GOFIPS140, do NOT add no_openssl
RUN go build -o /myapp .

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

At runtime, OpenSSL reads /proc/sys/crypto/fips_enabled and automatically enforces FIPS when the kernel flag is set. On a non-FIPS host, it runs normally. No build flags, no environment variables, no operator action required.

The catch: crypto/fips140.Enabled() returns false even when FIPS is active, because enforcement is happening in OpenSSL, not the Go native module. If your application code calls fips140.Enabled() to gate behaviour, it will not work correctly with this approach.

Also note: for this to work at runtime, the container must link against OpenSSL — meaning you need a runtime image that includes OpenSSL libraries (such as ubi-minimal or ubi), not a scratch image.

Approach 2: Build with the Red Hat toolset, no_openssl, and set GODEBUG=fips140=auto

This approach uses the Go native FIPS module and activates it via the GODEBUG=fips140=auto setting introduced by the Red Hat patches to the Go toolchain. At startup, the Go runtime reads /proc/sys/crypto/fips_enabled. If it is 1, FIPS mode activates and fips140.Enabled() returns true. On a non-FIPS host, auto resolves to off and the program runs normally.

There are two ways to set fips140=auto — pick whichever fits your workflow.

Option B: Set it in the Dockerfile

Bake it into the container image itself with ENV:

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
RUN go build -tags no_openssl -o /myapp .

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENV GODEBUG=fips140=auto
ENTRYPOINT ["/usr/local/bin/myapp"]

Option C: Set it in go.mod (Go 1.23+)

Add a godebug directive to your module’s go.mod:

godebug fips140=auto

The Go toolchain bakes this into the binary as a link-time default — no ENV in the Dockerfile and no Kubernetes configuration needed. The binary carries the setting with it regardless of where it is deployed.

FROM registry.access.redhat.com/ubi10/go-toolset:1.26 AS builder
RUN go build -tags no_openssl -o /myapp .

FROM registry.access.redhat.com/ubi10/ubi-minimal
COPY --from=builder /myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

The advantage over Approach 1: crypto/fips140.Enabled() correctly reflects the FIPS state, so your application code can query it reliably. The binary can also run on a scratch or non-OpenSSL runtime image since it has no OpenSSL dependency.

The caveat: GODEBUG=fips140=auto overrides the link-time default in both directions. If it is set (via ENV, go.mod, or the host environment) and you also deploy binaries built with GOFIPS140=latest, those binaries will have FIPS disabled on non-FIPS hosts. Only use auto if you want kernel-aware activation. If you need FIPS unconditionally, use GOFIPS140=latest and do not set a runtime GODEBUG override.


A note on GOFIPS140 unset with the Red Hat toolchain

The golang-fips patch is designed to make GOFIPS140 unset default to v1.0.0 (a NIST-certified snapshot) with GODEBUG=fips140=auto baked in at link time — giving automatic kernel-aware activation from an unmodified build, with no explicit go.mod entry and no runtime env var.

However, ubi10/go-toolset:1.26 does not yet implement this default: go env GOFIPS140 returns off when unset. Until the patch ships in this image, use Option C (go.mod) or Option B (ENV) from Approach 2 above to achieve the same result today.


What about the upstream golang:1.26 image?

The upstream golang:1.26 image does not include the Red Hat OpenSSL patches. GOFIPS140 unset is treated as off. The only way to get FIPS is GOFIPS140=latest, which activates it unconditionally on all hosts.

If you need a kernel-aware, automatic FIPS behaviour with the upstream image, you would need to detect /proc/sys/crypto/fips_enabled yourself at startup and set GODEBUG=fips140=on programmatically — but that is not standard Go behaviour and outside the scope of this post.

For FIPS requirements, the Red Hat UBI toolset images are the right choice.


Summary

Goal Approach
FIPS always active, all hosts GOFIPS140=latest, no_openssl, no runtime override
FIPS only on FIPS hosts (kernel-aware), fips140.Enabled() accurate no_openssl, GODEBUG=fips140=auto at runtime
FIPS only on FIPS hosts (kernel-aware), simpler setup Default UBI toolset build (OpenSSL backend, no no_openssl)
No FIPS GOFIPS140=off or upstream image

For production workloads that need to run on both FIPS and non-FIPS hosts from a single image, either the OpenSSL default or the GODEBUG=fips140=auto approach will work today. The OpenSSL approach requires no runtime configuration but does not expose FIPS state through crypto/fips140.Enabled(). The auto approach gives you accurate fips140.Enabled() reporting but requires setting a runtime environment variable until the v1.0.0 default patch ships in the toolchain.

The test code, Dockerfiles, and Makefile for all eleven combinations tested here are available at github.com/Jamstah/fips-go.