
September 12, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Your payment service trusts any request that reaches port 8443 on the private network. A compromised container, a mis-routed VPC rule, or a stolen internal DNS entry can impersonate a legitimate caller. Mutual TLS for Service-to-Service Auth closes that gap by requiring both sides to present X.509 certificates during the TLS handshake. The connection never reaches your application code unless cryptographic identity checks pass first. If you already understand one-way TLS from our SSL/TLS certificates guide, mTLS adds a client certificate requirement on top of server verification.
What is Mutual TLS for Service-to-Service Auth?
Standard HTTPS uses TLS in one direction. The browser verifies the server certificate. The server never verifies the client. Service-to-service traffic inside a VPC often repeats that pattern. An attacker who lands on the network can call internal APIs freely.
Mutual TLS flips part of that model. Both peers present certificates. Each peer validates the other's cert chain, expiry, and often custom fields such as SPIFFE IDs or organisational units. Authentication happens at the transport layer. Your JSON body and bearer tokens become a second line of defence, not the only gate.
On production Laravel stacks I maintain, mTLS sits between edge load balancers and internal workers. Public users still authenticate with Sanctum or session cookies. Backend jobs, webhooks, and cron-triggered calls use mTLS at the proxy. That split keeps human auth flexible while machine auth stays strict.
The trust anchor is usually a private certificate authority. Public CAs rarely issue client certs for internal microservices. You run step-ca, HashiCorp Vault PKI, cert-manager with a cluster issuer, or cloud CA services. Each service receives a short-lived cert and private key at deploy time. Rotation becomes an operational routine, not a yearly calendar event.
Core components you need
- Private CA or intermediate: Signs both server and client certificates. Never embed the CA private key on application hosts.
- Server certificate: Identifies the listening service. SAN entries must match internal DNS names such as
payments.internal. - Client certificate: Identifies the calling service. Map CN or URI SAN to an allowed caller list at the proxy.
- Revocation or short TTL: Prefer 24–72 hour cert lifetimes with automated renewal over long-lived CRL maintenance.
How does the mTLS handshake work between microservices?
The handshake extends standard TLS 1.2 or TLS 1.3. Both versions support client certificates. TLS 1.3 completes faster with fewer round trips. That matters when services chatter hundreds of times per second.
Our TLS 1.3 handshake breakdown covers one-way TLS in detail. With mTLS, add a CertificateRequest from the server and a Certificate message from the client before the finished messages exchange.
Validation checks the proxy performs
- Build a chain from the peer cert to a trusted root or intermediate in the local trust store.
- Confirm the cert is within its
notBeforeandnotAfterwindow. - Match hostname or SPIFFE URI against an allowlist. Reject unknown CNs even if the chain is valid.
- Optionally check OCSP or CRL if you use longer-lived certs.
- Pass verified identity to the app via headers such as
X-Client-Cert-Subjectonly after verification at the proxy. Never trust client-supplied identity headers.
Service meshes like Istio and Linkerd automate this flow. Sidecars terminate mTLS transparently. Your PHP or Node process sees plain HTTP on localhost. For smaller teams without a mesh, terminating mTLS at Nginx or HAProxy is simpler and easier to debug with OpenSSL s_client tests.
How do you configure mTLS in Nginx for backend services?
Nginx is the pattern I use on Ubuntu servers running Laravel APIs. The edge proxy holds the CA bundle. Upstream apps stay HTTP on a loopback port. That keeps PHP-FPM config unchanged while transport auth stays strict.
Generate a test CA and certificates
Use OpenSSL for local proof-of-concept before wiring cert-manager in Kubernetes. The commands below create a root CA, a server cert, and a client cert.
# Create private CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt -subj "/CN=Internal Services CA"
# Server key + CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=payments.internal"
# Sign server cert with SAN
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 -sha256 \
-extfile <(printf "subjectAltName=DNS:payments.internal")
# Client key + cert for order-service
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
-subj "/CN=order-service"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out client.crt -days 90 -sha256 Nginx server block with mandatory client certs
server {
listen 8443 ssl;
server_name payments.internal;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
location / {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn;
proxy_pass http://127.0.0.1:9000;
}
} Reload Nginx after deploy. Test from a caller host with the client cert attached. A missing cert should yield 403 or a TLS alert before HTTP starts. Our Nginx TLS 1.3 configuration guide covers cipher and protocol tuning that applies here too.
HAProxy alternative
Teams running HAProxy with TLS termination use verify required on the bind line and pass ssl_c_s_dn() as a header. The concept is identical. Pick whichever proxy your Linux infrastructure team already operates.
When should you use mTLS vs API keys or OAuth for service auth?
Not every internal call needs client certificates. Shared secrets are fine for low-risk cron jobs on a single host. mTLS earns its operational cost when blast radius, compliance, or network segmentation demands cryptographic caller proof.
| Method | Best for | Weakness | Rotation effort |
|---|---|---|---|
| mTLS | Microservices, zero-trust meshes, regulated data | CA ops, cert distribution, debugging handshakes | Automated if TTL is short |
| API keys / HMAC | Simple webhooks, third-party callbacks | Leaked key = full impersonation until rotated | Manual unless vault-backed |
| OAuth 2.0 client credentials | Multi-tenant SaaS, external partner APIs | Token endpoint dependency, clock skew, scope sprawl | Token expiry handles most cases |
| JWT service tokens | Stateless internal calls with short TTL | Shared signing key compromise affects all services | Key rotation needs coordination |
Compare with Laravel Sanctum vs Passport for human and mobile API auth. Those tools solve application-layer identity. mTLS solves transport-layer identity. Production systems combine both. A booking platform I built used mTLS between the public Laravel app and a document microservice while end users logged in with session cookies.
Choose mTLS when:
- Traffic crosses namespace or VPC boundaries you do not fully control.
- Compliance asks for encryption in transit plus mutual authentication.
- You deploy a service mesh that provides mTLS by default.
- You expose gRPC or internal REST on ports that must never accept anonymous callers. See gRPC vs REST for service-to-service for protocol trade-offs.
Skip mTLS when a single monolith talks to MySQL on localhost and all workers share one deploy unit. Database credentials and Unix permissions are enough there.
How do you implement mTLS in Kubernetes and Laravel stacks?
Most teams I work with run Laravel 12 or 13 on PHP 8.3+ behind Nginx or an ingress controller. Kubernetes adds cert-manager for automated issuance. The pattern mirrors bare-metal setups. Only the cert delivery mechanism changes.
Kubernetes with cert-manager
cert-manager can issue both ingress server certs and internal client certs from the same issuer. Define a Certificate resource per service account or per deployment. Mount certs as secrets at /etc/tls/. Our Kubernetes ingress and TLS guide covers the ingress side. For east-west traffic, pair cert-manager with a mesh or explicit sidecar config.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: order-service-client
namespace: production
spec:
secretName: order-service-client-tls
duration: 72h
renewBefore: 24h
commonName: order-service
usages:
- client auth
issuerRef:
name: internal-ca
kind: ClusterIssuer Laravel HTTP client with client cert
When Laravel must call an mTLS-protected internal API, configure Guzzle through the HTTP client facade. Store cert paths outside the web root. Restrict file permissions to the FPM user.
use Illuminate\Support\Facades\Http;
$response = Http::withOptions([
'cert' => [storage_path('certs/client.crt'), ''],
'ssl_key' => [storage_path('certs/client.key'), ''],
'verify' => storage_path('certs/ca.crt'),
])->timeout(10)->post('https://payments.internal:8443/charge', [
'amount' => 150000,
'currency' => 'NPR',
'order_id' => $order->uuid,
]); Validate responses server-side as you would for any payment call. mTLS proves which service connected. It does not replace idempotency keys or amount validation. For complex integrations, our API development practice treats transport auth and business auth as separate layers.
SPIFFE and SPIRE for dynamic identity
Static CN allowlists break at scale. SPIFFE assigns each workload a SPIFFE ID such as spiffe://prod/order-service. SPIRE agents rotate certs automatically. Istio and Linkerd consume SPIFFE IDs natively. Read observability with a service mesh for how identity and metrics tie together. The SPIFFE specification is maintained at spiffe.io.
How do you troubleshoot mutual TLS certificate failures?
Most production incidents are boring. Expired certs, wrong CA bundle, or hostname mismatch. Fix them with OpenSSL before you grep Laravel logs.
Test with openssl s_client
openssl s_client -connect payments.internal:8443 \
-cert client.crt -key client.key -CAfile ca.crt \
-servername payments.internal -tls1_3 Look for Verify return code: 0 (ok) at the bottom. Code 19 means hostname mismatch. Code 21 means the chain cannot be built. The OpenSSL project documents return codes in its verify manual at docs.openssl.org.
Common failure modes
- Clock skew: VMs restored from snapshot reject valid certs. Sync NTP on every node.
- Stale opcache or proxy cache: After cert rotation, reload PHP-FPM and Nginx. I hit this on Deployer releases when cron still pointed at an old release path.
- Missing intermediate: Server sends only leaf cert. Add the intermediate to the server bundle.
- Wrong extended key usage: Client certs need
clientAuthEKU. Server certs needserverAuth. - Trusting headers from clients: Only the terminating proxy may set identity headers after
ssl_verify_clientsucceeds.
Structured logging helps. Log TLS verify status, client subject, and connection source IP at the proxy. Correlate with application request IDs. Paste cert PEMs into a JSON formatter or base64 tool only in dev. Never log private keys.
For zero-trust parallels, internal mTLS resembles SSH key-only authentication and two-factor patterns for humans. Machines get cryptographic identity. Humans get passwords plus a second factor.
Key Takeaways
- Mutual TLS for Service-to-Service Auth verifies both caller and callee with X.509 certs before HTTP or gRPC traffic starts.
- Terminate mTLS at Nginx, HAProxy, or a mesh sidecar. Keep Laravel and PHP apps on loopback HTTP when possible.
- Use short-lived certs with cert-manager or SPIRE. Automate renewal instead of manual yearly purchases.
- Combine mTLS with application-layer checks. Transport identity does not replace idempotency, authorisation, or input validation.
- Debug handshake failures with
openssl s_clientfirst. Most errors are expiry, chain, or SAN mismatch. - Match auth method to risk. Same-host monolith calls rarely need mTLS. Cross-VPC microservices almost always do.
People Also Ask
Is mTLS the same as two-way SSL?
Yes. Two-way SSL is an older term for the same mechanism. Both the client and server present certificates during the TLS handshake. Modern documentation prefers mTLS or mutual TLS. The underlying RFC 5246 and RFC 8446 (TLS 1.3) define the wire format.
Does mTLS replace JWT or API keys?
No. mTLS proves which service connected. JWTs and API keys carry application claims such as scopes, tenant IDs, or user context. Production APIs often require mTLS at the gateway plus a signed service token in the request body or header.
How long should internal service certificates last?
Prefer 24 to 72 hours with automated rotation. Long-lived one-year certs simplify ops on paper but widen the window after a private key leak. cert-manager renews at one-third of TTL by default.
Can WordPress or WooCommerce plugins use mTLS?
Yes, but usually at the infrastructure layer. Terminate mTLS on the reverse proxy before traffic reaches PHP. Plugin code then uses standard HTTPS to localhost. WooCommerce webhook receivers can also demand client certs at Nginx if the partner supports it.
Ship Mutual TLS Without Guesswork
Mutual TLS for Service-to-Service Auth is the fastest way to stop anonymous internal traffic from reaching your APIs. Start with a private CA, enforce verification at Nginx or your ingress, and automate rotation before the first cert expires. On client portals such as Mijar Law Associates, transport security and document access control stack together. Need help wiring mTLS into a Laravel microservice split or hardening east-west traffic on Ubuntu? Custom software development covers architecture through deploy. Contact us to review your internal API topology.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

