Two way SSL authentication used in a private network with private CA - application to lighttpd 1.4.45

This post shows a minimalist configuration for two way authentication (https + Client Side Verification) on a secure closed-membership network. A self-generated private CA issues server and clients certificates. The specific full example shown is for lighttpd v1.4.45 as server and Firefox browser v65.0 as client - but the general method has wider applications.

Background: What is “two-way authentication” and “private CA” ?

Skip this section if you already know the terms “two-way authentication” and “private CA”.

show/hide details

One-way authentication is the most familiar model: that’s the system of provable trust that allows the green lock followed by https://… to be displayed in the browser address bar. The magic of public cryptography makes possible the system whereby a client requests a URL, and then receives authentication data from a server saying “I am the one allowed to serve this URL”, and it is believable because a CA (certificate authority) will vouch for it. Of course the response URL had better match the requested URL - otherwise you may be p*wned!

Two way authentication is an additional step where the server also receives authentication data from the client. However, there are some subtle differences. Firstly, instead of a URL it is an ID which is being verified. Secondly, the server does not request the ID first - the client initiates contact and then the server verifies that the client ID is on a list of allowed clients. Nevertheless, the core algorithms behind authenticating the ID and authenticating the URL are the same, and most (but not all) of the procedural glue is the same. This additional step is also called “client side verification”

Last but not least, two-way authentication is for situations where the servers and clients which will be communicating with each other are known in advance at the time the certificates are issued. Therefore two-way authentication is not applicable to general public browsing. Two-way authentication is a niche use case for secure, closed membership systems.

A private CA is also created for use on a secure, closed membership system. A private CA is not globally known. A private CA and two-way authentication are well suited to each other because they handle the same niche use case.

lighttpd documentation on two-way security

show/hide details

The lighttpd documentation describes the relevant parameters for one-way authentication:

Option Description
ssl.engine enable/disable ssl engine
ssl.pemfile path to the PEM file for SSL support (must contain both certificate and private key) path to the CA file for support of chained certificates

The additional parameters for two-way authentication, i.e. “Client Side Verification”, are also described:

Option Description
ssl.verifyclient.activate enable/disable client verification
ssl.verifyclient.enforce enable/disable enforcing client verification
ssl.verifyclient.depth certificate depth for client verification
ssl.verifyclient.username client certificate entity to export as env:REMOTE_USER (eg. SSL_CLIENT_S_DN_emailAddress, SSL_CLIENT_S_DN_UID, etc.)

Unfortunately, although apparently ssl.verifyclient.username enables an ID to be extracted from the client certificate, there is no explanation about how to use that information for client authentication at the lighttpd level.

Fortunately, I asked that question on stackoverflow and received an answer. Undocumented magic! That will be shown below

Full example with lighttpd v1.4.45 and Firefox v65.0

step 1

Create one private root and two leaf certificates with the following profiles:

Property CA cert Server cert Client cert
type CA Leaf Leaf
CN (common name) Root1 Server1 Client1
subjectAltName N/A DNS:pihole.home.lan,
Key filename Root1.key Server1.key Client1.key
Cert filname Root1.crt Server1.crt Client1.crt
Key+Cert filename N/A Server1.key-crt.pem Client1.p12

Note: The field DNS:pihole.home.lan,DNS:pihole,IP: is a single string with no spaces. It was broken into two lines only to fit in the table.

There are a number of programs capable for creating such files, but for convenience and brevity a humble minimalist openssl based batch file is provided with this document. It is described in the section privca Cert Creation Tool.

We pause for a birds eye view of what files go where, and what role they play in the web of Authentication.

The key+cert files are composed as follows:

  Key Part source file Cert Part source file Combined File
Server Server1.key Server1.crt Server1.key-crt.pem
Client Client1.key Client1.crt Client1.p12

The next table shows to where the files will eventually be exported and the role they will play:

Authenticator Authenticatee Server side file Client side file
Client Server Server.key-crt.pem Root1.crt
Sever Client Root1.crt Client1.p12

step 6 From Firefox -

Click through

Preferences | Privacy & Security | View Certificates | Authorities | Import

to upload


Then click through

Preferences | Privacy & Security | View Certificates | Your Certificates | Import

to upload


When uploading, Firefox will ask you for the password you set when creating it, if any.

step 7 Copy files to the serving device running lighttpd -

Source Dest Dir
./export/ca/public/HomeLan.crt /etc/lighttpd/ssl/public/
./export/private/PiSrv-HomeLan.key-crt.pem /etc/lighttpd/ssl/private/

(The destinations can be freely chosen, this is just an example).

Set the destination owner and permission as follows -

Dir or File owner:group perm
/etc/lighttpd/ssl/public/ root:www-data 755
/etc/lighttpd/ssl/public/HomeLan.crt root:www-data 644
/etc/lighttpd/ssl/private/ root:www-data 750
/etc/lighttpd/ssl/private/PiSrv-HomeLan.key-crt.pem root:www-data 640

These settings allow read access by www-data when serving.

step 8 Configure an existing lighttpd configuration file where it configures the https port 443. This might be in a file /etc/lighttpd/external.conf.

In the case that lighttpd is already configured for https one-way authentication, then modify/add the following parameter settings to achieve our two-way authentication:

 $SERVER["socket"] == ":443" {
ssl.pemfile = "/etc/lighttpd/ssl/private/PiSrv--HomeLan.key-crt.pem" = "/etc/lighttpd/ssl/public/HomeLan.crt"
ssl.verifyclient.activate = "enable"
ssl.verifyclient.enforce = "enable"
ssl.verifyclient.depth = "2"
ssl.verifyclient.username = "SSL_CLIENT_S_DN_CN"

In the case that lighttpd is not yet configured for https one-way authentication, then here is an example of settings for https two-way authentication:

$HTTP["host"] =~ "pihole($|\.home\.lan)" {
# Ensure the Pi-hole Block Page knows that this is not a blocked domain
#setenv.add-environment = ("fqdn" => "true")

# Enable the SSL engine with a LE cert, only for this specific host
$SERVER["socket"] == ":443" {
ssl.engine = "enable"
ssl.pemfile = "/etc/lighttpd/ssl/PiSrv--HomeLan.key-crt.pem" = "/etc/lighttpd/ssl/public/HomeLan.crt"
ssl.honor-cipher-order = "enable"
ssl.use-sslv2 = "disable"
ssl.use-sslv3 = "disable"
# client side authentification
ssl.verifyclient.activate = "enable"
ssl.verifyclient.enforce = "enable"
ssl.verifyclient.depth = "10"
ssl.verifyclient.username = "SSL_CLIENT_S_DN_CN"
## ssl.verifyclient.username = "SSL_CLIENT_S_DN_emailAddress"

# Redirect HTTP to HTTPS
$HTTP["scheme"] == "http" {
$HTTP["host"] =~ ".*" {
url.redirect = (".*" => "https://%0$0")

Note: The above two-way setting were adapted from [these one-way settings using an LE cert] (

step 9 Now create a new additional lighttpd configuration file

sudo nano /etc/lighttpd/conf-available/02-auth-cert.conf

with content

# comment out the next line to silence warnings if "mod_auth" already loaded
server.modules += ("mod_auth")
auth.require = ( "" =>
"method" => "extern",
"realm" => "certificate",
"require" => "user=Client1--HomeLan"

Note: To allow multiple client IDs, separate by | and prefix each ID with `user=”, e.g.,:

"require" => "user=Client1--HomeLan|user=Client2--HomeLan"

step 10 Restart the lighttpd daemon -

systemctl restart lighttpd


service lighttpd restart

Check the status is OK -

systemctl status lighttpd


service lighttpd status

step 11 Test access of the server from the Firefox browser, e.g., enter the address pihole.home.lan or into the address bar. On the first access Firefox will put up a dialog box for you to confirm the client certificate Client1--HomeLan.p12. If you don’t see the dialog box hunt around for it. I once found it in another workspace under an already existing window.

step 12 Add more clients and servers to the network using the same CA, if required.

privca Cert Creation Tool

privca Usage Document

show/hide details

To keep user required actions to a minumum, the simple bash script privca satisfies the following conditions:

  • No configuration files are used, minimal command line parameters only.
  • Two levels of certificates only, no intermediate certificates.
  • Certificate file names correspond to their certificate “Common names”, and leaf “common names” include their CA “common names”

The program must be executed in the directory which will be used for certificate storage.
Typically the directory would have the same name as the root CA common name. The certificate file owner will be set the program executor. All calls should made by the same executor, e.g., all as user, or all as sudo.

  • CreateCA <CA common name> <CA organization name>
    • Only one CA is allowed per directory.
    • No password is used for the CA private key. To enable it, modify the privca source to remove argument -nodes in the call to openssl in the function CreateCA.
    • <CA common name> will be the CA common name parameter, which will also be used in the file name. No spaces or non-filename characters should be used.
    • <CA organization name> is only used as a category index by the Firefox browser. If you create multiple CA’s and always use the same CA organization name, then the various CA’s with different common names will appear together under the shared organization name in the browser’s Authorities list.
    • The following files will be created:
    • <Server common name>.key : the CA private key
    • <Server common name>.crt : the CA public cert
  • CreateServer <Server common name> <subjectAltNames>
    • The CA must have been already created. It will be used.
    • <Server common name> will be the sever common name parameter, and that will also be used in the filename. No spaces or non-filename character should be used.
    • The following files will be created:
    • <Server common name>.key : server private key file
    • <Server common name>.crt : server public cert
    • <Server common name>.key-crt.pem : combined key and cert A file <Server common name>.key-crt.pem will be created. This file needs to copied to and configured by the lighttpd server with the ssl.pem-file parameter. It functions as the server certificate.
  • CreateClient <Client common name>
    • The CA must have been already created. It will be used.
    • <Client common name> will be the common name parameter, and that will also be used for the filename. No spaces or non-filename character should be used.
    • The following files will be created:
    • <Client common name>.key : server private key file
    • <Client common name>.crt : server public cert
    • <Client common name>.pks12 : combined key and cert

An example final directory tree:

# tree
├── ca
│ ├── private
│ │ └── Root1.key
│ └── public
│ └── Root1.crt
├── export
│ ├── private
│ │ ├── Client1.p12
│ │ └── Server1.key-crt.pem
│ └── public
│ └── Root1.crt -> ./ca/public/Root1.crt
├── private
│ ├── Client1.key
│ └── Server1.key
├── public
│ ├── Client1.crt
│ └── Server1.crt
└── temp
├── Client1.csr
└── Server1.csr

9 directories, 11 files

privca Source Code

step 1 Copy privca to your platform. Place in search path - e.g. /usr/local/sbin/.

step 2 Create a directory which will contain the private CA and all the leaf certificates the CA issues. Change to that directory. E.g., /etc/ssl/privca.d/HomeLan

sudo mkdir /etc/ssl/privca.d/HomeLan
cd /etc/ssl/privca.d/HomeLan

step 3 Create the CA cert.

privca CreateCA HomeLan MyOrg

step 4 Create the Server cert.

privca CreateServer PiSrv DNS:pihole.home.lan,DNS:pihole,IP:

Note: here it is assumed that you already have local DNS functionality to recognize pihole.home.lan and pihole. Test any address, including the IP, with ping.

step 5 Create the Client cert. Prepare a password to set for the .p12 certificate. You will need to use it again when uploading the certificate to Firefox browser. Hit return only to set no password.

privca CreateClient Client1
<interactively enter password>