This is a handshake lab, not a middlebox bypass. Target is nginx on 127.0.0.1:8443 with a lab certificate whose CN I redact here, plus openssl s_client -msg. Goal: name the records I can see in TLS 1.2 vs 1.3, paste a redacted -msg excerpt, and show a verify failure when the name does not match. I do not ship a custom ClientHello that downgrades a real server, and I do not disable certificate checks in anything but this lab command.
1Figure 1. After ChangeCipherSpec / EncryptedExtensions, the useful fields go dark.
2ClientHello -> ServerHello -> Certificate* -> Finished
3* cleartext on TLS 1.2; encrypted on TLS 1.3
Lab layout
1labs/tls_lab/
2 nginx.conf
3 certs/lab.pem # CN=[REDACTED], SAN=127.0.0.1
4 certs/lab.key
5 bad-name.conf # server_name mismatch for the fail case
1# nginx.conf — loopback only
2events { worker_connections 8; }
3http {
4 server {
5 listen 127.0.0.1:8443 ssl;
6 server_name 127.0.0.1;
7 ssl_certificate /labs/tls_lab/certs/lab.pem;
8 ssl_certificate_key /labs/tls_lab/certs/lab.key;
9 ssl_protocols TLSv1.2 TLSv1.3;
10 location / { return 200 "tls-lab\n"; }
11 }
12}
Certificate I minted with a throwaway CA. The CN is not a customer name.
1$ openssl x509 -in certs/lab.pem -noout -subject -issuer -dates
2subject=CN = [REDACTED]
3issuer=CN = lab-ca-[REDACTED]
4notBefore=Sep 1 00:00:00 2026 GMT
5notAfter=Sep 1 00:00:00 2027 GMT
6$ openssl x509 -in certs/lab.pem -noout -ext subjectAltName
7X509v3 Subject Alternative Name:
8 IP Address:127.0.0.1
Artifact: s_client -msg (TLS 1.2 forced, so records stay readable)
TLS 1.3 encrypts the certificate. For a first pass I force 1.2 so the notebook has a cleartext cert record, then I repeat on 1.3 and note what vanished.
1openssl s_client -connect 127.0.0.1:8443 -tls1_2 -msg -CAfile certs/lab-ca.pem \
2 -servername 127.0.0.1 </dev/null 2>/tmp/tls12.msg
Excerpt (-msg writes a mix of stderr and the hex dump; this is the text I keep):
1>>> TLS 1.2, Handshake [length 013c], ClientHello
2 01 00 01 38 03 03 [REDACTED_RANDOM]
3 cipher suites (truncated): TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
4<<< TLS 1.2, Handshake [length 0051], ServerHello
5 02 00 00 4d 03 03 [REDACTED_RANDOM]
6 cipher: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
7<<< TLS 1.2, Handshake [length 05c4], Certificate
8 0b 00 05 c0 00 05 bd 00 05 ba
9 subject: CN=[REDACTED]
10 issuer: CN=lab-ca-[REDACTED]
11<<< TLS 1.2, Handshake [length 012d], ServerKeyExchange
12<<< TLS 1.2, Handshake [length 0004], ServerHelloDone
13>>> TLS 1.2, Handshake [length 0046], ClientKeyExchange
14>>> TLS 1.2, Handshake [length 0010], Finished
15<<< TLS 1.2, ChangeCipherSpec [length 0001]
16<<< TLS 1.2, Handshake [length 0010], Finished
17---
18Verify return code: 0 (ok)
What I extract from that dump, and only that:
- Version actually negotiated (
TLS 1.2), not thessl_protocolswish list. - Cipher (
ECDHE_RSA_AES_128_GCM_SHA256) — AEAD, ECDHE. A CBC or RSA-key-transport suite would be a finding. - Cert CN/SAN, here already
[REDACTED]/IP:127.0.0.1. - Verify return code 0 against my lab CA.
verify error:num=18:self signedis a different ticket.
TLS 1.3 on the same server:
1$ openssl s_client -connect 127.0.0.1:8443 -tls1_3 -msg -CAfile certs/lab-ca.pem </dev/null
2>>> TLS 1.3, Handshake [length ...], ClientHello
3<<< TLS 1.3, Handshake [length ...], ServerHello
4<<< TLS 1.3, Handshake [length ...], EncryptedExtensions
5<<< TLS 1.3, Handshake [length ...], Certificate # encrypted on the wire
6<<< TLS 1.3, Handshake [length ...], CertificateVerify
7<<< TLS 1.3, Handshake [length ...], Finished
8Verify return code: 0 (ok)
9# s_client still prints the cert because it is the terminator
Packet capture of 1.3 shows Application Data where 1.2 showed a clear Certificate. Middleboxes that parsed 1.2 certs on the wire break here. That is a defender fact, not a request to strip TLS.
Analysis steps on a capture
- Failure before or after
ServerHello? No ServerHello → TCP/ACL/SNI mismatch, not a cipher problem. - Alert record:
handshake_failurevsbad_certificatevsprotocol_version. - SNI: did the client send the name the vhost expects?
- Resumption: 1.2 session id / ticket; 1.3 PSK. 0-RTT is a replay discussion, out of this lab.
- Do not trust a screenshot of a green padlock. Trust
Verify return codeagainst a CA you pin.
1$ echo | openssl s_client -connect 127.0.0.1:8443 -servername wrong.lab \
2 -CAfile certs/lab-ca.pem -tls1_2 2>&1 | tail -20
3# nginx still serves the only cert; OpenSSL:
4verify error:num=62:hostname mismatch
5Verify return code: 62 (hostname mismatch)
Hostname mismatch is the sanitized failure I keep. The HTTP layer may still 200 if a client ignores verify (curl -k). I do not use -k except to confirm the vhost is up, then I throw that output away.
Sanitized reproduction (alert / crash only)
1curl -sv --cacert certs/lab-ca.pem https://127.0.0.1:8443/ -o /tmp/body
2# * TLSv1.3 (IN), TLS handshake, Finished (20):
3# < HTTP/1.1 200 OK
4# tls-lab
5
6curl -sv --cacert certs/lab-ca.pem --resolve 'wrong.lab:8443:127.0.0.1' \
7 https://wrong.lab:8443/ -o /dev/null
8# * SSL: certificate subject name '[REDACTED]' does not match target host name 'wrong.lab'
9# curl: (60) SSL certificate problem: ...
A truncated handshake (client sends ClientHello, then FIN) shows up as nginx:
12026/09/18 14:41:02 [info] 4412#0: *9 SSL_do_handshake() failed
2 (SSL: error:0A000126:SSL routines::unexpected eof while reading)
3 while SSL handshaking, client: 127.0.0.1, server: 127.0.0.1:8443
ASAN is not in openssl here. Unexpected EOF is the crash analog I file for scanners that drop the handshake.
Failed-auth at the HTTP layer after a good handshake (so we do not mix TLS and app tickets):
12026-09-18T14:44:11+08:00 GET /admin
2 src: 127.0.0.1 tls: TLSv1.3 cipher: TLS_AES_256_GCM_SHA384
3 auth: 401 www-authenticate: Basic realm="lab"
4 user: [REDACTED]
TLS verified; HTTP did not. Two rows in two files.
Alerts I actually keep
openssl s_client will print Alert records when the server hates us. Two lab cases:
1# protocol the server disabled
2$ openssl s_client -connect 127.0.0.1:8443 -tls1 </dev/null
31404:error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available
4# nginx error.log:
5# SSL_do_handshake() failed (SSL: error:0A00006C:SSL routines::version too low)
6
7# cipher we removed (TLS1.2 only suite the server does not offer)
8$ openssl s_client -connect 127.0.0.1:8443 -tls1_2 -cipher 'RC4-SHA' </dev/null
9error:141A90B5:SSL routines:ssl_cipher_list_to_bytes:no ciphers available
version too low and no ciphers are expected after hardening. A sudden spike of unexpected eof from one /24 is a scanner; a spike of hostname mismatch from our own synthetic monitor is a broken SNI in the probe, not an attack.
Mitigation / what I tick
ssl_protocols TLSv1.2 TLSv1.3;— no 1.0/1.1 on anything I own.- Ciphers: GCM/CHACHA, ECDHE or TLS 1.3 suites. No RC4, no 3DES, no NULL, no export.
- Cert: SAN contains the names clients actually send (IP or DNS). CN is leftover; SAN is what
s_clientmatches. - Pin the CA in the client (
--cacert/ system store), notInsecureSkipVerify. - Log TLS version + cipher on the edge. The 401 line above is the shape.
- 1.3: expect encrypted certs in pcaps; use the terminator’s view (
s_client, nginx$ssl_client_verify) not Wireshark’s cleartext fields from 2014.
1log_format tls '$remote_addr $ssl_protocol $ssl_cipher $ssl_session_reused '
2 '$request $status';
What I file after this lab
- nginx
127.0.0.1:8443, cert SANIP:127.0.0.1, CN[REDACTED] - TLS 1.2
-msg: ClientHello → ServerHello → Certificate (CN redacted) → Finished, verify 0 - TLS 1.3: Certificate is encrypted on the wire;
s_clientstill verify 0 - Name mismatch: OpenSSL 62, curl 60, no
-kin the kept evidence - Truncated handshake: nginx
unexpected eof while reading - Fix: TLS 1.2/1.3 only, AEAD ciphers, SAN match, pin CA, log protocol+cipher
Commands appendix
1openssl s_client -connect 127.0.0.1:8443 -tls1_2 -msg -CAfile certs/lab-ca.pem </dev/null
2openssl s_client -connect 127.0.0.1:8443 -tls1_3 -CAfile certs/lab-ca.pem </dev/null
3openssl x509 -in certs/lab.pem -noout -ext subjectAltName
4curl -sv --cacert certs/lab-ca.pem https://127.0.0.1:8443/