FIPS 140-3 build
Ory Enterprise License releases include a dedicated FIPS 140-3 build of Ory Hydra next to the standard build. Use it when your deployment has to run cryptography inside a FIPS 140-3 validated module.
The FIPS build is compiled against the Go Cryptographic Module, which holds a NIST CMVP
certificate, and starts with GODEBUG=fips140=on by default. It also selects FIPS-approved algorithms where the standard build
uses an unapproved one, and refuses to start on a configuration that cannot be FIPS compliant.
The FIPS build is available only with an Ory Enterprise License. Contact us to discuss your requirements.
Get the FIPS build
The FIPS build ships as a binary named hydra_oel_fips_<version>_<os>_<arch> alongside the standard OEL release artifacts. It is
built for linux/amd64. There is no FIPS container image, so a deployment that runs the standard hydra-oel image today has to
switch to running the binary. Ask your Ory support contact for access to the artifact.
Everything else about the binary is the same as the standard OEL build: the same commands, the same configuration file, the same database schema, and the same migrations.
Verify that you have a FIPS binary
The certified module and its version are recorded in the binary. Check them with the Go toolchain:
go version -m ./hydra_oel_fips_<version>_linux_amd64 | grep -E 'GOFIPS140|DefaultGODEBUG'
The output must list both a GOFIPS140 module version and fips140=on in the default GODEBUG:
build GOFIPS140=v1.0.0-c2097c7c
build DefaultGODEBUG=fips140=on,x509usefallbackroots=1
If either line is missing, you are looking at the standard build.
Required configuration
In FIPS mode, hydra serve refuses to start unless the configuration below is in place. The hydra migrate and hydra janitor
commands are not affected, because they never touch the affected ciphers.
Dedicated secrets for every purpose
secrets.system, secrets.cookie, and secrets.pagination must all be set explicitly, and their active (first) secrets must
differ from one another. When secrets.cookie or secrets.pagination is unset, Hydra silently falls back to secrets.system,
which reuses one key across unrelated purposes.
secrets:
system:
- this-is-the-system-secret
cookie:
- this-is-the-cookie-secret
pagination:
- this-is-the-pagination-secret
Each secret must be at least 16 characters long. Older entries further down a list may repeat another list's secret, so key rotation keeps working while an old key is still needed for decryption.
If a secret is missing, startup fails with:
you must provide `secrets.cookie` for FIPS compliance
If two lists share the same active secret, startup fails with:
`secrets.system` and `secrets.pagination` must not share the same active (first) secret for FIPS compliance
PBKDF2 for OAuth 2.0 client secrets
Keep oauth2.hashers.algorithm at its default value, pbkdf2. Hydra hashes client secrets with PBKDF2-HMAC-SHA256, which is FIPS
140-3 approved and runs inside the validated module.
oauth2:
hashers:
algorithm: pbkdf2
pbkdf2:
iterations: 25000
The alternative, bcrypt, is not FIPS 140-3 approved and hashes outside the module, so hydra serve refuses to start with it:
`oauth2.hashers.algorithm` must not be `bcrypt` for FIPS compliance, because it hashes client secrets outside the validated
module: set it to `pbkdf2`
Client secrets that were hashed while the setting was bcrypt keep verifying, because Hydra picks the comparison algorithm from
the stored hash rather than from the configuration. Verifying an existing hash is not restricted; only producing new ones is. The
setting does not re-hash them either, so rotate those client secrets to move them onto PBKDF2.
What changes in FIPS mode
Hydra seals its transient payloads with XChaCha20-Poly1305, which is not a FIPS-approved construction. The FIPS build replaces it with AES-256-GCM. Everything else keeps the algorithm it already used.
| Data | Standard build | FIPS build |
|---|---|---|
| Login, consent, device, and logout challenges and verifiers | XChaCha20-Poly1305 | AES-256-GCM |
| Verifiable credential nonces | XChaCha20-Poly1305 | AES-256-GCM |
Authorization codes, when oauth2.authorize_code.format is aead | XChaCha20-Poly1305 | AES-256-GCM |
| Admin API pagination tokens | XChaCha20-Poly1305 | AES-256-GCM |
| JSON Web Keys and OAuth 2.0 session payloads at rest | AES-256-GCM | AES-256-GCM |
| Access token, refresh token, and authorization code signatures | HMAC-SHA-512/256 | HMAC-SHA-512/256 |
| OAuth 2.0 client secret hashes | PBKDF2-HMAC-SHA256 | PBKDF2-HMAC-SHA256 |
Data at rest is not re-encrypted, and the storage format does not change. Existing JSON Web Keys and session payloads stay readable, and no migration is needed.
ID tokens and, with the JWT access token strategy, access tokens are signed with the algorithm you pick when you create the JSON
Web Key set. Every algorithm Hydra can generate a key for is FIPS 140-3 approved, including the RS256 default, and the FIPS
build signs with the validated module.
Rotate the system secret regularly
Regular rotation of all secrets is mandatory in FIPS mode. The AES-256-GCM construction that FIPS 140-3 requires here carries a real chance of reusing a nonce, and that chance grows the longer Hydra runs on one system secret. Rotation is the only thing that resets it.
AES-256-GCM draws a fresh 96-bit nonce at random for every payload it seals, and every payload sealed under one key draws from the same nonce space. Two payloads can therefore draw the same nonce, and by the birthday bound that probability grows with the square of the number of payloads sealed — doubling the volume quadruples the risk. It is never zero, and it only ever increases while the key stays in place. XChaCha20-Poly1305, which the standard build uses, has a 192-bit nonce and does not have this limit.
A repeated nonce is not a graceful degradation. It reveals the XOR of the two plaintexts, and it leaks the GHASH authentication subkey, which lets an attacker forge valid tags for arbitrary payloads under that key. Confidentiality and integrity both fail.
NIST SP 800-38D section 8.3 caps a random-nonce AES-GCM key at 2^32 sealings for this reason. Treat that as the outer bound, not a target:
| Payloads sealed under one key | Chance that two share a nonce |
|---|---|
| 2^32 (4.3 billion) | 1 in 8.6 billion — NIST's cap |
| 2^40 (1.1 trillion) | 1 in 130,000 |
| 2^44 (18 trillion) | 1 in 500 |
| 2^48 (280 trillion) | 1 in 2.5 |
Hydra spends this budget on every login, consent, device, and logout challenge and verifier, every verifiable credential nonce,
every aead-format authorization code, and every admin API pagination token, so a single user flow seals several payloads. At a
sustained 1,000 sealings per second, one key reaches NIST's cap in about 50 days; at 10,000 per second, in about 5.
Rotating secrets.system promotes a new active key and restarts the count at zero. Older entries in the list keep decrypting
payloads sealed under previous keys, so rotation does not invalidate in-flight flows. For how to roll a secret without downtime,
see Secrets and key rotation.
Switch an existing deployment
The two builds cannot read each other's transient payloads. Switching in either direction invalidates in-flight login, consent, device, and logout flows, outstanding verifiable credential nonces, unredeemed authorization codes, and admin API pagination tokens. Users in the middle of a flow start over, and a list request that carries a pagination token across the switch starts at the first page. Issued access, refresh, and ID tokens are not affected.
Prefer a stop-then-start cutover: scale the old build to zero, then start the new one. A rolling update also converges, but flows that cross instances fail while both builds are running.
Startup log signals
When the FIPS build starts, it logs an info message confirming that FIPS 140-3 mode is active. Hydra evaluates the configuration once at startup and does not re-check it on configuration reloads, so a change that would fail the checks above only takes effect on the next restart.
Running the standard build in FIPS mode
The standard OEL build honors GODEBUG=fips140=on and switches the same ciphers when you set it, so treat that setting like a
build switch and follow Switch an existing deployment. What it does not give you is the
certified module: the standard build is compiled against the regular Go cryptography packages. Only the FIPS build runs the
validated module, so only the FIPS build supports a FIPS 140-3 compliance claim.