ies. The architecture must separate transit security (network layer) from rest security (storage layer), while maintaining a unified key lifecycle.
Step 1: Define Encryption Boundaries
- In Transit: All network hops must enforce TLS 1.3. This includes client-to-CDN, CDN-to-edge, edge-to-service, service-to-database, and service-to-cache.
- At Rest: Storage engines (PostgreSQL, Redis, S3-compatible, local disks) must use AES-256-GCM with envelope encryption. Keys never leave the KMS boundary.
Step 2: Architecture Decisions & Rationale
- TLS 1.3 Only: Eliminates legacy cipher suites, reduces handshake round trips, and provides forward secrecy by default.
- Envelope Encryption: Prevents direct KMS API calls per data operation. A locally generated data key encrypts payloads; the KMS encrypts the data key. Rotation only requires re-encrypting the data key, not the entire dataset.
- KMS Integration: Use AWS KMS, GCP Cloud KMS, or HashiCorp Vault. Never store master keys in environment variables or config files.
- Database-Level vs Application-Level: Prefer application-level encryption for PII/payment data. Database-level encryption protects against disk theft but exposes data to DB admins and query logging. Application-level ensures end-to-end confidentiality.
Step 3: TypeScript Implementation
Transit Configuration (Node.js HTTP Server + TLS)
import https from 'https';
import fs from 'fs';
import { createServer } from 'http';
const tlsOptions = {
key: fs.readFileSync('/etc/ssl/private/server.key'),
cert: fs.readFileSync('/etc/ssl/certs/server.crt'),
ca: fs.readFileSync('/etc/ssl/certs/ca-chain.crt'),
minVersion: 'TLSv1.3',
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
honorCipherOrder: true,
requestCert: true, // Optional: mTLS for service-to-service
rejectUnauthorized: true
};
const server = https.createServer(tlsOptions, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Secure endpoint');
});
server.listen(443, () => console.log('TLS 1.3 server running'));
At-Rest Envelope Encryption Utility
import crypto from 'crypto';
import { KMSClient, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms';
const kms = new KMSClient({ region: 'us-east-1' });
const KEY_ID = 'alias/app-data-key';
export async function encryptEnvelope(plaintext: string): Promise<{ ciphertext: string; encryptedKey: string }> {
// 1. Generate 256-bit data key
const dataKey = crypto.randomBytes(32);
// 2. Encrypt data key with KMS
const kmsRes = await kms.send(new EncryptCommand({
KeyId: KEY_ID,
Plaintext: dataKey
}));
if (!kmsRes.CiphertextBlob) throw new Error('KMS encryption failed');
// 3. Encrypt payload with AES-256-GCM
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', dataKey, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
ciphertext: `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`,
encryptedKey: Buffer.from(kmsRes.CiphertextBlob).toString('base64')
};
}
export async function decryptEnvelope(payload: { ciphertext: string; encryptedKey: string }): Promise<string> {
// 1. Decrypt data key via KMS
const keyBuffer = Buffer.from(payload.encryptedKey, 'base64');
const kmsRes = await kms.send(new DecryptCommand({ CiphertextBlob: keyBuffer }));
if (!kmsRes.Plaintext) throw new Error('KMS decryption failed');
const dataKey = Buffer.from(kmsRes.Plaintext);
// 2. Parse ciphertext components
const [ivHex, tagHex, encryptedHex] = payload.ciphertext.split(':');
const iv = Buffer.from(ivHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
// 3. Decrypt payload
const decipher = crypto.createDecipheriv('aes-256-gcm', dataKey, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Database Connection with Strict TLS
import { Pool } from 'pg';
import fs from 'fs';
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/etc/ssl/certs/db-ca.crt').toString(),
minVersion: 'TLSv1.3'
},
max: 20,
idleTimeoutMillis: 30000
});
Step 4: Operational Integration
- Store
encryptedKey alongside ciphertext in the database or object storage.
- Implement background key rotation: generate new data key, re-encrypt only the envelope key, not the payload.
- Use structured logging to track encryption/decryption latency and KMS API errors without logging secrets.
Pitfall Guide
-
Relying on Provider Defaults Without Verifying Key Ownership
Cloud provider SSE uses service-managed keys. If the provider's control plane is compromised or subpoenaed, your data is accessible. Always migrate to Customer-Managed Keys (CMK) or envelope encryption with explicit key policies.
-
Disabling Certificate Validation in Non-Production Environments
Setting rejectUnauthorized: false or NODE_TLS_REJECT_UNAUTHORIZED=0 in staging/test environments creates dangerous habit loops. Developers deploy the same configuration to production, or CI/CD pipelines bypass validation. Use self-signed CAs with explicit trust stores for internal testing.
-
Algorithm Debt: CBC Modes, SHA-1, and TLS 1.2
AES-CBC is vulnerable to padding oracle attacks. SHA-1 is deprecated for signatures. TLS 1.2 allows legacy cipher suites. Enforce AES-256-GCM, SHA-256+, and TLS 1.3 via infrastructure-as-code and runtime configuration.
-
Logging Encrypted Payloads or Key Material
Debug logs frequently capture raw ciphertext, base64-encoded keys, or KMS response metadata. This creates a secondary data lake that bypasses encryption controls. Implement log sanitization middleware and restrict KMS API logging to metadata only.
-
Synchronous Encryption Blocking the Event Loop
Node.js crypto module methods are synchronous by default. Encrypting large payloads blocks the main thread, causing request timeouts and degraded throughput. Use crypto.randomBytes and createCipheriv with streaming APIs or offload to worker threads for payloads >1MB.
-
Ignoring Metadata and Header Leakage
Encrypting the body while leaving URLs, query parameters, or HTTP headers plaintext exposes PII, authentication tokens, and system topology. Use opaque identifiers, route all sensitive data through POST/PUT bodies, and enforce strict CORS/Referrer policies.
-
Manual Key Rotation and Rotation Blind Spots
Rotating keys without a versioning strategy causes decryption failures for in-flight data. Implement envelope key versioning, maintain backward-compatible decryption paths for 30-90 days, and automate rotation via KMS scheduled policies or cron jobs with idempotent re-encryption.
Production Best Practices:
- Decouple key lifecycle from data lifecycle.
- Enforce TLS 1.3 at every network boundary, not just the edge.
- Use envelope encryption for all PII, financial, and health data.
- Monitor KMS API latency and error rates; set alerts for
ThrottlingException or InvalidStateException.
- Implement certificate pinning or trust-on-first-use (TOFU) for high-security service-to-service communication.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Multi-tenant SaaS handling PII | Envelope Encryption + KMS + TLS 1.3 | Isolates tenant data, satisfies SOC2/ISO27001, enables per-tenant key rotation | +8-12% infra cost (KMS API + key management) |
| Internal analytics dashboard | Provider SSE + TLS 1.3 termination | Low compliance requirement, high throughput, minimal operational overhead | Baseline cloud pricing |
| PCI-DSS payment processing | Application-level E2E + HSM-backed KMS + mTLS | Zero provider access, audit-ready key custody, strict boundary enforcement | +15-20% (HSM, mTLS infra, compliance audits) |
| High-throughput event streaming | TLS 1.3 + Storage-level AES-256-GCM | Balances latency with compliance, avoids per-record encryption overhead | +3-5% (managed storage encryption) |
Configuration Template
Nginx Ingress TLS Hardening
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
# Strict transport security
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Disable legacy features
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 valid=300s;
}
Kubernetes Secret + Node.js KMS Client
# k8s-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: kms-config
type: Opaque
stringData:
AWS_REGION: "us-east-1"
KMS_KEY_ID: "alias/app-data-key"
NODE_ENV: "production"
// kms-client.ts
import { KMSClient } from '@aws-sdk/client-kms';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
export const createKmsClient = () => {
return new KMSClient({
region: process.env.AWS_REGION || 'us-east-1',
credentialDefaultProvider: fromNodeProviderChain(),
maxAttempts: 3,
retryMode: 'adaptive'
});
};
Quick Start Guide
- Provision KMS Key: Create a Customer-Managed Key in your cloud provider. Attach an IAM policy granting
kms:Encrypt, kms:Decrypt, and kms:GenerateDataKey to your application role only.
- Update Ingress/Proxy: Apply the Nginx TLS template or equivalent load balancer configuration. Enforce TLS 1.3 and disable all legacy protocols.
- Integrate Envelope Encryption: Replace plaintext storage writes with the
encryptEnvelope utility. Store the returned ciphertext and encryptedKey in your database or object storage.
- Validate Transit: Test all endpoints with
openssl s_client -connect your-domain:443 -tls1_3. Verify certificate chain, protocol version, and cipher suite.
- Deploy & Monitor: Push configuration changes. Enable KMS CloudWatch metrics or equivalent. Set alerts for
Decrypt failures and latency spikes above 50ms.