Why does Apache return a 400 "bad request" error when connecting to it with "openssl s_client" even though it sends TLS SNI

Lionel Elie Mamane asked:

I have a website, accessible with HTTP over TLS. It is multi-homed, so there are several A DNS entries for the hostname. Additionally, there are several websites, with several hostnames, on the same set of IP addresses, so this is all setup with a few NameVirtualHost in Apache 2.

Accessing the website with a browser, or a WebDAV client, etc works like a charm.

But when I try to connect with "openssl s_client", I cannot for the life of me get it to work. Apache throws a 400 "Bad Request" error at me. Apache selects the correct, so the SNI must have been set correctly by openssl. This happens as soon as I press "enter" on the "GET" line, even before I can add any header. The HTTP syntax I use (basically "GET / HTTP/1.1\nHost: hostname" works on the non-TLS version of the website (which only redirects to the TLS version), so that should not be the problem. The same HTTP syntax also works to connect to e.g. www.google.com over TLS (port 443), so again it looks correct.

Logs of the Apache server show a request with a trailing newline, maybe that’s a hint. The log entry goes to the one for the default VirtualHost for that IP/port although Apache sent the X.509 certificate for the hostname I connect to, which is different from the default.

I have checked with wireshark that SNI is set in the ClientHello.

Here’s an example session:

INPUT
$ openssl s_client -connect hostname:443

OUTPUT

depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify return:1
depth=0 CN = hostname
verify return:1
---
Certificate chain
 0 s:CN = hostname
   i:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
 1 s:C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
   i:O = Digital Signature Trust Co., CN = DST Root CA X3
---
Server certificate
-----BEGIN CERTIFICATE-----
(...)
-----END CERTIFICATE-----
subject=CN = hostname

issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 3121 bytes and written 388 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: FBFD6172E3C659D4071B68A36BD5983A8571906C00190A745B5DAA1359D4087C
    Session-ID-ctx: 
    Resumption PSK: 21957A3C267DAA25CE2D7F6BFD3C3593CCFA1E6FC45C4DC5D0CE35C29175F7E9BBDD59EE6C213DC53B8291B7BD055238
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - 18 4b 25 ae 49 61 e8 f3-73 b5 bb 3b 50 2f 1d c4   .K%.Ia..s..;P/..
    0010 - 7e 02 e9 41 01 9e ed 8b-a1 b6 ec 48 4d 12 11 f2   ~..A.......HM...

    Start Time: 1602857943
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: 627DF7810556DB2918484F393E66DEA7BF22DD3A42288B47E46FCDCDEF176125
    Session-ID-ctx: 
    Resumption PSK: 851978077C93284CB3D69B541B77844A48D1CFC8800F60B6F7CF3D297911D0D93E0E935017B78777E2BD51A29E13329F
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - c0 1a 60 23 36 80 bf 27-83 0e 1f 26 4b 04 f2 53   ..`#6..'...&K..S
    0010 - a0 8a 2d 6e 41 44 17 c7-9b 11 5d dc 7f 08 71 fc   ..-nAD....]...q.

    Start Time: 1602857943
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK

INPUT (note no blank line after the first one)
GET / HTTP/1.1

OUTPUT

Date: Fri, 16 Oct 2020 14:19:11 GMT
Server: Apache/2.4.38 (Debian)
Content-Length: 322
Connection: close
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at default_hostname Port 443</address>
</body></html>
closed

Note: the error message mentions default_hostname, not hostname!

The corresponding Apache log entry is:
default_hostname:443 1.2.3.4 - - [16/Oct/2020:16:19:11 +0200] "GET / HTTP/1.1\n" 400 3279 "-" "-"

Note that default_hostname IS NOT hostname and that Apache sent the correct server certificate, so it has identified the correct VirtualHost! If I connect without SNI (openssl s_client -connect 4.3.2.1:443) then it sends the server certificate for default_hostname.

My answer:


You did actually send a malformed request. HTTP/1.1 requires the presence of the Host: header, and further with SNI, the content of the Host: header must match the SNI hostname.

Try again and this time provide the correct Host: header.


View the full question and any other answers on Server Fault.

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.