v26.3.13
v26.3.13
Hydra no longer generates a self-signed TLS certificate
When TLS was enabled and no certificate was configured, Hydra generated a self-signed certificate at startup, stored it in the
hydra.https-tls JSON Web Key Set, and served it. Hydra now starts only with a certificate you provide. Enabling TLS without one
fails at startup with an error naming the settings to set.
What you must do
You are affected if you set serve.tls.enabled (SERVE_TLS_ENABLED) to true without setting a certificate. Choose one:
- Configure a certificate and private key, either as file paths (
serve.tls.cert.pathandserve.tls.key.path) or base64-encoded (serve.tls.cert.base64andserve.tls.key.base64). Hydra reloads the certificate when the file changes. - Disable
serve.tls.enabledif a load balancer, ingress controller, or service mesh terminates TLS in front of Hydra. Hydra then serves HTTP.
Deployments that already configure a certificate, and deployments that leave TLS disabled, are unaffected. Once Hydra starts, you
can delete the now-unused key set with hydra delete jwks hydra.https-tls.
Why
The generated certificate could not authenticate Hydra to anything, and generating it caused problems of its own:
- The only name in it was
localhost, with no IP addresses, so no client that verifies hostnames could accept it for a real deployment. - It was valid for 31 days and was never renewed. Because it was stored in the database and reused on every restart, any installation older than a month has been serving an expired certificate. Only peers that skip verification, such as load balancers that do not validate backend certificates, kept working — which is why this change can surface as a startup failure on an installation that appeared healthy.
- It was a certificate authority certificate that could sign certificates. Adding it to a trust store, as our documentation used to suggest, made Hydra's database a trust anchor for any name.
- Its private key was stored alongside your signing keys and could be read through the admin API.
- Replicas starting at the same time against a fresh database each generated their own certificate, so nodes behind the same load balancer served different ones. Also, a replica that lost the race writing its certificate back could fail to start.
Generating a certificate nothing can verify is not something Hydra should do on your behalf. A deployment that needs a self-signed certificate is better served by one you create, distribute to the clients that must trust it, and rotate on your own schedule.
Hydra no longer supports the TLS termination allow-list
The serve.tls.allow_termination_from setting is gone. A configuration file that still sets it fails validation at startup, with
an error naming the key. Set through SERVE_TLS_ALLOW_TERMINATION_FROM, it is ignored.
What you must do
You are affected if you set serve.tls.allow_termination_from in a configuration file. Remove the setting. Nothing else about how
Hydra serves traffic changes.
If a load balancer, ingress controller, or service mesh terminates TLS in front of Hydra, leave serve.tls.enabled unset. Hydra
then serves HTTP, and the proxy is responsible for HTTPS. Make sure the proxy is the only way to reach Hydra.
Why
The setting was meant to name the CIDR ranges allowed to reach Hydra over plain HTTP and to require an X-Forwarded-Proto: https
header from them. It never took effect. Hydra installed the check only when serve.tls.enabled was true, and a server that
speaks TLS never receives the plain HTTP requests the check was written to reject. The deployments the setting was written for —
the ones that terminate TLS at a proxy and therefore leave serve.tls.enabled unset — never ran it.
Deciding which callers may reach Hydra over plain HTTP belongs to the proxy or the network in front of it, which is where deployments that need it already enforce it.
Reusing a device code revokes its access and refresh tokens
Presenting a device_code that has already been exchanged now revokes the access and refresh tokens issued from that code,
including the ones the device obtained by refreshing. This is the handling
RFC 6749 section 4.1.2 defines for a reused authorization code, applied to
device codes. The reused request itself is still rejected with invalid_grant.
Ory can't tell whether the first or the second request came from your device, so it assumes the device_code leaked and withdraws
the tokens issued under it. Only the client the code was issued to can trigger this: a request from another client_id is
rejected without touching the tokens. Reuse is only recognized while the consumed device_code is still on record; a code
presented after that is rejected as unknown and revokes nothing.
Breaking change
Devices that retry the token request must stop reusing a device_code. A token request that timed out or whose response was
lost may still have issued tokens. Retrying with the same device_code now revokes them, leaving the device without working
credentials. Retry only after a failed request, treat invalid_grant as final, and start a new device authorization request
instead of polling on.
Cleaning up consumed device codes
Consumed device codes are retained briefly so reuse can be detected, then collected by hydra janitor --tokens. If you already
run the janitor on a schedule, no action is required.
OAuth2 client key sets refresh at most once every 15 seconds
When a client authenticates with private_key_jwt or sends a signed request object, Ory now starts at most one refresh attempt
every 15 seconds per jwks_uri for the key set downloaded from the client's jwks_uri, even when the assertion names an unknown
key ID. A failed attempt may be retried by the HTTP client within that refresh's timeout. A newly published signing key can stay
unknown to Ory for up to 15 seconds after the previous refresh. Publish a new key at jwks_uri at least 15 seconds before signing
assertions with it, and keep the retired key published while assertions signed with it can still arrive.
If refreshing the key set fails, Ory keeps serving the previously downloaded keys for up to one hour past their regular
three-minute lifetime before requests fail with the refresh error. Publishing an empty key set ({"keys": []}) revokes every key:
Ory adopts it on the next refresh and rejects all assertions immediately after.