Lukas Z's Private Blog

(mostly about programming)

Client Certificates With Apache on OSX

It’s possible to authenticate a user on a website using client certificates instead of a username and a password. The webserver, in my case Apache, uses a server certificate and only clients with the correct client certificate are able to connect to it.

Last night, for an experiment, I’ve created such a setup on OS X 10.8.4 Mountain Lion using the Apache that is pre-installed on OS X.

To save others some headache, here’s the walkthrough on how to go about it. Please keep in mind that I am not an Apache2 config expert or an ssl-guru. There’s probably room for improvement in the configuration files and process listed below.

Make sure you check out the link I’ve pasted at the very bottom of this article. It was a huge help to make this work!

TODOs

  • Set up some small app to serve the protected content
  • Configure Apache to act as a proxy for that app, SSL Only
  • Create Certificate Authority so I can sign my self-made certs
  • Create certificates
  • Configure Apache to only accept (and proxy) clients that present the right client certificate
  • Profit

So let’s get to work.

1. Set up some small app to server the protected content

For me this was a minimal Rails app that is serving a static webpage. In my case I was using thin and I’ve just started it listening to port 9090.

thin start -p 9090

2. Configure Apache to act as a proxy for that app, SSL Only

The default Apache on OSX has its config files in the folder /etc/apache2/ which in fact is /private/etc/apache2. It has a subfolder users that has configs for each user on the system. So in my case, I edited /etc/apache2/users/lukas.conf.

/etc/apache2/users/lukas.conf
  <VirtualHost localhost:443> # -- already using SSL port here
    ServerName localhost
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:9090/
    <Location />
            ProxyPassReverse /
            Order deny,allow
    </Location>
  </VirtualHost>

This just means that every connection that comes in on port :443 will be forwarded to port :9090. Replace :443 with :80 and restart Apache and you should be able to see the app from Step 1. when typing http://localhost in your browser.

To restart apache you can type

sudo apachectl -k restart

Now, because we said “SSL Only” in the task description, we have to ensure that only SSL connections are accepted. So here’s what my lukas.conf looks like if we do that:

/etc/apache2/users/lukas.conf
  Listen 443

  <VirtualHost *:80>
          <Location />
            SSLRequireSSL
          </Location>
  </VirtualHost>

  <VirtualHost localhost:443>
    ServerName localhost
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:9090/
    SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5
    SSLEngine on
    SSLCertificateFile /etc/apache2/certs/web.crt
    SSLCertificateKeyFile /etc/apache2/certs/web.key
    <Location />
            ProxyPassReverse /
            Order deny,allow
    </Location>
  </VirtualHost>

The config mentions two files, web.crt and web.key. It’s time to create those.

Note: If this doesn’t work, then take a look at the apache config in /etc/apache2/httpd.conf and ensure that the Proxy and SSL modules are being loaded. (That the appropriate lines are not commented out.)

3. Create Certificate Authority so I can sign my self-made certs

I think a CA (and all certs) can be created using the keychain tool that is included in OS X. However, I prefer the command line approach with openssl. It also has the nice side-effect that the guide will be (mostly) valid on a Linux-machine.

I’ve created two directories, /etc/apache2/certs/ and /etc/apache2/certs/ca. And then I’ve used the shell script from here to create the CA. I’ve pasted the contents into create_ca.sh in /etc/apache2/certs.

/etc/apache2/certs/create_ca.sh
#!/bin/bash
CAROOT=/etc/apache2/certs/ca
mkdir -p ${CAROOT}/ca.db.certs   # Signed certificates storage
touch ${CAROOT}/ca.db.index      # Index of signed certificates
echo 01 > ${CAROOT}/ca.db.serial # Next (sequential) serial number

# Configuration
cat>${CAROOT}/ca.conf<<'EOF'
[ ca ]
default_ca = ca_default

[ ca_default ]
dir = REPLACE_LATER
certs = $dir
new_certs_dir = $dir/ca.db.certs
database = $dir/ca.db.index
serial = $dir/ca.db.serial
RANDFILE = $dir/ca.db.rand
certificate = $dir/ca.crt
private_key = $dir/ca.key
default_days = 365
default_crl_days = 30
default_md = md5
preserve = no
policy = generic_policy
[ generic_policy ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
EOF

sed -i "s|REPLACE_LATER|${CAROOT}|" ${CAROOT}/ca.conf

cd ${CAROOT}

# Generate CA private key
openssl genrsa -out ca.key 1024

# Create Certificate Signing Request
openssl req -new -key ca.key  \
                 -out ca.csr       

# Create self-signed certificate
openssl x509 -req -days 10000 \
              -in ca.csr      \
              -out ca.crt     \
              -signkey ca.key

As you can see, it (amongst other files) creates the ca.conf in /etc/apache2/certs/ca with the contents of everything between the two occurrences of EOF. Afterwards it creates a key, a signing request, and finally the singing certificate ca.crt in the ca-folder.

So now just run this shell script with bash create_ca.sh.

It will ask you to enter details for your CSR. Make sure you enter localhost when it asks for the FQDN. (Or your domain should you be following these steps for anything else than localhost.) Whatever it is, it should match what you have entered in the apache config as ServerName.

When it’s done make sure you open /etc/apache2/certs/ca/ca.conf and replace the string REPLACE_LATER with /etc/apache2/certs/ca.

Voila, we have our own GoDaddy now. Just a pity that no browser in the world, not even our own, will trust our certificates!

Let’s move on.

4. Create certificates

We need two certificates. The before mentioned web.crt and a client.crt that will be imported into the OS X-keychain so the browsers can access it.

Creating a certificate is straightforward. Here are the basic steps:

  1. Create a private key
  2. Use private key to generate a Certificate Signing Request
  3. Sign CSR with own CA and create the certificate.

So first let’s create the server certs (these will have passphrases that you must type and remember):

openssl genrsa -des3 -out web.key # create key
openssl req -new -key web.key -out web.csr # create csr
openssl ca -config ca/ca.conf -in web.csr -cert ca/ca.crt -keyfile ca/ca.key -out ./web.crt # sign and create certificate

When you now restart Apache it will ask for the passphrase. You should also be now able to connect to https://localhost in your browser. (Your browser will display an ugly, scary warning that you can ignore since it’s just your local box and you won’t scare away any users..)

Okay, now let’s create the client certificate. The one our browser will have to show to get access to the webapp.

openssl genrsa -des3 -out client.key 1024 # create key
openssl req -new -key client.key -out client.csr # create csr
openssl ca -config ca/ca.conf -in client.csr -cert ca/ca.crt -keyfile ca/ca.key -out client.crt # sign and create certificate

For the CN you can enter your own name, since the certificate is issued for you, the user that wants to access the webapp.

Finally, let’s convert the cert to pkcs#12.

openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12

It may ask for an export password. You will need it when you (or the user you issued this for) later import the cert into the keychain.

Before we move on to the last step, let me just mention that the whole CA-business doesn’t have to be in a subfolder of your apache config. It can be on a completely different machine. It’s just a few files that help with issuing certificates and they haven’t got anything to do with Apache. It’s in this folder here for convenience (you would only need the ca.crt file in this setup, btw.), but I would probably put them in an entirely different (and very safe) place if I for example was GoDaddy..

Now the grand finale:

5. Configure Apache to only accept (and proxy) clients that present the right client certificate

Back to the Apache config! We have to tell Apache to disconnect everyone with an SSL-error that doesn’t present the right client certificate.

/etc/apache2/users/lukas.conf
  Listen 443

  <VirtualHost *:80>
          <Location />
            SSLRequireSSL
          </Location>
  </VirtualHost>

  <VirtualHost localhost:443>
    ServerName localhost
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:9090/
    SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5
    SSLEngine on
    SSLCertificateFile /etc/apache2/certs/web.crt
    SSLCertificateKeyFile /etc/apache2/certs/web.key
    SSLVerifyClient require # <--- here we go, client must show his cert
    SSLCACertificateFile /etc/apache2/certs/ca/ca.crt # <--- the ca-cert
    <Location />
            ProxyPassReverse /
            Order deny,allow
    </Location>
  </VirtualHost>

Restart apache. You should no longer be able to connect to https://localhost.

6. Profit

In order to be able to connect again you must import the client certificate into the keychain.

Simply open client.p12 and keychain should automatically open and ask you to import the key. Now refresh your browser, you should be able to connect.

Finally, especially in case of any problems, don’t miss this post on garex.net. It’s a fantastic guide on how to make this work and I would probably still be scratching my head if it wasn’t for this text.

P.S.: You can follow me on Twitter.

Comments

Webmentions