Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
output files without loading NER models or downloading artifacts.
### Security

- Reject revoked REST mTLS client certificates from operator-supplied local CRL
files. CRL signatures are verified against the configured CA bundle at
startup, revocation and `nextUpdate` freshness (with a configurable grace
period) are checked per request before principal mapping, and invalid or
stale CRLs fail closed outside the `dev` and `test` profiles. Errors and logs
use only `client_certificate_revoked`, `crl_stale` and `crl_invalid`, never
subjects, SANs, serial numbers or file paths (#3922).

- Refresh explicitly configured SMART credentials through atomic injected custody,
validate token type/lifetime/granted permission unions, rotate refresh secrets,
and revoke uncertain or invalid refreshes with value-free findings. Bulk export
Expand Down
103 changes: 100 additions & 3 deletions docs/serving/mtls.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ python -m openmed.service.logging --host 127.0.0.1 --port 8080
```

`OPENMED_SERVICE_MTLS_CA_BUNDLE` must contain at least one trusted CA
certificate. It must not contain a production private key. Online CRL and OCSP
checking are not performed; certificate issuance and revocation infrastructure
remain the deployer's responsibility.
certificate. It must not contain a production private key. OpenMed can reject
revoked client certificates from operator-supplied local certificate revocation
lists (CRLs); see [Reject revoked client certificates](#reject-revoked-client-certificates).
Online CRL fetching and OCSP checking are not performed; certificate issuance,
CRL publication and distribution remain the deployer's responsibility.

## Terminate TLS at the application

Expand Down Expand Up @@ -99,6 +101,101 @@ The mapped `AuthPrincipal` is available as `request.state.auth_principal` and
`mtls:<sha256-fingerprint>` with no scopes. This authenticates the certificate
without silently granting application permissions.

## Reject revoked client certificates

When a client key is compromised, revoke its certificate at your CA and give
OpenMed the resulting CRL. OpenMed reads local CRL files only. It never fetches
CRLs from distribution points and never contacts an OCSP responder.

| Setting | Default | Meaning |
| --- | --- | --- |
| `OPENMED_SERVICE_MTLS_CRL_FILES` | unset | Comma-separated paths to PEM or DER CRL files, one CRL per file. |
| `OPENMED_SERVICE_MTLS_CRL_GRACE_SECONDS` | `0` | Seconds a CRL is still accepted after its `nextUpdate` time. |
| `OPENMED_SERVICE_MTLS_CRL_FAIL_CLOSED` | `true`, or `false` for the `dev` and `test` profiles | Whether invalid or stale CRLs reject requests. It cannot be set to `false` outside the `dev` and `test` profiles. |

```bash
OPENMED_SERVICE_MTLS_ENABLED=true \
OPENMED_SERVICE_MTLS_CA_BUNDLE=/etc/openmed/client-ca.pem \
OPENMED_SERVICE_MTLS_CRL_FILES=/etc/openmed/client-ca.crl.pem \
OPENMED_SERVICE_MTLS_CRL_GRACE_SECONDS=3600 \
python -m openmed.service.logging --host 127.0.0.1 --port 8080
```

How the checks work:

1. At startup, OpenMed reads every configured CRL file and verifies its
signature against a CA certificate in `OPENMED_SERVICE_MTLS_CA_BUNDLE`
with a matching subject. The CA must be allowed to sign CRLs (the
`cRLSign` key usage, when key usage is present). A CRL that cannot be read
or parsed, has a bad signature, is signed by a CA outside the bundle, has
no `nextUpdate` time, is a delta CRL, or holds more than one PEM CRL block
is invalid. Verified CRLs are cached in memory. Changing a file needs a
restart.
2. On every request, after the client chain is verified and before any
principal mapping runs, each certificate in the verified chain is looked up
by serial number in the CRLs signed by its issuer. A revoked leaf or
intermediate CA certificate is rejected with HTTP 401 and
`client_certificate_revoked`, in every profile.
3. On the same request, each CRL used for that chain must still be fresh: the
current time must not be later than its `nextUpdate` time plus the grace
period. This runs per request, so a CRL that goes stale while the service
runs is caught without a restart.

A certificate whose issuer has no configured CRL is not checked for
revocation. Configure one CRL per issuing CA that you need to cover. A CRL
signed by an intermediate CA can be verified only when that intermediate CA
certificate is also in the CA bundle.

### Profile behavior and error codes

`OPENMED_PROFILE` selects how CRL problems are handled. The `dev` and `test`
profiles report problems; every other profile, including the default `prod`
and custom profiles, fails closed.

| Condition | Code | Fail closed (`prod` and other profiles) | Report only (`dev`, `test`) |
| --- | --- | --- | --- |
| Certificate is listed in a CRL | `client_certificate_revoked` | HTTP 401 | HTTP 401 |
| CRL is past `nextUpdate` plus grace | `crl_stale` | HTTP 503 | One warning, request proceeds |
| CRL file is invalid | `crl_invalid` | Service refuses to start | One warning per file, file is skipped |

In report-only mode, "reported" means one `WARNING` record on the
`openmed.service.mtls` logger. It holds only the code, and for an invalid file
its 1-based position in `OPENMED_SERVICE_MTLS_CRL_FILES`. The record also
carries the code in its `openmed_code` attribute. Revoked certificates are
always rejected, even when the CRL that lists them is stale.

Responses, exceptions and log records never contain certificate subjects,
SANs, serial numbers, CRL file paths, or underlying parser or OS error text.
The startup error for an invalid CRL names only the file position.

### Produce and rotate a CRL with OpenSSL

The commands below are an example for a small OpenSSL-managed CA. The paths
are placeholders; keep the CA key off the OpenMed host.

```bash
# Revoke a compromised client certificate in the CA database.
openssl ca -config /path/to/ca.cnf -revoke /path/to/compromised-client.pem

# Issue a new CRL that is valid for 7 days.
openssl ca -config /path/to/ca.cnf -gencrl -crldays 7 \
-out /path/to/client-ca.crl.pem

# Check the CRL signature and dates before deploying it.
openssl crl -in /path/to/client-ca.crl.pem -CAfile /path/to/client-ca.pem \
-noout -verify -lastupdate -nextupdate
```

To rotate, issue a new CRL before the current `nextUpdate` time, replace the
file atomically, and restart the service. Set the grace period to cover the
time your rotation job may lag. Alert on `crl_stale` responses, because in a
fail-closed profile every client of that CA is rejected until a fresh CRL is
loaded.

If your sidecar or ingress can also check CRLs, you may enable that too. The
application-level check still runs, so the result is the same whether TLS ends
at OpenMed or at a trusted proxy.

## Combine mTLS with JWT or API keys

mTLS protects the connection and supplies a workload identity. REST
Expand Down
Loading
Loading