skip to content

Under GODEBUG=fips140=only a service that built cleanly fails on its first request. How do you find every rejected call before the next deploy?

level: seniorimportance: should knowfreq 42%

answer

  1. the compiler never sees it
  2. guards live inside the crypto packages
  3. some rejections error, some panic
  4. exercise the paths, do not just read them
  5. canary one instance, not the fleet

basics

~20 s

Strict enforcement is a run-time guard inside the standard library's crypto packages, so nothing shows at compile time. Run the test suite and a canary instance under GODEBUG=fips140=only in CI, and audit which non-approved algorithms the code actually reaches.

solid answer

~40 s

Strict mode adds run-time guards inside the standard library's crypto packages, so the compiler has nothing to complain about and the first call on the hot path is where it surfaces. What it rejects: MD5, SHA-1, RC4 and DES; RSA PKCS#1 v1.5 encryption and RSA keys under 2048 bits; HMAC over a hash other than SHA-2 or SHA-3, or with a very short key; `cipher.NewGCM` and its variants, which want `cipher.NewGCMWithRandomNonce` instead; and passing your own `io.Reader` where the API expects `crypto/rand.Reader`. It surfaces as an error where the API can return one and as a panic where it cannot — `md5.New().Write` errors, `md5.Sum` panics. So compile the test binaries and run them with `GODEBUG=fips140=only` as a required CI check, canary one instance under it, and review imports of the non-approved packages.

code

go · 7 lines
go
h := md5.New()
_, err := h.Write(data) // err: use of MD5 is not allowed in FIPS 140-only mode

sum := md5.Sum(data)    // no error to return, so this panics instead

_ = sum
_ = err

go deeper

for a junior

Remember that this mode is checked while the program runs, not while it compiles, so a clean build tells you nothing about whether the service will survive it.

for a middle

Be able to name the rejected families — MD5, SHA-1, RC4, DES, PKCS#1 v1.5 encryption, small RSA keys — and explain why some rejections are errors and others panics.

for a senior

Walk the diagnosis end to end: reproduce under the mode in CI, canary a single instance, read the error and panic messages, and decide per call site between migrating and scoping an exemption.

for a principal

Frame it as a release-gate design problem: where the check belongs in the pipeline, who pays for shared libraries staying compatible, and what you tell an auditor the mode does and does not prove.

## Why the compiler said nothing `GODEBUG=fips140=only` is not a compile-time restriction. The non-approved packages still exist, still compile and still link; strict mode works by placing a guard at the top of the affected functions that checks whether enforcement is on and refuses to proceed. That design choice is why the failure lands where it does: on the first request that reaches the code path, in production, after a deploy that passed every build gate. ## What strict mode actually rejects The guards sit in the standard library's cryptographic packages. The families worth knowing: - **Broken or legacy primitives.** `crypto/md5`, `crypto/sha1`, `crypto/rc4` and `crypto/des` refuse to operate. - **RSA restrictions.** PKCS#1 v1.5 *encryption* is refused, as are keys below 2048 bits and odd key sizes, and signing or verifying with a hash that is not SHA-2 or SHA-3. - **HMAC restrictions.** `hmac.New` refuses a hash outside SHA-2/SHA-3 and refuses very short keys. - **AEAD construction.** `cipher.NewGCM`, `NewGCMWithNonceSize` and `NewGCMWithTagSize` are all refused, because they let the caller supply arbitrary nonces; the approved construction is `cipher.NewGCMWithRandomNonce`. GCM over a non-AES block cipher is refused as well. - **The randomness source.** APIs that take an `io.Reader` for randomness — key generation, signing — refuse anything that is not the default `crypto/rand` reader. Code that passes a deterministic reader for testability breaks here. - **TLS.** In FIPS mode the TLS stack restricts itself to approved protocol versions, curves, signature algorithms and cipher suites, so a handshake with a peer that offers nothing approved simply fails to negotiate. ## Errors in some places, panics in others This is the detail that decides how bad the outage is. Where the function already returns an `error`, the guard returns one: `rc4.NewCipher`, `des.NewCipher`, `cipher.NewGCM` and `(*md5.digest).Write` all hand back an error saying the algorithm is not allowed in FIPS 140-only mode. Where the signature has nowhere to put an error, the guard **panics** — `md5.Sum` and `sha1.Sum` return a fixed-size array, and `hmac.New` returns a `hash.Hash`, so those take the process down instead. So an audit that only looks for unchecked errors will miss half the problem, and a single non-approved `hmac.New` on a startup path can crash every instance in a rolling deploy. ## Finding them before the deploy Three layers, cheapest first. **1. Static review of imports.** Grep the module — including internal shared libraries — for imports of `crypto/md5`, `crypto/sha1`, `crypto/rc4` and `crypto/des`, and for calls to `cipher.NewGCM`. This is fast and catches the obvious cases, but it cannot see a hash chosen at run time from configuration, and it says nothing about a short HMAC key. **2. Run the tests under strict enforcement.** This is the real gate. Compile the test binary and run it with the environment set, so every code path your suite covers is exercised with the guards active. Make it a required check on shared libraries too, so the failure lands in the library author's pull request instead of in the regulated service's production traffic. **3. Canary under the real mode.** Coverage is never complete, so start one instance with `GODEBUG=fips140=only`, send it real traffic, and watch for both crypto errors in logs and process restarts from panics. Do this on a separate instance, not fleet-wide, precisely because some rejections are panics. ## The legitimate non-security uses Plenty of code uses MD5 or SHA-1 for something that is not security at all — a cache key, an ETag, a legacy checksum baked into a wire format. Two honest options. Move off it, usually to SHA-256 truncated to whatever width the caller needs, which also ends the audit conversation. Or, if the digest is pinned by a format you cannot change yet, wrap exactly that call in `crypto/fips140.WithoutEnforcement(func(){ ... })`, added in Go 1.26, which suspends strict enforcement for the duration of the function and is inherited by goroutines started inside it. Its own documentation warns to apply it to tightly scoped functions, and that warning is the point: it is a scalpel, not a global off switch. ## The honest answer to the auditor Strict mode constrains the standard library. It does not constrain a vendored, hand-rolled or cgo-reached implementation compiled into the same binary — those are invisible to the GODEBUG. Demonstrating that the binary only uses approved cryptography therefore needs a dependency review alongside the recorded build settings from `go version -m`, not just the fact that the mode is on.

  • A handler uses MD5 for a cache key, not for security. What are the options under strict enforcement?
    Move off it or scope an exemption. SHA-256 truncated to the width the caller needs is the clean fix and ends the audit conversation. If the digest is pinned by a wire format you cannot change yet, `crypto/fips140.WithoutEnforcement(func(){ ... })`, added in Go 1.26, suspends strict enforcement for exactly that call; its documentation warns to keep the scope tight, and it is inherited by goroutines started inside it.
  • Does GODEBUG=fips140=only guarantee the binary uses only approved cryptography?
    No, and this is the honest answer for an auditor. The guards live inside the standard library's crypto packages, so they constrain `crypto/*` calls only. A vendored or hand-rolled implementation compiled into the same binary is untouched by the GODEBUG, as is anything reached through cgo. Supporting the claim needs a dependency review alongside the recorded build settings, not just the mode being on.
  • Why is a panic from strict enforcement more dangerous during a rolling deploy than an error?
    An error is returned to a caller that can log it and fail one request; a panic unwinds the goroutine and, unrecovered, takes the whole process down. Because `md5.Sum`, `sha1.Sum` and `hmac.New` have no error to return, they panic — and if one sits on a startup or first-request path, every replica in the rollout dies the same way. That is the argument for a canary instance rather than a fleet-wide flip.
  • Which non-approved call is easiest to miss when reviewing code for strict mode?
    Passing a custom `io.Reader` as the randomness source. Tests and deterministic key-generation helpers often pass a seeded reader for reproducibility, and strict mode refuses anything that is not the default `crypto/rand` reader. Nothing about the import list gives it away, the code looks idiomatic, and it only fails when that path executes — which is exactly why the test suite must run under the mode rather than just be read.

saying these in an interview costs you the question

  • Expects the compiler to reject non-approved algorithms
  • Assumes every rejection is an error, never a panic
  • Thinks strict mode also constrains vendored crypto code
  • Believes cipher.NewGCM is allowed under strict enforcement
  • Flips the mode fleet-wide without a canary