Bug 63925

Summary: Wrong "cert does not match for name"
Product: Apache httpd-2 Reporter: Idar Lund <idarlund>
Component: mod_sslAssignee: Apache HTTPD Bugs Mailing List <bugs>
Status: NEW ---    
Severity: normal CC: idarlund
Priority: P2    
Version: 2.4.6   
Target Milestone: ---   
Hardware: All   
OS: Linux   
Attachments: error log file

Description Idar Lund 2019-11-14 15:24:30 UTC
Created attachment 36884 [details]
error log file

Setup:
Client --https(443)-- Apache on server1 --https(8443)-- Backend web-server
The attached log file is from "server1".

Vhost config on server1:
<IfModule mod_ssl.c>
<VirtualHost *:443>
	DocumentRoot "/var/www/html/wrong/"
	ServerName server1.tld1
	SSLCertificateFile /etc/letsencrypt/live/server1.tld1/cert.pem
	SSLCertificateKeyFile /etc/letsencrypt/live/server1.tld1/privkey.pem
	Include /etc/letsencrypt/options-ssl-apache.conf
	SSLCertificateChainFile /etc/letsencrypt/live/server1.tld1/chain.pem

	LogLevel debug
	SSLProxyEngine On
	ProxyPreserveHost On
	ProxyPass / https://server2.tld2:8443/
	ProxyPassReverse / https://server2.tld2:8443/
</VirtualHost>
</IfModule>

mod_ssl is using the http header field "Host:" to check the certificate on a remote server. Using "ProxyPreserveHost On" in mod_proxy when also using mod_ssl to communicate with a backend server is unfortunate because certificates cannot be validated:

[Thu Nov 14 09:14:08.348553 2019] [ssl:debug] [pid 15010] ssl_util_ssl.c(495): AH02412: [server1.tld1:443] Cert does not match for name 'server1.tld1' [subject: CN=server2.tld2 / issuer: CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US / serial: 123456789 / notbefore: Nov  2 22:22:22 2019 GMT / notafter: Feb  2 22:22:22 2020 GMT]

This then throws the follwing in error to the client visiting the web site:
Proxy Error
The proxy server could not handle the request GET /.
Reason: Error during SSL Handshake with remote server

I suggest that this bug is being fixed by adding a configuration variable to mod_ssl so that mod_ssl is using what's in "ProxyPass*" to check that a cert provided from the backend server is valid or not.

Workaround is to use an other http header, ie "Via:", to tell the backend server what website to show.

I have attached log file and config file from server1.

To understand the attached log file and config file. Here's it's parameters:
11.11.11.11 = my client
server1.tld1 = apache server (the one that this log is from) this is the frontend server and is handeling requests from internet (the reverse proxy)
22.22.22.22 and server2.tld2 = the backend server which as several virtual hosts and hence needs "Host:" to be set correctly
abcdefghi = let's encrypt cert serial (no actual need to keep this hidden, but i did it anyway)
123456789 = https certificat on my remote server (server2.tld2)
Comment 1 Yann Ylavic 2019-11-14 17:58:38 UTC
(In reply to Idar Lund from comment #0)
> <VirtualHost *:443>
> 	ServerName server1.tld1
[snip]
> 	SSLProxyEngine On
> 	ProxyPreserveHost On
> 	ProxyPass / https://server2.tld2:8443/
> 	ProxyPassReverse / https://server2.tld2:8443/
> </VirtualHost>
> 
> mod_ssl is using the http header field "Host:" to check the certificate on a
> remote server.

mod_ssl is indeed using the "Host:" which is sent to the backend server to validate that the certificate given by that backend corresponds. This is the right think to do.

What happens in your case is that with "ProxyPreserveHost on" this "Host:" is "server1" (the one from the client/browser), so it fails to match the returned "server2" certificate. But why use ProxyPreserveHost in the first place if the backend really is "server2"?

I'd suggest to leave ProxyPreserveHost alone (i.e. default "off"), so that the "Host:" header is taken from the ProxyPass, or set "SSLProxyCheckPeerName off" if you don't want to verify the backend's CN (it can't match in your case).
Comment 2 Idar Lund 2019-11-14 18:23:39 UTC
(In reply to Yann Ylavic from comment #1)
> mod_ssl is indeed using the "Host:" which is sent to the backend server to
> validate that the certificate given by that backend corresponds. This is the
> right think to do.
> 
> I'd suggest to leave ProxyPreserveHost alone (i.e. default "off"), so that
> the "Host:" header is taken from the ProxyPass, or set
> "SSLProxyCheckPeerName off" if you don't want to verify the backend's CN (it
> can't match in your case).

I totally agree that this should be default behaviour, but in this case the backend server is serving several sites and needs a way to determine what site (or vhost for that matter) to serve the query. The standardized way to do that is to use the "Host:" HTTP header field.

If I turn "ProxyPreserveHost" off, then the backend server has no idea on what site it's supposed to serve. This is why I also mentioned the workaround with the "Via:" HTTP header setting.

Also; disabling the CN checking is not an option as this opens up for man in the middle attacks.

This is also why I'm suggesting that it should be configurable what mod_ssl is using to check the name.
Comment 3 Yann Ylavic 2019-11-14 18:36:51 UTC
(In reply to Idar Lund from comment #2)
> If I turn "ProxyPreserveHost" off, then the backend server has no idea on
> what site it's supposed to serve.

Why that? If ProxyPreserveHost is off then the "Host;" header will be the one of the ProxyPass, which is what you seem to want.
Comment 4 Idar Lund 2019-11-14 19:33:25 UTC
(In reply to Yann Ylavic from comment #3)
> (In reply to Idar Lund from comment #2)
> > If I turn "ProxyPreserveHost" off, then the backend server has no idea on
> > what site it's supposed to serve.
> 
> Why that? If ProxyPreserveHost is off then the "Host;" header will be the
> one of the ProxyPass, which is what you seem to want.

Because the backend server needs to know what site the _client_ was asking for. The http header to server1 is correct, but then when this is forwarded to server2 the host variable in the header has to be intact. If not, server2 has noe idea on what site (vhost) it's supposed to serve.
Comment 5 Yann Ylavic 2019-11-14 20:27:49 UTC
So I suppose your backend has a configuration like this:

<VirtualHost *:8443>
	ServerName server1
	SSLCertificateFile /path/to/server2.pem
	...
</VirtualHost>

and thus its certificate's CN ("server2") does not match its ServerName ("server1")?

My point is that if you want the proxy to validate the backend's CN, ServerName and SSLCertificateFile need to be consistent on the backend (usually with ProxyPreserveHost the same certificate is used on the proxy and the backend).
Otherwise, the proxy cannot accept that the returned certificate does not match the Host header _it_ requested.
Comment 6 Idar Lund 2019-11-14 20:36:20 UTC
(In reply to Yann Ylavic from comment #5)
> So I suppose your backend has a configuration like this:
> 
> <VirtualHost *:8443>
> 	ServerName server1
> 	SSLCertificateFile /path/to/server2.pem
> 	...
> </VirtualHost>
> 
> and thus its certificate's CN ("server2") does not match its ServerName
> ("server1")?

Something similar, yes.


> My point is that if you want the proxy to validate the backend's CN,
> ServerName and SSLCertificateFile need to be consistent on the backend
> (usually with ProxyPreserveHost the same certificate is used on the proxy
> and the backend).
> Otherwise, the proxy cannot accept that the returned certificate does not
> match the Host header _it_ requested.

We do share the same point - mod_ssl needs to know what name it should check against and today this is done with the "Host:" header variable. But I disagree that I should have the same certificate on both servers. Then I'd use the "ProxyVia On" directive instead and filter on "Via:" on server2.

The best way to fix this is to make it configurable. So that mod_ssl either can take a CN variable name to check against, or a directive to tell mod_ssl to use whatever is in ProxyPass/ProxyPassReverse.