This is not a polished crypto textbook.
This is exactly how I, as a DevOps engineer, tried to understand TLS by breaking it down, running commands, hitting errors, and finally connecting the dots.
I had worked with HTTPS, load balancers, certificates, and Kubernetes ingress for a long time — but if someone asked me “what exactly happens inside TLS?”, my answer used to be hand-wavy.
So I decided to rebuild TLS step by step using OpenSSL, starting from old TLS 1.1, moving to modern TLS 1.2 (ECDHE), and finally encrypting real data.
This blog documents that journey — including confusions, mistakes, and fixes.
Why I Started This
TLS is everywhere:
- ALBs
- Application Gateways
- NGINX
- Kubernetes Ingress
- Service Meshes
Yet most explanations stop at:
“TLS uses asymmetric encryption for handshake and symmetric encryption for data.”
That explanation never satisfied me.
I wanted to answer:
- Where does the key actually come from?
- Is the session key sent or generated?
- What changed between TLS 1.1, 1.2, and 1.3?
So I started from the old way first.
PART 1 — TLS 1.1 (RSA): Understanding the Old Model
Before ECDHE, TLS relied on RSA key exchange. This is important to understand because modern TLS exists specifically to fix its problems.
High-level idea (TLS 1.1)
- Server has an RSA key pair
- Server shares the public key
- Client generates a secret
- Client encrypts that secret using server public key
- Server decrypts using private key
- Both derive the same session key
At first glance, this feels reasonable.
Step 1: Server generates RSA key pair
openssl genpkey -algorithm RSA -out server_private.pem -pkeyopt rsa_keygen_bits:2048
When I ran this, it clicked:
- This private key is long-term
- It lives on the server
- If this key leaks, things get dangerous
Extract the public key:
openssl pkey -in server_private.pem -pubout -out server_public.pem
This public key is what eventually goes inside the TLS certificate.
Step 2: Client generates Pre-Master Secret
In real TLS, this is random binary data. For learning, I used text:
echo "tls11-pre-master-secret" > pre_master_secret.txt
At this point I clearly understood:
This secret is the root of all security in TLS 1.1
Step 3: Client encrypts secret using server public key
openssl pkeyutl -encrypt \
-pubin \
-inkey server_public.pem \
-pkeyopt rsa_padding_mode:oaep \
-in pre_master_secret.txt \
-out pre_master_secret.enc
This step is exactly what TLS 1.1 does.
Important realization:
- The secret is actually sent over the network (encrypted)
- Anyone who later gets the private key can decrypt old traffic
Step 4: Server decrypts using private key
openssl pkeyutl -decrypt \
-inkey server_private.pem \
-pkeyopt rsa_padding_mode:oaep \
-in pre_master_secret.enc \
-out recovered_pre_master.txt
Now both sides have the same secret.
At this moment I thought:
“So TLS security fully depends on this private key never leaking.”
And that’s exactly the problem.
Why TLS 1.1 Was Broken
After doing this hands-on, the problems became obvious:
- ❌ No forward secrecy
- ❌ Past traffic can be decrypted if key leaks
- ❌ Vulnerable to padding oracle attacks
This is why RSA key exchange was deprecated.
Modern TLS had to change the model completely.
PART 2 — TLS 1.2 (ECDHE): The Modern Fix
This is where things finally made sense.
The key idea of ECDHE is:
Never send the secret. Generate it independently on both sides.
Step 5: Client generates ephemeral EC key pair
openssl genpkey -algorithm EC \
-pkeyopt ec_paramgen_curve:prime256v1 \
-out client_private.pem
Extract public key:
openssl pkey -in client_private.pem -pubout -out client_public.pem
This key pair exists only for this connection.
Step 6: Server generates ephemeral EC key pair
openssl genpkey -algorithm EC \
-pkeyopt ec_paramgen_curve:prime256v1 \
-out server_private.pem
openssl pkey -in server_private.pem -pubout -out server_public.pem
At this point:
- Client has its EC key pair
- Server has its EC key pair
Only public keys will be exchanged.
Step 7: Deriving the shared secret (this was the “aha” moment)
Client side:
openssl pkeyutl -derive \
-inkey client_private.pem \
-peerkey server_public.pem \
-out client_shared_secret.bin
Server side:
openssl pkeyutl -derive \
-inkey server_private.pem \
-peerkey client_public.pem \
-out server_shared_secret.bin
When I verified both:
sha256sum client_shared_secret.bin server_shared_secret.bin
…and saw the hashes match, everything clicked.
No secret was sent. Yet both sides got the same value.
That is the heart of modern TLS.
Shared Secret ≠ Session Key (Important Realization)
Initially I assumed this shared secret was the session key.
It is not.
TLS treats this as input material, not the final key.
Step 8: Deriving a symmetric key
TLS uses HKDF internally. For learning, I hashed the shared secret:
openssl sha256 client_shared_secret.bin
This produced a 32-byte value — perfect for AES-256.
This is conceptually the application traffic key.
PART 3 — Encrypting Real Data
Now came the most satisfying part: actually encrypting data.
Plaintext
echo "Hello secure world from client" > message.txt
Encrypt (AES)
openssl enc -aes-256-cbc \
-K <derived_key_hex> \
-iv 00000000000000000000000000000000 \
-in message.txt \
-out message.enc
Decrypt on the other side
openssl enc -d -aes-256-cbc \
-K <derived_key_hex> \
-iv 00000000000000000000000000000000 \
-in message.enc \
-out message.dec
Seeing the original message come back confirmed:
I just implemented the core of TLS myself.
Where TLS 1.3 Fits In
TLS 1.3 removes all legacy paths:
- No RSA key exchange
- Only ECDHE
- Faster handshake
But conceptually, the ECDHE flow you saw here is exactly what TLS 1.3 uses.
Final Takeaway
Doing this hands-on changed how I think about TLS:
- TLS is not magic
- Keys are not sent
- Security comes from math + randomness
- Symmetric encryption does the heavy lifting
If you are a DevOps engineer working with HTTPS every day, this depth of understanding is worth it.
This blog reflects real learning, real commands, and real confusion along the way — exactly how engineering understanding is built.
Top comments (0)