Decrypt TLS encrypted HTTP traffic for debugging


To debug HTTP requests, it may be useful to capture traffic and look at the packets that are sent back and forth between the client and the server.

This is trivial when HTTP requests are sent over an unencrypted channel. In this case it is easy enough to use a tool like tcpdump to capture the packets and inspect them with a tool like Wireshark.

For the demonstrations below, tshark is used instead of Wireshark. Tshark is the CLI-based version of Wireshark and provides more or less the same capabilities for dissecting network packets. The debugging shown here can of course be done using the GUI-based Wireshark as well.

To start off, let us look at an example, of how we can debug HTTP traffic with tshark. First, we run a simple Apache httpd server that accepts plain-text connections on port 80 and TLS encrypted connections on port 443. We will run Apache httpd inside a Docker container for convenience but it would work just the same for non-containerized Apache httpd installations, whether they are installed from the distro’s software repository or self-compiled.

To run the Apache httpd container, create a file called Dockerfile with the following contents. This sets up an Apache httpd webserver that accepts plain-text connections on port 80 by default. Additionally, we enable TLS encrypted connections on port 443 and for that we use the snake oil certificates provided by the ssl-cert package. These are self-signed certificates that can be used for testing. Since they are self-signed, they will not be recognized as trusted certificates by most user-agents but that is not a problem here.

FROM httpd:2.4

# install snakeoil certificates
RUN apt update && apt install -y ssl-cert

# enable https on port 443 with snakeoil certificates
RUN sed -i \
-e 's/^#\(Include .*httpd-ssl.conf\)/\1/' \
-e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' \
-e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' \
conf/httpd.conf
RUN sed -i \
-e 's|^\(SSLCertificateFile\).*|\1 /etc/ssl/certs/ssl-cert-snakeoil.pem|' \
-e 's|^\(SSLCertificateKeyFile\).*|\1 /etc/ssl/private/ssl-cert-snakeoil.key|' \
conf/extra/httpd-ssl.conf

# additionally expose port 443
EXPOSE 443

Then build it with this command:

docker build -t simple-apache-httpd .

Now, run the Apache httpd server as a container:

# -i - Keep STDIN open even if not attached.
# -t - Allocate a pseudo-TTY.
# --rm - Automatically remove the container when it exits
# --net="host" - Connect a container to a network
# --name simple-apache-httpd - Assign a name to the container
# simple-apache-httpd - The IMAGE which starts the process
docker run -it --rm --net="host" --name simple-apache-httpd simple-apache-httpd

In a separate terminal, run tcpdump to capture traffic:

# -v - When  parsing and printing, produce (slightly more) verbose output.
# -i any - Listen on interface.
# "host localhost" - Filter for all packets related to localhost
# -w dump.pcap - Write the raw packets to file rather than parsing and printing them out.
sudo tcpdump -v -i any "src localhost || dst localhost" -w dump.pcap

In yet another terminal, run the curl command to perform a plaintext request to the container:

$ curl -v http://localhost
* Trying 127.0.0.1:80...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 12:55:40 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
<
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact

Stop tcpdump and look at the packet capture with tshark:

# -r dump.pcap - Read packet data from infile
# -Y http - Cause the specified filter to be applied before printing a decoded form of packets
$ tshark -r dump.pcap -Y http
   48  11.912607    127.0.0.1 → 127.0.0.1    HTTP 141 GET / HTTP/1.1 
   50  11.919104    127.0.0.1 → 127.0.0.1    HTTP 353 HTTP/1.1 200 OK  (text/html)

We can clearly see the HTTP request we just performed with curl.

With TLS encrypted connections this is not as simple anymore. We would not see any HTTP traffic at all, instead we only see a TLS handshake and encrypted data packets. Note that we now need to provide the -k flag for curl since the snake oil certificates are self-signed and are therefore not trusted:

$ curl -v -k https://localhost
*   Trying 127.0.0.1:443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=f31d50e8d088
*  start date: Dec  2 10:39:24 2021 GMT
*  expire date: Nov 30 10:39:24 2031 GMT
*  issuer: CN=f31d50e8d088
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 13:00:23 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
< 
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact

We cannot see the HTTP request anymore:

$ tshark -r dump.pcap -Y http

We can only see the encrypted TLS packets:

$ tshark -r dump.pcap -Y tls
   13   1.492489    127.0.0.1 → 127.0.0.1    TLSv1 585 Client Hello
   15   1.496532    127.0.0.1 → 127.0.0.1    TLSv1.3 1381 Server Hello, Change Cipher Spec, ...
   17   1.497260    127.0.0.1 → 127.0.0.1    TLSv1.3 148 Change Cipher Spec, Application Data
   19   1.497486    127.0.0.1 → 127.0.0.1    TLSv1.3 163 Application Data
   21   1.497556    127.0.0.1 → 127.0.0.1    TLSv1.3 355 Application Data
   23   1.497709    127.0.0.1 → 127.0.0.1    TLSv1.3 355 Application Data
   25   1.498220    127.0.0.1 → 127.0.0.1    TLSv1.3 375 Application Data
   27   1.498655    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Application Data
   29   1.498830    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Application Data

The traffic is now encrypted and we would need to decrypt the captured packets in order to inspect the HTTP traffic.

For the purpose of inspecting the traffic, TLS connections can be grouped into two groups. Connections that leverage Perfect Forward Secrecy (PFS) and connections that do not.

The main difference between the two types of connections is that connections without PFS can be decrypted after the fact using the long-lived private key of the server. For connections with PFS, this is not possible because short-lived, ephemeral keys are negotiated between the client and server. This is usually achieved by a Diffie-Hellman key exchange or a derivative thereof. The ephemeral keys generated during the key exchange are never persisted or reused for other connections. This prevents the traffic from being decrypted without being a party to the initial handshake of the connection.

Let us first look at connections without PFS. Here, it suffices to pass the private key of the server to Wireshark to decrypt traffic. For this example, we have to ensure, that we use TLS parameters that do not leverage PFS. To achieve that, we explicitly tell curl to use the protocol TLSv1.2 and the cipher suite CAMELLIA128-SHA.

We repeat the example above, capturing the traffic:

$ curl -v -k --tlsv1.2 --tls-max 1.2 --ciphers CAMELLIA128-SHA https://localhost
* Trying 127.0.0.1:443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: CAMELLIA128-SHA
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / CAMELLIA128-SHA
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=f31d50e8d088
* start date: Dec 2 10:39:24 2021 GMT
* expire date: Nov 30 10:39:24 2031 GMT
* issuer: CN=f31d50e8d088
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 13:34:01 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
<
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact
$ tshark -r dump.pcap -Y tls
    6   0.420061    127.0.0.1 → 127.0.0.1    TLSv1 216 Client Hello
    8   0.420403    127.0.0.1 → 127.0.0.1    TLSv1.2 943 Server Hello, Certificate, Server Hello Done
   10   0.420756    127.0.0.1 → 127.0.0.1    TLSv1.2 414 Client Key Exchange, Change Cipher Spec, ...
   12   0.421699    127.0.0.1 → 127.0.0.1    TLSv1.2 147 Change Cipher Spec, Encrypted Handshake Message
   14   0.421923    127.0.0.1 → 127.0.0.1    TLSv1.2 189 Application Data
   16   0.422142    127.0.0.1 → 127.0.0.1    TLSv1.2 397 Application Data
   18   0.422378    127.0.0.1 → 127.0.0.1    TLSv1.2 125 Encrypted Alert
   20   0.422497    127.0.0.1 → 127.0.0.1    TLSv1.2 125 Encrypted Alert

Now we extract the TLS private key of the server:

docker exec -it simple-apache-httpd cat /etc/ssl/private/ssl-cert-snakeoil.key > ssl-cert-snakeoil.key

This allow us to use the private key in tshark by specifying it using the -o flag:

# -r dump.pcap - Read packet data from infile
# -Y http - Cause the specified filter to be applied before printing a decoded form of packets
# -o tls.keys_list:,,,ssl-cert-snakeoil.key - override values from the preferences files
$ tshark -o tls.keys_list:,,,ssl-cert-snakeoil.key -r dump.pcap -Y tls
    6   0.420061    127.0.0.1 → 127.0.0.1    TLSv1 216 Client Hello
    8   0.420403    127.0.0.1 → 127.0.0.1    TLSv1.2 943 Server Hello, Certificate, Server Hello Done
   10   0.420756    127.0.0.1 → 127.0.0.1    TLSv1.2 414 Client Key Exchange, Change Cipher Spec, Finished
   12   0.421699    127.0.0.1 → 127.0.0.1    TLSv1.2 147 Change Cipher Spec, Finished
   14   0.421923    127.0.0.1 → 127.0.0.1    HTTP 189 GET / HTTP/1.1 
   16   0.422142    127.0.0.1 → 127.0.0.1    HTTP 397 HTTP/1.1 200 OK  (text/html)
   18   0.422378    127.0.0.1 → 127.0.0.1    TLSv1.2 125 Alert (Level: Warning, Description: Close Notify)
   20   0.422497    127.0.0.1 → 127.0.0.1    TLSv1.2 125 Alert (Level: Warning, Description: Close Notify)

As stated above, this approach will not work with PFS. This means that if only TLS parameters with PFS are available, it will not be possible to use the approach described above to decrypt traffic for debugging. For TLSv1.3 this is always the case because TLSv1.3 mandates PFS. We do not have to specify this explicitly as curl will automatically use TLSv1.3.

To illustrate this, we show an example just as before.

$ curl -v -k https://localhost
* Trying 127.0.0.1:443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=f31d50e8d088
* start date: Dec 2 10:39:24 2021 GMT
* expire date: Nov 30 10:39:24 2031 GMT
* issuer: CN=f31d50e8d088
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 14:06:14 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
<
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact

With PFS, we again see only the TLS handshake. Despite providing the servers private key as before, the packets remain encrypted.

$ tshark -o tls.keys_list:,,,ssl-cert-snakeoil.key -r dump.pcap -Y tls
   20   1.660863    127.0.0.1 → 127.0.0.1    TLSv1 585 Client Hello
   22   1.676658    127.0.0.1 → 127.0.0.1    TLSv1.3 1381 Server Hello, Change Cipher Spec, ...
   24   1.677228    127.0.0.1 → 127.0.0.1    TLSv1.3 148 Change Cipher Spec, Application Data
   26   1.677406    127.0.0.1 → 127.0.0.1    TLSv1.3 355 Application Data
   27   1.677415    127.0.0.1 → 127.0.0.1    TLSv1.3 163 Application Data
   30   1.677504    127.0.0.1 → 127.0.0.1    TLSv1.3 355 Application Data
   32   1.680826    127.0.0.1 → 127.0.0.1    TLSv1.3 375 Application Data
   34   1.681131    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Application Data
   36   1.681262    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Application Data

In order to decrypt this traffic we will have to log the ephemeral keys on either the client or the server side.
More and more technologies adopt the SSLKEYLOGFILE environment variable as a way to specify a log file where the ephemeral keys can be logged. These logs can then be passed to tshark to decrypt the traffic, similarly to how we used the servers private key before. Note that the ephemeral keys are not logged at all if this environment variable is not set.

In order to inspect traffic the SSLKEYLOGFILE environment variable must be set on the server or client side while the traffic is generated. With this log being generated, capturing and decrypting traffic works very similarly to how it worked before.

Let us first run an example where we set the SSLKEYLOGFILE environment variable on the client side with curl.
Make sure you use curl 7.58 or newer for this to work.

$ SSLKEYLOGFILE=sslkeylogfile.log curl -v -k https://localhost
* Trying 127.0.0.1:443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=f31d50e8d088
* start date: Dec 2 10:39:24 2021 GMT
* expire date: Nov 30 10:39:24 2031 GMT
* issuer: CN=f31d50e8d088
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 14:17:04 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
<
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact

With the key log written by curl, we can decrypt the traffic again. Note that the server’s private key is not required anymore, the key log file is all we need.

# -r dump.pcap - Read packet data from infile
# -Y http - Cause the specified filter to be applied before printing a decoded form of packets
# -o tls.keylog_file:sslkeylogfile.log - override values from the preferences files
$ tshark -o tls.keylog_file:sslkeylogfile.log -r dump.pcap -Y tls
    4   0.007706    127.0.0.1 → 127.0.0.1    TLSv1 585 Client Hello
    6   0.009024    127.0.0.1 → 127.0.0.1    TLSv1.3 1381 Server Hello, Change Cipher Spec, ...
    8   0.009576    127.0.0.1 → 127.0.0.1    TLSv1.3 148 Change Cipher Spec, Finished
   10   0.009790    127.0.0.1 → 127.0.0.1    HTTP 163 GET / HTTP/1.1 
   11   0.009796    127.0.0.1 → 127.0.0.1    TLSv1.3 355 New Session Ticket
   14   0.009941    127.0.0.1 → 127.0.0.1    TLSv1.3 355 New Session Ticket
   16   0.010132    127.0.0.1 → 127.0.0.1    HTTP 375 HTTP/1.1 200 OK  (text/html)
   18   0.010355    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Alert (Level: Warning, Description: Close Notify)
   20   0.010458    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Alert (Level: Warning, Description: Close Notify)

Similarly, we can set the SSLKEYLOGFILE environment variable on the server side.
For this to work, Apache httpd 2.4.49 or later is required.

We restart the server container and pass the SSLKEYLOGFILE environment variable.

docker run -it --rm --net="host" --name simple-apache-httpd -e SSLKEYLOGFILE="/usr/local/apache2/logs/sslkeylogfile.log" simple-apache-httpd

The following log entry signals that the key log file will be written:

[Tue Dec 21 20:03:20.441733 2021] [ssl:notice] [pid 1:tid 139780428569920] AH10227: Init: Logging SSL private key material to /usr/local/apache2/logs/sslkeylogfile.log

We are ready to repeat the request:

$ curl -v -k https://localhost
* Trying 127.0.0.1:443...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=f31d50e8d088
* start date: Dec 2 10:39:24 2021 GMT
* expire date: Nov 30 10:39:24 2031 GMT
* issuer: CN=f31d50e8d088
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.68.0
> Accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 02 Dec 2021 14:23:13 GMT
< Server: Apache/2.4.51 (Unix) OpenSSL/1.1.1k
< Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
< ETag: "2d-432a5e4a73a80"
< Accept-Ranges: bytes
< Content-Length: 45
< Content-Type: text/html
<
<html><body><h1>It works!</h1></body></html>
* Connection #0 to host localhost left intact

After we performed and captured the request, we need to get the log file from inside the container to use it with tshark.

docker exec -it simple-apache-httpd cat /usr/local/apache2/logs/sslkeylogfile.log > sslkeylogfile.log

Now we can decrypt the traffic as before by again passing the key log file to tshark:

$ tshark -o tls.keylog_file:sslkeylogfile.log -r dump.pcap -Y tls
    6   0.033156    127.0.0.1 → 127.0.0.1    TLSv1 585 Client Hello
    8   0.035385    127.0.0.1 → 127.0.0.1    TLSv1.3 1381 Server Hello, Change Cipher Spec, ...
   10   0.035890    127.0.0.1 → 127.0.0.1    TLSv1.3 148 Change Cipher Spec, Finished
   12   0.036026    127.0.0.1 → 127.0.0.1    HTTP 163 GET / HTTP/1.1 
   14   0.036078    127.0.0.1 → 127.0.0.1    TLSv1.3 355 New Session Ticket
   16   0.036194    127.0.0.1 → 127.0.0.1    TLSv1.3 355 New Session Ticket
   18   0.036511    127.0.0.1 → 127.0.0.1    HTTP 375 HTTP/1.1 200 OK  (text/html)
   20   0.036814    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Alert (Level: Warning, Description: Close Notify)
   22   0.036932    127.0.0.1 → 127.0.0.1    TLSv1.3 92 Alert (Level: Warning, Description: Close Notify)

This approach, using the SSLKEYLOGFILE environment variable works with some browsers as well. However, it might be disabled through a compile time variable because of the risk it poses. If an attacker manages to set this environment variable for either the client or server and has access to the key log, the attacker could easily break TLS encryption as shown above.

For this reason it might be a good idea to monitor the logs for the entry indicating the use of the SSLKEYLOGFILE environment variable as shown above or even add a separate log, where the SSLKEYLOGFILE variable is logged if present. This allows you to determine after the fact which requests were affected, if the environment variable was set.

Further, it might be a good idea to monitor the environment variables of all processes to determine if this variable is set anywhere. An easy way to do this manually is the following command. It shows that the SSLKEYLOGFILE environment variable is in use and where the key log file is written:

$ sudo cat /proc/*/environ | tr '\0' '\n' | grep SSLKEYLOGFILE
SSLKEYLOGFILE=/usr/local/apache2/logs/sslkeylogfile.log
SSLKEYLOGFILE=/usr/local/apache2/logs/sslkeylogfile.log
SSLKEYLOGFILE=/usr/local/apache2/logs/sslkeylogfile.log
SSLKEYLOGFILE=/usr/local/apache2/logs/sslkeylogfile.log

To summarize, we see that the introduction of the SSLKEYLOGFILE environment variable can be of great benefit when debugging TLS encrypted connections. However, it also introduces a new attack vector to break TLS encryption that needs to be addressed by disabling or at least by monitoring its use.