Background and goal
Suppose you want to secure some part of your website by authentication. You could use HTTP Basic authentication in combination with (plain) SSL to do that, but that requires a username/password combination to be stored on the remote host as well as communicating these credentials. The better way is to use SSL client authentication.
The main advantage of using SSL is that it uses asymmetrical encryption, which means your clients give you their public key, you can manage them as trusted/untrusted and they can simply use their private key. This is all done without communicating any secret information. It can be compared to SSH and public key authentication.
Note
In this how-to I will not deal with Certificate Authorities, but only with self-signed certificates which are trusted individually. This method is not recommended for large number of certificates to maintain. If you have lots of clients, you should set up your own Certificate Authority, sign the client certificates (using certificate signing requests) and trust the CAs explicitly everywhere.
Note
Before you move on, make sure you have an Apache 2.x running with a server certificate and mod_ssl
enabled. To test it, Make sure you can reach your server using https://1.2.3.4/
(replace 1.2.3.4
with the IP address of your server).
Configuring the client's browser
TODO, left for another post.
Generating client key and certificate
Clients (with their browsers) need to have a private key along with a public certificate. On Linux this can be generated as follows (as user):
# Generate a 2048 bits RSA private key in PEM format
openssl genrsa -out myprivatekey.pem 2048
# Generate a certificate with a validity of 1095 days.
openssl req -new -x509 -key myprivatekey.pem -out mycertificate.pem -days 1095
# Answer the questions about the certificate details.
Configuring the Apache server
Create a directory containing all the trusted certificates. By default, Apache uses your system-wide trusted CAs, but we only want to trust some certificates explicitly. Otherwise, anyone using any certificate signed by any trusted CA on the system can be validated, unless you set any checks on the contents of the certificate.
mkdir /etc/apache2/mytrustedclientcerts
Now copy over the client certificate you received from your client in that folder. The filename does not matter, only the file format does; it has to be in PEM format.
# ls -al /etc/apache2/mytrustedclientcerts
total 16
drwxr-xr-x 2 root root 4096 Sep 22 13:07 .
drwxr-xr-x 8 root root 4096 Sep 22 13:01 ..
-rw-r--r-- 1 root root 1679 Sep 22 13:01 mycertificate.pem
Suppose you have hundreds of these certificates, then Apache would need to open every certificate until it finds the right one.
You can imagine that would be very inefficient.
To speed that up, Apache looks for a file with the hash of the certificate it gets from the client.
For example, if my certificate would be hashed as 27e66395
then it would look for files with the name of 27e66395.X
where X
is a number starting with 0
.
This avoids hashing collisions.
For administration it would be nice if not all files would have their file names based on hashes.
We'll use symbolic links in order to point to the real certificate file and generate the symbolic links with a script.
This can be done using a proposed makefile as shown on the bottom of this page.
Place the makefile as Makefile
in the /etc/apache2/mytrustedclientcerts
folder and run it with just calling
make
You now should have something like this:
# ls -al /etc/apache2/mytrustedclientcerts
total 16
drwxr-xr-x 2 root root 4096 Sep 22 13:07 .
drwxr-xr-x 8 root root 4096 Sep 22 13:01 ..
lrwxrwxrwx 1 root root 15 Sep 22 13:06 27e66395.0 -> mycertificate.pem
-rw-r--r-- 1 root root 1679 Sep 22 13:01 mycertificate.pem
-rw-r--r-- 1 root root 1537 Sep 22 13:06 Makefile
Now you're ready to set the following Apache directives in the SSL-enabled site configuration:
SSLCACertificatePath /etc/apache2/mytrustedclientcerts
SSLVerifyClient require
SSLVerifyDepth 1
The SSLVerifyDepth
is set to 1
, so that it will not use any hierarchy in X.509, but only check against the certificates explicitly.
Note
The directives SSLVerifyClient
and SSLVerifyDepth
could be used within other directives to match a specific location, files or anything that Apache allows you to match.
Warning
It is strongly recommended to disable SSLv2 using the directive SSLProtocol all -SSLv2
in your site config.
This prevents the use of the old and deprecated SSL protocol version.
Check the syntax of your site configuration and restart Apache:
# apache2ctl configtest
Syntax OK
# /etc/init.d/apache2 restart
Restarting web server: apache2.
Appendix: Makefile generating symbolic links
Note
This script is based on pkg.sslcfg/Makefile.crt
from an older mod_ssl
release (http://www.modssl.org/source/mod_ssl-2.8.31-1.3.41.tar.gz).
Run it every time you add or remove a client certificate.
Adjusted are the SSL_PROGRAM
variable and the files to match.
for file in *.crt; do \
for file in *.pem; do \
The latter edit is in order to match client certificates (.pem
files) instead of CA certificates (usually stored as .crt
).
##
## Makefile to keep the hash symlinks in SSLCACertificatePath up to date
## Copyright (c) 1998-2001 Ralf S. Engelschall, All Rights Reserved.
##
SSL_PROGRAM=/usr/bin/openssl
update: clean
-@ssl_program="$(SSL_PROGRAM)"; \
if [ ".$$ssl_program" = . ]; then \
for dir in . `echo $$PATH | sed -e 's/:/ /g'`; do \
for program in openssl ssleay; do \
if [ -f "$$dir/$$program" ]; then \
if [ -x "$$dir/$$program" ]; then \
ssl_program="$$dir/$$program"; \
break; \
fi; \
fi; \
done; \
if [ ".$$ssl_program" != . ]; then \
break; \
fi; \
done; \
fi; \
if [ ".$$ssl_program" = . ]; then \
echo "Error: neither 'openssl' nor 'ssleay' program found" 1>&2; \
exit 1; \
fi; \
for file in *.pem; do \
if [ ".`grep SKIPME $$file`" != . ]; then \
echo dummy |\
awk '{ printf("%-15s ... Skipped\n", file); }' \
"file=$$file"; \
else \
n=0; \
while [ 1 ]; do \
hash="`$$ssl_program x509 -noout -hash <$$file`"; \
if [ -r "$$hash.$$n" ]; then \
n=`expr $$n + 1`; \
else \
echo dummy |\
awk '{ printf("%-15s ... %s\n", file, hash); }' \
"file=$$file" "hash=$$hash.$$n"; \
ln -s $$file $$hash.$$n; \
break; \
fi; \
done; \
fi; \
done
clean:
-@rm -f [0-9a-fA-F]*.[0-9]*