# Adding FIPS Variants

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

---

LLMS index: [llms.txt](/llms.txt) | Full content: [llms-full.txt](/llms-full.txt)

---

## Overview

FIPS variants provide container images that use only
[FIPS 140-3 validated][cmvp] 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.

[cmvp]: https://csrc.nist.gov/projects/cryptographic-module-validation-program

## 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 stack** — `crypto-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: Nginx, Node.js, Python, Ruby. Go images also include these packages
for system tools (e.g., `git-core`), but Go itself uses its own
[native FIPS 140-3 module][go-fips-blog].

**NSS stack** — `crypto-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:**

```yaml
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):

```yaml
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](image-configuration-reference.md) for
complete `properties.yml` options, including
[additional_variants](image-configuration-reference.md#additional_variants)
and [rpm_packages](image-configuration-reference.md#rpm_packages).

### 3. Generate Files

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

```bash
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):**

```yaml
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:

```yaml
# 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](test-configuration-reference.md) for
[variant filters](test-configuration-reference.md#variant-and-distro-filtering)
and [FIPS mode selection](test-configuration-reference.md#fips-mode-selection).

### 5. Build and Test

```bash
# 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 1.24+ includes a native FIPS 140-3 cryptographic module that has been
[CMVP validated][go-fips-blog] ([CMVP Certificate #5247][cmvp-cert]). Unlike the previous
`golang-fips` approach (which replaced the compiler with a fork using OpenSSL
as the crypto backend), native FIPS uses the same `golang` package with
build-time and runtime environment variables:

- **`GOFIPS140=v1.0.0`** (build-time): Tells `go build` to include the
  CMVP-validated FIPS module (v1.0.0) in the resulting binary. Set in the
  image environment so all builds in the container use it.
- **`GODEBUG=fips140=on`** (runtime): Enables FIPS 140-3 mode — uses NIST
  DRBG for randomness, negotiates only FIPS-approved TLS, and runs mandatory
  self-tests. Set in the image environment as the default for binaries run
  in the container.
- **Same package**: The FIPS variant installs the standard `golang1.25`
  package (no `version_package` override needed).
- **No cgo dependency**: Native FIPS works with both `CGO_ENABLED=0` and
  `CGO_ENABLED=1` binaries.

```yaml
main_package: golang1.25
rpm_packages:
  fips:
    - crypto-policies-config-fips
    - golang1.25
    - openssl-config-fips
```

The `openssl-config-fips` package is retained for system tools in the image
(e.g., `git-core`) that use OpenSSL for TLS connections.

Go-specific FIPS tests verify:

- `crypto/fips140.Enabled()` returns `true` (native FIPS module is active)
- `go version -m` output includes `build GOFIPS140=v1.0.0-c2097c7c`
  (CMVP-validated module, per the security policy section 11.1)
- FIPS works with `CGO_ENABLED=0` static binaries (a key advantage over the
  old `golang-fips` approach)

[go-fips-blog]: https://go.dev/blog/fips140
[cmvp-cert]: https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/5247

## 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`:

```yaml
# 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

- [Image Configuration Reference](image-configuration-reference.md) —
  `additional_variants`, `rpm_packages`, `version_package`
- [Test Configuration Reference](test-configuration-reference.md) — variant
  filters, FIPS mode selection
- [Testing Guide](testing-images.md) — how to run and write tests
