HAProxy mTLS: Securing API Access with Mutual TLS Authentication
Mutual TLS (mTLS) provides strong authentication by requiring both client and server to present certificates. This guide shows how to configure HAProxy for mTLS to secure API endpoints, internal services, and zero-trust networks.
What is mTLS?
In standard TLS, only the server presents a certificate. With mTLS:
- Server presents its certificate to the client
- Client verifies the server certificate
- Client presents its certificate to the server
- Server verifies the client certificate
Both parties are authenticated, providing strong identity verification.
Use Cases for mTLS
- Service-to-service communication: Microservices authenticating each other
- API security: Authenticating API clients without passwords
- Zero-trust networks: Verifying identity at every connection
- IoT devices: Authenticating embedded devices
- B2B integrations: Partner API access with certificate-based auth
Certificate Infrastructure Setup
Create a Certificate Authority (CA)
First, create your own CA for signing client certificates:
# Create CA directory structure
mkdir -p /etc/haproxy/ca/{certs,crl,newcerts,private,csr}
chmod 700 /etc/haproxy/ca/private
touch /etc/haproxy/ca/index.txt
echo 1000 > /etc/haproxy/ca/serial
# Create CA configuration
cat > /etc/haproxy/ca/openssl.cnf << 'EOF'
[ca]
default_ca = CA_default
[CA_default]
dir = /etc/haproxy/ca
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
private_key = $dir/private/ca.key
certificate = $dir/certs/ca.crt
default_md = sha256
default_days = 365
preserve = no
policy = policy_strict
[policy_strict]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[req]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256
x509_extensions = v3_ca
[req_distinguished_name]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name
emailAddress = Email Address
[v3_ca]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[usr_cert]
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[server_cert]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
EOF
# Generate CA private key
openssl genrsa -aes256 -out /etc/haproxy/ca/private/ca.key 4096
chmod 400 /etc/haproxy/ca/private/ca.key
# Generate CA certificate
openssl req -config /etc/haproxy/ca/openssl.cnf \
-key /etc/haproxy/ca/private/ca.key \
-new -x509 -days 3650 -sha256 \
-extensions v3_ca \
-out /etc/haproxy/ca/certs/ca.crt \
-subj "/C=US/ST=State/L=City/O=MyOrg/CN=MyOrg CA"
Generate Server Certificate
# Generate server key
openssl genrsa -out /etc/haproxy/certs/server.key 2048
# Generate server CSR
openssl req -new \
-key /etc/haproxy/certs/server.key \
-out /etc/haproxy/ca/csr/server.csr \
-subj "/CN=api.example.com"
# Sign server certificate with CA
openssl ca -config /etc/haproxy/ca/openssl.cnf \
-extensions server_cert \
-days 365 -notext -md sha256 \
-in /etc/haproxy/ca/csr/server.csr \
-out /etc/haproxy/certs/server.crt \
-batch
# Create combined PEM for HAProxy
cat /etc/haproxy/certs/server.crt \
/etc/haproxy/certs/server.key \
> /etc/haproxy/certs/server.pem
chmod 600 /etc/haproxy/certs/server.pem
Generate Client Certificate
#!/bin/bash
# /usr/local/bin/generate-client-cert.sh
CLIENT_NAME="$1"
if [ -z "$CLIENT_NAME" ]; then
echo "Usage: $0 <client-name>"
exit 1
fi
CA_DIR="/etc/haproxy/ca"
OUTPUT_DIR="/etc/haproxy/clients/$CLIENT_NAME"
mkdir -p "$OUTPUT_DIR"
# Generate client key
openssl genrsa -out "$OUTPUT_DIR/$CLIENT_NAME.key" 2048
# Generate client CSR
openssl req -new \
-key "$OUTPUT_DIR/$CLIENT_NAME.key" \
-out "$CA_DIR/csr/$CLIENT_NAME.csr" \
-subj "/CN=$CLIENT_NAME"
# Sign client certificate
openssl ca -config "$CA_DIR/openssl.cnf" \
-extensions usr_cert \
-days 365 -notext -md sha256 \
-in "$CA_DIR/csr/$CLIENT_NAME.csr" \
-out "$OUTPUT_DIR/$CLIENT_NAME.crt" \
-batch
# Create combined PEM (for applications that need it)
cat "$OUTPUT_DIR/$CLIENT_NAME.crt" \
"$OUTPUT_DIR/$CLIENT_NAME.key" \
> "$OUTPUT_DIR/$CLIENT_NAME.pem"
# Create PKCS12 bundle (for browsers and some clients)
openssl pkcs12 -export \
-out "$OUTPUT_DIR/$CLIENT_NAME.p12" \
-inkey "$OUTPUT_DIR/$CLIENT_NAME.key" \
-in "$OUTPUT_DIR/$CLIENT_NAME.crt" \
-certfile "$CA_DIR/certs/ca.crt" \
-passout pass:changeme
echo "Client certificate generated in $OUTPUT_DIR"
echo "Files: $CLIENT_NAME.key, $CLIENT_NAME.crt, $CLIENT_NAME.pem, $CLIENT_NAME.p12"
Basic mTLS Configuration
Required mTLS
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
user haproxy
group haproxy
daemon
# SSL/TLS settings
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
timeout connect 5s
timeout client 50s
timeout server 50s
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# Client certificate is required - connections without valid cert are rejected
default_backend api_servers
backend api_servers
server api1 192.168.1.10:8080 check
server api2 192.168.1.11:8080 check
Optional mTLS (Certificate if Available)
frontend mtls_optional
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify optional
# Check if client provided valid certificate
acl client_cert_valid ssl_c_verify 0
acl client_cert_provided ssl_fc_has_crt
# Route based on certificate status
use_backend authenticated_api if client_cert_valid
use_backend public_api unless client_cert_provided
# Reject invalid certificates
http-request deny if client_cert_provided !client_cert_valid
backend authenticated_api
server api1 192.168.1.10:8080 check
backend public_api
server public1 192.168.1.20:8080 check
Passing Client Certificate Info to Backend
Certificate Details as Headers
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# Client certificate subject (DN)
http-request set-header X-Client-DN %{+Q}[ssl_c_s_dn]
# Client certificate common name
http-request set-header X-Client-CN %{+Q}[ssl_c_s_dn(cn)]
# Certificate serial number
http-request set-header X-Client-Serial %[ssl_c_serial,hex]
# Certificate fingerprint (SHA1)
http-request set-header X-Client-Fingerprint %[ssl_c_sha1,hex]
# Certificate validity dates
http-request set-header X-Client-NotBefore %[ssl_c_notbefore]
http-request set-header X-Client-NotAfter %[ssl_c_notafter]
# Issuer DN
http-request set-header X-Client-Issuer %{+Q}[ssl_c_i_dn]
# Verification status (0 = success)
http-request set-header X-Client-Verify %[ssl_c_verify]
default_backend api_servers
Full Certificate as Header (Base64)
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# Pass entire certificate (PEM format, base64 encoded)
http-request set-header X-Client-Cert %[ssl_c_der,base64]
default_backend api_servers
Certificate-Based Authorization
Route by Client CN
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# ACLs based on client certificate CN
acl cn_admin ssl_c_s_dn(cn) -i admin-service
acl cn_api ssl_c_s_dn(cn) -i api-client
acl cn_monitoring ssl_c_s_dn(cn) -i monitoring
# Route to different backends
use_backend admin_backend if cn_admin
use_backend monitoring_backend if cn_monitoring
default_backend api_backend
Restrict Access by Certificate OU
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# Check organizational unit
acl ou_internal ssl_c_s_dn(ou) -i internal-services
acl ou_partner ssl_c_s_dn(ou) -i partner-api
acl ou_admin ssl_c_s_dn(ou) -i administrators
# Restrict admin endpoints
acl admin_path path_beg /admin
http-request deny if admin_path !ou_admin
# Partner access restrictions
acl partner_path path_beg /api/partner
http-request deny if partner_path !ou_partner
default_backend api_servers
Whitelist Specific Certificates
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem ca-file /etc/haproxy/ca/certs/ca.crt verify required
# Define allowed certificate fingerprints
acl allowed_cert ssl_c_sha1,hex -f /etc/haproxy/allowed-certs.txt
# Reject certificates not in whitelist
http-request deny unless allowed_cert
default_backend api_servers
Create the whitelist file:
# /etc/haproxy/allowed-certs.txt
# One SHA1 fingerprint per line
A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0
9T8S7R6Q5P4O3N2M1L0K9J8I7H6G5F4E3D2C1B0A
Certificate Revocation
Using CRL (Certificate Revocation List)
# Generate initial CRL
openssl ca -config /etc/haproxy/ca/openssl.cnf \
-gencrl -out /etc/haproxy/ca/crl/ca.crl
# Revoke a certificate
openssl ca -config /etc/haproxy/ca/openssl.cnf \
-revoke /etc/haproxy/clients/bad-client/bad-client.crt
# Regenerate CRL
openssl ca -config /etc/haproxy/ca/openssl.cnf \
-gencrl -out /etc/haproxy/ca/crl/ca.crl
frontend mtls_api
bind *:443 ssl crt /etc/haproxy/certs/server.pem \
ca-file /etc/haproxy/ca/certs/ca.crt \
crl-file /etc/haproxy/ca/crl/ca.crl \
verify required
default_backend api_servers
Automated CRL Updates
#!/bin/bash
# /usr/local/bin/update-crl.sh
CA_DIR="/etc/haproxy/ca"
CRL_FILE="$CA_DIR/crl/ca.crl"
# Regenerate CRL
openssl ca -config "$CA_DIR/openssl.cnf" \
-gencrl -out "$CRL_FILE"
# Reload HAProxy
systemctl reload haproxy
Schedule with cron:
# Update CRL every hour
0 * * * * /usr/local/bin/update-crl.sh
Complete Production Configuration
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 50000
nbthread 4
# Modern TLS settings
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
tune.ssl.default-dh-param 2048
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
timeout connect 5s
timeout client 50s
timeout server 50s
# Stats
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
http-request use-service prometheus-exporter if { path /metrics }
# Public HTTPS (no client cert required)
frontend public_https
bind *:443 ssl crt /etc/haproxy/certs/public.pem alpn h2,http/1.1
# Route to mTLS frontend for API paths
acl api_path path_beg /api
use_backend mtls_redirect if api_path
default_backend public_web
# mTLS API endpoint
frontend mtls_api
bind *:8443 ssl crt /etc/haproxy/certs/server.pem \
ca-file /etc/haproxy/ca/certs/ca.crt \
crl-file /etc/haproxy/ca/crl/ca.crl \
verify required \
alpn h2,http/1.1
# Log client certificate info
capture request header X-Client-CN len 50
log-format "%ci:%cp [%tr] %ft %b/%s %ST %B %TR/%Tw/%Tc/%Tr/%Ta client_cn:%{+Q}[ssl_c_s_dn(cn)] %r"
# Add client info headers
http-request set-header X-Client-CN %{+Q}[ssl_c_s_dn(cn)]
http-request set-header X-Client-DN %{+Q}[ssl_c_s_dn]
http-request set-header X-Client-Serial %[ssl_c_serial,hex]
http-request set-header X-Client-Verify %[ssl_c_verify]
http-request set-header X-Client-Fingerprint %[ssl_c_sha1,hex]
# Certificate-based authorization
acl cn_admin ssl_c_s_dn(cn) -i admin-service
acl cn_readonly ssl_c_s_dn(cn) -m sub readonly
acl ou_internal ssl_c_s_dn(ou) -i internal
acl ou_partner ssl_c_s_dn(ou) -i partner
# Path-based ACLs
acl admin_path path_beg /api/admin
acl write_method method POST PUT DELETE PATCH
# Authorization rules
http-request deny if admin_path !cn_admin
http-request deny if write_method cn_readonly
http-request deny if !ou_internal !ou_partner
# Route based on certificate
use_backend admin_api if cn_admin
use_backend partner_api if ou_partner
default_backend internal_api
# Redirect to mTLS port
backend mtls_redirect
http-request redirect code 301 location https://%[req.hdr(Host)]:8443%[capture.req.uri]
backend public_web
balance roundrobin
server web1 192.168.1.10:8080 check
server web2 192.168.1.11:8080 check
backend internal_api
balance leastconn
option httpchk GET /health
http-check expect status 200
server api1 192.168.1.20:8080 check
server api2 192.168.1.21:8080 check
backend admin_api
option httpchk GET /health
server admin1 192.168.1.30:8080 check
backend partner_api
balance roundrobin
option httpchk GET /health
server partner1 192.168.1.40:8080 check
server partner2 192.168.1.41:8080 check
Testing mTLS
Test with curl
# Test with client certificate
curl -v \
--cert /etc/haproxy/clients/api-client/api-client.crt \
--key /etc/haproxy/clients/api-client/api-client.key \
--cacert /etc/haproxy/ca/certs/ca.crt \
https://api.example.com:8443/api/status
# Test with combined PEM
curl -v \
--cert /etc/haproxy/clients/api-client/api-client.pem \
--cacert /etc/haproxy/ca/certs/ca.crt \
https://api.example.com:8443/api/status
# Test without certificate (should fail)
curl -v \
--cacert /etc/haproxy/ca/certs/ca.crt \
https://api.example.com:8443/api/status
Test with openssl
# Connect and show certificate exchange
openssl s_client \
-connect api.example.com:8443 \
-cert /etc/haproxy/clients/api-client/api-client.crt \
-key /etc/haproxy/clients/api-client/api-client.key \
-CAfile /etc/haproxy/ca/certs/ca.crt \
-verify_return_error
Client Configuration Examples
Python requests
import requests
response = requests.get(
'https://api.example.com:8443/api/data',
cert=('/path/to/client.crt', '/path/to/client.key'),
verify='/path/to/ca.crt'
)
Node.js
const https = require('https');
const fs = require('fs');
const options = {
hostname: 'api.example.com',
port: 8443,
path: '/api/data',
method: 'GET',
cert: fs.readFileSync('/path/to/client.crt'),
key: fs.readFileSync('/path/to/client.key'),
ca: fs.readFileSync('/path/to/ca.crt')
};
const req = https.request(options, (res) => {
// Handle response
});
Go
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"net/http"
)
func main() {
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
caCert, _ := ioutil.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
},
},
}
resp, _ := client.Get("https://api.example.com:8443/api/data")
// Handle response
}
Troubleshooting
Common Errors
- SSL alert handshake failure: Certificate not signed by trusted CA
- SSL certificate verify failed: CA certificate mismatch
- Connection reset: Client didn't provide certificate when required
- Certificate revoked: Certificate is in CRL
Debug Commands
# Verify certificate chain
openssl verify -CAfile ca.crt client.crt
# Check certificate details
openssl x509 -in client.crt -text -noout
# Check CRL contents
openssl crl -in ca.crl -text -noout
# Test SSL connection with debug
openssl s_client -connect api.example.com:8443 -debug
Next Steps
This post covered mTLS for secure API access. In the next post, we'll explore the HAProxy Data Plane API for dynamic configuration management.
Summary
Key points for HAProxy mTLS:
- Use
verify requiredfor mandatory client certificates - Use
verify optionalfor mixed authentication - Pass certificate details to backends via headers
- Implement authorization based on CN, OU, or fingerprint
- Maintain CRL for certificate revocation
- Generate unique certificates for each client/service
- Test thoroughly with curl and openssl
mTLS provides strong mutual authentication without passwords, perfect for service-to-service communication and secure API access.