Implementing mTLS in Kubernetes with Cert-Manager

Introduction

Securing internal communication between services in Kubernetes is a critical security practice. Mutual TLS (mTLS) ensures encrypted traffic while also verifying the identity of both the client and server. In this guide, we will configure mTLS between two microservices using Cert-Manager for automated certificate issuance and renewal.

Problem Statement

By default, Kubernetes services communicate in plaintext, making them vulnerable to man-in-the-middle attacks. We need a solution that:

  • Encrypts communication between services.
  • Ensures only trusted services can talk to each other.
  • Automates certificate management to avoid manual rotation.

Solution: mTLS with Cert-Manager

We will deploy:
✅ A Certificate Authority (CA) to issue certificates.
✅ A Kubernetes Issuer to generate TLS certificates.
✅ Two microservices (App One and App Two) configured with mTLS.
✅ A test pod to verify secure service-to-service communication.

Step 1: Install Cert-Manager

Cert-Manager automates TLS certificate lifecycle management. If you haven’t installed it yet, deploy it using Helm:

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set installCRDs=true

Verify installation:

kubectl get pods -n cert-manager

Step 2: Configure a Certificate Authority (CA)

First, we need a CA to issue certificates for our services.

Create a self-signed root CA:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: ca-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: root-ca
  namespace: cert-manager
spec:
  secretName: root-ca-secret
  isCA: true
  duration: 365d
  renewBefore: 30d
  subject:
    organizations:
      - MyOrg
  commonName: root-ca
  privateKey:
    algorithm: RSA
    size: 2048
  issuerRef:
    name: ca-issuer
    kind: ClusterIssuer
Click Here to Copy YAML

Apply it:

kubectl apply -f ca.yaml

Step 3: Issue TLS Certificates for Services

Now, let’s create an Issuer that will generate certificates signed by our CA:

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: ca-issuer
  namespace: default
spec:
  ca:
    secretName: root-ca-secret
Click Here to Copy YAML

Apply it:

kubectl apply -f issuer.yaml

Now, request certificates for App One and App Two:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-one-tls
  namespace: default
spec:
  secretName: app-one-tls-secret
  duration: 90d
  renewBefore: 2160h
  issuerRef:
    name: ca-issuer
    kind: Issuer
  dnsNames:
    - app-one.default.svc.cluster.local
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-two-tls
  namespace: default
spec:
  secretName: app-two-tls-secret
  duration: 90d
  renewBefore: 2160h
  issuerRef:
    name: ca-issuer
    kind: Issuer
  dnsNames:
    - app-two.default.svc.cluster.local
Click Here to Copy YAML

Apply it:

kubectl apply -f app-certs.yaml

Step 4: Deploy the Services with TLS

Now, let’s deploy App One and App Two, mounting the certificates.

App One Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-one
  labels:
    app: app-one
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-one
  template:
    metadata:
      labels:
        app: app-one
    spec:
      containers:
      - name: app-one
        image: nginx
        ports:
        - containerPort: 443
        volumeMounts:
        - name: tls
          mountPath: "/etc/tls"
          readOnly: true
      volumes:
      - name: tls
        secret:
          secretName: app-one-tls-secret
Click Here to Copy YAML

Apply it:

kubectl apply -f app-one.yaml

App Two Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-two
  labels:
    app: app-two
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-two
  template:
    metadata:
      labels:
        app: app-two
    spec:
      containers:
      - name: app-two
        image: nginx
        ports:
        - containerPort: 443
        volumeMounts:
        - name: tls
          mountPath: "/etc/tls"
          readOnly: true
      volumes:
      - name: tls
        secret:
          secretName: app-two-tls-secret
Click Here to Copy YAML

Apply it:

kubectl apply -f app-two.yaml

Step 5: Test mTLS Communication

We will now test service-to-service communication using mTLS.

Run a test pod with curl:

kubectl run curl-test --rm -it --image=curlimages/curl -- /bin/sh

Inside the pod, run:

curl --cacert /etc/tls/ca.crt --cert /etc/tls/tls.crt --key /etc/tls/tls.key https://app-two.default.svc.cluster.local:443

Expected Output:

Hello from App Two

If the request fails, check logs and ensure the correct ports are used.

Conclusion

With this setup, we’ve successfully implemented Mutual TLS (mTLS) in Kubernetes using Cert-Manager.

✅ Encrypted Communication – All traffic is secured via TLS.
✅ Mutual Authentication – Both services verify each other.
✅ Automated Certificate Lifecycle – Cert-Manager handles issuance & renewal.

Have you implemented mTLS in your Kubernetes clusters? Share your experiences in the comments! 👇

Leave a comment