Wednesday, November 14, 2007

Leopard for the Web Developer - Multiple Virtualhosts with SSL in Apache

The goal is simple but lofty -- configure Apache for multiple SSL virtualhosts in Mac OS X Leopard.

In practice, this gets a little complicated. Here are the basic set of steps to take:

  1. Configure domain name resolution for the development host names
  2. Configure distinct IP address aliases for those host names (critical for multiple SSL Virtualhosts)
  3. Create the self-signed SSL certificate(s)
  4. Enable the correct modules and configuration files for Apache.
However, before we begin, lets do a little bit of preparation. The first thing you'll want to do is determine which host names you're going to use. For example, let's say you have 2 development domains -- local.lo-fi.net and dev.lo-fi.net -- each of these will be a different development project, pointing to different resources on the file system. Once you know which host names you'll be configuring your environment for, you need to choose the distinct IP address to give them. For the record, distinct IP addresses aren't important if you don't plan on using SSL. However, if you do need SSL on these domains, a distinct IP address is critical. I don't know all the details, but, from experience I can tell you that an SSL certificate somehow binds to a single IP address. If you're a little lost, don't worry, more details about the SSL stuff follow. The point is, you need to decide which IP addresses you want to use for the host names.

For my purposes, I want my host names to work much like "localhost" works. I just want a host name to point to my local computer. Basically, here's what I want:

host: localhost = ip: 127.0.0.1
host: local.lo-fi.net = ip: 127.0.0.2
host: dev.lo-fi.net = ip: 127.0.0.3

Now that you've made this decision, you can move forward.

Configuring Domain Name Resolution for the Development Host Name

This is the easiest part, by far. Simply open /etc/hosts with your favorite text editor, and add a few lines. The following is what mine will look like.


##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
fe80::1%lo0 localhost

#Development domains
127.0.0.2 local.lo-fi.net
127.0.0.3 dev.lo-fi.net

All of the "localhost" lines should already exist for you. The 2 lines that are relevant to you are the ones showing the desired ip addresses with the development domains. After you save this, if you try pinging "local.lo-fi.net", your computer will try to connect to the ip address "127.0.0.2" However, that IP address doesn't exist yet. That's next.

Configuring IP Aliases with ifconfig and launchd

Since I want to have my host names act like localhost does, I'm going to add aliases to the loopback network interface. You can do this in the terminal by typing:

sudo ifconfig lo0 alias 127.0.0.2 netmask 255.255.255.0
sudo ifconfig lo0 alias 127.0.0.3 netmask 255.255.255.0
Now, when you ping "local.lo-fi.net", your computer tries to contact 127.0.0.2, and you get a result. Great! This is exactly what we want -- well, kind of. When you log out or re-boot, this configuration is lost. What we really want is for this to happen at startup, so we don't have to re-configure ifconfig every time we start our computer.

Making this happen is one of the coolest things in this process -- at least, that's what I think. Before I get to the decided solution, I'll back up just a bit. In previous versions of Mac OS X, you could edit the /etc/iftab file, consisting of lines with the arguments you'd send to ifconfig (eg. lo0 alias 127.0.0.2 netmask 255.255.255.0), and this would get picked up and work when starting up. However, in Leopard iftab is gone. What's a developer to do? One solution I found leveraged Automator, but I figured out something much more elegant.

Launchd to the rescue!

Launchd is a daemon running in OS X that is responsible for lots of process management. It's designed to be a powerful replacement for other types of service management tools like inetd, rc, and even cron. What's neat is that you can create a plist configuration file pointing to the executable file you want to run, put it in a specific place, and the system executes it at the right time. What we're going to do is create a plist file for each of our network aliases. These plist files will actually execute ifconfig with the arguments we need, and do it as the privileged root user without any manual intervention.

We're going to create 2 plist files:

/Library/LaunchDaemons/net.lo-fi.local.ifconfig.plist
/Library/LaunchDaemons/net.lo-fi.dev.ifconfig.plist
OK, here's the content of one of these files (net.lo-fi.local.ifconfig):

<plist version="1.0">
<dict>
<key>Label</key>
<string>net.lo-fi.local.ifconfig</string>
<key>ProgramArguments</key>
<array>
<string>/sbin/ifconfig</string>
<string>lo0</string>
<string>alias</string>
<string>127.0.0.2</string>
<string>netmask</string>
<string>255.255.255.0</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

I won't go into the nitty gritty about launchd plist files, but basically this file does 3 things.
  1. Declares a unique Label name (net.lo-fi.local.ifconfig)
  2. Declares the program to run (ifconfig), as well as the arguments that follow
  3. Tells launchd to run this at load time
Finally, these files need to be owned by root. So, if you haven't already, do this:
sudo chown root:wheel /Library/LaunchDaemons/net.lo-fi.local.ifconfig.plist
sudo chown root:wheel /Library/LaunchDaemons/net.lo-fi.dev.ifconfig.plist
Now, if you re-boot, these files will execute ifconfig with the arguments you need. It's so simple, it's beautiful.

Self Signed SSL Certificates

I have to admit, I followed the instructions for creating a self signed certificate here, with splendid results. If you're interested in the details about how creating a self signed certificate works, it's worth your time to read it. If you don't care, and just want something to work, you probably won't need more than what I show you below. However, here's the high level overview of what I'm about to do.
  1. Create a certificate for our own personal signing authority
  2. Create a certificate request for a domain
  3. Sign the certificate signing request, and generate a signed certificate
  4. Make a copy of the signed certificate that doesn't need a password when apache starts
I did this all in a "ssl" directory I created in /etc/apache2

sudo mkdir /etc/apache2/ssl
cd /etc/apache2/ssl


1. Generate your own Certificate Authority (CA). Make sure to remember the passphrase you're prompted for. This is what you use to sign certificates.

sudo openssl genrsa -des3 -out ca.key 4096
sudo openssl req -new -x509 -days 1825 -key ca.key -out ca.crt

2. Generate a server key and request for signing (csr). When prompted for the Common Name (CN), enter the domain name you want the certificate for. In my case, the Common Name would be "local.lo-fi.net"

sudo openssl genrsa -des3 -out local.lo-fi.net.key 4096
sudo openssl req -new -key local.lo-fi.net.key -out local.lo-fi.net.csr


3. Sign the certificate signing request with the self-created certificate authority that you made earlier

sudo openssl x509 -req -days 1825 -in local.lo-fi.net.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out local.lo-fi.net.cst

4. Make a key which doesn't cause apache to prompt for a password.

sudo openssl rsa -in local.lo-fi.net.key -out local.lo-fi.net.key.insecure
sudo mv local.lo-fi.net.key local.lo-fi.net.key.secure
sudo mv local.lo-fi.net.key.insecure local.lo-fi.net.key


Repeat steps 2 through 4 for each distinct domain you want to create a certificate for. Another thing that you can do, is name wildcards for your common name. For example, if I wanted a certificate that I could use an all subdomains of lo-fi.net, I can enter a Common Name of "*.lo-fi.net"

Just to review, here's what you should have in your directory

$ ls /etc/apache2/ssl
ca.crt
ca.key
dev.lo-fi.net.crt
dev.lo-fi.net.csr
dev.lo-fi.net.key
dev.lo-fi.net.key.secure
local.lo-fi.net.crt
local.lo-fi.net.csr
local.lo-fi.net.key
local.lo-fi.net.key.secure


Now, you have everything you need for setting up the virtualhost for your domains.

Here's what you've all been waiting for! Configuring multiple virtualhosts with SSL.

Here's the high level review of what we need to do:
  1. Configure virtualhosting
  2. Configure mod_ssl
  3. Add virtualhost configurations for our new hosts.
The first step to take is to tell Apache to include some files. The specific files we need to have included are the ones designed for virtualhost and ssl configuration. By default, these are not included. To include them, open /etc/apache2/httpd.conf, and go to the bottom of the file. Around lines 461 amd 473, you'll have the opportunity to un-comment the relevant include lines. Here's what it should look like once you're done.
# Virtual hosts
Include /private/etc/apache2/extra/httpd-vhosts.conf
and
# Secure (SSL/TLS) connections
Include /private/etc/apache2/extra/httpd-ssl.conf

Once that is done, you'll need to edit these files somewhat. httpd-vhosts.conf configures virtualhosts running on port 80, designed to apply name-based virtualhosting. I include it here, because I want to test sites on both port 80 and port 443 (https). Using name-based virtualhosting is actually pretty nice for domains when ssl isn't required. All you have to do is create a new <virtualhost> entry, with a "ServerName [whatever.com]" line, and you have a new virtualhost. However, for this file, I'm just going remove the dummy virtualhosts that are in this file as examples, and set a default. Here is what I like to use.

#
# Use name-based virtual hosting.
#
NameVirtualHost *:80

#
# VirtualHost example:
# Almost any Apache directive may go into a VirtualHost container.
# The first VirtualHost section is used for all requests that do not
# match a ServerName or ServerAlias in any <VirtualHost> block.
#
<VirtualHost _default_:80>
ServerAdmin eric@mac.com
DocumentRoot "/Library/WebServer/Documents"
ServerName localhost
ErrorLog /private/var/log/apache2/error_log
CustomLog /private/var/log/apache2/access_log common
</VirtualHost>

This configuration does 2 things, it names a wildcard virtualhost for port 80, and it defines a virtualhost that will be used for everything other than a name-based virtualhost that we might define later. That does it for the httpd-vhosts.conf file.

Next up is the mod_ssl configuration in the httpd-ssl.conf file. The modification for this should be pretty simple. This file does 2 things. First, it sets up the basic configuration for the ssl module. Second, it contains virtualhost configuration for a default virtualhost for port 443. The easiest way to deal with this file is to comment out the default virtualhost configuration (which starts around line 75). This configuration is designed to give you an ssl virtualhost for any host that isn't matched by another specific virtualhost. If this is something you'd like to do, all you have to do is make sure that you have a valid certificate and a key. Look for these lines (line 99 and 107):
SSLCertificateFile "/private/etc/apache2/server.crt"
and
SSLCertificateKeyFile "/private/etc/apache2/server.key"
If you want to enable this default ssl virtualhost, adjust them as necessary, such that they point to a certificate and key that you have created. OK, now save that file and you're good to go.

The final step is to create some virtualhost files for your domains. I create one per domain, and place them in /etc/apache2/other. Any file with a ".conf" extension is picked up by apache when it starts up. I like to name my files with the domain I'm configuring.
local.lo-fi.net.conf
dev.lo-fi.net.conf


Finally, we can configure the virtualhosts. Here is what mine look like:

local.lo-fi.net.conf


<VirtualHost *:80>
ServerName local.lo-fi.net
DocumentRoot /Users/eric/WebApps/local.lo-fi.net/webroot
ServerAdmin eric@guesswhere.net
<Directory "/Users/eric/WebApps/local.lo-fi.net/webroot">
AllowOverride All
Options
Order allow,deny
Allow from all
</Directory>
</VirtualHost>

#Note the loopback ip address we set up for this host
#local.lo-fi.net = 127.0.0.2
<VirtualHost 127.0.0.2:443>
ServerName local.lo-fi.net
DocumentRoot /Users/eric/WebApps/local.lo-fi.net/webroot
ServerAdmin eric@guesswhere.net
<Directory "/Users/eric/WebApps/local.lo-fi.net/webroot">
AllowOverride All
Options
Order allow,deny
Allow from all
</Directory>

# SSL Configuration
SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP
SSLOptions +FakeBasicAuth +ExportCertData +StdEnvVars +StrictRequire

#Self Signed certificates
SSLCertificateFile /etc/apache2/ssl/local.lo-fi.net.crt
SSLCertificateKeyFile /etc/apache2/ssl/local.lo-fi.net.key
SSLCertificateChainFile /etc/apache2/ssl/ca.crt

#DON'T DO ANY INTENSIVE SSL OPERATIONS UNLESS THE FILE IS html OR php
<Files ~ "\.(html|php?)$">
SSLOptions +StdEnvVars
</Files>
SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

</VirtualHost>


dev.lo-fi.net.conf


<VirtualHost *:80>
ServerName dev.lo-fi.net
DocumentRoot /Users/eric/WebApps/dev.lo-fi.net/webroot
ServerAdmin eric@guesswhere.net
<Directory "/Users/eric/WebApps/dev.lo-fi.net/webroot">
AllowOverride All
Options
Order allow,deny
Allow from all
</Directory>
</VirtualHost>

#Note the loopback ip address we set up for this host
#dev.lo-fi.net = 127.0.0.3
<VirtualHost 127.0.0.3:443>
ServerName dev.lo-fi.net
DocumentRoot /Users/eric/WebApps/dev.lo-fi.net/webroot
ServerAdmin eric@guesswhere.net
<Directory "/Users/eric/WebApps/dev.lo-fi.net/webroot">
AllowOverride All
Options
Order allow,deny
Allow from all
</Directory>

# SSL Configuration
SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP
SSLOptions +FakeBasicAuth +ExportCertData +StdEnvVars +StrictRequire

#Self Signed certificates
SSLCertificateFile /etc/apache2/ssl/dev.lo-fi.net.crt
SSLCertificateKeyFile /etc/apache2/ssl/dev.lo-fi.net.key
SSLCertificateChainFile /etc/apache2/ssl/ca.crt

#DON'T DO ANY INTENSIVE SSL OPERATIONS UNLESS THE FILE IS html OR php
<Files ~ "\.(html|php?)$">
SSLOptions +StdEnvVars
</Files>
SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

</VirtualHost>


And there you have it. SSL for multiple virtualhosts in Apache for Mac OS X Leopard. Once you restart Apache, you should be up and running, sending the correct Self Signed certificates to the browser.