Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FIPS 140-3

Available since 0.3.0 as ghcr.io/zerosandonesllc/ridm:<version>-fips, next to the standard image. The work was tracked in #7.

FIPS 140-3 is the US and Canadian government standard for cryptographic modules. Some organizations must run software whose cryptography comes only from a module that NIST has validated. Typical examples are US federal agencies, their contractors (FedRAMP, CMMC) and regulated industries. These organizations often run FIPS-mode clusters, such as OpenShift on RHEL CoreOS installed with fips: true.

Most deployments don’t need any of this. The standard build and image are the default and are unaffected. Nothing on this page changes how they behave or how you configure them. You only need to read further if your organization requires FIPS.

Two builds, one codebase

rIDM has a single build switch, the fips cargo feature, which is off by default. Every feature works in both builds:

Standard build (default)FIPS build
How to get itcargo build, or the ghcr.io/zerosandonesllc/ridm:<version> image--features fips, or the ghcr.io/zerosandonesllc/ridm:<version>-fips image
Cryptographic moduleaws-lc-rs (AWS-LC)aws-lc-rs on the AWS-LC FIPS module, for everything: neither build links OpenSSL
New password hashesargon2idPBKDF2-HMAC-SHA512
Secrets at restXChaCha20-Poly1305, AES-256-GCM from the next release (reads both)AES-256-GCM
TLSTLS 1.2 and 1.3 with every rustls suite and group, including ChaCha20 and X25519TLS 1.2 and 1.3 with AES-GCM suites only, and the P-256, P-384 and X25519MLKEM768 groups
Starts on a host without FIPS modeyesno, unless FIPS_ALLOW_NON_FIPS_HOST=true (development and CI only)
Platformsimage (amd64, arm64) and static musl binariesimage only (see What isn’t covered)

The two builds use the same database schema, the same tenant documents and the same configuration. You can move a deployment from one build to the other in either direction (see Moving an existing deployment).

Wherever the FIPS-approved way of doing something is just as good for everyone, both builds share it, so there is only one path to test. This applies to hashing, HMAC, key generation, random numbers, TOTP and Kerberos crypto, all of which go through aws-lc-rs in both builds. Both builds also read both ciphers for secrets at rest; the standard build keeps writing XChaCha20-Poly1305 for one more release, so a rolling update or a rollback to the release before never meets a value it can’t read. The builds only differ where the standard build has a better choice for the average deployment, such as argon2id for passwords.

Building the FIPS image

Each release publishes the FIPS image as ghcr.io/zerosandonesllc/ridm:<version>-fips (also <major>.<minor>-fips and latest-fips), for linux/amd64 and linux/arm64. It’s signed and carries an SBOM like the standard image (see Releases and verification). Pin it by digest.

To build it yourself, use api/Dockerfile.fips from the repository root:

docker build -f api/Dockerfile.fips -t ridm:fips .

It’s built on Red Hat Universal Base Image 9 (ubi9/ubi for the build, ubi9/ubi-minimal at run time). Every package it installs comes from the freely available UBI repositories, so no RHEL subscription is needed to build it. It builds the same way on an entitled OpenShift cluster, a RHEL host or a laptop. The build stage:

  • installs golang, cmake, clang and perl. The AWS-LC FIPS module is compiled from source by aws-lc-fips-sys, and its build needs Go;
  • builds with --no-default-features --features fips,embedded-ui,kerberos,hsm-pkcs11,kms-aws,kms-vault,kms-gcp,kms-azure. Every feature of the standard image is included;
  • fails if the binary links anything but the FIPS module’s AWS-LC, links ring, or links any OpenSSL (scripts/fips/check-binary.sh).

The runtime image runs as the same non-root user (65532) as the standard one, with the same healthcheck and environment. Outside a container, the build command is:

cargo build --release --locked -p ridm-api \
  --no-default-features \
  --features fips,embedded-ui,kerberos,hsm-pkcs11,kms-aws,kms-vault,kms-gcp,kms-azure

Running it

  • The host must be in FIPS mode. For OpenShift, that means installing the cluster with fips: true. For RHEL, run fips-mode-setup --enable and reboot.

  • At start-up the server runs the FIPS module’s self-test, checks that the host is in FIPS mode (/proc/sys/crypto/fips_enabled is 1), and logs the module version:

    INFO FIPS build: the AWS-LC FIPS module passed its self-test module_version="4.2.0" fips_version=Some(4) host_fips=true
    

    Every TLS configuration it builds (the HTTPS and mutual-TLS listeners, LDAP, the audit syslog sink, the healthcheck) must pass rustls’ FIPS check. If any check fails, the server refuses to start and says which. The ridm command line and every ridm-api subcommand, the container healthcheck included, run the same checks.

  • Developing or running CI on a machine that isn’t in FIPS mode: set FIPS_ALLOW_NON_FIPS_HOST=true. The module self-test still has to pass, and the server prints a warning at start-up and logs it again. Never set this in production. A host that isn’t in FIPS mode isn’t a FIPS-validated configuration.

  • Postgres: connect with client-certificate authentication over TLS (sslmode=verify-full with sslcert/sslkey) rather than a password. The Postgres driver sqlx still implements password authentication (SCRAM) with non-validated code. That’s tracked upstream in transact-rs/sqlx#4416. On Kubernetes the chart’s database.tls and migrations.database.tls mount the certificates and build the URLs (Kubernetes).

  • Valkey: likewise, authenticate by client certificate rather than a password: rediss:// with REDIS_TLS_CA_FILE, REDIS_TLS_CERT_FILE and REDIS_TLS_KEY_FILE, and Valkey’s tls-auth-clients-user CN mapping the certificate onto a user with no password, so Valkey’s own password hashing never runs (Postgres and Valkey). SMTP, LDAP, webhooks and the KMS backends all use the FIPS TLS provider.

Settings only the FIPS build reads

VariableDefaultMeaning
PBKDF2_ITERATIONS210000PBKDF2-HMAC-SHA512 iterations for new password hashes, 210,000 to 1,000,000. Raising it re-hashes each user at their next sign-in.
FIPS_TRANSITIONfalseAllows the one-time legacy operations described below. Turn it off when the move is done.
FIPS_ALLOW_NON_FIPS_HOSTfalseLets the FIPS build start on a host that isn’t in FIPS mode, with a warning. For development and CI only.

The standard build ignores these variables, and its ARGON2_* settings are unchanged. A fresh FIPS deployment sets neither FIPS_TRANSITION nor FIPS_ALLOW_NON_FIPS_HOST: the first exists only for the move described below, the second only for development and CI. The Helm chart’s FIPS example (Kubernetes) sets neither.

Moving an existing deployment

Some data that a standard deployment already holds can only be read with algorithms that FIPS doesn’t approve:

  • password hashes made with argon2id, and imported bcrypt, MD5 or salted-digest hashes;
  • secrets at rest written before the AES-256-GCM change, which use XChaCha20-Poly1305.

No library can make reading this data FIPS-approved, but rIDM doesn’t make you throw it away either. Turning on FIPS_TRANSITION=true lets the FIPS build do those reads during a migration window. Every one of them is logged as a warning and counted in the ridm_fips_non_approved_total metric (labelled with the operation), so you can watch it drop to zero. With the flag off, which is the default, the FIPS build refuses them and says why: a FIPS server that finds XChaCha20 secrets in its database refuses to start, and names the steps below.

  1. Upgrade to a release that includes the FIPS work, still on the standard build. It reads AES-256-GCM as well as XChaCha20-Poly1305, so you can go back to it later.
  2. Switch to the -fips image with FIPS_TRANSITION=true. It writes new secrets with AES-256-GCM.
  3. Re-encrypt secrets at rest with ridm-api rotate-master-key (or ridm master-key rotate) on the FIPS build. It rewrites every XChaCha20 row onto AES-256-GCM, including rows already under the current master-key generation, so you don’t need a new master key. rotate-master-key --status shows what’s left in legacy_cipher_rows.
  4. Let passwords move over. When a user with an older hash signs in, rIDM checks the password once with the old algorithm and stores a PBKDF2-HMAC-SHA512 hash. PBKDF2 hashes from elsewhere (SHA-256 or SHA-512, passlib or Django form) are verified the approved way and need no transition, though they’re re-hashed too. To see who hasn’t moved yet, list a tenant’s users by hash format through the admin API, GET /admin/tenants/{slug}/users?password_algo=argon2id (also bcrypt, md5, …); a user’s detail shows its format as well. Reset those users’ passwords whenever you decide.
  5. Turn FIPS_TRANSITION off. From then on, the deployment runs only approved operations.

To import users with legacy hashes later, for example from another identity provider, turn the flag on for the import and the sign-ins that follow, then turn it off again.

Going back to the standard build needs no preparation. The standard build reads PBKDF2 hashes and AES-256-GCM rows, and it re-hashes passwords to argon2id as users sign in.

Feature by feature

FeatureIn the FIPS buildIssue
TLS (listener, mutual TLS, outbound connections)Uses rustls’ FIPS provider: AES-GCM suites only, with the P-256, P-384 and X25519MLKEM768 key exchange groups. X25519MLKEM768 is a post-quantum hybrid. rustls allows it in FIPS mode under NIST SP 800-56C rev 2, which permits a hybrid shared secret when one part (here ML-KEM, FIPS 203) is approved. Each configuration rIDM builds is checked with ServerConfig::fips() / ClientConfig::fips().#1
Token signing (RS256/384/512, ES256)Runs on aws-lc-rs, as it already does in both builds.#5
EdDSA signing keysAvailable in both builds. Whether Ed25519 is inside the AWS-LC FIPS module boundary is being confirmed against the module’s security policy. If it isn’t, the FIPS build marks EdDSA as non-approved, and admins can turn it off per tenant.#5
Key generation, hashing, HMAC, random numbersUses aws-lc-rs in both builds. Random values come from the module’s SP 800-90A DRBG.#5
TOTP (authenticator apps)RFC 6238 on aws-lc-rs HMAC in both builds. Existing enrolments keep working.#5
Secrets at restAES-256-GCM under a key derived per value (HKDF-SHA-256 with a random salt), with the IV generated inside the module. The standard build reads it too, and writes it from the next release.#3
PasswordsPBKDF2-HMAC-SHA512 (SP 800-132) with a 128-bit salt. Other formats move over at sign-in; only PBKDF2 is verified outside FIPS_TRANSITION. Bulk imports of other formats need FIPS_TRANSITION too.#4
Passkeys (WebAuthn)rIDM’s own relying party on aws-lc-rs in both builds: ES256 and RS256 signatures (what registration offers), challenges from the module’s DRBG. It replaced webauthn-rs, which needed OpenSSL, and stores passkeys in the same format, so existing passkeys keep working. RHEL’s OpenSSL FIPS provider couldn’t have done it alone: it has no Ed25519.#2
Kerberos desktop sign-inThe AES encryption types on aws-lc-rs in both builds: RFC 3962’s (17/18) and RFC 8009’s (19/20). The RFC 8009 types use only approved algorithms: AES, HMAC-SHA-256/384 and the SP 800-108 key derivation. RFC 3962’s key derivation (RFC 3961 “DK”) is built from AES, but it isn’t an SP 800-108 KDF, so prefer RFC 8009 keys where the KDC has them. Active Directory issues only RFC 3962 types, and RHEL IdM in FIPS mode issues the RFC 8009 ones.#6
AWS KMSUses the AWS SDK, as in the standard build. Its TLS goes through the FIPS provider. Its request signing is the SDK’s own code (see What isn’t covered).#1
Vault, Google Cloud KMS, Azure Key VaultREST over the FIPS TLS provider. The key itself is protected by the service’s own validation.—
PKCS#11 HSMThe HSM performs the key operations, so its own FIPS validation is the one that counts.—
SAML and OIDCSHA-1 signatures are already refused. RSA-OAEP with SHA-1 (JWE RSA-OAEP, SAML rsa-oaep-mgf1p) stays available for compatibility with existing integrations. NIST plans to retire SHA-1, so the FIPS build may make it opt-in.#7

What isn’t covered

  • The static musl release binaries. These are standard builds only. The AWS-LC FIPS module is validated on specific operating environments, and a static musl binary isn’t one of them.
  • The Postgres driver’s own code for password authentication, migration checksums and advisory-lock hashing, which still uses non-validated hashing. rIDM’s own advisory locks are hashed by the Postgres server. Using client-certificate authentication avoids the password path. The rest is tracked upstream in transact-rs/sqlx#4416, #4418 and #4421.
  • The AWS SDK’s request signing (SigV4, for KEY_WRAPPER=aws-kms), which uses its own, non-validated HMAC. The AWS SDK tracks this upstream. If you need every piece validated, the other key custody backends don’t have this gap.
  • The Postgres driver’s TLS settings. sqlx builds its own TLS configuration, which offers the non-approved ChaCha20 and X25519 next to the approved suites. A Postgres server in FIPS mode only accepts approved ones, so the connection still uses an approved suite.
  • Everything outside the rIDM process: your load balancer or ingress, Postgres, Valkey and the users’ browsers. Each needs its own FIPS configuration.

FIPS validation belongs to the cryptographic module (AWS-LC), not to rIDM. The FIPS build makes sure every security function runs inside it, apart from the gaps listed above. Whether a particular deployment counts as compliant is up to your assessor.

Checking a build

# The binary links only the FIPS module's AWS-LC:
nm target/release/ridm-api | grep -oE 'aws_lc_(fips_)?[0-9]+_[0-9]+_[0-9]+' | sort -u

# ChaCha20 and plain X25519 are refused:
openssl s_client -connect ridm.example.com:443 -tls1_3 -groups X25519   # must fail
openssl s_client -connect ridm.example.com:443 -tls1_2 -cipher ECDHE-RSA-CHACHA20-POLY1305   # must fail

The nm line should print only aws_lc_fips_.... cargo tree still lists aws-lc-sys, because rustls’ fips feature compiles it alongside the FIPS module, but nothing in the build uses it and the linker drops it. scripts/fips/check-binary.sh runs these checks (and that no OpenSSL is linked) on a binary, and scripts/fips/smoke.sh <image> boots an image and probes its TLS. CI runs both on every pull request, along with a cargo deny ban list for the FIPS build (deny-fips.toml).