mTLS Certificates

📦v1.0.0📅2026-04-28🔄Updated 2026-04-28👤Admin Team
administrationconfigurationmtlssecurity

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

VariableDefaultPurpose
PROXY_TLS_CERT_FILE./certs/client/core.crtClient cert for Proxy mTLS
PROXY_TLS_KEY_FILE./certs/client/core.keyPrivate key for Proxy mTLS
PROXY_TLS_CA_FILE./certs/ca/ca.crtCA cert to verify Proxy's certificate
PROXY_SERVER_NAMEproxy.localTLS SNI name (must match Proxy cert CN/SAN)
CORE_TLS_CERT_FILE./certs/client/core.crtClient cert for Core admin API
CORE_TLS_KEY_FILE./certs/client/core.keyPrivate key for Core admin API
CORE_TLS_CA_FILE./certs/ca/ca.crtCA cert to verify Core's certificate
CORE_TLS_SERVER_NAMEcore.localTLS 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:

  1. The old undici Agent connections are closed (best-effort).
  2. New certificate bytes are read from disk.
  3. 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 OK if 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

SymptomCauseFix
UNABLE_TO_GET_ISSUER_CERT_LOCALLYWrong or missing CA fileVerify CORE_TLS_CA_FILE points to the signing CA
certificate verify failedServer cert not signed by the configured CACheck Proxy/Core use certs from the same CA
ERR_TLS_CERT_ALTNAME_INVALIDSNI mismatchSet CORE_TLS_SERVER_NAME to match the cert's CN/SAN
certificate has expiredCert past its NotAfter dateRotate using the procedure above
Core API shows OK but admin actions failAdmin endpoint uses different cert pathVerify CORE_TLS_CERT_FILE (not only PROXY_TLS_CERT_FILE) is set

Next Steps