Encryption at Rest and in Transit: Implementation Guide

Type: Software Reference Confidence: 0.94 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Algorithm Recommendation Table

#AlgorithmUse CaseKey SizeMode/TypePerformanceNotes
1AES-256-GCMEncryption at rest (primary)256-bitAEADFast with AES-NINIST approved; 12-byte nonce recommended
2AES-128-GCMEncryption at rest (resource-constrained)128-bitAEADFaster than AES-256Sufficient security for most applications
3ChaCha20-Poly1305Encryption at rest (mobile/no AES-NI)256-bitAEAD3x faster without HW accelPreferred on ARM CPUs without AES extensions
4XChaCha20-Poly1305Encryption at rest (large nonce)256-bitAEADSimilar to ChaCha2024-byte nonce; safer for random nonce generation
5RSA-OAEPAsymmetric encryption (key wrapping)2048+ bitAsymmetricSlow; key transport onlyNever use PKCS#1 v1.5 padding; prefer 4096-bit
6X25519Key exchange (ECDH)256-bitAsymmetric (DH)Very fastDefault curve for TLS 1.3; constant-time
7Ed25519Digital signatures256-bitAsymmetric (signing)Very fastNot for encryption; for authentication/signing
8TLS 1.3Encryption in transit (preferred)N/AProtocolFaster handshake (1-RTT)Only AEAD ciphers; no legacy negotiation
9TLS 1.2Encryption in transit (compat)N/AProtocol2-RTT handshakeOnly use with AEAD cipher suites
10Argon2idPassword hashing (NOT encryption)N/AKDFTunableWinner of PHC; use for passwords

TLS Cipher Suite Priority (Mozilla Intermediate)

PriorityCipher SuiteProtocolKey ExchangeNotes
1TLS_AES_128_GCM_SHA256TLS 1.3ECDHEDefault TLS 1.3 suite
2TLS_AES_256_GCM_SHA384TLS 1.3ECDHEStronger key, slightly slower
3TLS_CHACHA20_POLY1305_SHA256TLS 1.3ECDHEBetter on mobile/ARM
4ECDHE-ECDSA-AES128-GCM-SHA256TLS 1.2ECDHEForward secrecy + AEAD
5ECDHE-RSA-AES128-GCM-SHA256TLS 1.2ECDHERSA cert + ECDHE exchange
6ECDHE-ECDSA-AES256-GCM-SHA384TLS 1.2ECDHE256-bit AES variant
7ECDHE-RSA-AES256-GCM-SHA384TLS 1.2ECDHERSA cert + 256-bit AES
8ECDHE-ECDSA-CHACHA20-POLY1305TLS 1.2ECDHEMobile-friendly alternative
9ECDHE-RSA-CHACHA20-POLY1305TLS 1.2ECDHERSA cert + ChaCha20

Decision Tree

START: What type of data are you encrypting?
├── Data in transit (network communication)?
│   ├── YES → Use TLS 1.3 (preferred) or TLS 1.2 with AEAD ciphers
│   │   ├── Web server (Nginx/Apache)? → See Nginx TLS config example
│   │   ├── Application-level (API calls)? → Use HTTPS with cert validation
│   │   └── Database connections? → Enable SSL/TLS on DB connection string
│   └── NO ↓
├── Data at rest (storage/database)?
│   ├── YES ↓
│   │   ├── Server has AES-NI? → Use AES-256-GCM
│   │   ├── Mobile/ARM without AES HW? → Use ChaCha20-Poly1305
│   │   ├── Database field-level? → Use AES-256-GCM with per-row IV
│   │   ├── Full-disk? → Platform native (LUKS, BitLocker, FileVault)
│   │   └── Cloud storage? → Provider KMS (AWS KMS, Azure Key Vault)
│   └── NO ↓
├── Key exchange / wrapping?
│   ├── YES → X25519 (ECDH) or RSA-OAEP (4096-bit) for envelope encryption
│   └── NO ↓
└── DEFAULT → AES-256-GCM for symmetric, X25519 for key exchange, TLS 1.3 for transport

Step-by-Step Guide

1. Choose your encryption algorithm and mode

Select AES-256-GCM for encryption at rest on servers with AES-NI hardware acceleration. Use ChaCha20-Poly1305 on mobile/ARM without AES hardware. Always use AEAD mode. [src1]

AES-256-GCM: Confidentiality + Integrity + Authentication (single pass)
AES-CBC + HMAC: Confidentiality + Integrity (two passes, error-prone -- avoid)
AES-ECB: NEVER USE -- leaks data patterns

Verify: Check CPU AES-NI support: grep -c aes /proc/cpuinfo (Linux) → non-zero means AES-NI available.

2. Generate cryptographically secure keys

Use your platform's CSPRNG. Never derive keys from passwords without a proper KDF. [src6]

import os
key = os.urandom(32)  # 256 bits from OS CSPRNG

Verify: Key length: len(key) should return 32 bytes for AES-256.

3. Implement encryption at rest with unique IVs

Every encryption operation MUST use a unique IV/nonce. For AES-GCM, use 12 bytes (96 bits) generated randomly. [src1]

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)  # unique per encryption
ciphertext = aesgcm.encrypt(nonce, plaintext_bytes, associated_data)

Verify: Decrypt with wrong key → InvalidTag exception.

4. Configure TLS for data in transit

Deploy TLS 1.3 as the primary protocol with TLS 1.2 AEAD fallback. Enable HSTS. [src3]

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

Verify: openssl s_client -connect example.com:443 -tls1_3Protocol : TLSv1.3. Test at SSL Labs for A+ rating.

5. Implement envelope encryption for key management

Encrypt data with a DEK, encrypt the DEK with a KEK stored in KMS/HSM. [src5]

Plaintext → [DEK] → Ciphertext
DEK → [KEK] → Encrypted DEK
KEK → stored in KMS/HSM (never leaves)

Verify: Confirm plaintext DEK is never persisted to disk.

6. Enable HSTS and HTTP-to-HTTPS redirect

Force all HTTP to HTTPS and set HSTS headers. [src4]

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Verify: curl -sI http://example.com → 301 redirect to HTTPS.

Code Examples

Python: AES-256-GCM Encryption at Rest

# Input:  plaintext bytes + optional associated data (AAD)
# Output: nonce + ciphertext (authenticated)

from cryptography.hazmat.primitives.ciphers.aead import AESGCM  # cryptography>=41.0.0
import os
import base64

def encrypt(plaintext: bytes, key: bytes, aad: bytes = None) -> bytes:
    """Encrypt with AES-256-GCM. Returns nonce || ciphertext."""
    if len(key) != 32:
        raise ValueError("Key must be 32 bytes (256 bits)")
    aesgcm = AESGCM(key)
    nonce = os.urandom(12)  # 96-bit nonce, unique per operation
    ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
    return nonce + ciphertext  # prepend nonce for storage

def decrypt(data: bytes, key: bytes, aad: bytes = None) -> bytes:
    """Decrypt AES-256-GCM. Expects nonce || ciphertext."""
    aesgcm = AESGCM(key)
    nonce, ciphertext = data[:12], data[12:]
    return aesgcm.decrypt(nonce, ciphertext, aad)

# Usage:
key = AESGCM.generate_key(bit_length=256)
encrypted = encrypt(b"sensitive data", key, aad=b"context")
decrypted = decrypt(encrypted, key, aad=b"context")

Node.js: AES-256-GCM Encryption at Rest

// Input:  plaintext string + key (32 bytes)
// Output: IV + authTag + ciphertext (base64)

const crypto = require('crypto');  // Node.js built-in

function encrypt(plaintext, key) {
  const iv = crypto.randomBytes(12);           // 96-bit IV
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();         // 128-bit auth tag
  return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}

function decrypt(data, key) {
  const buf = Buffer.from(data, 'base64');
  const iv = buf.subarray(0, 12);
  const authTag = buf.subarray(12, 28);
  const ciphertext = buf.subarray(28);
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
  decipher.setAuthTag(authTag);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
}

const key = crypto.randomBytes(32);
const encrypted = encrypt('sensitive data', key);
const decrypted = decrypt(encrypted, key);

Nginx: TLS 1.3 + 1.2 Configuration

# Mozilla Intermediate TLS configuration for A+ SSL Labs rating

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;
    ssl_ecdh_curve X25519:prime256v1:secp384r1;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

PostgreSQL: Field-Level Encryption with pgcrypto

-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Encrypt on insert
INSERT INTO users (email, ssn_encrypted)
VALUES (
  '[email protected]',
  pgp_sym_encrypt('123-45-6789', current_setting('app.encryption_key'))
);

-- Decrypt on select
SELECT email,
  pgp_sym_decrypt(ssn_encrypted, current_setting('app.encryption_key')) AS ssn
FROM users WHERE email = '[email protected]';

-- Set key per session (load from vault at connection time)
SET app.encryption_key = 'load-from-vault-at-connection-time';

Anti-Patterns

Wrong: Using ECB mode

# BAD -- ECB encrypts each block independently, leaking patterns
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
cipher = Cipher(algorithms.AES(key), modes.ECB())
# The "ECB penguin": identical plaintext blocks produce identical ciphertext blocks

Correct: Using GCM mode (authenticated encryption)

# GOOD -- GCM provides confidentiality + integrity + authentication
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)

Wrong: Reusing nonces with AES-GCM

// BAD -- same IV reused for multiple encryptions with the same key
const STATIC_IV = Buffer.from('000000000000', 'hex');
const cipher1 = crypto.createCipheriv('aes-256-gcm', key, STATIC_IV);
const cipher2 = crypto.createCipheriv('aes-256-gcm', key, STATIC_IV);
// Nonce reuse with GCM reveals XOR of plaintexts AND breaks authentication

Correct: Random nonce per encryption

// GOOD -- unique random IV for every encryption operation
const iv = crypto.randomBytes(12);  // fresh 96-bit IV each time
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
// Store IV alongside ciphertext (IV is not secret)

Wrong: Hardcoding encryption keys

# BAD -- key in source code, visible in version control
ENCRYPTION_KEY = b'mysecretkey12345mysecretkey12345'
cipher = AESGCM(ENCRYPTION_KEY)

Correct: Loading keys from a secure vault

# GOOD -- load key from environment or secrets manager
import os, base64
key = base64.b64decode(os.environ['ENCRYPTION_KEY'])
# Or from AWS KMS / HashiCorp Vault / Azure Key Vault

Wrong: Using MD5/SHA-1 for password hashing

# BAD -- MD5/SHA-1 are fast hashes, trivially brute-forced
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()  # crackable in seconds

Correct: Using Argon2id for password hashing

# GOOD -- Argon2id is memory-hard, GPU-resistant
from argon2 import PasswordHasher  # argon2-cffi>=21.0.0
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hashed = ph.hash(password)

Wrong: Self-signed certificates in production

# BAD -- no identity verification, clients must disable cert validation
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout server.key -out server.crt -subj "/CN=example.com"

Correct: Using Let's Encrypt

# GOOD -- trusted CA, automatic renewal, free
sudo certbot certonly --nginx -d example.com -d www.example.com
# Auto-renewal: sudo certbot renew --dry-run

Common Pitfalls

Diagnostic Commands

# Check TLS version and cipher of a remote host
openssl s_client -connect example.com:443 -tls1_3 2>/dev/null | grep -E "Protocol|Cipher"

# Check if server supports TLS 1.3
openssl s_client -connect example.com:443 -tls1_3 < /dev/null 2>&1 | grep "Protocol"

# Check if server still accepts TLS 1.0 (should fail)
openssl s_client -connect example.com:443 -tls1 < /dev/null 2>&1 | grep "Protocol"

# Show full certificate chain
openssl s_client -connect example.com:443 -showcerts < /dev/null 2>&1

# Verify certificate expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

# Check HSTS header
curl -sI https://example.com | grep -i strict-transport-security

# Check CPU AES-NI support (Linux)
grep -c aes /proc/cpuinfo

# Verify Nginx TLS configuration syntax
nginx -t

# List supported ciphers for a server
nmap --script ssl-enum-ciphers -p 443 example.com

Version History & Compatibility

StandardVersionStatusKey Feature
TLS 1.3RFC 8446Current (2018)1-RTT handshake, AEAD-only, no RSA key exchange
TLS 1.2RFC 5246SupportedForward secrecy with ECDHE; use AEAD ciphers only
TLS 1.1RFC 4346Deprecated (RFC 8996)Do not use
TLS 1.0RFC 2246Deprecated (RFC 8996)Do not use -- vulnerable to BEAST, POODLE
AES-GCMNIST SP 800-38DCurrentAEAD mode; NIST approved since 2007
ChaCha20-Poly1305RFC 8439Current (2018)IETF standard; alternative to AES-GCM
X25519RFC 7748Current (2016)ECDH curve; default in TLS 1.3
NIST PQCFIPS 203/204/205Finalized (2024)Post-quantum: ML-KEM, ML-DSA, SLH-DSA

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Storing sensitive data in databases (SSN, PII, financial)Storage layer already encrypts (e.g., AWS S3 SSE)Verify provider encryption meets compliance
Transmitting data over any networkCommunication within an HSMHSM internal operations handle encryption
Compliance requires encryption (PCI DSS, HIPAA, GDPR)You need to hash passwords (one-way)Argon2id, bcrypt, or scrypt
Building an API handling sensitive user dataYou need integrity only (no confidentiality)HMAC-SHA256 for message authentication
Encrypting backups, log files, data exportsYou need computation on encrypted dataFHE libraries (Microsoft SEAL, TFHE)

Important Caveats

Related Units