Production Deployment Guidelines
This guide covers the critical configuration changes you must make before deploying ThunderID in a production environment. The default configuration is designed for local development and evaluation only. Do not use it in production without applying the changes below.
Prerequisites
Before you begin, ensure you have:
- A running ThunderID instance configured for your deployment target (see Choose Your Deployment).
- Access to a supported production database (PostgreSQL 13 or later recommended).
- Valid TLS certificates issued by a trusted Certificate Authority (CA).
- The
opensslcommand-line tool installed on your system.
Configure a Production Database
The default configuration uses SQLite, which stores data in local files. SQLite does not support concurrent writes from multiple processes and is not suitable for production use. Use PostgreSQL for production deployments.
ThunderID uses four separate databases:
| Database | Purpose |
|---|---|
config | Application configuration, flows, and identity providers |
runtime_transient | Short-lived OAuth state: authorization codes, requests, and nonces |
entity | User accounts and credentials |
runtime_persistent | Long-lived operational state: revoked tokens, SSO sessions, and consents |
Configure each database in deployment.yaml:
database:
config:
type: "postgres"
postgres:
hostname: "your-db-host"
port: "5432"
name: "configdb"
username: "dbuser"
password: "dbpassword"
sslmode: "require"
max_open_conns: 500
max_idle_conns: 100
conn_max_lifetime: 3600
runtime_transient:
type: "postgres"
postgres:
hostname: "your-db-host"
port: "5432"
name: "runtime_transient"
username: "dbuser"
password: "dbpassword"
sslmode: "require"
max_open_conns: 500
max_idle_conns: 100
conn_max_lifetime: 3600
entity:
type: "postgres"
postgres:
hostname: "your-db-host"
port: "5432"
name: "entitydb"
username: "dbuser"
password: "dbpassword"
sslmode: "require"
max_open_conns: 500
max_idle_conns: 100
conn_max_lifetime: 3600
runtime_persistent:
type: "postgres"
postgres:
hostname: "your-db-host"
port: "5432"
name: "runtime_persistent"
username: "dbuser"
password: "dbpassword"
sslmode: "require"
max_open_conns: 500
max_idle_conns: 100
conn_max_lifetime: 3600
Key settings:
- Set
sslmodeto"require"to enforce encrypted connections to the database. - Adjust
max_open_conns,max_idle_conns, andconn_max_lifetimebased on your expected traffic and database capacity. - Never store database credentials in
deployment.yamldirectly. Use environment variables, Kubernetes secrets, or a secrets manager.
Replace TLS Certificates
For quickstart and development, ThunderID generates a self-signed TLS certificate per deployment during setup. Self-signed certificates trigger browser warnings and are not trusted by clients in production. Replace the server certificate with one issued by a trusted CA, or terminate TLS at your ingress or load balancer with a CA-issued certificate.
Step 1: Get a TLS Certificate
Get a certificate and private key for your production domain from a trusted CA. You can use:
- Let's Encrypt for free, automated certificates.
- Your organization's internal CA.
- A commercial CA.
Step 2: Replace the Default Certificates
Copy your certificate and private key to the ThunderID security directory:
cp your-domain.cert config/certs/server.cert
cp your-domain.key config/certs/server.key
Step 3: Update the Configuration
Verify the paths in deployment.yaml point to the correct files:
tls:
min_version: "1.3"
cert_file: "config/certs/server.cert"
key_file: "config/certs/server.key"
If you store certificates in a different location, update cert_file and key_file accordingly.
When deploying with Docker, mount the certificates as volumes:
docker run --rm \
-p 8090:8090 \
-v $(pwd)/deployment.yaml:/opt/thunder/deployment.yaml \
-v $(pwd)/certs/server.cert:/opt/thunder/config/certs/server.cert \
-v $(pwd)/certs/server.key:/opt/thunder/config/certs/server.key \
ghcr.io/thunder-id/thunderid:latest
Generate a Unique Encryption Key
ThunderID uses a symmetric encryption key to protect sensitive data at rest, including credentials and tokens, along with separate keys to sign JWTs. During setup, each deployment generates its own unique key material. For production, either keep the generated per-deployment keys (ensuring they are persisted and backed up) or provide and manage your own through a secure process such as a secrets manager. The steps below show how to provide your own encryption key.
Step 1: Generate a New Key
Generate a cryptographically secure 32-byte key:
openssl rand -hex 32
This command outputs a 64-character hexadecimal string. Copy the output.
Step 2: Store the Key
Write the generated value to the key file:
printf '%s' "<generated-key>" > config/certs/crypto.key
Replace <generated-key> with the output from the previous command. Do not include whitespace or newline characters.
Step 3: Verify the Configuration
Confirm deployment.yaml references the key file:
crypto:
encryption:
key: "file://config/certs/crypto.key"
You can also supply the key directly as an inline value, but this is not recommended for production. Prefer the file:// path form, an environment variable, or a secrets manager to avoid storing the key in deployment.yaml:
crypto:
encryption:
key: "<generated-key>"
Protect the encryption key with the same level of care as a database password. If the key is lost, encrypted data becomes unrecoverable. If the key is compromised, all protected data is exposed. This key material lives in config/certs (the certs PVC on Helm) and must persist across restarts and replicas. If it is lost or changed, previously encrypted data cannot be decrypted and previously issued tokens can no longer be validated.
Provide the Direct Auth Secret
ThunderID gates the Direct API endpoints (/auth/**, /register/passkey/**) and the protected AuthZEN access endpoints (/access/**) with a server-level Direct Auth Secret. During setup, each deployment generates its own secret. The default deployment.yaml references it as a file, so the value stays out of the config file:
server:
security:
direct_auth_secret: "file://config/secrets/direct_auth_secret"
For production, either keep the generated per-deployment secret (ensuring config/secrets is persisted) or provide your own. To provide your own, generate a strong value and write it to the secret file:
openssl rand -hex 32
printf '%s' "<generated-secret>" > config/secrets/direct_auth_secret
Replace <generated-secret> with the output from the previous command. Do not include whitespace or newline characters. Callers must send this value in the Direct-Auth-Secret header. You can also supply the secret directly as an inline value, but prefer the file:// path form or a secrets manager to keep it out of deployment.yaml.
Configure a CORS Allowlist
Cross-Origin Resource Sharing (CORS) controls which browser origins can call ThunderID. Origins are stored in the server-config cors section. No origins are allowed by default, so list each production app explicitly.
Create config/resources/server_configs/cors.yaml:
name: cors
value:
allowedOrigins:
- "https://app.example.com"
- "https://admin.example.com"
To change origins without a restart, update the same section with PUT /server-config/cors. Runtime values are merged with the declarative file.
Guidelines:
-
List only origins that your applications actively use. Do not use wildcards (
*). -
Use the full origin format: scheme, hostname, and port if non-standard (for example,
https://app.example.com:8443). -
If your domain follows a consistent pattern, you can use a regular expression:
name: cors
value:
allowedOrigins:
- "https://app.example.com"
- regex: '^https://[a-z0-9-]+\.example\.com$'Anchor regex patterns with
^and$. ThunderID logs a startup warning for unanchored patterns because they can match unintended origins.
Configure Caching
ThunderID supports two cache backends: in-memory and Redis.
| Cache Type | Suitable For |
|---|---|
| In-memory | Single-pod deployments only |
| Redis | Multi-pod (replicated) deployments |
Do not use in-memory caching when running multiple replicas. Each pod maintains a separate cache, which causes cache inconsistency across the deployment. Cached entries written in one pod are invisible to other pods, leading to incorrect authentication or authorization decisions.
Single-Pod Deployments
For a single-pod deployment, in-memory caching is acceptable:
cache:
disabled: false
type: "inmemory"
size: 1000
ttl: 3600
evictionPolicy: "LRU"
cleanupInterval: 300
Multi-Pod Deployments
Use Redis when running more than one replica:
cache:
disabled: false
type: "redis"
ttl: 3600
redis:
address: "your-redis-host:6379"
username: ""
password: "your-redis-password"
db: 0
keyPrefix: "thunderid:"
Key settings:
- Set a
keyPrefixwhen multiple services share the same Redis instance. This avoids key collisions. - Secure Redis with authentication and network-level access controls. Do not expose Redis publicly.
- Use a managed Redis service (such as Amazon ElastiCache or Google Memorystore) for high availability.
Next Steps
After applying the production configuration:
- Restrict access to
deployment.yamland theconfig/certs/directory. These files contain sensitive credentials and keys. - Enable database backups on a regular schedule.
- Set up monitoring and alerting for ThunderID and its dependent services.
- Review the Kubernetes deployment guide for Helm-based configuration of the settings covered here.