mTLS (Layer 5) is a core security layer in Termopus. The relay authenticates
phones via client certificates issued by your CA. This is set up automatically
by scripts/setup.sh.
- Provisioning API generates a client certificate signed by your CA
- Phone stores the certificate in Keychain/Keystore
- When connecting to the relay, the phone receives an
auth_challenge(random nonce) - Phone signs the nonce with its private key and sends back the signature + certificate
- Relay verifies the certificate fingerprint exists in
PROVISIONED_DEVICESKV - Relay verifies the signature using the certificate's public key
mTLS is configured automatically during initial setup (scripts/setup.sh).
To regenerate certificates or reconfigure manually, see below.
./scripts/setup-mtls.shopenssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout ca-key.pem -out ca-cert.pem -days 3650 -nodes \
-subj "/CN=Termopus Self-Hosted CA/O=Termopus"# Provisioning API needs both key and cert to sign device certificates
wrangler secret put CA_PRIVATE_KEY --env dev < ca-key.pem
wrangler secret put CA_CERTIFICATE --env dev < ca-cert.pem
# Relay needs the cert to verify device certificates
wrangler secret put CA_CERTIFICATE --env dev < ca-cert.pemRun these in both provisioning_api/ and relay_worker/ directories respectively.
cd relay_worker && npm run deploy:dev
cd ../provisioning_api && npm run deploy:devIf you regenerate your CA after the app has already been installed and provisioned:
- The app has a self-signed certificate from the initial CSR key generation
- This certificate is NOT signed by your CA
- The relay will reject it (fingerprint not in
PROVISIONED_DEVICESKV) - You get
Authentication failed (code 4001)
Fix: Uninstall and reinstall the app so it provisions a fresh certificate signed by your CA.
# Android
adb uninstall com.termopus.app
# iOS
# Delete from Settings > General > iPhone StorageThen rebuild and run: cd app && flutter run
Keep ca-key.pem private. Anyone with the CA key can issue valid client
certificates. The .ca/ directory is gitignored by default.