How to Encrypt Database Data
Protect sensitive data with encryption at every level
TL;DR
Enable encryption at rest (most managed databases do this by default), use TLS for connections (add ?sslmode=require to your connection string), and encrypt sensitive fields in your app before storing them using Node's built-in crypto module or a KMS service. About 30 minutes to implement the basics.
Prerequisites:
- Database access (PostgreSQL, MySQL, MongoDB, etc.)
- Understanding of your data sensitivity requirements
- Node.js, Python, or similar backend language
Why This Matters
Encryption is the last control that holds when everything else fails. If an attacker reaches your database files or intercepts a backup, encrypted data is useless to them without the key.
Regulations like GDPR and HIPAA require encryption of personal and payment data. If you're not regulated yet, it still matters: a breach without encrypted fields is far harder to contain and disclose.
Understanding Encryption Layers
- Encryption at rest: Data encrypted on disk - protects against physical theft or unauthorized disk access
- Encryption in transit: Data encrypted over the network - protects against eavesdropping
- Application-level encryption: Data encrypted before storage - protects even if database is compromised
Step-by-Step Guide
Enable encryption at rest
Most managed database services enable this by default:
# Check encryption status for various platforms:
# PostgreSQL (RDS) - enabled by default with AWS KMS
# Verify in AWS Console → RDS → Database → Configuration
# MongoDB Atlas - enabled by default
# Verify in Atlas → Security → Encryption at Rest
# Supabase - enabled by default with AES-256
# No action needed
# Self-hosted PostgreSQL - enable in postgresql.conf
# Requires pgcrypto extension or disk-level encryption
For self-hosted databases, consider full-disk encryption (LUKS on Linux, BitLocker on Windows) or Transparent Data Encryption (TDE) if your database supports it.
Enable encryption in transit (TLS/SSL)
Always use encrypted connections to your database:
// PostgreSQL connection with SSL
const connectionString =
'postgresql://user:pass@host:5432/db?sslmode=require';
// MongoDB with TLS
const mongoUri =
'mongodb+srv://user:pass@cluster.mongodb.net/db?tls=true';
// MySQL with SSL
const mysqlConfig = {
host: 'your-host.com',
user: 'user',
password: 'pass',
database: 'db',
ssl: {
rejectUnauthorized: true
}
};
SSL modes for PostgreSQL:
disable- No SSL (never use in production)require- Encrypt connection, don't verify certificateverify-ca- Verify server certificate is signed by trusted CAverify-full- Verify certificate and hostname match (most secure)
Implement application-level encryption
Encrypt sensitive data before storing it:
// Node.js with built-in crypto
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
// Return IV + authTag + encrypted data
return iv.toString('hex') + ':' + authTag + ':' + encrypted;
}
function decrypt(encryptedData) {
const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Usage
const ssn = '123-45-6789';
const encryptedSSN = encrypt(ssn);
// Store encryptedSSN in database
const decryptedSSN = decrypt(encryptedSSN);
// Returns: '123-45-6789'
Use a key management service
Don't hardcode encryption keys. Use a KMS instead:
// AWS KMS example
import { KMSClient, EncryptCommand, DecryptCommand } from "@aws-sdk/client-kms";
const kms = new KMSClient({ region: "us-east-1" });
const KEY_ID = process.env.KMS_KEY_ID;
async function encryptWithKMS(plaintext) {
const command = new EncryptCommand({
KeyId: KEY_ID,
Plaintext: Buffer.from(plaintext)
});
const response = await kms.send(command);
return Buffer.from(response.CiphertextBlob).toString('base64');
}
async function decryptWithKMS(ciphertext) {
const command = new DecryptCommand({
CiphertextBlob: Buffer.from(ciphertext, 'base64')
});
const response = await kms.send(command);
return Buffer.from(response.Plaintext).toString('utf8');
}
Encrypt specific fields in your models
Create a pattern for handling encrypted fields:
// Prisma middleware example
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const SENSITIVE_FIELDS = ['ssn', 'taxId', 'bankAccount'];
// Encrypt on write
prisma.$use(async (params, next) => {
if (['create', 'update'].includes(params.action)) {
for (const field of SENSITIVE_FIELDS) {
if (params.args.data?.[field]) {
params.args.data[field] = encrypt(params.args.data[field]);
}
}
}
return next(params);
});
// Decrypt on read
prisma.$use(async (params, next) => {
const result = await next(params);
if (result && typeof result === 'object') {
for (const field of SENSITIVE_FIELDS) {
if (result[field]) {
result[field] = decrypt(result[field]);
}
}
}
return result;
});
Handle encryption key rotation
Plan for key rotation from the start:
// Store key version with encrypted data
function encryptWithVersion(text, keyVersion = 'v1') {
const key = getKeyByVersion(keyVersion);
const encrypted = encrypt(text, key);
return `${keyVersion}:${encrypted}`;
}
function decryptWithVersion(data) {
const [version, encrypted] = data.split(':', 1);
const key = getKeyByVersion(version);
return decrypt(encrypted, key);
}
// Re-encrypt data with new key
async function rotateEncryption(records) {
for (const record of records) {
const decrypted = decryptWithVersion(record.sensitiveField);
record.sensitiveField = encryptWithVersion(decrypted, 'v2');
await record.save();
}
}
Critical Security Considerations:
- Never store encryption keys in your database or code repository
- Use authenticated encryption (AES-GCM) to prevent tampering
- Generate keys using cryptographically secure random generators
- Encrypted fields can't be searched - plan your queries accordingly
- Back up encryption keys separately from database backups
How to Verify It Worked
- Check SSL connection: Verify your connection is encrypted
- Inspect stored data: Look at the raw database values; they should be unreadable ciphertext, not the original strings
- Test decryption: Ensure data round-trips correctly (encrypt a known value, store it, read it back, decrypt it)
-- PostgreSQL: Check if connection is encrypted
SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid();
-- View raw encrypted data
SELECT id, ssn FROM users LIMIT 1;
-- Should show something like: "v1:a3f2c1..."
-- MongoDB: Check TLS status
db.serverStatus().security
Common Errors & Troubleshooting
Error: "bad decrypt"
Wrong key or corrupted data. Check you're using the correct key version and the data wasn't modified.
Error: "IV length must be 16 bytes"
Your initialization vector is wrong size. Always generate a fresh 16-byte IV for each encryption.
Performance issues with encryption
Encryption is CPU-intensive. Encrypt only truly sensitive fields; don't blanket-encrypt the whole schema. Caching helps for frequently read encrypted fields.
Can't search encrypted fields
Expected. Encrypted data can't be searched directly. Your options: deterministic encryption for searchable fields (searchable but weaker), or a separate search index that stores only non-sensitive metadata.
Which fields should I encrypt?
Encrypt PII (SSN, government IDs), financial data (bank accounts, credit cards), health records, and any data that would cause significant harm if exposed. Don't encrypt everything - it adds complexity and performance overhead.
Should I hash or encrypt passwords?
Always hash passwords with bcrypt or argon2 - never encrypt them. Hashing is one-way; even if your database is compromised, the original passwords can't be recovered.
Is encryption at rest enough?
It protects against physical theft and some attack vectors, but not against attackers who gain database access through your application. For sensitive data, combine it with application-level encryption.
Related guides:Hash Passwords Securely · HashiCorp Vault Basics · AWS Secrets Manager