Client Certificate Authentication Setup

Client certificate authentication (mutual TLS) requires both server and client to present valid certificates, providing strong two-factor authentication. This guide covers CA creation, client certificate generation, Nginx/Apache configuration, and CRL management.

Table of Contents

Prerequisites

Before setting up client certificate authentication, ensure you have:

  • OpenSSL installed
  • Root or sudo access
  • Web server (Nginx, Apache)
  • Understanding of PKI concepts
  • Test clients supporting client certificates

Creating a Private CA

Create a Certificate Authority for signing client certificates.

Create CA directory:

mkdir -p ~/ca/{certs,csr,newcerts,private}
cd ~/ca

chmod 700 private
touch index.txt
echo 1000 > serial

Generate CA private key:

openssl genrsa -aes256 -out private/ca.key 4096

Enter passphrase when prompted.

Create CA certificate (self-signed):

openssl req -config /etc/ssl/openssl.cnf \
  -key private/ca.key \
  -new -x509 -days 3650 \
  -extensions v3_ca \
  -out certs/ca.crt \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=Client-CA"

Verify CA certificate:

openssl x509 -noout -text -in certs/ca.crt

Generating Client Certificates

Generate individual client certificates for authentication.

Create client certificate configuration:

cat > client.conf <<EOF
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = req_distinguished_name
req_extensions = v3_req

[req_distinguished_name]
C = US
ST = State
L = City
O = Organization
CN = client-user-1

[v3_req]
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth

[client_cert]
basicConstraints = CA:FALSE
nsCertType = client
nsComment = "Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF

Generate client private key:

openssl genrsa -out private/client-user-1.key 2048

Create client certificate signing request:

openssl req -config client.conf \
  -key private/client-user-1.key \
  -new -out csr/client-user-1.csr

Sign client certificate with CA:

openssl x509 -req -in csr/client-user-1.csr \
  -CA certs/ca.crt -CAkey private/ca.key \
  -CAcreateserial -out certs/client-user-1.crt \
  -days 730 -sha256 \
  -extfile client.conf -extensions client_cert

Create PKCS12 keystore for browser/application import:

openssl pkcs12 -export \
  -in certs/client-user-1.crt \
  -inkey private/client-user-1.key \
  -out certs/client-user-1.p12 \
  -name "client-user-1" \
  -passout pass:clientpassword

Verify client certificate:

openssl x509 -noout -text -in certs/client-user-1.crt

Nginx Client Certificate Setup

Configure Nginx to require client certificate authentication.

Nginx configuration:

server {
    listen 443 ssl http2;
    server_name api.example.com;
    
    # Server certificate
    ssl_certificate /etc/ssl/certs/server.crt;
    ssl_certificate_key /etc/ssl/private/server.key;
    
    # Client certificate verification
    ssl_client_certificate /etc/ssl/certs/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;
    
    # Trusted client CA bundle
    ssl_trusted_certificate /etc/ssl/certs/ca.crt;
    
    # Session configuration
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;
    
    # Pass client certificate info to backend
    location / {
        # Client certificate as header
        proxy_set_header SSL-Client-Cert $ssl_client_cert;
        proxy_set_header SSL-Client-S-DN $ssl_client_s_dn;
        proxy_set_header SSL-Client-Verify $ssl_client_verify;
        
        proxy_pass http://backend:8080;
    }
}

Optional client verification (allow requests without cert):

ssl_verify_client optional;
ssl_verify_client_depth 2;

location /public {
    # Public endpoint, certificate not required
    proxy_pass http://backend:8080;
}

location /private {
    # Private endpoint, certificate required
    if ($ssl_client_verify != SUCCESS) {
        return 403 "Client certificate required";
    }
    proxy_pass http://backend:8080;
}

Test Nginx configuration:

sudo nginx -t
sudo systemctl reload nginx

Apache Client Certificate Setup

Configure Apache for client certificate authentication.

Apache VirtualHost configuration:

<VirtualHost *:443>
    ServerName api.example.com
    
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/server.crt
    SSLCertificateKeyFile /etc/ssl/private/server.key
    
    # Client certificate verification - REQUIRED
    SSLVerifyClient require
    SSLVerifyDepth 2
    SSLCACertificatePath /etc/ssl/certs
    SSLCACertificateFile /etc/ssl/certs/ca.crt
    
    # Policy for client certificates
    SSLRequire %{SSL_CLIENT_VERIFY} eq "SUCCESS"
    
    # Pass client certificate to backend
    <IfModule mod_headers.c>
        RequestHeader set X-SSL-Client-Cert %{SSL_CLIENT_CERT}e
        RequestHeader set X-SSL-Client-S-DN %{SSL_CLIENT_S_DN}e
        RequestHeader set X-SSL-Client-Verify %{SSL_CLIENT_VERIFY}e
    </IfModule>
    
    # Proxy to backend
    ProxyPreserveHost On
    ProxyPass / http://localhost:8080/
    ProxyPassReverse / http://localhost:8080/
</VirtualHost>

Optional client verification:

<VirtualHost *:443>
    ServerName api.example.com
    
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/server.crt
    SSLCertificateKeyFile /etc/ssl/private/server.key
    
    # Optional client certificate verification
    SSLVerifyClient optional
    SSLVerifyDepth 2
    SSLCACertificateFile /etc/ssl/certs/ca.crt
    
    <Location /public>
        # No certificate required
        ProxyPass http://localhost:8080/public
    </Location>
    
    <Location /private>
        # Certificate required
        SSLRequire %{SSL_CLIENT_VERIFY} eq "SUCCESS"
        ProxyPass http://localhost:8080/private
    </Location>
</VirtualHost>

Enable modules:

sudo a2enmod ssl
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo systemctl reload apache2

Certificate Revocation Lists

Revoke compromised client certificates using CRL.

Initialize CRL database:

cd ~/ca
touch index.txt.attr
echo "unique_subject = no" > index.txt.attr

Revoke a client certificate:

openssl ca -config /etc/ssl/openssl.cnf \
  -revoke certs/client-user-1.crt \
  -keyfile private/ca.key \
  -cert certs/ca.crt

Generate CRL (Certificate Revocation List):

openssl ca -config /etc/ssl/openssl.cnf \
  -gencrl -out certs/ca.crl \
  -keyfile private/ca.key \
  -cert certs/ca.crt \
  -crldays 30

View CRL contents:

openssl crl -in certs/ca.crl -text -noout

Deploy CRL to web servers:

sudo cp ~/ca/certs/ca.crl /etc/ssl/certs/
sudo chown root:root /etc/ssl/certs/ca.crl
sudo chmod 644 /etc/ssl/certs/ca.crl

Configure Nginx with CRL:

ssl_crl /etc/ssl/certs/ca.crl;
ssl_verify_client on;

Configure Apache with CRL:

SSLCARevocationFile /etc/ssl/certs/ca.crl
SSLCARevocationCheck chain

Automate CRL generation:

#!/bin/bash
# /usr/local/bin/update-crl.sh

cd ~/ca
openssl ca -config /etc/ssl/openssl.cnf \
  -gencrl -out certs/ca.crl \
  -keyfile private/ca.key \
  -cert certs/ca.crt \
  -crldays 30

# Deploy
sudo cp certs/ca.crl /etc/ssl/certs/
sudo systemctl reload nginx
sudo systemctl reload apache2

Add to crontab:

# Regenerate CRL weekly
0 2 * * 0 /usr/local/bin/update-crl.sh

Testing Client Authentication

Test client certificate authentication.

Test with curl:

# Without client certificate (should fail)
curl https://api.example.com
# Output: curl: (60) SSL: certificate problem

# With client certificate
curl --cert certs/client-user-1.crt \
  --key private/client-user-1.key \
  https://api.example.com

# With PKCS12 keystore
curl --cert certs/client-user-1.p12:clientpassword \
  https://api.example.com

Test with openssl:

# Show client certificate in handshake
openssl s_client -connect api.example.com:443 \
  -cert certs/client-user-1.crt \
  -key private/client-user-1.key \
  -showcerts

Test with Python:

import requests
from requests.auth import HTTPCertAuth

# Requests library
response = requests.get(
    'https://api.example.com',
    cert=('client.crt', 'client.key'),
    verify='/path/to/ca.crt'
)

# Using PKCS12
import ssl
context = ssl.create_default_context()
context.load_cert_chain('client.p12', password='password')

import urllib.request
opener = urllib.request.build_opener(
    urllib.request.HTTPSHandler(context=context)
)
response = opener.open('https://api.example.com')

Troubleshooting

Certificate verification failures:

# Check certificate chain
openssl verify -CAfile ~/ca/certs/ca.crt \
  ~/ca/certs/client-user-1.crt

# Verify certificate against CA
openssl x509 -in ~/ca/certs/client-user-1.crt \
  -issuer -noout

Common issues:

# Certificate expired
openssl x509 -in client.crt -noout -dates

# Wrong CA configured
openssl verify -CAfile /etc/ssl/certs/ca.crt client.crt

# Certificate revoked
openssl verify -CAfile ca.crt -CRLfile ca.crl client.crt

Nginx debugging:

# Enable SSL debug
error_log /var/log/nginx/error.log debug;

# Check for certificate validation errors
sudo tail -f /var/log/nginx/error.log

Apache debugging:

# Enable SSL debug
LogLevel ssl:debug

# Check for certificate validation errors
sudo tail -f /var/log/apache2/error.log | grep SSL

Automation

Automate client certificate generation and deployment.

Create certificate generation script:

#!/bin/bash
# /usr/local/bin/generate-client-cert.sh

USERNAME=$1

if [ -z "$USERNAME" ]; then
    echo "Usage: $0 <username>"
    exit 1
fi

cd ~/ca

# Generate key
openssl genrsa -out private/${USERNAME}.key 2048

# Create CSR
openssl req -config client.conf \
  -key private/${USERNAME}.key \
  -new -out csr/${USERNAME}.csr \
  -subj "/C=US/ST=State/L=City/O=Organization/CN=${USERNAME}"

# Sign certificate
openssl x509 -req -in csr/${USERNAME}.csr \
  -CA certs/ca.crt -CAkey private/ca.key \
  -CAcreateserial -out certs/${USERNAME}.crt \
  -days 730 -sha256 \
  -extfile client.conf -extensions client_cert

# Create PKCS12
openssl pkcs12 -export \
  -in certs/${USERNAME}.crt \
  -inkey private/${USERNAME}.key \
  -out certs/${USERNAME}.p12 \
  -name "${USERNAME}" \
  -passout pass:password123

echo "Certificate generated: certs/${USERNAME}.p12"

Make executable and use:

chmod +x /usr/local/bin/generate-client-cert.sh
/usr/local/bin/generate-client-cert.sh user-123

Conclusion

Client certificate authentication provides strong mutual authentication without password-related vulnerabilities. This guide covered CA creation, client certificate generation, Nginx/Apache configuration, CRL management, and testing. For production deployments, implement proper key management, rotate certificates regularly, maintain CRLs, monitor certificate expiration, and automate certificate generation and revocation. Client certificate authentication excels for securing APIs, microservices, and internal communication channels requiring strong authentication.