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 the ssl_protocols wish 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 signed is 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

  1. Failure before or after ServerHello? No ServerHello → TCP/ACL/SNI mismatch, not a cipher problem.
  2. Alert record: handshake_failure vs bad_certificate vs protocol_version.
  3. SNI: did the client send the name the vhost expects?
  4. Resumption: 1.2 session id / ticket; 1.3 PSK. 0-RTT is a replay discussion, out of this lab.
  5. Do not trust a screenshot of a green padlock. Trust Verify return code against 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_client matches.
  • Pin the CA in the client (--cacert / system store), not InsecureSkipVerify.
  • 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 SAN IP: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_client still verify 0
  • Name mismatch: OpenSSL 62, curl 60, no -k in 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/