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
.
<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:
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
.
#!/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:
- Create a private key
- Use private key to generate a Certificate Signing Request
- 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.
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.