Apache Virtual Host Best Practices (Multi-Tenant Redirect Server)
The default-vhost trap
- Apache loads files in
sites-enabled/alphabetically. - For any given
IP:port, the first vhost Apache reads becomes the implicit default — served whenever a request'sHostheader (HTTP) or SNI (HTTPS) doesn't match any configuredServerName/ServerAlias. - On a multi-tenant server, this means an unmapped or half-configured domain silently gets served by whichever client's vhost happens to sort first — not an error page.
Standards to follow
- Always have an explicit catch-all vhost, forced to load first via a
numeric filename prefix (
000-catchall.conf), that denies all unmatched requests. See Apache Config Templates. - Never hand-copy another live client's vhost to create a new one —
cert paths and
ServerName/redirect targets get missed. Use a clean template instead (no cert paths hardcoded). - Let certbot write the
:443block. Don't write SSL vhosts by hand —certbot --apachegenerates correct cert paths automatically, removing an entire class of copy-paste error. - Prefix vhost filenames with a number (
010-,020-, ...) so load order is deliberate, not an accident of alphabetical client names. - Enable
SSLStrictSNIVHostCheck Onon the catch-all's:443block to guard against SNI/Host header mismatches being served by the wrong cert. - Verify after every change with
apache2ctl -S— confirms which vhost is the default for eachIP:port. - Test with real SNI, not just the
Hostheader —curl --resolve domain:443:<server-ip> https://domain/sends the domain as SNI while still hitting the target server, matching real browser behavior. Testing with-H "Host: domain"against a raw IP triggers a421 Misdirected Requestand is not a valid test of vhost routing.
Quick health check (run after any vhost change)
for d in <list-of-domains>; do
echo -n "$d -> "
curl -sk -o /dev/null -w "%{http_code} " --resolve "$d:443:127.0.0.1" "https://$d/"
curl -sk -I --resolve "$d:443:127.0.0.1" "https://$d/" | grep -i "^location" || echo "(no redirect header)"
done
Expect: 301 + correct Location: header for every real domain, and 403
for anything unmapped.