Skip to main content

HOWTO: Securing A Website With Client SSL Certificates

Let's assume that you have an Apache webserver and a website that you want VERY finite access controls on. You could do it a number of ways, right?

You could craft a clever login page and use cookies, session IDs, etc.

You could use a simple authentication method like .htaccess.

Or, you could create a custom SSL certificate and give that certificate to very specific users that should have access to your site. In a perfect world, only these users could communicate with your site AND the connection would be secured via the magic of SSL.

I know what you're thinking.

"But Chief, what if someone STEALS your certificate?"

Hey, what if someone steals your car keys? :-)

I have always wanted a good how-to document on doing this, and reader 'Null' (who I must also point out is a heck of a nice guy, and a fun sort to hang out at Def Con with) came through big time.

Without further ado, here is Null's write-up on the subject. Enjoy! ~ Chief



Reader 'Null' writes:

Someone has been bugging me to write this up since I figured it out and figuring it out has been a thorn in my side for a couple years.

About a week ago, things just clicked and I figured it out.

This isn't authentication in the classical sense; you are just saying which SSL certificates (that you have signed) you would like to be able to access a particular site.

This is in no way a new thing; there is just little to no usable documentation on how to accomplish this. The impetus for finally tackling this problem was setting up an internal wiki for use at $work that we can control who will be able to see it since our department is privy to info that shouldn't be available to all.

The interesting thing here is that a few of us are road warriors and need access to everything from anywhere meaning IP based ACL's are out and plain authentication is easy to grab on the wire (well not really but it is safer to assume it is by default). The best of both worlds would be to use some sort of decent encryption combined with a filtering mechanism which will allow us to say who we like versus who we don't.

The solution is signed certificates under SSL (in this case Apache's mod_ssl) as the distinguished name of an SSL certificate is perfect for filtering and the SSL handshake is about as secure as you are going to get on the wild wild web (assuming the server and client keys are trusted and not self signed but more on that later).

I first tried this using FreeBSD for this though getting and installing packages for PHP+Apache+MySQL+Mediawiki was far too much inconsistency for me to handle. There were too many package based (too lazy to compile source in the ports tree) inconsistencies so I just gave up and went to my old faithful OpenBSD. This should also work on any other OS with mod_ssl but considering I wanted to setup a Mediawiki I had significant overhead.

There are a few steps needed to get this going.
They are:
1) Setup openssl for use in being a certificate authority.
2) Create a root certificate authority key.
3) Create a web server key signed by your certificate authority's key.
4) Setup apache.
5) Create client certificates.
6) Install client certificates on hosts.
7) Profit.

1)
This was pretty much hit and miss and I took info from all over the Internet. Unfortunately no one place gave me enough information to hang myself so I stole bits and pieces from each one until it worked. Get yourself a nice large caffeinated beverage... if you make mistakes you'll need the concentration and tenacity to work it out.

The very first thing to do here is to setup your openssl.cnf file (in OpenBSD /etc/ssl/openssl.cnf). Setup your root common name, organization and all that jazz. Don't skip this or use defaults since the distinguished name is our filtering mechanism.

Now we setup openssl to recognize us as the default certificate authority.

[ ca ]
default_ca = foo

Openssl will now look for a section called foo with the various knobs needed.
[ foo ]
dir = /etc/ssl/private
database = $dir/index.txt
serial = $dir/serial
private_key = $dir/ca.key
certificate = $dir/ca.crt
default_days = 3650
default_md = md5
new_certs_dir = $dir
policy = policy_match

After you've set the defaults and tuned the various other knobs to your liking you need to define the policy (specifically 'policy_match').
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = match
commonName = supplied
emailAddress = optional

This just says that the country, state, organization and unit names must match if the certificate is valid. The commonName is supplied in the certificate you receive and email address on the certificate is optional. For my purposes here, this is enough as I'm going to allow anyone issued a key for my department access to the department wiki.

Because I'm lazy I chose 10 years as the default key validity period. You can choose this to be anything you want with the caveat is the longer the period the less work for you (and the less secure your keys are).

2)
Now OpenSSL knows about our CA (even though the keys haven't been generated or signed yet) so now we move on to creating the root Certificate Authority key.

new_ca.sh:
#!/bin/sh
# Generate the key.
openssl genrsa -out private/ca.key
# Generate a certificate request.
openssl req -new -key private/ca.key -out private/ca.csr
# Self signing key is bad... this could work with a third party signed key... registeryfly has them on for $16 but I'm too cheap lazy to get one on a lark.
# I'm also not 100% sure if any old certificate will work or if you have to buy a special one that you can sign with. I could investigate further but since this
# service will never see the light of an unencrypted Internet see the cheap and lazy remark.
# So self sign our root key.
openssl x509 -req -days 3650 -in private/ca.csr -signkey private/ca.key -out private/ca.crt
# Setup the first serial number for our keys... can be any 4 digit hex string... not sure if there are broader bounds but everything I've seen uses 4 digits.
echo FACE > private/serial
# Create the CA's key database.
touch private/index.txt
# Create a Certificate Revocation list for removing 'user certificates.'
openssl ca -gencrl -out /etc/ssl/private/ca.crl -crldays 7


This is your root certificate authority key. I would suggest giving it a nice strong password and not storing it on the webserver or network the webserver it is on (of course 99% of us will not heed that warning but hell for completion's sake it is healthy to mention).

3)
All this effort is useless if we don't have a web server so let's create a key and sign it with our CA's key.

new_server.sh:
# Create us a key. Don't bother putting a password on it since you will need it to start apache. If you have a better work around I'd love to hear it.
openssl genrsa -out private/apache.key
# Take our key and create a Certificate Signing Request for it.
openssl req -new -key apache.key -out apache.csr
# Sign this bastard key with our bastard CA key.
openssl ca -in private/apache.csr -cert private/ca.crt -keyfile private/ca.key -out private/apache.crt



4) Now we've got our CA and web server's key let's setup apache to deal with it, so let's get over the Apache SSL boiler plate.


< IfDefine SSL >
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl .crl
< /IfDefine>

< IfModule mod_ssl.c >
SSLPassPhraseDialog builtin
SSLSessionCache dbm:logs/ssl_scache
SSLSessionCacheTimeout 300
SSLMutex sem
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLRandomSeed startup file:/dev/arandom 512
SSLLog logs/ssl_engine_log
SSLLogLevel info
< /IfModule >


Blah, ugly ugly stuff but once you understand it is not so bad.
Now let's setup the rest.

# Mon deus, I'm a lazy bastard.

< VirtualHost _default_:443 >
DocumentRoot /var/www/htdocs.ssl/
# ServerName must be the same as the CommonName given for the web server certificate.
ServerName foo.foo.net
ServerAdmin root@foo.net
ErrorLog logs/error_log
TransferLog logs/access_log
SSLEngine on
# Remember the apache certificate we created? Well let apache know we can use it.
SSLCertificateFile /etc/ssl/private/apache.crt
# Only necessary if the key is not supplied in ther certificate.
SSLCertificateKeyFile /etc/ssl/private/apache.key
# CA Validation!
# Remember only certificate is needed not the KEY.
SSLCACertificateFile /etc/ssl/private/ca.crt

# REQUIRE valid certificates from clients.
SSLVerifyClient require
# FakeBasicAuth... if cert is good, auth is granted.
SSLOptions +FakeBasicAuth +ExportCertData +CompatEnvVars
CustomLog logs/ssl_request_log \
"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"

# Depth is 1 because our root is a self signed cert, increase for the number of certs in the chain (ie. >2 for purchased certificated that were not self signed).
SSLVerifyDepth 1

# Note we can change the rules for any Location, Directory, URL we wish.
# This is very flexible as we can allow some types of certificates in one place
# but not in others.
<Location /wiki>
# Let's also provide a list of the certificates we want to purposely deny.
SSLCARevocationFile /etc/ssl/private/ca.crl

# Time for the Authentication filtering mojo...
SSLRequire ( %{SSL_CIPHER} !~ m/^(EXP|NULL)-/ and %{SSL_CLIENT_S_DN_O} eq "Foo Net" and %{SSL_CLIENT_S_DN_OU } in {"Department of Department Creation"} )





This is kind of weird here so I'll break each of these down.
%{SSL_CIPHER} !~ m/^(EXP|NULL)-/ <--- Certificate must not be empty or expired.
%{SSL_CLIENT_S_DN_O} eq "Foo Net" <--- The organization string in the signed certificate's distinguished name must be "Foo Net"
%{SSL_CLIENT_S_DN_OU} in {"Department of Department Creation"} <-- The organizational unit string in the signed certificate's distinguished name must bee "Department of Department Creation"

The SSLRequire line states that this statement must evaluate to true if access will be granted. Note this is a chained and meaning all or nothing and since I'm only validating two parts of the certificate (because I'm lazy) we let OpenSSL deal with policy matches. This is terrible for the Internet so you should be checking the common names (ie. names of people you assigned certs to and setup a revocation list for then they are no longer welcome).

Start up apache (make sure to set httpd_flags="-DSSL" in /etc/rc.conf for OpenBSD) via apachectl startssl.
Check for errors and make sure the damned thing is running. You won't have access to https://server/wiki because the SSL conversation won't work (since you haven't a corresponding valid certificate in your browser yet).

5)
Now let's create some certificates for our clients/users/losers/friends/random folks...

new_user.sh:
#!/bin/sh
# The base of where our SSL stuff lives.
base="/etc/ssl/private"
# Were we would like to store keys... in this case we take the username given to us and store everything there.
mkdir -p $base/users/$1/

# Let's create us a key for this user... yeah not sure why people want to use DES3 but at least let's make us a nice big key.
openssl genrsa -des3 -out $base/users/$1/$1.key 1024
# Create a Certificate Signing Request for said key.
openssl req -new -key $base/users/$1/$1.key -out $base/users/$1/$1.csr
# Sign the key with our CA's key and cert and create the user's certificate out of it.
openssl ca -in $base/users/$1/$1.csr -cert $base/ca.crt -keyfile $base/ca.key -out $base/users/$1/$1.crt

# This is the tricky bit... convert the certificate into a form that most browsers will understand PKCS12 to be specific.
# The export password is the password used for the browser to extract the bits it needs and insert the key into the user's keychain.
# Take the same precaution with the export password that would take with any other password based authentication scheme.
openssl pkcs12 -export -clcerts -in $base/users/$1/$1.crt -inkey $base/users/$1/$1.key -out $base/users/$1/$1.p12

Wow now you have signed certificates whose distinguished names will match your CA and Web's keys.
Now you have to get these things to your clients to use them. As well keep in mind that the .p12 file for each user contains both the public and private keys so if your user doesn't keep that key safe and use a decent export password then this certificate is about as good as writing an all access password with a sharpie on a post-it for display in a public place.

On the inverse when you need to remove a certificate's access you can prematurely expire the certificate by adding it to the Certificate Revocation List

remove_user.sh
#!/bin/sh
# Revoke a certificate and update the CRL.
base=/etc/ssl/private
# Revoke a particular user's certificate.
openssl -revoke $base/$1/$1.pem
# Update the CRL with the new info from the database (ie. index.txt)
openssl ca -gencrl -out $base/ca.crl -crldays 7


6)
Send the username.p12 to clients. Don't bother sending the other bits. They are more useful to you should the user lose the thing.

FireFox Setup For Clients:
Go into preferences.
Advanced.
View Certificates.
Import.
Enter master password for FireFox (if you don't have one set one here otherwise stolen laptop = easy access).
Enter in the export password given to you by the dude who created your cert.
Hit OK like a mad man.

Go to site and it will ask you if you want to send the certificate in question (look at the distinguished name to make sure it matches what you expect it to).

Now you have access!

Safari Setup For Clients.
Open a shell.
open username.p12
Keychain access will open up and ask you what chain to import into... choose 'login' and enter export password.
Now safari should work!

7)
Now go talk and wiki with your encrypted cohorts in an encrypted fashion to all your hearts content.

I didn't go through each of the SSL user input steps since I figured this audience can figure it out. If not let me know and I'll be even more verbose on the topic.

It would be really nice to get this working with valid third party signed keys thereby making this safe for the Internet at large and thereby open to a setting up a private wiki/forum/whatever for a small group of friends (ie. SSLVerifyDepth >1). Self signed keys are bad; really bad; since they can be spoofed easily (ie generate a new self signed certificate and MITM the hell out of the site since one invalid certificate is as good as any other). Until someone has figured out of a nice paid certificate will work then please don't use this on the Internet at large... yeah cheap certs are still largely 'unsafe' but are leagues safer than free self signed ones.

On second thought I don't think this will be possible without an inordinate amount of money as I just realized most CA's would dissalow keys to sign others as then you wouldn't need to pay them after the first key.

If you need to know how to check to see if your key can sign others:
openssl x509 -in third_party_issued_key.pem -noout -text
Look for this:
X509v3 Basic Constraints: critical
CA:FALSE

CA:FALSE means no deal. We will have to live with our insecure self signed keys. So be wary and not use this stuff on the Internet at large (or at least for anything you truely care about).

With that given, my future work will have be to figure out a way to make this easier and less of a hackish mess. As well some time should be spent to make the SSLRequire statement check more of the fields in the distinguished name. More fields checked makes for slightly increased certainty.

You can make it even better by using htaccess info and the keys for a cheap/crappy two factor authentication. The biggest benefit on the Internet at large would be for corporate Intranet sites that restrict what users can see on the web server in question without having to give them new tokens, passwords, etc. Pretty flexible if you ask me.

Comments

Popular posts from this blog

Karakteristik TCP dan UDP

Kali ini kita akan membahas tentang TCP dan UDP. Pada bagian ini, kita akan membahas tentang karakteristik masing-masing dari TCP dan UDP. Dengan mengenal TCP dan UDP, kita dapat tahu kapan kita akan menempatkan suatu protokol yang tepat dalam implementasi jaringan. 1.Kapan menggunakan TCP dan UDP?? DNS menggunakan TCP dan UDP di port komputer 53 untuk melayani permintaan DNS. Nyaris semua permintaan DNS berisi permintaan UDP tunggal dari klien yang diikuti oleh jawaban UDP tunggal dari server. Dan pada saat kapan protocol TCP digunakan? Umumnya TCP dipergunakanhanya ketika ukuran data jawaban melebihi 512 byte, atau untuk pertukaran zona DNS zone transfer. DNS zone transfer adalah sebuah mekanisme untukmereplikasi DNS data dari satu DNS ke DNS server lain. Zone transfer digunakan pada saat kita ingin mereplikasi DNS data pada DNS server kita dalam upaya menghemat bandwidth, untuk meningkatkankecepatan terhadap suatu permintaaan atau untuk membuat DNS data selalu tersedia pada saat
Did you ever got this error message, "Could not start the "application" Service service on Local Computer. Error 1069 : The Service did not start due to a logon failure.” I ever got this message when i try to start an application on windows server 2003 or NT. I try to retype the password in "services" menu. But, it still did not work. Now, i check the permission on file that refer to path that show in "services". And i set it to full access for everyone. And then, i back to the "services" menu again, and starting this. And it's working now. This issue show, it cannot to access the file on windows services. It needs to make sure that the files is able to execute. So if you got this message, try to register in Control Panel > Administrative > Services. And Type the password and start it after you put your password.