Apache and the Default VHOST Problem
This article describes configuration issues caused by Apache mischievously picking a default VHOST without telling the user.
By. Jacob
Edited: 2024-03-12 11:32
There is one problem with the Apache HTTP Server that has been annoying me for a very long time, that is the behavior of serving a Default Virtual Host when a requested host is not recognized. The behavior is both non-intuitive and contrary to what people expect to happen.
When Apache, for whatever reason, does not recognize a requested host, it will instead serve up the "default" virtual host in the configuration. As far as I know, it is not possible to disable this behavior, and we are instead advised to keep a 000-default.conf VHOST file to handle requests for unknown domains. We will need one for both HTTPS and HTTP traffic though, and the exact configuration for HTTPS is more like a hack, because you need to intentionally serve up an invalid certificate to have it catch HTTPS traffic properly.
If you do not do this, the default host is picked automatically by Apache, which can lead to unintentional and unpredictable siturations. In fact, if you use a CloudFlare as a CDN proxy in front of your server, then you might even create a situration where the wrong virtual host is served with a valid certificate!
The host that is picked by Apache is always the first VHOST loaded. Apache loads the VHOST files beginning with numerical order, then alphabetic order. So, if we want to be sure that the host we want as default is actually also picked, then we must name the default configuration file beginning with a zero. Hence, something like "0-default.conf" should work; you can then name the rest of your .conf files as you normally would.
The problem typically occurs when you just installed Apache and started configuring your VHOSTs, or when you have made configuration changes, such as adding or removing a domain name.
Force Apache to only Respond to Known V-Hosts
Configuring Apache to only respond to known v-host entities is an good way to prevent certain virtual host probing that could, in some cases, be an open door for privacy issues. E.g. Perhaps you have multiple network interfaces configured (Multiple WAN IP addresses that hosts different websites), and you do not want to reveal that the websites are hosted on the same server.
Or, maybe you are simply a perfectionist that value concise configurations with little room for ambiguity. Obviously, a web server should not be responding to HTTP requests for unknown hosts. Some administrators may be of the opinion that it should, if nothing else to tell users that the requested host is unknown. E.g. throw them a 400 error explaining what happened. That's no problem. You can do that by serving up a "void" directory and even have your own professional design on your error pages.
I highly recommend an IP-based virtual host configuration for the sake of conciseness. This means, no room for ambiguous configurations like *:80 and _default_:80 – this is an advantage if you later decide to connect another WAN IP address to your server.
Now, Apache, when first installed in Ubuntu, has a nasty default configuration that will serve up the first loaded v-host configuration, even for unknown host names that are obviously not handled by your server. This, is, in my opinion a serious design-flaw, but probably done on purpose for some technical reason.
It gets even more confusing for HTTPS; if a configuration does not exist for a given website, the same thing will happen for HTTPS! Typically it will result in a certificate error and a wrong site being served if the user ignores the invalid certificate – but this will not necessarily happen! E.g. If you are using Cloudflare, you could actually have a situation where non-configured HTTPS will instead serve up a different website (virtual host) than what a client requested – and with a valid certificate!!
This is so stupid. And dangerous! Someoe should fix this horrible mess!
Having said that, let me give you the best solution I found to this problem.
Explicit whitelisting host names
As far as I know, Apache has no official or straight-forward, foolproof, way of creating a host white list, or telling Apache which domain names you intend to serve from it. E.g. I would personally prefer a simple list of white-listed domains, and a way to direct all unknown hosts to a specific "unknown" directory to give a 400 error. But, there actually is a way to do what I want, although it's more like a hack.
The solution is surprisingly simple. You need only create two files in your sites-available folder. E.g:
- 000-catch-all.conf
- 000-catch-all-443.conf
Apache will load these files before all other files, because of the number in the file name. E.g. First it will load files in numeric order, and then alphabetic. By having a file beginning with "0", we tell Apache to load that before all other files.
The 000-catch-all.conf file should have the following content:
<VirtualHost 127.0.1.1:80>
DocumentRoot "/var/www/void"
<Location />
Require all granted
Options None
</Location>
</VirtualHost>
This will handle all HTTP traffic on the standard port, 80, and serve up the "void" directory; I personally have a custom index.php throw a 400 error, explaining that the host is not handled by the server, but you could also serve a default Apache ErrorDocument if you wanted.
A ServerName is not needed for plain HTTP traffic, but it is for HTTPS because the it is compared with the name in the certificate.
Note. The solution for HTTPS requires SNI (Server Name Indication) to work, because the browser must send the server-name as part of the initial SSL handshake in order for Apache to know if the Server is handled, and if not, serve our default v-host.
For HTTPS traffic, create a file like 000-catch-all-443.conf with the following content:
<IfModule mod_ssl.c>
<VirtualHost 127.0.1.1:443>
DocumentRoot "/var/www/void"
ServerName catch-all
<Location />
Require all granted
Options None
ErrorDocument 403 Forbidden.
</Location>
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
Note. ServerName catch-all has no special meaning, but it is required for the HTTPS configuration to work. Then, you might even provide a standard "snake oil" certificate that does not even match the requested host. The fact the configuration is invalid is intentional; what you care about is not serving the wrong site unintentionally!
I found you will also need to provide a certificate (E.g. even a dummy Snake oil certificate will work). This is needed because otherwise your other HTTPS configurations will become invalid. In addition, you need to be careful which certificate you use, because it will be served if someone requests an invalid host name. If you reuse another, valid, certificate, then that could actually be a privacy issue, as the certificate can reveal the fact that your server is hosting the website that the certificate belongs to!
The certificate does not needs to be a valid one. E.g. You could give it a default "snake oil" certificate from /etc/ssl/, and the configuration should still work for our purpose. And yes, this will give a warning in the users browser, and that is actually what we want to happen – we just don't want to serve an unintended virtual host!
You can use the snake oil certificates like this:
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
The ServerName is required here as well, but again, this does not need to be a valid host name, because all we care about is preventing virtual host probing and not serving up unintended v-hosts.
If, then, the user choses to ignore the error in their browser and proceed "insecurely" to view the served host, then they will only see our 400 error message.
Avoid wildcard (*) in your 000-default.conf
Many examples you find on the internet will either use the domain name, or wildcards (*) in their VirtualHost blocks, but this will not work with a default VHOST.
The problem is that when you have a default configuration, such as a 000-default.conf file that uses the following;
<VirtualHost *:80>
# ...
It appears to "override" all other VirtualHost blocks. That is a problem, because we only want it to apply when the name request header does not match any of the VHOSTs that we defined in other VHOST files. This problem has often happened to me on fresh installs of ubuntu+apache.
Note. It is possible to define VHOSTs in the same .conf file, but in Ubuntu we typically keep them in separate files in /etc/apache2/sites-available/.
To enable and disable VHOSTs, we can use the a2ensite [site-name] and a2dissite [site-name] commands — this will create a symbolic link in /etc/apache2/sites-enabled/; we can also do this manually. Regardless, we still need to reload or restart Apache afterwards.
To solve the problem, we should instead specify the local IP address of the server; to determine what the local IP address is, we can use the following command:
ip addr show
Result:
ink/ether 0a:12:f5:df:1b:26 brd ff:ff:ff:ff:ff:ff
inet 172.31.1.1/20 brd 172.31.15.255 scope global dynamic eth0
valid_lft 3110sec preferred_lft 3110sec
inet 172.31.15.25/20 scope global secondary eth0
valid_lft forever preferred_lft forever
inet6 fe30::815:f2ff:fedd:1b26/64 scope link
valid_lft forever preferred_lft forever
The local IP address is one of theinet addresses. I.e. 172.31.15.25; we can use this in our 000-default.conf file:
<VirtualHost 172.31.1.1:80>
# ...
If your server got multiple WAN IP addresses (and thereby multiple network interfaces), and you want the default configuration to apply for all of them, simply include them, separated by space:
<VirtualHost 172.31.15.25:80 172.31.1.1:80>
# ...
Wildcards can be unpredictable, so I recommend using IP-based virtual hosting — and by "IP-based" is really just meant that you should configure the VHOSTs to listen on a specific network interface. Unless you got multiple WAN IPs configured, there is probably only one interface. What's weird is, perhaps dependent on the hosting infrastructure, sometimes you might need to use the actual WAN IP in the VirtualHost block rather than the internal LAN IP of the interface – I had that happen on Hetzner, and that's while on AWS EC2 I had to use the LAN IP.
Also, Apache's official documentation mentions you have to use one IP per host, but that's not true; even when using IP-based hosting, you can in fact have multiple hosts on same IP. Or, perhaps, this setup is more of a hybrid of IP-based and name-based, since we use both? Regardless, it can work just fine.
Picking a default Virtual Host
Apache's way of defining a default VHOST is horrible; I would much prefer a way to explicitly define a default host, or simply have Apache reject the HTTP request while returning a 403 forbidden or 400 Bad Request.
I have even had cases where I did actually have a VirtualHost block that matched the requested host, but the request would still be passed on to the "default host", and I would be fumbling in the dark as to why Apache was serving up the wrong website.
In my case, it turned out I had more than one VHOST configuration file that contained the same ServerName value. This apparently causes Apache to instead serve up the default host for both domains.
When you got domains that share the same directory—probably because they are handled by the same code base—then this problem tends to become less apparent. If one domain uses a different DirectoryIndex, then you might notice that the same index file is being used to handle requests for all of the domains.
Note. Unless you got reason to serve a random default host, I suggest you create a 000-default.conf file and explicitly define what is to happen when a unknown domain is requested.
For example, you could create a /var/www/default directory that you link in your 000-default.conf file, which is then served when Apache gets confused.
Safely adding new Virtual Hosts
Another thing you need to be aware of is when adding a new domain (Virtual Host) to your Apache server. It is tempting to simply copy one of the existing files and replace the relevant directives; the problem with doing this, however, is that you often overlook something, and then you might not only end up with a VHOST that is incorrectly configured, you might also break the VHOST that you based your new configuration on.
For example, I have myself, quite a few times, made the above mistake, even thinking I had memorized everything I needed to replace, only to find out I forgot to change the ServerName directive, resulting in all kinds of strange behaviour.
Instead, you could create a VHOST template file, and then copy this whenever you need to setup a new domain name.
Then you might ask, who is manually setting up new domain names anyway? Hosting companies will handle this automatically, no doubt? Probably they will. They probably should. I still have not automated it, and I do not think I will do that for my own personal use. I rarely need to add new domains, and when I do, it is usually because I want to create a local domain on my laptop to test my code.
Tell us what you think: