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

  1. Download the RDS global trust bundle:
    https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
  2. Provide it to Drizzle Kit and node-postgres via ssl: { ca: <file>, rejectUnauthorized: true }.
  3. Use the RDS endpoint hostname (not IP) to pass hostname verification.
  4. Prefer secrets/volumes for production deployments.

That’s it—you’ve got encrypted, verified connections from NestJS + Drizzle to AWS RDS.

Sign up to receive updates on new content & exclusive offers

We don’t spam! Cancel anytime.