Catch-All HTTPS Vhost on Apache 2.4
The problem was solved but there was some misunderstandings. There really is the requirement that HTTPS needs a matching certificate, but the problem caused by this is that the connection won't be trusted with hostname not matching certificates Common Name or listed in Subject Alternative Name:
The same mismatch stays even with the
RewriteRule
solution given in the other answer.If the "catch-all" hostnames are all sub-domains of
example.com
and you have a wildcard certificate for*.example.com
, it will match.On the other hand most people, when trying to access
something.example.com
, types it to browser address bar without thehttp://
orhttps://
prefix, and browsers defaults to HTTP. Therefore having a "catch-all" redirect on HTTPS even with mismatching certificate won't usually cause any actual problems: only a few people ever sees theSSL_ERROR_BAD_CERT_DOMAIN
error.
The Virtual Host Matching works the same way with or without TLS.
If you don't have SNI:
The first name-based vhost in the configuration file for a given
IP:port
pair is significant because it is used for all requests received on that address and port for which no other vhost for thatIP:port
pair has a matchingServerName
orServerAlias
. It is also used for all SSL connections if the server does not support Server Name Indication.
Without SNI the certificate from the first VirtualHost
is used for handshake:
In reality, Apache will allow you to configure name-based SSL virtual hosts, but it will always use the configuration from the first-listed virtual host (on the selected IP address and port) to setup the encryption layer.
The main problem with your original try was having ServerAlias *
and not having any ServerName
. For a "catch-all" host it would have worked with anything but the other ServerName
s from other VirtualHost
s. If no another match, Apache falls back to the default VirtualHost
section; whichever is the first section (that matches IP based lookup, when name-based lookup fails).
Name-based virtual hosts for the best-matching set of
<virtualhost>
s are processed in the order they appear in the configuration. The first matchingServerName
orServerAlias
is used, with no different precedence for wildcards (nor forServerName
vs.ServerAlias
).
There must be SOME ServerName
because:
The
ServerName
directive may appear anywhere within the definition of a server. However, each appearance overrides the previous appearance (within that server).If no
ServerName
is specified, the server attempts to deduce the client visible hostname by first asking the operating system for the system hostname, and if that fails, performing a reverse lookup on an IP address present on the system.
This would result in configuration like this:
<VirtualHost *:443>
# Default catch-all (everything that won't match the following VirtualHosts)
ServerName catch-all.example.com
ServerAlias www.example.com
SSLEngine on
SSLCertificateFile "C:/prod/hosts.crt.pem"
SSLCertificateKeyFile "C:/prod/hosts.key.pem"
SSLCertificateChainFile "C:/prod/intermediate.crt.pem"
Redirect permanent / https://example.com
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile "C:/prod/hosts.crt.pem"
SSLCertificateKeyFile "C:/prod/hosts.key.pem"
SSLCertificateChainFile "C:/prod/intermediate.crt.pem"
Include conf/sites/example.com.conf
</VirtualHost>
<VirtualHost *:443>
ServerName dev.example.com
SSLEngine on
SSLCertificateFile "C:/prod/hosts.crt.pem"
SSLCertificateKeyFile "C:/prod/hosts.key.pem"
SSLCertificateChainFile "C:/prod/intermediate.crt.pem"
Include conf/sites/dev.example.com.conf
</VirtualHost>
Please notice the other things I have changed:
dev.example.com
uses the same certificate as it would do so anyway without SNI.Use
<VirtualHost *:443>
instead of_default_:443
as_default_
has a special purpose:Any vhost that includes the magic
_default_
wildcard is given the same ServerName as the main server.(This also means could use
_default_:443
in your "catch-all", not in the others. You can try!)Domain is replaced with Reserved Example Domain Names.
I'd prefer having
www.example.com
as a part of the "catch-all" (rather than as an alias) in order to have only one canonical address for your site. Therefore I have moved it there.
If you had SNI, the processing mimics the same behavior but is a bit different in details:
Before there is even an SSL handshake, Apache finds the best match for the IP address and TCP port the connection is established on (IP-based virtual hosting)
If there is a
NameVirtualHost
directive that has the same literal arguments as this best-matchingVirtualHost
, Apache will instead consider ALLVirtualHost
entries with identical arguments to the matched VirtualHost. Otherwise, SNI processing has no selection to perform.If the client sends a hostname along with its TLS handshake request, Apache will compare this TLS hostname to the
ServerName
/ServerAlias
of the candidateVirtualHost
set determined in the preceding steps.Whichever VirtualHost is selected on the preceding basis will have its SSL configuration used to continue the handshake. Notably, the contents of the certificates are not used in any comparison.
With SNI you can have the additional certificate for dev.example.com
.
If all the prerequisites for SNI are met, it should work automatically and error.log
would show [warn] Init: Name-based SSL virtual hosts only work for clients with TLS server name indication support (RFC 4366)
.
While it is possible to redirect all unknown HTTPS traffic to a specific virtual host, Apache did not make it easy:
- Each HTTPS VirtualHost needs a
ServerName
, which we don't have for the catch-all host. This is a requirement of HTTPS since certificates are typically associated to hosts (ServerName
orServerAlias
). - Apache will take the first virtual host as the default one when everything else fails. Make sure that you do not have any other configuration with the same port IP configured or your catch-all will fail.
- I had typos in my original config which probably caused some of the redirected loops (I had 2
ServerName
statements in some VirtualHost). I would love to understand a bit more the details here but it's not the focus of the question.
Based on this there are two solutions. I prefer the first since it's probably more scalable (no need to updated exceptions) and also performant (no need to use extra modules).
Catch-all using a fake ServerName (suggested by Esa)
#
# Catch-all virtual hosts.
#
<VirtualHost _default_:80>
# Default catch-all virtual host.
Redirect permanent / https://example-prod.com
</VirtualHost>
<VirtualHost _default_:443>
ServerName catch-all
SSLEngine on
SSLCertificateFile "C:/dev/hosts.crt.pem"
SSLCertificateKeyFile "C:/dev/hosts.key.pem"
SSLCertificateChainFile "C:/dev/intermediate.crt.pem"
Redirect permanent / https://example-prod.com
</VirtualHost>
#
# Real virtual hosts.
#
<VirtualHost _default_:80>
ServerName example-prod.com
ServerAlias www.example-prod.com
Include conf/sites/example-prod.com.conf
</VirtualHost>
<VirtualHost _default_:80>
ServerName example-dev.com
Include conf/sites/example-dev.com.conf
</VirtualHost>
<VirtualHost _default_:443>
ServerName example-prod.com
ServerAlias www.example-prod.com
SSLEngine on
SSLCertificateFile "C:/prod/hosts.crt.pem"
SSLCertificateKeyFile "C:/prod/hosts.key.pem"
SSLCertificateChainFile "C:/prod/intermediate.crt.pem"
Include conf/sites/example-prod.com.conf
</VirtualHost>
<VirtualHost _default_:443>
ServerName example-dev.com
SSLEngine on
SSLCertificateFile "C:/dev/hosts.crt.pem"
SSLCertificateKeyFile "C:/dev/hosts.key.pem"
SSLCertificateChainFile "C:/dev/intermediate.crt.pem"
Include conf/sites/example-dev.com.conf
</VirtualHost>
mod_rewrite (suggested by Alexis)
<VirtualHost _default_:80>
# Default catch-all virtual host.
Redirect permanent / https://example-prod.com
</VirtualHost>
<VirtualHost _default_:80>
ServerName example-prod.com
ServerName www.example-prod.com
Include conf/sites/example-prod.com.conf
</VirtualHost>
<VirtualHost _default_:80>
ServerName example-dev.com
Include conf/sites/example-dev.com.conf
</VirtualHost>
<VirtualHost _default_:443>
ServerName example-prod.com
ServerName www.example-prod.com
SSLEngine on
SSLCertificateFile "C:/prod/hosts.crt.pem"
SSLCertificateKeyFile "C:/prod/hosts.key.pem"
SSLCertificateChainFile "C:/prod/intermediate.crt.pem"
Include conf/sites/example-prod.com.conf
# Default catch-all HTTPS virtual host.
# Make sure to add all valid SSL domains on this host to avoid conflicts.
RewriteEngine on
RewriteCond %{HTTP_HOST} !^example-prod\.com$ [NC]
RewriteCond %{HTTP_HOST} !^www\.example-prod\.com$ [NC]
RewriteCond %{HTTP_HOST} !^example-dev\.com$ [NC]
RewriteRule .* https://example-prod [R=permanent,L]
</VirtualHost>
<VirtualHost _default_:443>
ServerName example-dev.com
SSLEngine on
SSLCertificateFile "C:/dev/hosts.crt.pem"
SSLCertificateKeyFile "C:/dev/hosts.key.pem"
SSLCertificateChainFile "C:/dev/intermediate.crt.pem"
Include conf/sites/example-dev.com.conf
</VirtualHost>
Now why is something so simple so complex? Is Apache showing signs of age? At least there are ways to solve this situation.
HTTPS requires a domain name, which matches the certificate, so *:443
without a corresponding ServerName
does not make sense.
However, you could use a redirect in your other <VirtualHost>
entries, with a RewriteRule
.
RewriteEngine on
RewriteCond %{HTTP_HOST} ^(something-else.example-prod.com|whatever.example-prod.com|...others...)$
RewriteRule ^/(.*) https://www.example-prod.com/$1 [R=permanent,L]
You want a condition (RewriteCond
) which checks that only the given domains get redirect as expected. You should know of all the possible names, although if you dynamically add new domain names, hopefully you can use a regex that matches all of those dynamic sub-domains.