Mutual TLS (mTLS) is the gold standard for service-to-service authentication in self-hosted infrastructure. Unlike standard TLS, which only verifies the server’s identity, mTLS requires both the client and server to present and validate certificates — ensuring that only authorized services can communicate with each other.
Whether you’re securing microservices, protecting internal APIs, or building a zero-trust network on bare metal, this guide walks through mTLS setup across four popular self-hosted proxies: nginx, Caddy, Traefik, and Envoy. Each offers different tradeoffs in complexity, automation, and ecosystem fit.
Here’s how the four tools compare in terms of community adoption:
| Project | GitHub Stars | Last Updated | Primary Language |
|---|---|---|---|
| Caddy | 71,787 | April 2026 | Go |
| Traefik | 62,846 | April 2026 | Go |
| nginx | 30,051 | April 2026 | C |
| Envoy | 27,884 | April 2026 | C++ |
Why Self-Host mTLS
Running your own mTLS infrastructure gives you complete control over certificate lifecycle, trust anchors, and access policies — without depending on external certificate authorities or service mesh vendors. Here’s why self-hosted mTLS matters:
- Zero-trust architecture: Every service must prove its identity before any data is exchanged. Network perimeter security is no longer sufficient.
- Regulatory compliance: Standards like PCI DSS, HIPAA, and SOC 2 increasingly require mutual authentication for sensitive data flows.
- Cost control: Commercial service mesh platforms (Istio, Linkerd) add operational overhead. Simple mTLS via a reverse proxy handles most use cases without the complexity.
- Private CA control: You manage the root of trust. Certificates never leave your infrastructure, and revocation is immediate.
- Defense in depth: Even if an attacker gains network access, they cannot impersonate a service without a valid client certificate signed by your CA.
For related reading on securing self-hosted infrastructure, see our web application firewall comparison and TLS termination proxy guide.
How mTLS Works
Before diving into configurations, here’s a quick overview of the mTLS handshake:
- Client connects to the server and requests a TLS session.
- Server presents its certificate to the client (standard TLS).
- Server requests the client’s certificate via the
CertificateRequestmessage. - Client presents its certificate to the server.
- Both sides validate each other’s certificates against their trusted CA bundles.
- Encrypted communication begins — only if both certificates are valid and trusted.
The key difference from standard TLS is step 3-5: the server actively requests and validates the client’s certificate. If the client has no certificate, or its certificate isn’t signed by a trusted CA, the connection is rejected.
Certificate Infrastructure You’ll Need
Every mTLS setup requires three certificate components:
- Root CA: A self-signed certificate authority that signs both server and client certificates.
- Server certificate: Issued by the Root CA, presented by the proxy to clients.
- Client certificates: Issued by the Root CA, presented by each service or user connecting to the proxy.
For automated certificate management, check our cert-manager vs Lego vs ACME.sh comparison.
Prerequisites: Generate a Root CA and Certificates
Before configuring any proxy, you need a certificate authority. Here’s how to generate one with OpenSSL:
| |
You now have: ca.crt (trusted by both sides), server.crt/server.key (for the proxy), and client.crt/client.key (for connecting services).
nginx mTLS Configuration
nginx is the most battle-tested option with straightforward mTLS directives. It’s ideal when you need simple, reliable mutual authentication without additional dependencies.
Docker Compose Setup
| |
nginx Configuration
| |
Testing nginx mTLS
| |
Caddy mTLS Configuration
Caddy stands out for its automatic TLS and mTLS capabilities with minimal configuration. Its native tls directive handles both server certificates and client verification in just a few lines.
Docker Compose Setup
| |
Caddyfile Configuration
| |
Caddy with Automatic Client Certificate Issuance
For advanced use cases, Caddy can also act as the CA and issue client certificates on demand using the On-Demand TLS feature combined with a custom CA module:
| |
Caddy’s simplicity makes it ideal for teams that want mTLS without managing OpenSSL manually. It handles certificate rotation automatically when paired with its built-in ACME support for the server certificate.
Traefik mTLS Configuration
Traefik provides mTLS through its TLS options system, making it a natural fit for Docker and Kubernetes environments where dynamic service discovery is essential.
Docker Compose Setup
| |
traefik.yaml (Main Configuration)
| |
tls-config.yaml (mTLS Options)
| |
Traefik’s label-based routing means you can enable or disable mTLS per-service by adding or removing the tls.options label. This granular control is valuable when migrating a mixed environment — some services require mTLS while others remain on standard TLS during a transition period.
Envoy mTLS Configuration
Envoy is the most powerful option for mTLS, offering deep traffic management, observability, and advanced certificate validation. It’s the foundation of many service mesh data planes (Istio, Linkerd).
Docker Compose Setup
| |
envoy.yaml Configuration
| |
Envoy’s require_client_certificate: true enforces mTLS at the transport layer. The configuration is more verbose than nginx or Caddy, but it unlocks advanced features like certificate-based routing, SPIFFE/SPIRE identity integration, and fine-grained access control policies.
mTLS Comparison: Choosing the Right Tool
| Feature | nginx | Caddy | Traefik | Envoy |
|---|---|---|---|---|
| Configuration complexity | Low | Very Low | Medium | High |
| mTLS directives | ssl_verify_client | client_auth block | clientAuth in TLS options | DownstreamTlsContext |
| Per-route mTLS | Yes (via location blocks) | Yes (via site blocks) | Yes (via labels) | Yes (via filter chains) |
| Automatic cert issuance | No (manual or certbot) | Built-in (ACME) | Via ACME/Let’s Encrypt | No |
| Docker integration | Basic | Basic | Native (Docker provider) | Basic |
| Kubernetes support | Ingress controller | Limited | Ingress controller | Service mesh data plane |
| Certificate rotation | Manual reload | Automatic | Hot reload | Hot reload |
| Observability | Access logs | Structured JSON logs | Metrics + dashboard | Prometheus + detailed stats |
| Best for | Simple, reliable setups | Quick deployment, small teams | Docker/K8s environments | Service mesh, advanced routing |
When to Use Each
Choose nginx when you need a proven, lightweight solution with straightforward configuration. It’s the most widely deployed and has the largest community for troubleshooting.
Choose Caddy when simplicity is the priority. Its declarative Caddyfile handles mTLS in under 10 lines, and automatic certificate management reduces operational overhead.
Choose Traefik when you’re running containerized workloads and want mTLS with dynamic service discovery. Its label-based configuration pairs naturally with Docker Compose and Kubernetes.
Choose Envoy when you need advanced traffic management, observability, or plan to integrate with a service mesh. It’s the most powerful but requires the most configuration expertise.
For deeper infrastructure security, also consider our service mesh guide and fail2ban vs Crowdsec comparison.
Client Certificate Management at Scale
Managing individual client certificates becomes unwieldy beyond a handful of services. Here are practical strategies for production:
Certificate Naming Convention
Use the certificate Common Name (CN) or Subject Alternative Name (SAN) to encode service identity:
| |
Certificate Expiry Monitoring
Set up automated checks for expiring client certificates:
| |
Revocation
When a service is decommissioned or compromised, revoke its certificate. You have two options:
- CRL (Certificate Revocation List): Maintain a list of revoked serial numbers. All proxies check this list on every connection.
- Short-lived certificates: Issue certificates with 24-hour validity and rotate automatically. No revocation needed — expired certs are rejected.
For short-lived certificates, consider using step-ca or Vault’s PKI secrets engine as an internal CA with built-in renewal APIs.
FAQ
What is the difference between TLS and mTLS?
Standard TLS (Transport Layer Security) only authenticates the server to the client — this is what secures HTTPS websites. mTLS (Mutual TLS) adds a second layer: the server also verifies the client’s certificate. Both sides prove their identity before any data is exchanged.
Can I use Let’s Encrypt certificates for mTLS?
Let’s Encrypt only issues server certificates, not client certificates. For mTLS, you need your own Certificate Authority (CA) to sign client certificates. You can still use Let’s Encrypt for the server certificate while using a self-signed CA for client authentication.
Do I need to restart nginx after adding or revoking client certificates?
If you add or remove entries from the CA bundle (ssl_client_certificate), you need to reload nginx with nginx -s reload. However, if you’re using CRL-based revocation, you can update the CRL file and reload without regenerating the CA bundle. Caddy and Traefik support hot-reloading of TLS configuration without restart.
How do I test mTLS without a real backend service?
You can use a simple echo server to verify mTLS is working. Run python3 -m http.server 8080 as a backend, or use the traefik/whoami Docker image which returns request headers — useful for verifying that X-Client-CN headers are being forwarded correctly.
Is mTLS enough for complete service-to-service security?
mTLS provides authentication and encryption in transit, but a complete zero-trust architecture also needs authorization (what each service is allowed to do), auditing (logging all connections), and potentially encryption at rest. Consider pairing mTLS with API authorization middleware, centralized logging, and network segmentation for defense in depth.
Can I disable mTLS for specific endpoints?
Yes. In nginx, use ssl_verify_client off; inside specific location blocks. In Caddy, create a separate site block or matcher without client_auth. In Traefik, apply different TLS options per-route using labels. In Envoy, configure separate filter chains with different require_client_certificate settings.