Encryption protects data in two different states, and each state has its own threat model:
- In transit: data moving over a network, where anyone on the path between client and server could read or alter it.
- Encryption at rest: data sitting on a disk, where a stolen drive, a leaked backup, or a shared snapshot could expose it.
A system needs both. Encrypting the wire does nothing for the database backup someone copied to a laptop, and an encrypted disk does nothing for the password crossing a coffee-shop network in plaintext.
Encryption in transit
When a request leaves the client, it passes through Wi-Fi access points, routers, ISPs, and load balancers before it reaches your server. Over plain HTTP, every one of those hops can read the request and change it. A password, a session cookie, an API response: all of it is in the clear.
TLS (Transport Layer Security) is what fixes this. It's the "S" in HTTPS . You'll also hear "SSL": that was TLS's predecessor, now obsolete, but the name stuck, so "SSL certificate" almost always means a TLS certificate today.
TLS does two jobs
People think of TLS as "encryption," but it does two distinct things, and the second is the one that's easy to forget:
- Encryption: scrambles the traffic so an eavesdropper on the path sees only ciphertext. Confidentiality.
- Authentication: proves the server is who it claims to be, using a certificate signed by a Certificate Authority (CA) the browser already trusts. Identity.
Encryption without authentication is useless against a man-in-the-middle. If you encrypt a conversation but can't verify who's on the other end, you may have set up a perfectly secure channel to the attacker.
The man-in-the-middle attack
A man-in-the-middle (MITM) sits between the client and the real server, relaying traffic while reading or altering it. Picture connecting to coffee-shop Wi-Fi: a malicious access point can pose as your bank's server, take the client's connection, and open its own connection to the bank, creating two encrypted tunnels with the attacker reading everything in the middle.
The certificate is what stops this. When the client connects, the server presents a certificate
that says "I am bank.com," signed by a trusted CA. The attacker can relay the
bank's real certificate, but a certificate alone is useless. Completing the TLS handshake
requires proving possession of the private key that matches it, and only the bank has that key.
The attacker can't substitute their own certificate either, because no trusted CA will sign one
for a domain they don't control. Either way the browser refuses to continue.
This is why clicking through a certificate warning is dangerous, and why your code must never disable certificate validation to "make the error go away." That warning is exactly the MITM defense doing its job.
How to use it
Most apps terminate TLS at a load balancer or a service like Cloudflare, so your app speaks plain HTTP internally. Two things stay your responsibility:
// 1. Redirect any plaintext request to the encrypted equivalent
app.use((req, res, next) => {
if (req.headers["x-forwarded-proto"] !== "https") {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
// 2. Tell the browser to never even try HTTP for this domain again (HSTS)
app.use((req, res, next) => {
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
next();
});
The Strict-Transport-Security header ( HSTS ) narrows a gap: the
first request a user makes is often plain HTTP, before the redirect: a window a MITM can
exploit. Once the browser has seen the HSTS header, it upgrades all future requests to HTTPS on
its own, before any request leaves the machine. The true first-ever visit over HTTP is still a
window. HSTS preloading closes this by submitting your domain to browser
build lists (see hstspreload.org), so HTTPS is enforced even before the first
request.
Encryption at rest
Encryption at rest scrambles data before it's written to storage and decrypts it when read back. The threat it addresses is physical or out-of-band access to the storage itself:
- A stolen laptop or a drive pulled from decommissioned hardware.
- A database backup or snapshot copied somewhere it shouldn't be, or shared with the wrong account.
- Storage media a cloud provider retires and recycles.
Know what it doesn't protect against: a running application reads decrypted data by design. If an attacker compromises your app or gets SQL injection, encryption at rest does nothing, because they're reading through the same decryption path your code uses. It's a complement to access control, not a substitute.
In practice you almost never implement this yourself. Storage services encrypt with keys managed by a key management service (KMS) ; your job is to make sure it's turned on and that the keys have sensible access policies.
Turning it on: S3
S3 has encrypted all new objects by default (SSE-S3, where AWS manages the keys) since January 2023. Stepping up to SSE-KMS gives you a key you control: you can audit every decrypt in CloudTrail and revoke access by policy, so even someone with read access to the bucket can't get plaintext without permission to use the key.
# S3: new objects are encrypted by default (SSE-S3) since January 2023.
# To upgrade a bucket to a KMS key you control (better auditing + access control):
aws s3api put-bucket-encryption \
--bucket my-app-uploads \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:123456789012:key/my-key-id"
},
"BucketKeyEnabled": true
}]
}' Turning it on: databases and volumes
Most managed data stores make this a single flag, but often only at creation time, so decide early:
# RDS: encryption must be enabled at creation time — it can't be
# switched on later without snapshotting to a new encrypted instance.
aws rds create-db-instance \
--db-instance-identifier my-app-db \
--engine postgres \
--storage-encrypted \
...
# EBS: make every new volume in the region encrypted automatically.
aws ec2 enable-ebs-encryption-by-default - RDS / Aurora:
--storage-encryptedencrypts the instance, its automated backups, read replicas, and snapshots together. - DynamoDB, MongoDB Atlas: encrypted at rest by default; both let you swap in a customer-managed key.
- Self-hosted databases (Postgres, MySQL): rely on encrypting the volume or filesystem underneath (LUKS, encrypted EBS), since the databases themselves don't encrypt their own data files.
- Laptops: full-disk encryption (FileVault on macOS, BitLocker on Windows) is
the same idea. That local
.envfile or database dump is only as safe as the disk it sits on.
One caveat that surprises people: an unencrypted snapshot shared publicly or cross-account exposes everything in it, and snapshots of unencrypted volumes stay unencrypted. Enabling default encryption up front is much cheaper than migrating data later.
Check your understanding
TLS is on: why does the fake access point still fail?
An attacker on the same Wi-Fi sets up a fake access point and proxies traffic to your bank. TLS is in use. Why does the attack still fail?