qasim@wiki:~$ ▍

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's Host header (HTTP) or SNI (HTTPS) doesn't match any configured ServerName/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

  1. 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.
  2. 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).
  3. Let certbot write the :443 block. Don't write SSL vhosts by hand — certbot --apache generates correct cert paths automatically, removing an entire class of copy-paste error.
  4. Prefix vhost filenames with a number (010-, 020-, ...) so load order is deliberate, not an accident of alphabetical client names.
  5. Enable SSLStrictSNIVHostCheck On on the catch-all's :443 block to guard against SNI/Host header mismatches being served by the wrong cert.
  6. Verify after every change with apache2ctl -S — confirms which vhost is the default for each IP:port.
  7. Test with real SNI, not just the Host header — 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 a 421 Misdirected Request and 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.

← Back to the DNS series index