API Security with mTLS and Certificate Pinning: Zero-Trust Implementation

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.

API security mTLS handshake process
mTLS handshake: both client and server verify each other’s certificates
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

Setting 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 management and PKI infrastructure
Managing certificates and PKI infrastructure for production mTLS deployment

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-tls

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.

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.

Zero trust security architecture
Implementing zero-trust API security with mTLS in microservices architectures

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.

Scroll to Top