Skip to content

JWT Key Persistence

By default, OctoFHIR generates a new JWT signing key pair on each server startup. This means all access tokens become invalid when the server restarts, requiring users to re-authenticate.

For production deployments, you should provide persistent JWT signing keys via configuration to maintain token validity across server restarts.

OctoFHIR supports three JWT signing algorithms. Choose based on your requirements:

AlgorithmTypeKey SizeSecurityVerify SpeedSMART on FHIRRecommendation
RS384RSA3072-bit128 bits🚀 Fast✅ PreferredRecommended
RS256RSA2048-bit112 bits🚀 Fast✅ CompatibleGood for compatibility
ES384ECDSAP-384192 bits🐢 Slow✅ PreferredSmaller tokens, slower
  • Fast verification: RSA verification uses small public exponent (65537), making it ~10x faster than ECDSA
  • SMART on FHIR compliant: RS384 is listed as a preferred algorithm in the SMART specification
  • Sufficient security: 128-bit security level is more than adequate for short-lived JWT tokens
  • Wide compatibility: Supported by all JWT libraries and clients
  • You need the smallest possible token size (EC signatures are ~96 bytes vs ~384 bytes for RSA)
  • You have low request volume and verification speed doesn’t matter
  • Your infrastructure already uses P-384 keys

In high-throughput scenarios, ES384 can consume 20-30% of CPU time on signature verification alone. Switching to RS384 can significantly reduce this overhead.

Section titled “1. Generate RSA Keys (for RS256 or RS384) — Recommended”
Terminal window
# Generate a 3072-bit RSA private key (for RS384, provides 128-bit security)
openssl genpkey -algorithm RSA -out jwt_private.pem -pkeyopt rsa_keygen_bits:3072
# Extract the public key
openssl rsa -in jwt_private.pem -pubout -out jwt_public.pem
# For RS256, you can use 2048-bit keys (112-bit security, still secure)
# openssl genpkey -algorithm RSA -out jwt_private.pem -pkeyopt rsa_keygen_bits:2048
Terminal window
# Generate a P-384 EC private key (PKCS#8 format)
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out jwt_private.pem
# Extract the public key
openssl ec -in jwt_private.pem -pubout -out jwt_public.pem

You have three options for providing the keys:

Edit octofhir.toml:

[auth.signing]
algorithm = "RS384"
kid = "production-key-2024" # Optional but recommended
private_key_pem = """
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClWJYT...
... (your full private key) ...
-----END PRIVATE KEY-----
"""
public_key_pem = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApViWEyoNK...
... (your full public key) ...
-----END PUBLIC KEY-----
"""
Terminal window
export OCTOFHIR__AUTH__SIGNING__PRIVATE_KEY_PEM="$(cat jwt_private.pem)"
export OCTOFHIR__AUTH__SIGNING__PUBLIC_KEY_PEM="$(cat jwt_public.pem)"
export OCTOFHIR__AUTH__SIGNING__KID="production-key-2024"
# Start the server
cargo run

Create a separate jwt-keys.toml:

[auth.signing]
private_key_pem = """
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
"""
public_key_pem = """
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
"""
kid = "production-key-2024"

Then merge it with your main config using the config manager.

  1. Never commit private keys to version control

    • Add *.pem to .gitignore
    • Use environment variables or secure secret management systems
  2. Protect private key files

    Terminal window
    chmod 600 jwt_private.pem
  3. Use secure storage

    • For production, consider using HashiCorp Vault, AWS Secrets Manager, or similar
    • Mount secrets as environment variables or files at runtime

The configuration includes key_rotation_days and keys_to_keep settings for future automatic key rotation support:

[auth.signing]
algorithm = "RS384"
key_rotation_days = 90 # Rotate keys every 90 days
keys_to_keep = 3 # Keep 3 old keys for validation

Note: Automatic rotation is planned for a future release. Currently, you must rotate keys manually.

The kid (Key ID) field helps clients identify which key was used to sign a token. Best practices:

  1. Use descriptive, stable IDs: production-key-2024-01 instead of random UUIDs
  2. Include rotation date: Makes it easy to track key age
  3. Keep it stable: Don’t change the kid unless you rotate the actual key

Problem: Tokens are invalidated even with keys configured.

Solutions:

  1. Verify both private_key_pem and public_key_pem are set
  2. Check that the kid is stable (set explicitly)
  3. Ensure the algorithm matches (RS256, RS384, or ES384)
  4. Check server logs for key loading errors

”Failed to load JWT signing key from configuration”

Section titled “”Failed to load JWT signing key from configuration””

Problem: Server logs show an error loading keys.

Solutions:

  1. Verify PEM format includes BEGIN/END markers
  2. Ensure no extra whitespace or formatting issues
  3. Check that private/public keys are a matching pair
  4. Verify the algorithm matches the key type (RSA for RS256/RS384, EC for ES384)

Run validation:

Terminal window
# Check if keys are valid
openssl rsa -in jwt_private.pem -check
openssl ec -in jwt_private.pem -check # For EC keys
# Verify public key matches private key
openssl rsa -in jwt_private.pem -pubout | diff - jwt_public.pem

For Docker deployments, use secrets or environment variables:

# Dockerfile
FROM rust:latest AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/octofhir-server /usr/local/bin/
# Don't copy keys to the image!
CMD ["octofhir-server"]
docker-compose.yml
services:
octofhir:
image: octofhir-server
environment:
OCTOFHIR__AUTH__SIGNING__PRIVATE_KEY_PEM: ${JWT_PRIVATE_KEY}
OCTOFHIR__AUTH__SIGNING__PUBLIC_KEY_PEM: ${JWT_PUBLIC_KEY}
OCTOFHIR__AUTH__SIGNING__KID: "production-2024"
secrets:
- jwt_private_key
- jwt_public_key
secrets:
jwt_private_key:
file: ./secrets/jwt_private.pem
jwt_public_key:
file: ./secrets/jwt_public.pem

Use Kubernetes secrets:

# Create secret from files
kubectl create secret generic jwt-keys \
--from-file=private=./jwt_private.pem \
--from-file=public=./jwt_public.pem
# Deployment
apiVersion: v1
kind: Pod
metadata:
name: octofhir
spec:
containers:
- name: octofhir
image: octofhir-server
env:
- name: OCTOFHIR__AUTH__SIGNING__PRIVATE_KEY_PEM
valueFrom:
secretKeyRef:
name: jwt-keys
key: private
- name: OCTOFHIR__AUTH__SIGNING__PUBLIC_KEY_PEM
valueFrom:
secretKeyRef:
name: jwt-keys
key: public
- name: OCTOFHIR__AUTH__SIGNING__KID
value: "production-2024"

If you’re migrating from auto-generated keys:

  1. Generate and configure new keys following the steps above
  2. Deploy the new configuration to your server
  3. Users will need to re-authenticate once (their old tokens will be invalid)
  4. Future restarts will maintain token validity

To minimize disruption, deploy during a maintenance window.

After configuration, the server logs should show:

INFO octofhir_server::server: Loaded JWT signing key from configuration
algorithm: RS384
kid: production-key-2024

Instead of:

WARN octofhir_server::server: Generated new JWT signing key - tokens will be invalidated on server restart.
Consider setting auth.signing.private_key_pem in configuration for production.