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.
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 mTLS adoption difficult and provide practical solutions for each.
Understanding mTLS vs Standard TLS
Standard TLS (HTTPS) only verifies the server’s identity — the client checks that the server’s certificate is valid. With mTLS, the server also verifies the client’s certificate, creating bidirectional authentication that is impossible to bypass without possessing the correct private key.
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 verifiedSetting Up a Private Certificate Authority
For internal mTLS, you need your own Certificate Authority (CA). We use step-ca (from Smallstep) as an automated CA that handles issuance and rotation.
# 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"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());
}
}// 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 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.
# 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-tlsWhen 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.
Additionally, mTLS between a web frontend and your API is impractical because browser JavaScript cannot present client certificates. Use OAuth 2.0 or session tokens for user-facing API authentication instead. 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
API security mTLS 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 mTLS on your most sensitive internal APIs and expand as your PKI infrastructure matures.
For related security topics, explore our guide on OAuth 2.0 security best practices and Kubernetes security hardening. The step-ca documentation and cert-manager documentation provide comprehensive setup guides.