HAProxy mTLS: Securing API Access with Mutual TLS Authentication

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:

  1. Server presents its certificate to the client
  2. Client verifies the server certificate
  3. Client presents its certificate to the server
  4. 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

  1. SSL alert handshake failure: Certificate not signed by trusted CA
  2. SSL certificate verify failed: CA certificate mismatch
  3. Connection reset: Client didn't provide certificate when required
  4. 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 required for mandatory client certificates
  • Use verify optional for 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.

Read more

HAProxy Monitoring with Prometheus: Complete Observability Guide

HAProxy Monitoring with Prometheus: Complete Observability Guide

Monitoring HAProxy is essential for maintaining reliable load balancing infrastructure. Prometheus provides powerful metrics collection, alerting capabilities, and seamless Grafana integration for visualizing HAProxy performance and health. Why Prometheus for HAProxy? Prometheus offers: * Pull-based metrics - Prometheus scrapes HAProxy metrics endpoints * Time-series database - Store historical data for trend analysis

By Patrick de Ruiter