Adding FIPS Variants

How FIPS variants work and step-by-step guide for adding them

Overview

FIPS variants provide container images that use only FIPS 140-3 validated cryptographic modules. FIPS is implemented as a cross-cutting modifier variant (like builder), not a separate image — the FIPS variant uses the same base image structure but layers FIPS configuration on top via additional RPM packages.

FIPS variants are restricted to the Hummingbird distro only, because FIPS-validated packages are not available in Fedora Rawhide.

FIPS images must not contain any non-validated cryptographic libraries. The presence of an unvalidated crypto library (such as libgcrypt or gnutls) would undermine the FIPS compliance guarantee, even if the application itself does not use that library. Global tests enforce this constraint.

FIPS Validation Scope

The cryptographic modules shipped in FIPS images are FIPS 140-3 validated through the NIST Cryptographic Module Validation Program (CMVP). This validation applies only when running on RHEL systems installed in FIPS mode. Running FIPS images outside this environment (e.g., on Fedora, on RHEL not in FIPS mode, or on other Linux distributions) is outside the validated configuration.

As a best-effort goal, FIPS images aim to behave similarly on both FIPS and non-FIPS hosts — restricting cryptographic operations to FIPS-approved algorithms regardless of host configuration. This is not a guarantee; some images (particularly NSS-based ones like OpenJDK) may behave differently depending on host FIPS mode.

Validated Cryptographic Modules

All validated modules in Hummingbird are pre-built binaries from RHEL 9.2, submitted to NIST for FIPS 140-3 validation:

Module Package Verified by global test
OpenSSL FIPS provider openssl-config-fips fips-provider-matches-ubi9
NSS softokn nss-softokn-fips fips-nss-modules-match-rhel92
NSS freebl nss-softokn-freebl-fips fips-nss-modules-match-rhel92

Global tests validate each module’s checksum against the known-good RHEL 9.2 validated binaries. Checksums are architecture-specific because the binaries differ per architecture.

All FIPS variants also install crypto-policies-config-fips, which sets the system-wide crypto policy to FIPS.

Two FIPS Stacks

Two FIPS stacks exist depending on which crypto library the image’s software uses:

OpenSSL stackcrypto-policies-config-fips + openssl-config-fips. The OpenSSL FIPS provider is enabled via a drop-in configuration file at /etc/pki/tls/openssl.d/fips-provider-enable.cnf. Enforcement is container-side: non-approved algorithms are rejected even on non-FIPS hosts. Used by: Go, Nginx, Node.js, Python, Ruby.

NSS stackcrypto-policies-config-fips + nss-softokn-fips + nss-softokn-freebl-fips. NSS checks the host kernel’s FIPS mode (/proc/sys/crypto/fips_enabled), so enforcement behavior differs between FIPS and non-FIPS hosts. The primary Java security provider switches to SunPKCS11-NSS-FIPS. Used by: OpenJDK.

Both stacks share crypto-policies-config-fips for the system crypto policy, but differ in which cryptographic library provides the validated implementation.

Adding a FIPS Variant

1. Determine the FIPS Stack

Identify which crypto library the image’s software uses:

  • OpenSSL — most languages and servers (Python, Node.js, Ruby, Nginx, Go)
  • NSS — Java/OpenJDK (uses NSS via PKCS#11)

This determines which FIPS packages to install.

2. Update properties.yml

Add fips to additional_variants with a Hummingbird distro restriction, and add the FIPS packages to rpm_packages.fips.

OpenSSL-based image:

additional_variants:
  - name: fips
    distros: [hummingbird]
rpm_packages:
  fips:
    - crypto-policies-config-fips
    - openssl-config-fips

NSS-based image (OpenJDK):

OpenJDK has existing runtime and runtime-builder base variants. FIPS is added as both fips (full JDK) and runtime-fips (headless JRE):

additional_variants:
  - name: fips
    distros: [hummingbird]
  - name: runtime-fips
    distros: [hummingbird]
rpm_packages:
  fips:
    - java-21-openjdk-devel              # variant-specific packages
    - crypto-policies-config-fips
    - nss-softokn-fips
    - nss-softokn-freebl-fips
  runtime-fips:
    - crypto-policies-config-fips
    - nss-softokn-fips
    - nss-softokn-freebl-fips

See the Image Configuration Reference for complete properties.yml options, including additional_variants and rpm_packages.

3. Generate Files

Run make to generate the hummingbird/fips/ directory structure (Containerfile, RPM lockfiles, VERSION, TAGS):

make

4. Write FIPS Tests

Add FIPS-specific tests to tests-container.yml using filters.variants to target FIPS variants. A standard set of FIPS tests validates:

  1. Non-approved algorithm rejected — e.g., MD5 (for security use), Blowfish, RC2
  2. Approved algorithm works — e.g., SHA-256
  3. Non-approved cipher rejected — e.g., 3DES, CHACHA20-POLY1305
  4. Approved cipher works — e.g., AES-256-GCM (encrypt/decrypt roundtrip)

Example (Python):

fips-rejects-md5:
  filters:
    variants: ["*fips*"]
  command: |
    test_engine_run --rm "${TEST_IMAGE:?}" python3 -c "
    import hashlib
    try:
        hashlib.new('md5')
        print('MD5_ALLOWED')
    except ValueError:
        print('MD5_REJECTED')
    " | grep -q MD5_REJECTED \
        || test_fail "FIPS mode should reject MD5"

fips-allows-sha256:
  filters:
    variants: ["*fips*"]
  command: |
    test_engine_run --rm "${TEST_IMAGE:?}" python3 -c "
    import hashlib
    print(hashlib.sha256(b'test').hexdigest())
    " | grep -q 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 \
        || test_fail "FIPS mode should allow SHA-256"

Host FIPS mode considerations:

Where possible, write tests without filters.fips so they validate behavior on both FIPS and non-FIPS hosts. This supports the best-effort goal of consistent behavior across host types. Use filters.fips: true only for tests that inherently require a FIPS-enabled host kernel — primarily NSS-based images where enforcement depends on the host:

# NSS-based: rejection only works on FIPS hosts
fips-rejects-blowfish:
  filters:
    variants: ["*fips*"]
    fips: true                    # requires FIPS-enabled host kernel
  command: |
    ...

See the Test Configuration Reference for variant filters and FIPS mode selection.

5. Build and Test

# Build the FIPS variant
ci/build_images.sh <image>/hummingbird/fips

# Run tests
ci/run_tests_container.sh <image>/hummingbird/fips

# Run linters
make check

Go-Specific FIPS Requirements

Go FIPS uses a completely different compiler toolchain from the standard Go variant:

  • Package: golang-fips1.25 instead of golang1.25 — a FIPS-patched Go build that uses OpenSSL as the cryptographic backend
  • Environment variable: GOLANG_FIPS=1 must be set in the Containerfile (via the Jinja2 template)
  • Version package override: Because the FIPS variant installs a differently-named package, version_package tells the version/tag macros which package to resolve from:
main_package: golang1.25
version_package:
  fips: golang-fips1.25
rpm_packages:
  fips:
    - crypto-policies-config-fips
    - golang-fips1.25
    - openssl-config-fips

Go-specific FIPS tests verify:

  • crypto/boring.Enabled() returns true (OpenSSL FIPS backend is active)
  • GOEXPERIMENT=strictfipsruntime blocks CGO_ENABLED=0 binaries on FIPS hosts (this test uses filters.fips: true)

See version_package in the Image Configuration Reference.

OpenJDK-Specific FIPS Requirements

OpenJDK uses the NSS stack instead of OpenSSL, which has several implications:

  • No openssl-config-fips — uses nss-softokn-fips + nss-softokn-freebl-fips instead
  • Host FIPS mode dependency — NSS checks /proc/sys/crypto/fips_enabled, so FIPS enforcement behavior differs between FIPS and non-FIPS hosts. Crypto rejection tests must use filters.fips: true.
  • Compound variants — OpenJDK introduces runtime-fips alongside fips, combining the runtime base variant with the FIPS modifier. Both share the same NSS FIPS packages; fips additionally includes java-*-openjdk-devel.
  • Security provider — in FIPS mode, the primary Java security provider becomes SunPKCS11-NSS-FIPS instead of the default SUN/SunJCE providers.

Global FIPS Tests

The following tests run automatically for all FIPS images via ci/global-tests/tests-container.yml. Each test skips gracefully for images using the other FIPS stack.

Test Validates Applies to
fips-provider-matches-ubi9 OpenSSL FIPS module checksum matches RHEL 9.2 binaries OpenSSL stack
fips-nss-modules-match-rhel92 NSS module checksums match RHEL 9.2 validated binaries NSS stack
fips-no-libgcrypt libgcrypt is not present (not a validated module) All FIPS
openssl-fips-config-installed OpenSSL FIPS drop-in config exists OpenSSL stack
crypto-policy-is-fips System crypto policy is set to FIPS All FIPS

Image-specific FIPS tests (algorithm rejection/acceptance) are defined in each image’s tests-container.yml.

Testing on FIPS Hosts

The test runner detects host FIPS mode from /proc/sys/crypto/fips_enabled and exports the TEST_FIPS environment variable (true or false). Tests can filter on this using filters.fips:

# Runs only on FIPS-enabled hosts
fips-host-required:
  filters:
    fips: true
  command: |
    ...

# Runs only on non-FIPS hosts
non-fips-only:
  filters:
    fips: false
  command: |
    ...

# Runs regardless of host FIPS mode (default when filters.fips is omitted)
always-runs:
  command: |
    ...

OpenSSL-based images: Most FIPS tests run regardless of host FIPS mode, because the OpenSSL FIPS provider enforces algorithm restrictions at the container level.

NSS-based images: Crypto rejection tests require filters.fips: true, because NSS enforcement depends on the host kernel’s FIPS mode.

Next Steps