build the image with the Microsoft build of Go for FIPS - #60
Conversation
The test-fips job ran the suite inside the FIPS image but never checked that the systemcrypto backend was actually in use, so a toolchain or image change that silently dropped it would still pass CI green while testing Go's own crypto. Mirror the build-info guard the Dockerfile already applies.
There was a problem hiding this comment.
Pull request overview
This PR updates the build and certificate-generation/rotation logic so the published webhook-tls-manager image and generated certificates are compatible with FIPS requirements (Microsoft Go + OpenSSL backend), including rotating existing non-compliant certs on upgrade.
Changes:
- Switch container build to Microsoft Go on Azure Linux with
CGO_ENABLED=1, plus a build-time check formicrosoft_systemcrypto=1. - Move generated certificate key size to RSA-3072 and add rotation when an existing cert’s RSA key size differs.
- Add
make test-fipsand a CI job to run the test suite under the OpenSSL-backed toolchain; document the behavior in README.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
Dockerfile |
Builds with Microsoft Go + GOEXPERIMENT=systemcrypto, enables CGO, asserts systemcrypto, moves runtime base to Azure Linux core. |
Makefile |
Adds test-fips target using the Microsoft Go container image. |
.github/workflows/go-test.yml |
Adds test-fips CI job running make test-fips. |
toolkit/certificates/certgenerator/cert_generator.go |
Changes generated RSA key size constant from 4096 to 3072 with FIPS rationale. |
toolkit/certificates/utils.go |
Adds helper to read RSA key size from PEM cert; updates test helper cert generation to RSA with configurable key size. |
goalresolvers/goal_resolver.go |
Rotates certs not only on expiry but also when key size mismatches the expected RSA size. |
goalresolvers/goal_resolver_test.go |
Updates existing tests for new cert helper signature and adds a key-size rotation test. |
README.md |
Documents FIPS build approach, RSA-3072 usage, auto-rotation, and make test-fips. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The stated reason for both commits does not hold. crypto/rsa.GenerateKey in
the Microsoft build of Go routes 2048, 3072 AND 4096 bit keys to the OpenSSL
backend, and returns an error instead of falling back when the backend cannot
satisfy the request:
// /usr/local/go/src/crypto/rsa/rsa.go:283 (go1.25.12)
if boring.Enabled && random == boring.RandReader &&
(bits == 2048 || bits == 3072 || bits == 4096) {
bN, ..., err := boring.GenerateKeyRSA(bits)
if err != nil {
return nil, err
}
Identical in 1.23.12 and 1.24.13. The claim that only 2048 and 3072 are
implemented comes from the FIPS UserGuide, which is stale relative to the
shipped toolchain.
The consumer of these certificates, vpa-admission-controller, is built from
Azure/dalec-build-defs with msft-golang, GOEXPERIMENT=systemcrypto and
CGO_ENABLED=1, but imports neither crypto/tls/fipsonly nor -tags=requirefips,
so it runs under plain runtime FIPS mode. RSA-4096 certificates complete a TLS
handshake there, verified under GODEBUG=fips140=on.
So RSA-4096 was already FIPS compliant end to end. Dropping to 3072 bought no
compliance, weakened the key, and would have forced a one-time cert rotation on
every existing cluster.
The build stage floated on 1.25-azurelinux3.0 while the comment next to GOTOOLCHAIN=local claimed a specific patch version, so the compiler could change under a rebuild without anything saying so. Pin both the Dockerfile build stage and GO_FIPS_IMAGE to 1.25.12-azurelinux3.0 and keep them in sync. The runtime base stays on the rolling 3.0 tag on purpose: that is where OpenSSL and the SymCrypt provider come from, so it should pick up CVE fixes on rebuild.
Code reviewReviewed at Found 1 issue:
The certificate consumer, Verification performed, not just CI signal:
The image FIPS work itself (5278caf) is sound and kept. The four Copilot comments are addressed inline; the build toolchain is pinned to an exact patch tag in 0f62f49. |
Pinning the build stage to an exact patch tag put a second copy of the Go toolchain version in the Makefile. Go stdlib CVE bumps in this repo are manual and have always been a two-file change -- the Dockerfile FROM tag and go.mod (0f216f3, 5afef1e, a6cc0f0) -- so that second copy would go stale and CI would test a different toolchain than the one shipping in the image. Parse the tag out of the Dockerfile instead, so the existing bump ritual keeps working untouched, and fail with a clear message if the parse yields nothing.
868d4e2 to
1069776
Compare
Addresses #59, with one correction to that issue — see below.
Makes the published image FIPS compliant by changing how it is built. The certificate code is not changed: the RSA key size part of #59 turned out to rest on a stale premise, and the commits that implemented it are reverted in this branch.
Net change against
mainis four files:Dockerfile,Makefile,.github/workflows/go-test.yml,README.md.What lands
crypto/*through the platform's OpenSSLCGO_ENABLED=1, required by the OpenSSL backend on Linux through Go 1.26scratchtoazurelinux/base/core:3.0. The binarydlopenslibcryptoat startup and static linking to OpenSSL is not permitted, soscratchcannot work. That base ships OpenSSL 3 plussymcryptprovider.so.microsoft_systemcrypto=1, so this cannot silently regressmake test-fipsplus a CI job that runs the suite against the OpenSSL backend. It repeats the samemicrosoft_systemcrypto=1assertion before running the tests — without it, a toolchain or image change that quietly dropped the backend would still pass CI green while exercising Go's own crypto.1.25.12-azurelinux3.0in the Dockerfile; the Makefile'sGO_FIPS_IMAGEis parsed out of thatFROMline rather than duplicated, so a Go stdlib CVE bump stays the same two-file change it has always been (Dockerfile +go.mod, as in 0f216f3 / 5afef1e) and CI cannot end up testing a different toolchain than the one shipping in the image. The runtime base stays on the rolling3.0tag deliberately: that layer is where OpenSSL and the SymCrypt provider come from, so it should pick up CVE fixes on rebuild.Image size goes from roughly 15 MB to roughly 90 MB. That is the cost of needing OpenSSL present at runtime.
What was dropped, and why
#59 asked to move certificates from RSA-4096 to RSA-3072 and to force-rotate existing certificates, on the basis that the OpenSSL backend only implements
rsa.GenerateKeyfor 2048 and 3072 bits and silently falls back to non-FIPS Go crypto otherwise.That is not true of the shipped toolchain.
crypto/rsa.GenerateKeyroutes 2048, 3072 and 4096 to the backend, and returns an error rather than falling back if the backend cannot satisfy the request:Identical in go1.23.12 and go1.24.13. The "2048 or 3072 only" claim traces to the FIPS UserGuide, which is stale relative to the code it documents.
The remaining argument was the x509/TLS restriction in golang/go#41147, which is real but only applies under
crypto/tls/fipsonly. The consumer of these certificates,vpa-admission-controller, is built fromAzure/dalec-build-defswithmsft-golang,GOEXPERIMENT=systemcryptoandCGO_ENABLED=1, but imports neithercrypto/tls/fipsonlynor-tags=requirefips. It runs under plain runtime FIPS mode, where an RSA-4096 certificate completes a TLS handshake normally — verified.So RSA-4096 was already FIPS compliant end to end. Moving to 3072 would have bought no compliance, weakened the key, and forced a one-time certificate rotation on every existing cluster.
f398aeeande1c42abare reverted in8869c2e;goalresolvers/andtoolkit/are byte-identical tomain.#59 items 3 and 4 should be considered invalid, not deferred.
Verification
Covered by CI on every pull request
go test ./...on the upstream toolchainmake test-fips— the suite on the OpenSSL backend, gated onmicrosoft_systemcrypto=1before the tests runCI does not build the image. The Dockerfile's build-stage guard runs on every
make docker-build, so it does gate the published artifact, just at release time rather than on a PR.Run locally against the built image
Since nothing in CI produces an image, I verified the artifact itself by hand — the binary after it is copied onto the runtime base, and that base's ability to load the backend. The script that does this is kept local on purpose and is not part of this PR: it is release-time assurance, not a PR gate.
microsoft_systemcrypto=1after the copy into the runtime imageCGO_ENABLED=1— without cgo it cannotdlopenlibcrypto/usr/lib/ossl-modules/symcryptprovider.soOpenSSL 3.3.7 7 Apr 2026GODEBUG=fips140=on+OPENSSL_FORCE_FIPS_MODE=1lddon the binary shows nolibcrypto: it isdlopened during runtime init, so linkage cannot be confirmed statically. Reaching application code is what proves the load succeeded — a missing or unloadable libcrypto panics the Go runtime beforemain.Negative controls, so that a green result means something:
microsoft_systemcrypto=1scratchruntime basesymcryptprovider.soCrypto behaviour
fips140.Enabled()underGODEBUG=fips140=ontrueGODEBUG=fips140=only, which panics rather than falling backfips140=onThe
fips140=onlyrun is the meaningful one: that mode is genuinely enforcing here — an unrelated TLS probe panicked under it withcrypto/hkdf: use of hash functions other than SHA-2 or SHA-3 is not allowed in FIPS 140-only mode— so a green result is not vacuous.Not verified: an arm64 build under QEMU emulation, and behaviour on a genuinely FIPS-enabled node. Both builder and runtime base images publish
linux/amd64andlinux/arm64.Follow-up
GOEXPERIMENT=systemcryptois accepted on Go 1.25 and 1.26 but rejected on Go 1.27, where the backend is selected automatically. It is commented in the Dockerfile and will need removing at that upgrade.