Mix authentication with htpasswd and certs

Following on from a previous post where I went over using certificates for authentication with Apache; this post is going to look at using certificates along side HTTP Basic authorization.

Note: this post assumes Apache has already been configured for certificate based authentication.

Making cert based authentication optional

The SSLVerifyClient directive is used to control whether client certificates should be validated. For mixed authentication this needs to be set to optional in /etc/httpd/conf.d/ssl.conf:

SSLVerifyClient optional

After this change has been applied, certificate based authentication will no longer be required and users will be able to access pages anonymously:

$ curl --insecure  https://localhost/test.txt
Hello World

Setting up HTTP Basic Authorization

The mod_auth_basic Apache module can be used to configure password based authentication. When authenticating users Apache can reference a flat file containing usernames and password hashes, this file can be created with the htpasswd command:

htpasswd -c /etc/httpd/conf/htpasswd bob

This will create a file with contents similar to the following:

bob:$apr1$4eyyuAoD$eh08dhqXnJF0f7PZaDrIz0

It's generally a good idea to restrict permission on the file:

chown root:apache /etc/httpd/conf/htpasswd
chmod 0640 /etc/httpd/conf/htpasswd

Once the password file has been created, Apache needs to be updated to require a valid user and check for users in the file. This can be done with configuration similar to the following:

<Location '/'>
  SSLRequireSSL

  AuthType Basic
  AuthName "Authentication required..."
  AuthBasicProvider file
  AuthUserFile "/etc/httpd/conf/htpasswd"

  Require valid-user
</Location>

After Apache has been restarted to pick up the configuration change, a username and password will need to be supplied when accessing pages. This can be done using the -u option in curl:

$ curl --insecure -u 'bob:foo'  https://localhost/test.txt
Hello World

Alternatively you can add an Authorization header:

$ curl --insecure \
  -H "Authorization: Basic $(echo -n bob:foo|base64)"  \
  https://localhost/te st.txt
Hello World

Using FakeBasicAuth

After configuring HTTP Basic Authorization you will no longer be able to authenticate with certificates:

$ curl --insecure \
    --cert /etc/httpd/conf/users/alice/cert.pem \
    --key /etc/httpd/conf/users/alice/key.pem \
    https://localhost/test.txt
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>401 Unauthorized</title>
...

One way to get around this is to set the FakeBasicAuth option. This can be done by adding the following Apache configuration:

SSLOptions +FakeBasicAuth

When this option is enabled the Subject Distinguished Name (DN) of the client X509 cert will be translated into a HTTP Basic Authorization username. Therefore the user needs to be added to the password file with htpasswd:

htpasswd /etc/httpd/conf/htpasswd \
  '/C=GB/ST=England/O=Alice Ltd/CN=alice/emailAddress=alice@example.com'

Note: the password should just be password.

After Apache has been restarted certificate based authentication should work again:

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

Is using password safe?

All certificate accounts use the password password. Initially this seems like a fairly big security hole because in theory you could just pass the Subject Distinguished Name from the certificate along with password:

curl --insecure \
  -u '/C=GB/ST=England/O=Alice Ltd/CN=alice/emailAddress=alice@example.com:password' \
  https://localhost/test.txt

The request above will actually return a 403 because the SSL module has the following code to mitigate this:

if ((username[0] == '/') && strEQ(password, "password")) {
    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02035)
        "Encountered FakeBasicAuth spoof: %s", username);
    return HTTP_FORBIDDEN;
}

Using Require instead

Using the FakeBasicAuth option requires you to update the htpasswd file for each new user account. One way to get around this is to use the following alternative configuration:

Require ssl-verify-client

By default only one Require directive needs to match to authorize the request. So configuration similar to the following will allow requests if they have a valid client certificate, or if they provide a valid user details in an authorization header:

Require ssl-verify-client
Require valid-user

It is however possible to require multiple conditions, for example if you only want to allow HTTP Basic Authorization from a local subnet you could use configuration similar to the following:

<RequireAll>
  Require valid-user
  Require ip 192.168.0.0/16
</RequireAll>

This technique can also be used to place additional requirements on client certificates. For example:

<RequireAll>
  Require ssl-verify-client
  Require expr %{SSL_CLIENT_I_DN_O} == "Alice Ltd"
</RequireAll>

Note: one downside of using Require ssl-verify-client is the username will not appear in the access log by default.