mTLS Certificates
mTLS Certificates
Message Center uses mutual TLS (mTLS) to authenticate itself to both Proxy and Core. Certificates are never embedded in the image — they are injected at runtime via file paths or Kubernetes Secrets.
Certificate File Structure
sms-sender/
└── admin/core_admin/
└── certs/ ← git-ignored; copy from core's CA
├── ca/
│ └── ca.crt ← Root CA that signed all service certs
└── client/
├── core.crt ← Message Center's identity certificate
└── core.key ← Private key — keep secret, never log
The same certificate set is used for both the Proxy connection and the Core admin connection. The paths are configured independently via separate env vars, allowing different certs per service if required.
Environment Variables
| Variable | Default | Purpose |
|---|---|---|
PROXY_TLS_CERT_FILE | ./certs/client/core.crt | Client cert for Proxy mTLS |
PROXY_TLS_KEY_FILE | ./certs/client/core.key | Private key for Proxy mTLS |
PROXY_TLS_CA_FILE | ./certs/ca/ca.crt | CA cert to verify Proxy's certificate |
PROXY_SERVER_NAME | proxy.local | TLS SNI name (must match Proxy cert CN/SAN) |
CORE_TLS_CERT_FILE | ./certs/client/core.crt | Client cert for Core admin API |
CORE_TLS_KEY_FILE | ./certs/client/core.key | Private key for Core admin API |
CORE_TLS_CA_FILE | ./certs/ca/ca.crt | CA cert to verify Core's certificate |
CORE_TLS_SERVER_NAME | core.local | TLS SNI name (must match Core cert CN/SAN) |
mTLS is enabled automatically when CORE_API_URL starts with https:// and cert/key paths are set.
Kubernetes Setup
Create the Secret (before first deploy)
kubectl create secret generic message-center-mtls-certs \
--from-file=ca.crt=certs/ca/ca.crt \
--from-file=core.crt=certs/client/core.crt \
--from-file=core.key=certs/client/core.key \
-n default
Mount in the Deployment
The k8s/deployment.yaml mounts the Secret as a read-only volume at /app/certs:
volumes:
- name: mtls-certs
secret:
secretName: message-center-mtls-certs
containers:
- name: core-admin
volumeMounts:
- name: mtls-certs
mountPath: /app/certs
readOnly: true
Point the env vars to the mounted paths in k8s/configmap.yaml:
CORE_TLS_CERT_FILE: /app/certs/core.crt
CORE_TLS_KEY_FILE: /app/certs/core.key
CORE_TLS_CA_FILE: /app/certs/ca.crt
PROXY_TLS_CERT_FILE: /app/certs/core.crt
PROXY_TLS_KEY_FILE: /app/certs/core.key
PROXY_TLS_CA_FILE: /app/certs/ca.crt
Hot-Reload Behavior
Message Center does not require a process restart when certificates are rotated. On every request, the HTTP client (coreClient.ts) compares the modification timestamp (mtime) of each certificate file against the last-seen value. If any file has changed:
- The old undici Agent connections are closed (best-effort).
- New certificate bytes are read from disk.
- A fresh Agent is created with the updated TLS configuration.
This means updating the Kubernetes Secret and waiting for propagation is sufficient — no pod rollout required. The new certificate takes effect on the first request after the file is updated on disk.
Kubernetes propagates Secret updates to mounted volumes within ~60 seconds by default (controlled by
kubelet's--sync-frequency).
Certificate Rotation Procedure
Development
# Replace files in certs/ — the BFF picks them up on the next request
cp /path/to/new/core.crt certs/client/core.crt
cp /path/to/new/core.key certs/client/core.key
cp /path/to/new/ca.crt certs/ca/ca.crt
Kubernetes
# Update the Secret in-place (idempotent apply)
kubectl create secret generic message-center-mtls-certs \
--from-file=ca.crt=certs/ca/ca.crt \
--from-file=core.crt=certs/client/core.crt \
--from-file=core.key=certs/client/core.key \
--dry-run=client -o yaml | kubectl apply -f -
# Wait ~60 s for propagation, then verify a request succeeds.
# No pod rollout needed — hot-reload handles it automatically.
Verifying mTLS is Active
Check the Diagnostics page (/diagnostics) in the UI:
- Core API section shows
OKif the mTLS handshake succeeded on the last health check. - TLS errors appear in container logs:
certificate verify failed,unknown ca,UNABLE_TO_GET_ISSUER_CERT_LOCALLY,ERR_TLS_CERT_ALTNAME_INVALID.
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
UNABLE_TO_GET_ISSUER_CERT_LOCALLY | Wrong or missing CA file | Verify CORE_TLS_CA_FILE points to the signing CA |
certificate verify failed | Server cert not signed by the configured CA | Check Proxy/Core use certs from the same CA |
ERR_TLS_CERT_ALTNAME_INVALID | SNI mismatch | Set CORE_TLS_SERVER_NAME to match the cert's CN/SAN |
certificate has expired | Cert past its NotAfter date | Rotate using the procedure above |
Core API shows OK but admin actions fail | Admin endpoint uses different cert path | Verify CORE_TLS_CERT_FILE (not only PROXY_TLS_CERT_FILE) is set |
Next Steps
- MongoDB & Migrations — database schema management
- Environment Variables Reference — full TLS environment variable reference