Skip to content

build the image with the Microsoft build of Go for FIPS - #60

Merged
huizhifan merged 7 commits into
mainfrom
users/xiaochengfei/fips-compliance
Aug 3, 2026
Merged

build the image with the Microsoft build of Go for FIPS#60
huizhifan merged 7 commits into
mainfrom
users/xiaochengfei/fips-compliance

Conversation

@xiaocheng139

@xiaocheng139 Xiaocheng Fei (xiaocheng139) commented Aug 3, 2026

Copy link
Copy Markdown

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 main is four files: Dockerfile, Makefile, .github/workflows/go-test.yml, README.md.

What lands

  • Builds with the Microsoft build of Go on Azure Linux 3.0, which routes crypto/* through the platform's OpenSSL
  • CGO_ENABLED=1, required by the OpenSSL backend on Linux through Go 1.26
  • Runtime base moves from scratch to azurelinux/base/core:3.0. The binary dlopens libcrypto at startup and static linking to OpenSSL is not permitted, so scratch cannot work. That base ships OpenSSL 3 plus symcryptprovider.so.
  • A build step that fails if the resulting binary does not report microsoft_systemcrypto=1, so this cannot silently regress
  • make test-fips plus a CI job that runs the suite against the OpenSSL backend. It repeats the same microsoft_systemcrypto=1 assertion 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.
  • Build toolchain pinned to 1.25.12-azurelinux3.0 in the Dockerfile; the Makefile's GO_FIPS_IMAGE is parsed out of that FROM line 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 rolling 3.0 tag 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.GenerateKey for 2048 and 3072 bits and silently falls back to non-FIPS Go crypto otherwise.

That is not true of the shipped toolchain. crypto/rsa.GenerateKey routes 2048, 3072 and 4096 to the backend, and returns an error rather than falling back if the backend cannot satisfy the request:

// /usr/local/go/src/crypto/rsa/rsa.go:283 — go1.25.12, from the build image
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 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 from Azure/dalec-build-defs with msft-golang, GOEXPERIMENT=systemcrypto and CGO_ENABLED=1, but imports neither crypto/tls/fipsonly nor -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. f398aee and e1c42ab are reverted in 8869c2e; goalresolvers/ and toolkit/ are byte-identical to main.

#59 items 3 and 4 should be considered invalid, not deferred.

Verification

Covered by CI on every pull request

check result
go test ./... on the upstream toolchain pass
make test-fips — the suite on the OpenSSL backend, gated on microsoft_systemcrypto=1 before the tests run pass

CI 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.

check result
shipped binary reports microsoft_systemcrypto=1 after the copy into the runtime image pass
shipped binary reports CGO_ENABLED=1 — without cgo it cannot dlopen libcrypto pass
runtime image ships /usr/lib/ossl-modules/symcryptprovider.so pass
runtime image ships OpenSSL 3 OpenSSL 3.3.7 7 Apr 2026
binary reaches application code, default environment pass
binary reaches application code under GODEBUG=fips140=on + OPENSSL_FORCE_FIPS_MODE=1 pass

ldd on the binary shows no libcrypto: it is dlopened 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 before main.

Negative controls, so that a green result means something:

deliberately broken image rejected because
built with the crypto backend disabled binary does not report microsoft_systemcrypto=1
correct binary on a scratch runtime base runtime image is missing symcryptprovider.so

Crypto behaviour

check result
fips140.Enabled() under GODEBUG=fips140=on true
suite under GODEBUG=fips140=only, which panics rather than falling back passes, including RSA-4096 key generation
RSA-4096 certificate, TLS handshake under fips140=on succeeds

The fips140=only run is the meaningful one: that mode is genuinely enforcing here — an unrelated TLS probe panicked under it with crypto/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/amd64 and linux/arm64.

Follow-up

GOEXPERIMENT=systemcrypto is 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.

xiaochengfei added 4 commits August 3, 2026 14:24
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for microsoft_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-fips and 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.

Comment thread toolkit/certificates/utils.go Outdated
Comment thread Dockerfile
Comment thread Makefile Outdated
Comment thread goalresolvers/goal_resolver_test.go Outdated
xiaochengfei added 2 commits August 3, 2026 17:23
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.
@xiaocheng139

Copy link
Copy Markdown
Author

Code review

Reviewed at 0f62f49d6023035bfbc4a3caa95754e6b7bbdc31. An earlier pass of mine returned "no issues found" — that was wrong. It checked the change for internal consistency but never checked whether the FIPS premise was true. Verifying the premise against the shipped toolchain turned up 1 issue, now fixed.

Found 1 issue:

  1. The RSA-4096 -> 3072 downgrade and the forced rotation rested on a claim that does not hold for this toolchain. crypto/rsa.GenerateKey in the Microsoft build of Go routes 2048, 3072 and 4096 to the OpenSSL backend, and returns an error rather than falling back when the backend cannot satisfy the request (/usr/local/go/src/crypto/rsa/rsa.go:283, identical in go1.23.12 / 1.24.13 / 1.25.12). The "2048 or 3072 only" wording comes from the FIPS UserGuide, which is stale relative to the shipped code.

const (
// The OpenSSL FIPS backend only implements RSA key generation for 2048 and 3072 bits;
// any other size silently falls back to non-FIPS Go crypto.
KeySize = 3072
)

The certificate consumer, vpa-admission-controller, is built from Azure/dalec-build-defs with msft-golang + GOEXPERIMENT=systemcrypto + CGO_ENABLED=1, but imports neither crypto/tls/fipsonly nor -tags=requirefips, so it runs under plain runtime FIPS mode where RSA-4096 certificates complete a TLS handshake normally. RSA-4096 was therefore 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. Reverted in 8869c2e.

Verification performed, not just CI signal:

check result
runtime image ships OpenSSL + FIPS provider OpenSSL 3.3.7, /usr/lib/ossl-modules/symcryptprovider.so
backend loads at startup (dlopen, not linked — ldd shows no libcrypto) app reaches its normal startup path
fips140.Enabled() under GODEBUG=fips140=on true
suite under GODEBUG=fips140=only, which panics instead of falling back passes, incl. RSA-4096 keygen
RSA-4096 cert TLS handshake under fips140=on succeeds

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.

@xiaocheng139 Xiaocheng Fei (xiaocheng139) changed the title make the image FIPS compliant build the image with the Microsoft build of Go for FIPS Aug 3, 2026
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.
@xiaocheng139
Xiaocheng Fei (xiaocheng139) force-pushed the users/xiaochengfei/fips-compliance branch from 868d4e2 to 1069776 Compare August 3, 2026 08:09
@huizhifan
huizhifan merged commit bb22892 into main Aug 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants