Data is the ultimate target. Attackers compromise systems to reach data — credentials, PII, financial records, health information. This lesson covers the practical choices developers make when protecting data at rest and in transit.
Encryption in transit
TLS everywhere
All data in transit must be encrypted with TLS. No exceptions, including:
- Browser to server
- Server to database
- Service to service (even within a private network)
- Server to third-party APIs
"It's internal" is not a valid reason to skip TLS. Internal networks get compromised. Service meshes, VPNs, and private subnets reduce risk but do not eliminate it.
TLS configuration
- Use TLS 1.2 or 1.3. Disable TLS 1.0 and 1.1.
- Use strong cipher suites. Prefer AEAD ciphers (AES-GCM, ChaCha20-Poly1305).
- Enable HSTS (HTTP Strict Transport Security) to prevent protocol downgrade attacks.
- Use valid certificates from a trusted CA. Automate renewal with Let's Encrypt or your cloud provider's certificate manager.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Certificate pinning
Certificate pinning binds your application to a specific certificate or public key, preventing man-in-the-middle attacks even if a CA is compromised. However, pinning is operationally dangerous — if you rotate certificates without updating the pins, your application breaks.
For most applications, standard TLS with a trusted CA is sufficient. Reserve pinning for high-security contexts (banking, healthcare) and ensure you have a pin rotation plan.
Encryption at rest
Database encryption
Most cloud databases support transparent encryption at rest:
- AWS RDS — AES-256 encryption enabled at instance creation
- GCP Cloud SQL — encrypted by default
- Azure SQL — Transparent Data Encryption (TDE) enabled by default
This protects against physical media theft and some storage-level attacks, but it does not protect against application-level data access. If an attacker compromises your application or database credentials, they can read decrypted data through the application layer.
Application-level encryption
For sensitive fields (SSNs, credit card numbers, health data), encrypt at the application level before storing:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # Store securely — not in code
cipher = Fernet(key)
# Encrypt before storing
encrypted_ssn = cipher.encrypt(ssn.encode())
# Decrypt when needed
ssn = cipher.decrypt(encrypted_ssn).decode()
Application-level encryption means even a database administrator or a SQL injection attacker sees only ciphertext.
Choosing encryption algorithms
| Use case | Algorithm | Notes |
|---|---|---|
| Symmetric encryption (general) | AES-256-GCM | Authenticated encryption — provides confidentiality and integrity |
| Symmetric encryption (alternative) | ChaCha20-Poly1305 | Good performance on devices without AES hardware acceleration |
| Asymmetric encryption | RSA-OAEP (2048+ bit) or ECIES | For key exchange, digital signatures |
| Hashing (non-reversible) | SHA-256, SHA-3 | For checksums, integrity verification |
| Password hashing | Argon2id, bcrypt, scrypt | Deliberately slow; resistant to brute-force |
Do not use: DES, 3DES, RC4, MD5 (for security), SHA-1 (for security). These are broken or deprecated.
Key management
The encryption is only as strong as the key management:
- Never hardcode keys in source code. Use environment variables, secrets managers, or KMS.
- Rotate keys periodically. Design your encryption layer to support key versioning.
- Use a KMS (Key Management Service) when possible. AWS KMS, GCP Cloud KMS, Azure Key Vault, and HashiCorp Vault handle key storage, rotation, and access control.
- Separate encryption keys per purpose. The key that encrypts user data should not be the same key that signs JWTs.
Password storage
Passwords are a special case. They must be hashed, not encrypted, because you should never need to reverse them.
Use the right algorithm
# Argon2id — recommended
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password)
ph.verify(hash, password) # Raises exception on mismatch
| Algorithm | Status |
|---|---|
| Argon2id | Recommended — winner of the Password Hashing Competition; resistant to GPU and side-channel attacks |
| bcrypt | Good — widely supported, well-tested, max 72-byte input |
| scrypt | Good — memory-hard, less widely used than bcrypt |
| PBKDF2 | Acceptable — Django's default, but less resistant to GPU attacks than Argon2 or bcrypt |
| SHA-256 / MD5 | Never — fast hashes are trivially brute-forced |
Salting
All modern password hashing algorithms include a unique salt per password automatically. If you are using argon2, bcrypt, or scrypt through a standard library, salts are handled for you. Do not implement salting manually unless you are building a password hashing library.
Data minimisation
The best protection for data is not collecting it in the first place.
- Do you actually need this data? If a feature works without a phone number, do not collect one.
- Retention limits. Define how long you keep data and automate deletion. Old data you do not need is liability, not an asset.
- Anonymisation and pseudonymisation. For analytics, use anonymised or aggregated data instead of raw PII.
- Log scrubbing. Do not log sensitive data. Mask or redact fields like passwords, tokens, and credit card numbers before they reach your logging pipeline.
Tokenisation
For data like credit card numbers, replace the sensitive value with a non-reversible token:
Actual: 4111-1111-1111-1111
Token: tok_abc123def456
The mapping from token to actual value is stored in a secure vault (or by a payment processor like Stripe). Your application never sees or stores the real card number, reducing PCI DSS scope dramatically.
Summary
Encrypt all data in transit with TLS 1.2+. Encrypt sensitive data at rest at the application level for fields that need it, and use transparent encryption for baseline storage protection. Hash passwords with Argon2id (or bcrypt). Manage keys with a KMS — never hardcode them. Minimise the data you collect and define retention limits. Tokenise highly sensitive data when possible. Every layer of encryption reduces the impact of the next breach.
