Client SSL certificate authentication with Apache

Passwords are one of the most common authentication methods used for websites. There are however alternative methods. This post is going to go over configuring Apache to authenticate users using Client X.509 certificates.

Note: the examples given in this post are for CentOS 7, they will need to be adapted slightly for other Linux distributions.

Installing required packages

Start by installing Apache, the Apache mod_ssl module and OpenSSL using yum:

yum install -y httpd mod_ssl openssl

Once Apache is installed, enable and start the service with systemctl:

systemctl enable httpd.service
systemctl start httpd.service

If everything goes well you should now be able to make a test file and request it with curl:

echo 'Hello World' > /var/www/html/test.txt
curl --insecure https://localhost/test.txt

Note: the --insecure option is used because Apache will be using a default self signed certificate.

Setup a certificate authority

Start by making a copy of the /etc/pki/CA/ directory, and restricting the permissions on the private directory:

cp -r /etc/pki/CA/ /etc/httpd/conf/ca
chmod 700 /etc/httpd/conf/ca/private

Then take a copy of the default openssl.cnf configuration and update the CA path:

cp /etc/pki/tls/openssl.cnf /etc/httpd/conf/ca/openssl.cnf
sed -i 's|/etc/pki/CA|/etc/httpd/conf/ca|' /etc/httpd/conf/ca/openssl.cnf

The openssl req command can then be used to generate a private key and a new CA certificate.

openssl req \
  -config /etc/httpd/conf/ca/openssl.cnf \
  -nodes \
  -newkey rsa:4096 \
  -keyout /etc/httpd/conf/ca/private/cakey.pem \
  -new \
  -x509 \
  -days 7300 \
  -sha256 \
  -extensions v3_ca \
  -subj '/C=GB/ST=England/O=Alice Ltd/OU=Alice Ltd Certificate Authority/CN=Alice Ltd Root CA' \
  -out /etc/httpd/conf/ca/cacert.pem

Note: the -nodes option will remove the need to encrypt the private key with a passphrase. Skipping this option to use a passphrase would be better from a security point of view.

Once the new CA certificate has been generated create an new index and serial file:

touch /etc/httpd/conf/ca/index.txt
echo 1000 > /etc/httpd/conf/ca/serial

/etc/httpd/conf/ca/ should now have a folder structure similar to the following:

.
+-- cacert.pem
+-- certs
+-- crl
+-- index.txt
+-- newcerts
+-- openssl.cnf
+-- private
¦   +-- cakey.pem
+-- serial

The CA created with the steps above is fine as an example. However it does not do things like use an intermediary CA. For a more detailed guide on setting up a CA with OpenSSL I would recommend having a look at OpenSSL certificate authority by Jamie Nguyen.

Create a client certificate

Commands similar the following can be used to generate a private key and certificate signing request for a user:

CLIENT_NAME='alice'
CLIENT_EMAIL="alice@example.com"
mkdir -p "/etc/httpd/conf/users/${CLIENT_NAME}/"
openssl req -newkey rsa:4096 \
  -keyout "/etc/httpd/conf/users/${CLIENT_NAME}/key.pem" \
  -out "/etc/httpd/conf/users/${CLIENT_NAME}/request.pem" \
  -days 365 -nodes  \
  -subj "/C=GB/ST=England/O=Alice Ltd/CN=${CLIENT_NAME}/emailAddress=${CLIENT_EMAIL}"

The request can then be signed with the CA:

openssl ca \
  -config /etc/httpd/conf/ca/openssl.cnf \
  -in /etc/httpd/conf/users/alice/request.pem \
  -out "/etc/httpd/conf/users/${CLIENT_NAME}/cert.pem"

Configure Apache

The following directives need to be set in /etc/httpd/conf.d/ssl.conf:

SSLCACertificateFile /etc/httpd/conf/ca/cacert.pem
SSLVerifyClient require
SSLVerifyDepth 1

The SSLCACertificateFile directive tells Apache which CA certificate to use when verifying if client certificates are valid, the SSLVerifyClient directive ensures clients have to authenticate with a valid certificate, and the SSLVerifyDepth directive tells Apache how many issuers in a certificate chain should be checked before giving up. For the examples in this post client certificates are always signed directly by the root CA, so 1 is fine.

To avoid files being accessed over HTTP create /etc/httpd/conf.d/ssl_required.conf with the following contents:

<Location '/'>
  SSLRequireSSL
</Location>

Finally restart Apache using systemctl to load the new configuration:

systemctl restart httpd.service

If everything goes well, the previous curl command should now only work if a valid client certificate is used:

$ curl --insecure https://localhost/test.txt
curl: (35) NSS: client certificate not found (nickname not specified)

$ curl \
  --insecure \
  --cert /etc/httpd/conf/users/alice/cert.pem \
  --key /etc/httpd/conf/users/alice/key.pem \
  https://localhost/test.txt
Hello World

Future improvements

The configuration in this post skips over a few important topics such as certificate revocation. Next weeks post will look at some of these topics in more detail.