HashiCorp Vault — mTLS

2 minute read

Mutual TLS (mTLS) requires both client and server to present certificates, providing authentication in both directions. Vault PKI is ideal for issuing the certificates needed.


How mTLS Works

Standard TLS:
  Client ────▶ Server presents cert ────▶ Client verifies
  (Server authenticated, client anonymous)

Mutual TLS:
  Client ────▶ Server presents cert ────▶ Client verifies ✅
  Server ────▶ Client presents cert ────▶ Server verifies ✅
  (Both sides authenticated)

Setup Roles for mTLS

# Server certificate role
vault write pki_int/roles/server-cert \
    allowed_domains="service.internal" \
    allow_subdomains=true \
    max_ttl=72h \
    server_flag=true \
    client_flag=false \
    key_type=ec \
    key_bits=256

# Client certificate role
vault write pki_int/roles/client-cert \
    allowed_domains="service.internal" \
    allow_subdomains=true \
    max_ttl=72h \
    server_flag=false \
    client_flag=true \
    key_type=ec \
    key_bits=256

Issue Certificates

# Server cert
vault write -format=json pki_int/issue/server-cert \
    common_name="api.service.internal" \
    ttl=24h > /tmp/server-cert.json

jq -r '.data.certificate' /tmp/server-cert.json > server.crt
jq -r '.data.private_key' /tmp/server-cert.json > server.key

# Client cert
vault write -format=json pki_int/issue/client-cert \
    common_name="webapp.service.internal" \
    ttl=24h > /tmp/client-cert.json

jq -r '.data.certificate' /tmp/client-cert.json > client.crt
jq -r '.data.private_key' /tmp/client-cert.json > client.key

# CA certificate (both sides need this)
vault read -field=certificate pki_int/ca/pem > ca.crt

Test mTLS with curl

# Server configured for mTLS (e.g., Nginx)
# Client connects with its certificate:
curl --cacert ca.crt \
     --cert client.crt \
     --key client.key \
     https://api.service.internal:8443/health

Nginx mTLS Configuration

server {
    listen 8443 ssl;
    server_name api.service.internal;

    # Server certificate
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Client certificate verification (mTLS)
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    location / {
        proxy_pass http://localhost:8080;
        # Pass client DN to backend
        proxy_set_header X-Client-DN $ssl_client_s_dn;
    }
}

Go Server Example

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "net/http"
    "os"
)

func main() {
    // Load CA cert
    caCert, _ := os.ReadFile("ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
        ClientCAs:  caCertPool,
        ClientAuth: tls.RequireAndVerifyClientCert,
    }

    server := &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            cn := r.TLS.PeerCertificates[0].Subject.CommonName
            fmt.Fprintf(w, "Hello, %s!\n", cn)
        }),
    }

    server.ListenAndServeTLS("server.crt", "server.key")
}

mTLS in Microservices

┌────────────┐  mTLS   ┌────────────┐  mTLS   ┌────────────┐
│  Frontend  │────────▶│    API     │────────▶│  Database  │
│  Service   │         │  Gateway   │         │  Proxy     │
│            │         │            │         │            │
│ client cert│         │server+client│        │ server cert│
└────────────┘         └────────────┘         └────────────┘

Each service has:
• Its own cert (issued by Vault)
• The CA cert (to verify peers)
• Short-lived certs (24h) auto-rotated

Quick Reference

Command Description
vault write pki_int/roles/NAME server_flag=true client_flag=false Server cert role
vault write pki_int/roles/NAME server_flag=false client_flag=true Client cert role
vault write pki_int/issue/ROLE common_name=CN Issue certificate
vault read -field=certificate pki_int/ca/pem Get CA cert

Next: Practice Exercises