Encrypting database traffic isn’t optional—especially if you’re handling user data or building anything that might one day need audits or certifications. Fortunately, enabling SSL/TLS for Postgres on AWS RDS with Drizzle ORM (node-postgres) in a NestJS app is straightforward.
Below is a clean, production-ready setup that:
- Uses the official AWS RDS global trust store
- Verifies the server certificate (no
rejectUnauthorized: false
shenanigans) - Works with Drizzle Kit and your NestJS app
- Plays nicely with Docker/Kubernetes through secrets
What you’ll use
- AWS RDS Postgres
- Drizzle ORM with
node-postgres
- AWS RDS trust bundle:
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
Why this file? It’s Amazon’s CA bundle for RDS instances across regions. Supplying it ensures TLS cert verification succeeds without disabling hostname checks.
Step 1: Add the RDS trust bundle to your project (or store it as a secret)
Option A — Commit it (fastest to demo; fine for public, non-rotating CA bundles):
/global-bundle.pem
Option B — Provision it at build/runtime (recommended for infra):
- In Dockerfile (build-time):
RUN curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o /app/global-bundle.pem
- Or mount it via Kubernetes Secret or your platform’s secret manager.
Tip: Don’t disable verification. Avoid
ssl: { rejectUnauthorized: false }
. You want full cert validation to prevent MITM.
Step 2: Configure Drizzle Kit to connect with SSL
Your Drizzle Kit config should pass the CA certificate via dbCredentials.ssl.ca
. Example:
import { defineConfig } from 'drizzle-kit';
import * as fs from 'fs';
import * as path from 'path';
const certificate = fs
.readFileSync(path.resolve(__dirname, 'global-bundle.pem'))
.toString();
export default defineConfig({
schema: './src/**/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
host: process.env.DATABASE_HOST!,
port: parseInt(process.env.DATABASE_PORT!, 10),
user: process.env.DATABASE_USER!,
password: process.env.DATABASE_PASSWORD!,
database: process.env.DATABASE_NAME!,
ssl: { ca: certificate }, // verification enabled by default
},
});
Running migrations with SSL
Once your env vars are set, run:
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
If you see self signed certificate in certificate chain
, you’re either missing the CA bundle or the file path is wrong.
Step 3: Create a NestJS DatabaseModule
with verified SSL
Use pg
’s Pool
with the same CA. In production, enable SSL; for local dev against a local Postgres, you can skip SSL.
import { Module } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/node-postgres';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Pool } from 'pg';
import * as fs from 'fs';
import * as path from 'path';
import { DATABASE_CONNECTION } from './database-connection';
export const schema = {
...schema
};
@Module({
imports: [ConfigModule],
providers: [
{
provide: DATABASE_CONNECTION,
useFactory: (configService: ConfigService) => {
let ssl: any = false;
if (configService.get('NODE_ENV') === 'production') {
const certPath = path.resolve(__dirname, '../../global-bundle.pem');
const certificate = fs.readFileSync(certPath).toString();
// rejectUnauthorized is true by default when CA is provided.
// Explicitly set it for clarity:
ssl = { ca: certificate, rejectUnauthorized: true };
}
const pool = new Pool({
host: configService.getOrThrow('DATABASE_HOST'),
port: parseInt(configService.getOrThrow('DATABASE_PORT'), 10),
user: configService.getOrThrow('DATABASE_USER'),
password: configService.getOrThrow('DATABASE_PASSWORD'),
database: configService.getOrThrow('DATABASE_NAME'),
ssl,
});
return drizzle(pool, { schema });
},
inject: [ConfigService],
},
],
exports: [DATABASE_CONNECTION],
})
export class DatabaseModule {}
Why not always-on SSL?
const certificate = fs.readFileSync('/app/global-bundle.pem', 'utf8');
const ssl = { ca: certificate, rejectUnauthorized: true };
You can keep SSL on everywhere. Many teams keep:
…and use it in all environments. For local Docker/Compose you might also run Postgres with TLS. The split above is just a pragmatic default.
Important: Don’t use DATABSE_URL
Avoid using the DATABASE_URL environment variable for configuring the connection string. Using it will override the SSL option. Instead, explicitly provide the connection string parts like in our example.
Step 4: Environment variables
Typical .env
:
NODE_ENV=production
DATABASE_HOST=mydb.abcdefg12345.us-east-1.rds.amazonaws.com
DATABASE_PORT=5432
DATABASE_USER=app_user
DATABASE_PASSWORD=supersecret
DATABASE_NAME=app_db
Ensure the RDS hostname is the actual endpoint hostname (not an IP). Hostname verification is part of TLS security.
Step 5: Docker/Kubernetes tips
Dockerfile snippet
# Install CA bundle during build
RUN curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem -o /app/global-bundle.pem
Kubernetes Secret (recommended)
kubectl create secret generic rds-ca \
--from-url=https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
Then mount it:
volumeMounts:
- name: rds-ca
mountPath: /app/certs
readOnly: true
volumes:
- name: rds-ca
secret:
secretName: rds-ca
And update your code to read /app/certs/global-bundle.pem
.
Step 6: Quick local verification with psql
If you have psql
:
psql "host=mydb.abcdefg12345.us-east-1.rds.amazonaws.com \
port=5432 dbname=app_db user=app_user sslmode=verify-full \
sslrootcert=./global-bundle.pem"
If that works, your CA bundle and hostname verification are sound.
Common pitfalls & fixes
self signed certificate in certificate chain
The CA bundle isn’t being read. Check the path, ensure the file exists in the container/pod, and verify permissions.- Using
rejectUnauthorized: false
This disables cert and hostname verification; don’t do this in production. It defeats the purpose of TLS. - Wrong host
TLS verification uses the hostname. If you connect by IP or a mismatched hostname, verification fails. Always use the RDS endpoint. - RDS Proxy
Works fine with the same CA approach; use the proxy endpoint hostname.
TL;DR
- Download the RDS global trust bundle:
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
- Provide it to Drizzle Kit and node-postgres via
ssl: { ca: <file>, rejectUnauthorized: true }
. - Use the RDS endpoint hostname (not IP) to pass hostname verification.
- Prefer secrets/volumes for production deployments.
That’s it—you’ve got encrypted, verified connections from NestJS + Drizzle to AWS RDS.