Skip to main content

FIPS Mode At Install

MOD supports and helps with cryptographic-protection controls (NIST SC-12 / SC-13). MOD does not provide, claim, certify or validate compliance with anything, and nothing in this page means the install is running FIPS-approved cryptography. Read the "Not available yet" section before you act on this page.

Where the install stands today​

There is no FIPS-validated crypto path in a stock MOD install. Concretely, as grepped in this repo:

  • CORE/Worker/Dockerfile builds on golang:1.25-alpine with no GOEXPERIMENT and no boringcrypto replace. Since GY.C2b the Dockerfile carries a GOFIPS140 build arg — empty (the default, the stock build) or --build-arg GOFIPS140=v1.0.0 for a FIPS build, which sets GODEBUG=fips140=only for every go build in the image — but the FIPS worker release built that way is held and not shipped; stock workers report fips140_mode: "off".
  • CORE/API/Dockerfile:2 is FROM python:3.11-slim — stock OpenSSL, no OpenSSL 3 FIPS provider, no FIPS_MODE=1 in CORE/API.
  • The Keycloak image (keycloak/keycloak:26.3 in CORE/startup/docker-compose.yml) carries no KC_BOOTSTRAP_FIPS*; the Node frontends are stock node:*-alpine.
  • There is no FIPS provider switch: MOD reports the crypto posture, it does not install one. GET /v1/platform/compliance/crypto-status (platform admin, GY.C2a) is the reporting surface — see "What MOD reports" below — and the platform setting crypto.fips_mode_required (group crypto, default false) decides whether that report is informational or gates readiness. The crypto_status section in the evidence bundle (CORE/API/services/evidence_bundle.py) is separate: it reports the declared compliance environment and which crypto controls that environment needs — it declares intent, it does not report an algorithm mode.

The register rows for this are docs/security-audit/REGISTER.md R-061 ("No FIPS 140-2/140-3 path") and R-150 ("No component is FIPS 140-2/3 validated"), with the detail in docs/security-audit/findings/GSEC-4-crypto.md.

What is actually in place​

1. Non-approved digests are declared, not hidden​

docs/security-audit/FIPS_CRYPTO_EXCEPTIONS.md is the standing inventory of every non-test md5/sha1 use in CORE/API. Three sites carry an explicit usedforsecurity=False annotation, which is what lets the code import and run on a FIPS-mode Python build instead of raising:

SiteDigestWhy it is annotated
CORE/API/utils/docs_ingester.py:294MD5chunk id / cache key
CORE/API/security/password_policy.py:111SHA-1HIBP k-anonymity lookup key (protocol-mandated)
CORE/API/routers/support_chat/_shared.py:3647SHA-1dedup fingerprint key

The annotation is a declaration that the digest is not a security control. It is not a claim that the digest is FIPS-approved. Two entries in that document are flagged for the lead and are not resolved: the Postmark webhook HMAC (CORE/API/routers/postmark_webhooks.py:93, HMAC-SHA-1 — a security use, cannot simply be switched because Postmark signs with SHA-1) and the HIBP SHA-1 lookup if your posture requires all password hashing to be FIPS-approved.

2. Audit integrity already uses HMAC-SHA-256​

CORE/API/audit.py signs every audit row with hmac.new(AUDIT_HMAC_KEY, ..., hashlib.sha256) and chains rows per tenant. AUDIT_HMAC_KEY is generated at install (CORE/startup/docker-compose.yml → secrets/audit_hmac_key.env). SHA-256 is in the FIPS-approved set; the key is a separate secret, so rotating it invalidates older rows' hashes.

3. Front TLS termination at install​

MOD puts a Caddy front in front of the web tier and terminates TLS there (--tls internal / public / off, CORE/startup/lib/front-tls.sh). See Front TLS at install. Termination at a front you control is the practical way to get encryption-in-transit (SC-8) with a FIPS-validated OpenSSL in front of MOD — MOD does not itself supply a FIPS OpenSSL build.

4. What MOD reports (GY.C2a) — a report, not a switch​

GET /v1/platform/compliance/crypto-status (platform admin) reports the crypto posture the API process actually runs on:

  • the OpenSSL version and whether the FIPS provider is the default. There is no public Python API for "is the FIPS provider active", so it is inferred from behaviour: on a FIPS-enforced build hashlib.md5(b"", usedforsecurity=True) raises. /proc/sys/crypto/fips_enabled is read when readable (a FIPS-registered OS kernel) and reported unknown otherwise — never guessed.
  • the cryptography backend version and its FIPS flag when that build exposes one (backend._fips_enabled), reported unknown when it does not;
  • the standing exception inventory above, parsed from the markdown table into rows, with security_use flagging the security-classified entries;
  • per-worker crypto mode — since GY.C2b the Go worker reports a crypto block on its heartbeat ({go_version, fips140_mode, fips140_enabled, boringcrypto}), stored on the worker's hardware_specs and shown per worker. A worker on a pre-GY.C2b binary omits the block and stays unknown — never guessed;
  • the boot known-answer self-test result: SHA-256, HMAC-SHA256, AES-256-GCM encrypt + decrypt round-trip, and ECDSA P-384 sign + verify against fixed vectors. ECDSA signatures are randomised, so the known answer is the derived public point and the signature is required only to verify. The result is cached in-process and runs once at startup.

Platform setting crypto.fips_mode_required (group crypto, bool, default false) decides what a failure means. While false the report is informational — a stock non-FIPS install is not unhealthy and the conformance check crypto.fips_mode (SC-13) is not-applicable. While true, a failed self-test or a non-enforced build marks the crypto_self_test readiness component degraded in GET /v1/health?verbose=true and fails the check. The self-test logs ERROR on failure and never crashes the process.

5. Non-approved algorithms refused + generated crypto inventory (GY.C2)​

Two supporting pieces for the "non-approved algorithms refused" half of GY.C2:

  • Postgres password storage (SCRAM only). The conformance check crypto.pg_scram (SC-13) passes when SHOW password_encryption is scram-sha-256 and zero roles still store an md5 verifier; it is read-only, falls back from pg_authid to pg_shadow, and reports unknown (manual) rather than failing when neither catalog is readable. Fresh installs set password_encryption=scram-sha-256 at install time (the postgres-app compose command); existing long-lived roles still stored as md5 need one re-hash with bash CORE/startup/db-tls/rehash-scram.sh apply. MOD supports a SCRAM-only Postgres posture; it does not alter a running database by itself.
  • Generated crypto inventory. cd CORE/API && python3 tools/crypto_inventory.py scans CORE/API (Python AST) and CORE/Worker (Go imports) and rewrites docs/security-audit/CRYPTO_INVENTORY.md — a deterministic file:line / algorithm / purpose / approval table (approved per FIPS 140-3; Ed25519 per FIPS 186-5; MD5/SHA-1 only with usedforsecurity=False). python3 tools/crypto_inventory.py --check is the CI gate: exit 1 when a non-approved security use appears that FIPS_CRYPTO_EXCEPTIONS.md does not list.

Planned, not available​

These are on the backlog (docs/security-audit/CODE_READY_BACKLOG.md R-061, docs/security-audit/VERTICAL_GAP_MATRIX.md "FIPS 140-3 mode", feature GY.C2) and are not in a shipped install:

  • a Go worker BUILT with the FIPS module: the build switch exists since GY.C2b (--build-arg GOFIPS140=v1.0.0 in CORE/Worker/Dockerfile, which sets GODEBUG=fips140=only for the build and makes the running worker report fips140_mode: "only"), but the FIPS worker release is held — no worker in the fleet runs it yet;
  • an OpenSSL 3 FIPS provider image for the Python tier;
  • refusing non-approved algorithms at runtime (the crypto-status endpoint reports whether the build refuses them; it cannot make a stock build do so);
  • a boot self-test and a crypto-status endpoint — landed (GY.C2a, see "What MOD reports" above); what is still missing is the provider itself;
  • Keycloak in FIPS mode.

CORE/LicenseServer/API/crypto/key_manager.py reads CRYPTO_FIPS_MODE (default false) and uses it only to log "FIPS" vs "standard" in a startup line — it does not change any algorithm. Do not treat that as a FIPS switch.

What an operator can do today​

  1. Host-level FIPS mode. On a FIPS-registered OS (RHEL/Alma/Rocky in FIPS mode, or a FIPS kernel), userspace inherits the OS OpenSSL. MOD does not configure this, but it does report what it finds — the crypto-status endpoint's behavioural probe plus /proc/sys/crypto/fips_enabled; verify on the host (openssl fipsinstall-style checks are the OS's, not MOD's). Expect the annotated usedforsecurity=False sites above to keep working, and expect the Postmark HMAC-SHA-1 router to remain SHA-1 — that is the open exception in the inventory document.
  2. FIPS-validated TLS termination in front of MOD. Put your own FIPS-validated OpenSSL/mTLS terminator in front and set --tls off, so MOD forwards to something that already does encryption in transit. The off mode is only correct when something else in front does the encryption.
  3. Keep the audit chain on SHA-256 (already the case) and keep AUDIT_HMAC_KEY in the secrets file rather than reusing a value across installs.
  4. Read the exception inventory before you claim a FIPS posture. If you add a digest use, add a row to docs/security-audit/FIPS_CRYPTO_EXCEPTIONS.md — the inventory is the record, and MOD will not find new violations for you.

MOD provides features that support your FIPS 140-3 work; MOD itself is not a validated cryptographic module, and nothing on this page means that MOD is FIPS compliant, certified, validated, or that it satisfies a control. Describe what MOD does as supporting your work — the validation, and the claim of it, belongs to the module you run and to your assessment.

  • Secure configuration profiles — the fips control is a build-level capability, not a setting; the profile tables list what each profile actually locks.
  • SIEM hookup — the HMAC-SHA-256 audit chain described above, and how its per-row hash reaches your SIEM.
  • Giving your assessor read-only access — the crypto-status report and the exception inventory are sections of the evidence bundle an assessor can export.
  • Internal TLS — the internal-hop posture behind gsec.transport.internal_tls_enforce.
  • Front TLS at install — the termination point discussed under item 3.