Apache: dynamic vhosts with fallback

It has been almost five years since I wrote an answer to a serverfault question about dynamic vhosting with fallback using Apache. I repost it here on my very own blog, for historic reasons.

RemyNL answered:

I also came across this question googling for apache2 dynamic vhost fallback and Luc’s answer helped me a lot with solving my problem, but I still want to show what I did to achieve my goals, mainly because it involved some extra works and because I think it could be helpful for any future googlers…

My goals

  • dynamic vhosting for all domains and subdomains pointing at my VPS
  • foo.com should serve the same content as www.foo.com
  • fallback for unknown domains to some sort of default
  • fallback for unknown subdomains of foo.com to www.foo.com unless the www is not available, fallback to default instead

DNS

I have a couple of domains (and all their subdomains) pointing at my VPS, for example:

  • foo.com
  • bar.com
  • foobar.com

Filesystem

I have the following directories, domains contain directories with the names of the available subdomains, the www directory is required, but the config should be able to deal with the situation where it is not present. Localhost is used as default fallback:

/var
  /www
    /localhost
    /foo.com
       /www
       /bar
    /bar.com
       /foo

Tests

Translating my goals into testable cases:

  • foo.com should be served from foo.com/www
  • www.foo.com should be served from foo.com/www
  • bar.foo.com should be served from foo.com/bar
  • foo.foo.com should be served from foo.com/www (foo.com/foo does not exist)
  • bar.com should be served from localhost (bar.com/www does not exist)
  • www.bar.com should be served from localhost (bar.com/www does not exist)
  • foo.bar.com should be served from bar.com/foo
  • bar.bar.com should be served from localhost (bar.com/bar does not exist)
  • foobar.com should be served from localhost (foobar.com does not exist)
  • www.foobar.com should be served from localhost (foobar.com does not exist)
  • foo.foobar.com should be served from localhost (foobar.com does not exist)

The Solution

This uses: mod_rewrite, mod_proxy_http and ofcourse mod_vhost_alias.

ServerName my.domain
ServerAdmin admin@my.domain

<VirtualHost *:80>
    ServerName localhost
    VirtualDocumentRoot /var/www/localhost
</VirtualHost>

<VirtualHost *:80>
    ServerName sub.domain
    ServerAlias *.*.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3 !-d
    RewriteRule (.*) http://localhost/$1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteCond /var/www/%2.%3/www !-d
    RewriteRule (.*) http://localhost/$1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteRule (.*) http://%2.%3/$1 [P]
</VirtualHost>

<VirtualHost *:80>
    ServerName bare.domain
    ServerAlias *.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/www

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%1.%2 !-d [OR]
    RewriteCond /var/www/%1.%2/www !-d
    RewriteRule (.*) http://localhost/$1 [P]
</VirtualHost>

How does this work? There are three virtual hosts defined:

localhost

The localhost serves as a default. All requests that are not resolvable are served by localhost. Setting up a symlink from localhost to any of your domains is like setting up that site as a default.

sub.domain

The sub.domain vhost is taking all requests in the form of *.*.*. By default all requests are served from /domain.com/sub as defined by VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3.

fallback:

The first RewriteRule takes care of unknown domains, eg. domain.com directory does not exist, by proxying the localhost website.

The second RewriteRule also proxies to localhost when both the domain.com/sub and the domain.com/www directories are not present.

The third RewriteRule proxies to domain.com when domain.com/sub does not exist. We know domain.com/www does exist because of the second rewrite block.

bare.domain

The bare.domain vhost is taking the *.*requests and serves them /domain.com/www

Here the RewriteRule will proxy to localhost when domain.com or domain.com/www do not exist.

^$%.*!!!

I had some trouble wrapping my head around all those $ and % signs in the RewriteCond and RewriteRule so I will explain about them here:

    ServerAlias *.*.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3
    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteRule (.*) http://%2.%3/$1 [P]
  • The * in the ServerAlias are just wildcards.
  • The %n in the VirtualDocumentRoot are from the document name interpolation.
  • The %n in the second RewriteCond refer to the selections (.*) from the first RewriteCond, eg. the parts of the requested domain.
  • The %n in the RewriteRule do too.
  • The $1 in the RewriteRule refers to the selection (.*) at the beginning of the RewriteRule. Which captures everything from the domain till the ? in the request url. Any querystring is automatically added to the url by mod_proxy.

Random parts of command line foo that make your life really easy but I have to google everytime because I am still a *nix n00b

Although I worked with Ubuntu for quite some years I still have to google some of the simpliest task, just because I hardly ever need them. The most simple examples being how to add a user to a group, or see how much space I have left on my disks…

Instead of googling everything again and again, I am going to collect them in this post:

Users

Add a user to a group:

# usermod -aG sudo remy

Diskspace

See how much space the subdirectories of your pwd are taking up:

$ du -sk *

Or when you want to look one level deeper, which comes in handy when I want to see which domains and subdomains I have in my /var/www/vhosts directory:

$ du -kd 1 *

Prompt

# cool colored version:
coolcolor='\033[33m' # with the 2nd 33 being the colorcode [30-37]
normal='\033[0m'
export PS1="\u@\[$coolcolor\]\h\[$normal\]:\w \[$coolcolor\]$\[$normal\] "

OpenSSL

Generate new private key and certificate signing request:

$ openssl req -out server.csr -new -newkey rsa:4096 -nodes -keyout private.key

Generate csr for existing private key:

$ openssl req -out server.csr -key private.key -new

Check a certificate:

$ openssl x509 -noout -text -in certificate.crt

Get the fingerprint of a certificate, possible flags are -sha1 -sha256 or -md5.

$ openssl x509 -noout -fingerprint -sha1 -in certificate.crt

Remember: transporting .csr of .crt can be done in the clear without any fear, they are useless without the corresponding private.key

Generate Diffie-Hellman parameters: (takes a long time)

$ openssl dhparam -out dhparam.pem 4096

Check an SSL connection:

$ openssl s_client -connect www.example.com:443

Create a pfx or pkcs#12 file that contains both private key and certificate and is password protected:

$ openssl pkcs12 -export -out key_and_cert.pfx -inkey privkey.pem -in fullchain.pem

More useful stuff on openssl-commands

SSH keys

Generate Ed25519 key pair:

$ ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519 -C "foo@bar.com"

Although I prefer ed25519, you might want old fashion rsa keys:

$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -C "foo@bar.com"

To add your key(s) to your server, use this nifty little tool:

$ ssh-copy-id user@host

Or when you just want to add one specific key:

$ ssh-copy-id -i ~/.ssh/id_rsa user@host

Add one file to the other

Okay, this is like basic stuff, but getting it wrong can be painful, so to append file1 to file2:

$ cat file1 >> file2

Compressing and decompressing files

Creating a simple zip from a directory:

$ zip -r filename.zip /path/to/directory

Unzip it with:

$ unzip filename.zip

Or you want to password protect it?

$ zip -e filename.zip /path/to/directory

Since the encryption used by zip is weak, use 7z instead:

$ 7z a -p filename.7z /path/to/directory

Lots of archive on the net will be .tar.gz which you extract with:

$ tar -xf archive.tar.gz

Reboot server tonight

Sometimes I want to reboot a machine but keep the impact from the downtime as low as possible, so I schedule the reboot at 06:00 in the morning, hoping the number of users currently using any of its services is close to 0 and in the case something went wrong, I can fix it when at the office at 09:00 (This does indeed imply that we are okay with the 3 hours downtime)

$ sudo at 06:00
warning: commands will be executed using /bin/sh
at> /sbin reboot
[ctrl-d]

You can check the queue of scheduled jobs with:

$ sudo atq
1	Tue Jan 14 10:00:00 2020 a root

And remove a job with:

$ sudo atrm 1

Send a test mail

$ mail -s "TEST!" remy.blom@hku.nl < /dev/null