HashiCorp Vault — mTLS
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