Here is a short tutorial on how to setup a multi stage authentication with an Apache HTTPd 2.4 server.

The first stage will do a 2-way SSL encryption, so both server and client will need to present a certificate.
The second stage will be password authentication, the username has to match the username in the CN of the client certificate.

This is similar to the Belgian eID, instead of needing a PIN to unlock the certificate.
We require a certificate and a password. Although this is not 100% the same, the security it offers is comparable for most purposes.

In my 2 test cases the password is queried from PAM, but a simple htpasswd file will work as well.

I assume you can configure a normal SSL based virtual host on httpd and have this working. I also assume you are able to sign and create the clients certificates yourself using openssl and a private CA. If there is demand, I may write a short post on that at a later date.

Below is a full configuration1

Listen 8443

<VirtualHost *:8443>
	ServerName		secure.blackdot.be
	ServerAlias		nara

	CustomLog		/srv/http/logs/access_log common
	ErrorLog		/srv/http/logs/error_log

	SSLEngine		on
	SSLCertificateFile	/srv/http/ssl/server.crt
	SSLCertificateKeyFile	/srv/http/ssl/server.key
	SSLCACertificateFile	/srv/http/ssl/ca.crt
	SSLCARevocationFile	/srv/http/ssl/ca.crl

	SSLProtocol		TLSv1.2 TLSv1.1 TLSv1
	SSLCipherSuite		HIGH:!LOW:!aNULL:!MD5

	# php lockdown
	php_admin_value open_basedir "/srv/http/htdocs:/srv/http/tmp:/tmp"
	php_admin_value upload_tmp_dir "/srv/http/tmp"
	php_admin_value session.safe_path "/srv/http/tmp/sessions"


	<FilesMatch "\.(cgi|shtml|phtml|php)$">
		#SSLOptions +StdEnvVars +ExportCertData
		SSLOptions +StdEnvVars
	</FilesMatch>
	
	## Fancy SSL Authentication 
	# /: optional client ceritificate
	# /*: require client certificate + 2 step authentication
	# /gatekeeper: don't need client certificate
	DefineExternalAuth pwauth pipe /srv/http/bin/pwauth
	DocumentRoot	/srv/http/htdocs
	<Directory /srv/http/htdocs>
		SSLVerifyClient require
		
		AuthType Basic
		AuthName "Secure Area"
		AuthBasicProvider external
		AuthExternal pwauth	
	
		# work around a bug in the new authentication code, should be fixed in 2.4.3
		#<RequireAll>
		#	Require valid-user
		#	<RequireAny>
		#		Require user workaround_for_PR_52892
		#		Require expr ( \
		#			(%{SSL_CLIENT_S_DN_O} == "Blackdot") && \
		#			((%{SSL_CLIENT_S_DN_OU} == "Admins") || (%{SSL_CLIENT_S_DN_OU} == "Users")) && \
		#			(%{SSL_CLIENT_S_DN_CN} == %{REMOTE_USER}) \
		#		)
		#	</RequireAny>
		#</RequireAll>
		<RequireAll>
			Require valid-user
			Require expr ( \
				(%{SSL_CLIENT_S_DN_O} == "Blackdot") && \
				((%{SSL_CLIENT_S_DN_OU} == "Admins") || (%{SSL_CLIENT_S_DN_OU} == "Users")) && \
				(%{SSL_CLIENT_S_DN_CN} == %{REMOTE_USER}) \
			)
		</RequireAll>

	</Directory>

	# fix upload of large files (bug in renegotiate)
	<Directory /srv/http/htdocs>
		SSLRenegBufferSize 134217728
	</Directory>
</VirtualHost>

Server Certificate

    SSLCertificateFile      /srv/http/ssl/server.crt
	SSLCertificateKeyFile	/srv/http/ssl/server.key
	SSLCACertificateFile	/srv/http/ssl/ca.crt
	SSLCARevocationFile     /srv/http/ssl/ca.crl

It is very important to include your CA, it is required for client certificate validation.
I also recommend setting the Certificate Revocation List, that way you can revoke access to compromised client certificates.

Enabling Client Certificate Authentication

    <Directory /srv/http/htdocs>
		...
		SSLVerifyClient require
		...
	</Directory>

You can also set this to optional, this can be useful if you have a portal to retrieve the client certificate. But it’s safer to have it set to require for the entire virtual host.

Enabling HTTP Authentication

    <Directory /srv/http/htdocs>
		...
		AuthType Basic
		AuthName "Secure Area"
		AuthBasicProvider external
		AuthExternal pwauth
		...
	</Directory>

This is the second stage of the authentication, I’m using mod_auth_external in combination with pwauth to authenticate against PAM.

This makes it easy for users with shell access to change their own passwords, however you probably want to use file base authentication instead. Every authentication provided should work.

Linking Client Certificate to the HTTP User

    <Directory /srv/http/htdocs>
		...
		<RequireAll>
			Require valid-user
			Require expr ( \
				(%{SSL_CLIENT_S_DN_O} == "Blackdot") && \
				((%{SSL_CLIENT_S_DN_OU} == "Admins") || (%{SSL_CLIENT_S_DN_OU} == "Users")) && \
				(%{SSL_CLIENT_S_DN_CN} == %{REMOTE_USER}) \
			)
		</RequireAll>
		...
	</Directory>

Here is where the magic happens! This is why this only works on 2.4 branch: we can now nest requirements! The <RequireAll> block will require all the require statements inside to validate to true, if not the request is rejected.

The valid-user should be self explaining. The real interesting stuff is the expr line. Here we will check if the following conditions are met for the client certificate:

  1. Organization matched ‘blackdot’
  2. OU matched ‘Admins’ or ‘Users’
  3. Common Name matched the username provided via HTTP Authentication

Of course the validity of the certificate is already checked by SSLVerifyClient. This is probably where the most editing will be needed to get this to fit your needs.

The main example also has a alternative, with a workaround for pre 2.4.3 releases.

Fix file uploads

    <Directory /srv/http/htdocs>
		SSLRenegBufferSize 134217728
	</Directory>

There seems to be some issues with file uploads, setting the SSLRenegBufferSize large enough seems to solve this.

That’s it, enjoy!


  1. I will add an explanation to the important parts below. ↩︎