Internal encryption
How to flip Postgres and Redis from plaintext to TLS across every service without an outage. Each step is reversible and the system stays usable in every intermediate state.
The shape of the rollout
- Postgres has two legs: app → PgBouncer (Leg 1) and PgBouncer → Postgres (Leg 2). PgBouncer brokers between them so clients can upgrade independently.
- Redis ships in plaintext mode and supports a parallel TLS listener; both stay up during transition. Each consumer flips at its own pace.
Cert material
All three TLS variables live in Secrets Manager and are decoded to the filesystem by init.sh. The same server cert serves Postgres, PgBouncer, and Redis — SANs must cover the Database instance's private DNS.
| Name | Source | Scope | Default | Description |
|---|---|---|---|---|
INTERNAL_CA_CRT_B64 | Secrets Manager | all | — | Base64 PEM of the internal CA. |
INTERNAL_TLS_CERT_B64 | Secrets Manager | all | — | Base64 PEM of the server cert. |
INTERNAL_TLS_KEY_B64 | Secrets Manager | all | — | Base64 PEM of the server private key. Decoded with mode 0600, owner 70:70. |
Postgres rollout
Order, one step per environment / deploy cycle:
- Provision certs — push the three base64 PEMs into Secrets Manager.
POSTGRES_TLS_ENABLED=on— appends-c ssl=onto the Postgres command. Plaintext listener still works.PGBOUNCER_SERVER_TLS_SSLMODE=verify-ca— PgBouncer → Postgres now uses TLS.PGBOUNCER_CLIENT_TLS_SSLMODE=allow— clients can negotiate TLS to PgBouncer but aren't required.- Flip each app's
DATABASE_SSL_MODE=verify-full— one service at a time:api→web→voice→ops→ others. PGBOUNCER_CLIENT_TLS_SSLMODE=require— refuse plaintext on Leg 1.- (Optional)
POSTGRES_FORCE_SSL=on— rewritespg_hba.conftohostssl-only. Belt-and-braces.
Each app's DATABASE_SSL_CA_FILE should point at /etc/ssl/internal/ca.crt; init.sh writes the decoded CA there from INTERNAL_CA_CRT_B64.
Redis rollout
Redis runs both listeners during transition: plaintext on :6379 and TLS on REDIS_TLS_PORT (default :6380).
REDIS_TLS_ENABLED=true— restarts Redis with both listeners; the OTel collector'sREDIS_ENDPOINTis auto-switched to:6380byinit.sh.- Flip each app — set
REDIS_TLS_ENABLED=trueandREDIS_TLS_CA_FILE=/etc/ssl/internal/ca.crtper service. Cycle order:api,web,voice,ops,telpro. REDIS_FORCE_TLS=true— once every client moved, set--port 0to drop the plaintext listener.
The voiceai-telpro Kamailio image needs hiredis_ssl baked in for ndb_redis to talk TLS. Plain redis-cli health probes and bash tooling don't need the SSL-enabled image. Ship the SSL build of voiceai-telpro before flipping REDIS_TLS_ENABLED=true on TelPro.
Verification
# Postgres TLS server-side
docker compose exec voiceai-postgres psql -U voiceai \
-c "SELECT ssl, version FROM pg_stat_ssl WHERE pid=pg_backend_pid();"
# Postgres TLS client-side from an app instance
psql "postgresql://voiceai@<db-host>:5432/voiceai?sslmode=verify-full&sslrootcert=/etc/ssl/internal/ca.crt" -c "SELECT 1;"
# Redis TLS listener
docker exec voiceai-redis redis-cli -p 6380 --tls \
--cacert /tmp/redis-tls/ca.crt --insecure ping
# Redis TLS client-side from an app instance
redis-cli -h <db-host> -p 6380 --tls \
--cacert /etc/ssl/internal/ca.crt -a "${REDIS_PASSWORD}" ping
Rollback
Every step is reversible by setting the variable back and rerunning ./update.sh --restart-only on the relevant host. Because plaintext listeners stay up during the rollout, a partial rollback leaves the system functional.
See also
- Database operations — runtime view.
- AWS security setup — provisioning the CA and server cert in Secrets Manager.