Wireshark Cheatsheet
TLS Decryption
Use this Wireshark reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Which Method Works
There are two ways to decrypt TLS in Wireshark and only one of them works on modern traffic.
| Method | Needs | Works on |
|---|---|---|
Key log file (SSLKEYLOGFILE) | Cooperation from the client or server process | Everything, including TLS 1.3 and QUIC |
| RSA private key | The server's private key file | Only non-forward-secret ciphers, so RSA key exchange |
Every current cipher suite uses ephemeral Diffie-Hellman (ECDHE), which is the
point of forward secrecy: the session keys are never derivable from the long-term
private key. TLS 1.3 removed static RSA key exchange entirely. So the private key
method is a legacy path for old TLS 1.2 endpoints deliberately configured with
TLS_RSA_WITH_* suites, and nothing else.
Decrypt only traffic you are authorized to decrypt. This is a debugging technique for systems you control.
Key Log File
The client writes secrets to a text file as it negotiates, and Wireshark reads
them. Set SSLKEYLOGFILE in the environment of the process that makes the
connection, before it starts.
export SSLKEYLOGFILE="$HOME/tls-keys.log" # Browsers, Chrome and Firefox both honor it google-chrome firefox open -a "Google Chrome" # macOS, only if the var is exported first # curl 7.58+, and anything built on OpenSSL 1.1.1+ or NSS curl -sS https://example.com/ >/dev/null # Node NODE_OPTIONS="--tls-keylog=$HOME/tls-keys.log" node app.js # Python, requests and urllib3 via OpenSSL honor SSLKEYLOGFILE from 3.8 on python3 -c "import requests; requests.get('https://example.com')" # Go needs code, there is no environment variable # tls.Config{KeyLogWriter: f}
Then point Wireshark at it:
Preferences → Protocols → TLS → (Pre)-Master-Secret log filename
Or from the command line, no GUI needed:
tshark -o "tls.keylog_file:$HOME/tls-keys.log" -r cap.pcapng -Y http tshark -o "tls.keylog_file:$HOME/tls-keys.log" -r cap.pcapng \ -z follow,tls,ascii,0 -q
The file is a plain text list, one secret per line. TLS 1.2 uses one label, TLS 1.3 uses four, two per direction plus the exporter and early-data secrets.
CLIENT_RANDOM <64 hex chars> <96 hex chars> CLIENT_HANDSHAKE_TRAFFIC_SECRET <client random> <secret> SERVER_HANDSHAKE_TRAFFIC_SECRET <client random> <secret> CLIENT_TRAFFIC_SECRET_0 <client random> <secret> SERVER_TRAFFIC_SECRET_0 <client random> <secret>
Same mechanism decrypts QUIC and HTTP/3, since QUIC derives its keys from the TLS 1.3 handshake and uses the same key log labels. No extra setting is needed.
RSA Private Key
For the legacy case, Preferences → RSA Keys → + (a top-level preferences page
since Wireshark 3.0) takes a PEM or PKCS#12 key file. Older builds put the same
thing under Protocols → TLS → RSA keys list, with an IP, port, protocol, and
key path per row.
# Confirm the key matches the certificate before blaming Wireshark openssl rsa -in server.key -noout -modulus | openssl sha256 openssl x509 -in server.crt -noout -modulus | openssl sha256
It only decrypts when all of these hold:
- The cipher suite is RSA key exchange, not ECDHE or DHE. Check
tls.handshake.ciphersuite on the ServerHello and look for a name starting
TLS_RSA_WITH_.
- The capture contains the full handshake, starting at the ClientHello. A
capture that begins mid-session has no key exchange to unwrap.
- Session resumption did not skip the key exchange. An abbreviated handshake with
a session ticket carries no encrypted premaster secret.
- The key is the one for that certificate, unencrypted or with the passphrase
supplied.
Embed Secrets in the pcapng
pcapng can carry decryption secrets as a block inside the capture file, so the file decrypts on any machine with no side-channel key log. This is how you hand a TLS capture to a colleague.
# Bake a key log into the capture editcap --inject-secrets tls,tls-keys.log cap.pcapng cap-with-keys.pcapng # What can be injected editcap --inject-secrets help # Pull the secrets back out of a capture that already has them tshark -r cap-with-keys.pcapng --export-tls-session-keys out.log
In the GUI, File → Export TLS Session Keys writes the same file, and
Edit → Inject TLS Secrets embeds the currently loaded keys into the open capture.
Edit → Discard All Secrets strips them again before sharing a file you should
not have decrypted for someone else.
Verify and Troubleshoot
When decryption is set up, the packet list Protocol column changes from TLSv1.3
to HTTP2 or HTTP, a Decrypted TLS tab appears in the byte pane, and
Follow → TLS Stream shows plaintext.
tls.handshake.type == 1 # ClientHello, is the handshake captured tls.handshake.type == 2 # ServerHello, which suite was chosen tls.handshake.ciphersuite # add as a column tls.record.content_type == 23 # application data, the encrypted bulk tls.record.content_type == 21 # alert tls.alert_message.desc # why it failed http2 and tls # proof that decryption worked
| Symptom | Cause |
|---|---|
Still shows Application Data | No matching CLIENT_RANDOM line, or the key log was written after the capture started |
| Some streams decrypt, others do not | Key log missing entries for connections opened before SSLKEYLOGFILE was set |
| Nothing decrypts with an RSA key | Forward-secret cipher suite. Check tls.handshake.ciphersuite |
| Handshake present, decryption fails | Capture missing the start of the session, or a resumed session |
| Browser writes no key log | Variable not exported to that process. A browser already running does not pick it up |
| Certificates not visible in TLS 1.3 | Correct, they are encrypted. Decrypt first, then look |
Common TLS alert descriptions, since tls.alert_message.desc is a number:
| Value | Alert |
|---|---|
| 0 | close_notify, a clean shutdown |
| 40 | handshake_failure, usually no shared cipher or version |
| 42 | bad_certificate |
| 46 | certificate_unknown |
| 47 | illegal_parameter |
| 48 | unknown_ca, the classic self-signed or missing-intermediate error |
| 49 | access_denied |
| 51 | decrypt_error, often a client-certificate signature problem |
| 70 | protocol_version, one side refused the offered version |
| 112 | unrecognized_name, SNI does not match a configured server |