API Security with mTLS and Certificate Pinning
API security mTLS (mutual TLS) provides the strongest form of service-to-service authentication by requiring both the client and server to present valid certificates. Unlike API keys or JWT tokens that can be stolen and replayed, mTLS cryptographically verifies the identity of both parties in every connection. Combined with certificate pinning, it creates a zero-trust network where only explicitly authorized services can communicate — and where a leaked bearer token alone is no longer enough to impersonate a service.
This guide covers implementing mTLS from certificate authority setup through production deployment, including certificate rotation strategies, Kubernetes integration, and mobile app certificate pinning. We address the common operational challenges that make adoption difficult and provide practical solutions for each. The goal is not to convince you to encrypt everything mutually, but to help you apply this control precisely where it earns its operational cost.
Understanding mTLS vs Standard TLS
Standard TLS (HTTPS) only verifies the server’s identity — the client checks that the server’s certificate is valid and signed by a trusted authority. With mTLS, the server also verifies the client’s certificate, creating bidirectional authentication that is impossible to bypass without possessing the correct private key. The handshake gains one extra step, but the security model changes fundamentally: identity is proven by cryptographic possession, not by a string that travels in a header.
TLS vs mTLS Handshake
Standard TLS:
Client → ClientHello → Server
Client ← ServerHello, ServerCert ← Server
Client → (validates server cert)
Client → KeyExchange, Finished → Server
Client ← Finished ← Server
✅ Server identity verified
❌ Client identity NOT verified
Mutual TLS (mTLS):
Client → ClientHello → Server
Client ← ServerHello, ServerCert,
CertificateRequest ← Server
Client → ClientCert, KeyExchange → Server
Server → (validates client cert)
Client ← Finished ← Server
✅ Server identity verified
✅ Client identity verified
It helps to be precise about what mTLS does and does not give you. It provides strong authentication and a confidential channel, but it is not authorization. Knowing that a connection genuinely comes from order-service tells you who is calling; it does not tell you whether order-service is allowed to read the customer table. Treat the verified certificate identity as the input to your authorization layer, never as the decision itself.
Setting Up a Private Certificate Authority
For internal mTLS, you need your own Certificate Authority (CA). Below we use step-ca (from Smallstep) as an automated CA that handles issuance and rotation. A private CA is the right model for service-to-service traffic because you control the trust root and never expose internal service identities to a public CA.
# Install step CLI and step-ca
curl -sSL https://dl.smallstep.com/install-step-ca.sh | bash
# Initialize CA
step ca init \
--name "Internal Services CA" \
--dns "ca.internal.example.com" \
--address ":8443" \
--provisioner "admin@example.com"
# Configure ACME provisioner for automated cert issuance
step ca provisioner add acme --type ACME
# Start CA server
step-ca $(step path)/config/ca.json &
# Issue a server certificate
step ca certificate "api.internal.example.com" \
server.crt server.key \
--provisioner "admin@example.com" \
--not-after 720h
# Issue a client certificate
step ca certificate "order-service" \
client.crt client.key \
--provisioner "admin@example.com" \
--not-after 720h \
--san "order-service.production.svc.cluster.local"
One decision deserves early attention: protecting the CA’s root private key. If that key leaks, an attacker can mint trusted certificates for any service in your mesh, and recovery means rotating the entire trust chain. A common pattern is to keep the root offline and use it only to sign a short-lived intermediate CA, which does the day-to-day issuance. Hardware-backed storage such as an HSM or a cloud KMS for the root raises the bar further.
Implementing mTLS in Applications
// Spring Boot mTLS server configuration
@Configuration
public class MtlsServerConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory>
mtlsCustomizer() {
return factory -> {
factory.setSsl(createSslConfig());
};
}
private Ssl createSslConfig() {
Ssl ssl = new Ssl();
ssl.setEnabled(true);
ssl.setKeyStore("classpath:keystore.p12");
ssl.setKeyStorePassword("changeit");
ssl.setKeyStoreType("PKCS12");
// Enable client certificate requirement
ssl.setClientAuth(Ssl.ClientAuth.NEED);
ssl.setTrustStore("classpath:truststore.p12");
ssl.setTrustStorePassword("changeit");
ssl.setProtocol("TLSv1.3");
return ssl;
}
}
// Extract client identity from certificate
@RestController
public class SecureApiController {
@GetMapping("/api/data")
public ResponseEntity<?> getData(HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("javax.servlet.request.X509Certificate");
if (certs == null || certs.length == 0) {
return ResponseEntity.status(403).body("No client certificate");
}
String clientIdentity = certs[0].getSubjectX500Principal().getName();
String commonName = extractCN(clientIdentity);
// Authorize based on certificate CN
if (!allowedServices.contains(commonName)) {
return ResponseEntity.status(403).body("Service not authorized");
}
return ResponseEntity.ok(dataService.getData());
}
}
Note the distinction between ClientAuth.NEED and ClientAuth.WANT in the Spring configuration. NEED rejects any connection without a valid client certificate, which is what you want for a locked-down internal API. WANT requests a certificate but still allows the connection if none is presented, which is occasionally useful during a migration but is a frequent source of accidental security gaps if it ships to production unchanged.
// Go mTLS client and server
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
)
func createMtlsServer() *http.Server {
// Load CA cert for client verification
caCert, _ := os.ReadFile("ca.crt")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
ClientCAs: caPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS13,
// Certificate pinning — only accept specific certs
VerifyPeerCertificate: func(rawCerts [][]byte,
verifiedChains [][]*x509.Certificate) error {
for _, chain := range verifiedChains {
cn := chain[0].Subject.CommonName
if !isAllowedService(cn) {
return fmt.Errorf("service %s not authorized", cn)
}
}
return nil
},
}
return &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
}
func createMtlsClient() *http.Client {
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
caCert, _ := os.ReadFile("ca.crt")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caPool,
MinVersion: tls.VersionTLS13,
},
},
}
}
Certificate Pinning Beyond the Common Name
The Go example pins on the certificate’s common name, which is a reasonable start, but production teams often pin on something more robust. Pinning to the SHA-256 hash of the public key (a “SPKI pin”) survives certificate reissuance as long as the key stays the same, while pinning to the full certificate breaks on every rotation. Choose deliberately: tight pinning maximizes security but multiplies the operational pain of rotation, so it belongs on a small number of critical paths rather than everywhere.
For mobile clients, pinning takes a different form. A banking or healthcare app typically embeds the expected server public key hash so that a man-in-the-middle using a fraudulently issued certificate is rejected even if the device trusts the attacker’s CA. The standard advice is to pin at least two keys — the current one and a backup — and ship the backup well before you need it, so an emergency rotation does not require an app-store release to recover.
Certificate Rotation Strategy
Moreover, automated certificate rotation is critical for production mTLS. Short-lived certificates (24-72 hours) reduce the impact of compromise but require reliable automation. The trade-off is straightforward: the shorter the lifetime, the smaller the window an attacker has with a stolen key, but the more your platform depends on the issuance pipeline never failing.
# Kubernetes cert-manager for automated rotation
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: order-service-mtls
namespace: production
spec:
secretName: order-service-tls
duration: 72h
renewBefore: 24h
isCA: false
privateKey:
algorithm: ECDSA
size: 256
usages:
- server auth
- client auth
dnsNames:
- order-service
- order-service.production.svc.cluster.local
issuerRef:
name: internal-ca-issuer
kind: ClusterIssuer
---
# Pod auto-reloads certs when secret changes
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
metadata:
annotations:
# Triggers rollout when cert secret changes
checksum/tls: "{{ include (print .Template.BasePath) . | sha256sum }}"
spec:
containers:
- name: order-service
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: order-service-tls
The renewBefore: 24h setting is the safety margin that makes short certificates survivable. It tells cert-manager to renew a full day before expiry, so a transient CA outage has time to resolve before any certificate actually lapses. The most painful mTLS incidents in production are almost never sophisticated attacks — they are expired certificates that nobody noticed because alerting on impending expiry was never wired up.
The Service Mesh Alternative
Hand-rolling the configuration above for every service quickly becomes a burden, which is why many teams delegate mTLS to a service mesh such as Istio or Linkerd. The mesh injects a sidecar proxy that handles certificate issuance, rotation, and mutual authentication transparently, so application code stays unaware that the connection is mutually authenticated at all. This is often the pragmatic path: you get mesh-wide mTLS by policy rather than by editing dozens of services, at the cost of running and understanding the mesh itself. If you are weighing this, our guide on Kubernetes security hardening covers where a mesh fits among other controls.
When NOT to Use mTLS
mTLS adds significant operational complexity: certificate lifecycle management, CA infrastructure, debugging TLS handshake failures, and handling certificate expiration incidents. For internal APIs within a single trust boundary (same Kubernetes namespace), network policies and service accounts may provide sufficient security with less overhead. Reach for the heavier control only when the threat model genuinely calls for it.
Additionally, mTLS between a web frontend and your API is impractical because browser JavaScript cannot present client certificates programmatically. Use OAuth 2.0 or session tokens for user-facing API authentication instead — our guide on OAuth 2.0 security best practices covers that path. Therefore, reserve mTLS for service-to-service communication where both endpoints are under your control and you have the operational maturity to manage certificate infrastructure.
Key Takeaways
- Use mTLS for service-to-service auth where stolen tokens are an unacceptable risk
- Test thoroughly in staging before deploying to production environments
- Monitor certificate expiry and rotation health, not just request errors
- Treat verified certificate identity as input to authorization, not authorization itself
- Document your PKI and rotation runbooks for future team members
Final Recommendations
Mutual TLS provides the strongest authentication for service-to-service communication, eliminating the risks of stolen tokens or API keys. Use an automated CA like step-ca with cert-manager for Kubernetes deployments, implement short-lived certificates (24-72h) with automated rotation, and add certificate pinning for critical paths. Start with your most sensitive internal APIs and expand as your PKI infrastructure matures, rather than attempting a big-bang rollout across every service at once.
For related security topics, explore our guides linked above. The step-ca documentation and cert-manager documentation provide comprehensive setup guides.
In conclusion, API security mTLS is an essential topic for modern software development. By applying the patterns and practices covered in this guide, you can build more robust, scalable, and maintainable systems. Start with the fundamentals, iterate on your implementation, and continuously measure results to ensure you are getting the most value from these approaches.