Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Mutual TLS for Service-to-Service Auth

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.

mTLS Service-to-Service OverviewService AClient certService BServer certTLS HandshakeBoth certs verifiedAgainst shared CAEncrypted API TrafficOnly after mutual verification
Mutual TLS for Service-to-Service Auth requires both caller and callee to present valid certificates before application data flows.

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.

mTLS Handshake SequenceClient ServiceServer Service1. ClientHello2. ServerHello + server cert3. CertificateRequest4. Client cert + proof5. Finished (both sides)Application DataHTTP, gRPC, or JSON-RPC
The mTLS handshake adds client certificate verification between ServerHello and encrypted application traffic.

Validation checks the proxy performs

  1. Build a chain from the peer cert to a trusted root or intermediate in the local trust store.
  2. Confirm the cert is within its notBefore and notAfter window.
  3. Match hostname or SPIFFE URI against an allowlist. Reject unknown CNs even if the chain is valid.
  4. Optionally check OCSP or CRL if you use longer-lived certs.
  5. Pass verified identity to the app via headers such as X-Client-Cert-Subject only 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.

MethodBest forWeaknessRotation effort
mTLSMicroservices, zero-trust meshes, regulated dataCA ops, cert distribution, debugging handshakesAutomated if TTL is short
API keys / HMACSimple webhooks, third-party callbacksLeaked key = full impersonation until rotatedManual unless vault-backed
OAuth 2.0 client credentialsMulti-tenant SaaS, external partner APIsToken endpoint dependency, clock skew, scope sprawlToken expiry handles most cases
JWT service tokensStateless internal calls with short TTLShared signing key compromise affects all servicesKey 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.

mTLS Decision TreeInternal service call?Same hostlocalhost onlyCross-networkVPC or K8sUnix socketor shared secretUse mTLSor service meshOAuth if externalpartner API
Use Mutual TLS for Service-to-Service Auth when traffic crosses network boundaries; keep simpler auth on same-host calls.

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.

Production mTLS StackPrivate CAor Vault PKIcert-manager72h rotationNginx / Ingressverify clientLaravel APIPHP 8.3+Operational Checklist• Mount secrets read-only• Alert 7 days before expiry• Log verify failures• Never commit ca.key• Test with openssl s_client• Reload FPM after rotation
Production Mutual TLS for Service-to-Service Auth chains private CA, automated cert issuance, proxy verification, and application logic.

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 clientAuth EKU. Server certs need serverAuth.
  • Trusting headers from clients: Only the terminating proxy may set identity headers after ssl_verify_client succeeds.

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_client first. 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

Mutual TLS requires both the calling service and the receiving service to present X.509 certificates during the TLS handshake. Each side validates the other's certificate chain, expiry, and often custom identity fields before any application data flows. Authentication happens at the transport layer, so spoofed internal requests are blocked before they reach your API code.

Yes. Two-way SSL is an older term for the same mechanism defined in TLS RFCs. Both client and server present certificates during the handshake. Modern documentation prefers mTLS or mutual TLS.

The handshake extends standard TLS 1.2 or TLS 1.3. After ServerHello, the server sends a CertificateRequest and the client responds with its own Certificate message before encrypted application traffic begins. TLS 1.3 completes faster with fewer round trips, which matters when services exchange hundreds of requests per second. The proxy validates the chain, expiry, hostname or SPIFFE URI, and optionally OCSP or CRL before passing verified identity to the app via trusted headers.

Choose mTLS when traffic crosses VPC or namespace boundaries you do not fully control, compliance requires encryption plus mutual authentication, you run a service mesh, or internal REST and gRPC ports must never accept anonymous callers. API keys suit simple webhooks on a single host. OAuth client credentials fit multi-tenant partner APIs. Same-host monolith calls to a local database rarely justify the operational overhead of client certificates.

Terminate mTLS at Nginx while upstream Laravel or PHP apps stay on loopback HTTP. Set ssl_certificate and ssl_certificate_key for the server, ssl_client_certificate to your CA bundle, ssl_verify_client on, and ssl_verify_depth 2. Reject connections where ssl_client_verify is not SUCCESS with a 403. Pass X-Client-Cert-Subject to the app only after Nginx verifies the client cert. Reload Nginx after deploy and test with a client certificate attached from the caller host.

No. mTLS proves which service connected at the transport layer. 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 header or body. On Laravel stacks, Sanctum or Passport handles human and mobile auth while mTLS secures backend jobs, webhooks, and cron-triggered internal calls at the proxy.

Prefer 24 to 72 hours with automated rotation via cert-manager or SPIRE. Long-lived one-year certs simplify ops on paper but widen the exposure window after a private key leak.

You need a private CA or intermediate that signs both server and client certificates, with the CA private key kept off application hosts. Each listening service gets a server certificate with SAN entries matching internal DNS names such as payments.internal. Each caller gets a client certificate mapped to an allowed list by CN or URI SAN. Prefer short 24 to 72 hour lifetimes with automated renewal over long-lived CRL maintenance. Options include step-ca, HashiCorp Vault PKI, cert-manager with a cluster issuer, or cloud CA services.

Run Laravel 12 or 13 on PHP 8.3 or higher behind Nginx or an ingress controller. Use cert-manager to issue both ingress server certs and internal client certs from the same ClusterIssuer. Define a Certificate resource per deployment with 72-hour duration and mount secrets at /etc/tls/. When Laravel calls an mTLS-protected API, configure Guzzle through the HTTP client facade with cert, ssl_key, and verify paths stored outside the web root with restricted FPM permissions. Validate business logic server-side regardless of transport auth.

Start with openssl s_client against the target host using the client cert, key, and CA file. Verify return code 0 means success; code 19 is hostname mismatch and code 21 means the chain cannot be built. Common causes include expired certs, wrong CA bundle, missing intermediate in the server bundle, clock skew on restored VMs, client certs missing clientAuth extended key usage, or stale Nginx or PHP-FPM after rotation. Never trust client-supplied identity headers; only the terminating proxy sets them after ssl_verify_client succeeds.

Yes. HAProxy uses verify required on the TLS bind line and passes ssl_c_s_dn as a header to upstream services. The concept is identical to Nginx: the proxy holds the CA bundle, validates client certificates, and forwards verified identity. Pick whichever reverse proxy your Linux infrastructure team already operates. Both work well on Ubuntu servers running Laravel APIs with PHP-FPM unchanged on loopback ports.

Static CN allowlists break at scale across many microservices. SPIFFE assigns each workload a SPIFFE ID such as spiffe://prod/order-service. SPIRE agents rotate certificates automatically, and service meshes like Istio and Linkerd consume SPIFFE IDs natively. Use SPIFFE when you need dynamic workload identity instead of manually maintaining per-service CN lists. The specification is maintained at spiffe.io and pairs well with automated observability in mesh deployments.

Sidecars terminate mTLS transparently between services. Your PHP, Node, or Laravel process sees plain HTTP on localhost while the mesh handles certificate exchange and validation automatically. This removes the need to configure client certs in application code but adds mesh operational complexity. For smaller teams without a mesh, terminating mTLS at Nginx or HAProxy is simpler and easier to debug with OpenSSL s_client tests.

No. When a single monolith talks to MySQL on localhost and all workers share one deploy unit, database credentials and Unix permissions are sufficient. mTLS earns its operational cost when blast radius, compliance, or network segmentation demands cryptographic proof of the caller. Use it for traffic crossing VPC boundaries, not for loopback connections within the same host.

Yes, but usually at the infrastructure layer rather than inside plugin PHP code. Terminate mTLS on the reverse proxy before traffic reaches WordPress. Plugin code then communicates via standard HTTPS to localhost. WooCommerce webhook receivers and payment callbacks benefit from this pattern when an external partner or internal service must cryptographically prove identity before hitting PHP-FPM, without modifying core or plugin authentication logic.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: