Traffic between pods in a standard Kubernetes cluster is not encrypted automatically. To require services to communicate with each other exclusively through TLS or mTLS, configure this yourself or use a service mesh.
In this guide, you configure application-level mTLS without an additional service mesh, such as Linkerd. You create your own Certificate Authority (CA), server and client certificates, expose an internal service through Nginx with mandatory client authentication, and then test both successful and rejected connections.
- Use this approach to enforce mTLS for a specific application or internal API.
- To enforce mTLS centrally across the cluster, use a service mesh such as Linkerd or Istio.
- Never store private keys in Git. Store them in Secrets or an external secret manager.
- For this guide, you need kubectl.
When do you use mTLS?
- Use this: for internal APIs, compliance requirements, or services that must not accept unsecured pod-to-pod traffic.
- Do not use this as the only measure: always combine mTLS with namespaces, NetworkPolicies, and least privilege.
- Use a service mesh: if you do not want to manage certificates and Nginx configurations for each application.
The setup in this guide explicitly encrypts and authenticates traffic between pods. This prevents internal services from accepting unsecured connections.
Create the TLS certificates
Step 1
and create a local working directory for the certificates:
mkdir -p ~/kb-mtls
cd ~/kb-mtls
Step 2
Create a CA certificate that will sign the server and client certificates:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout ca.key \
-out ca.crt \
-subj "/CN=kb-mtls-ca" \
-days 365The `-nodes` flag prevents OpenSSL from prompting for a passphrase. This is practical for test environments, but use additional protection for private keys in production.
Step 3
Next, create a server certificate for the internal service:
openssl req -newkey rsa:2048 -nodes \
-keyout server.key \
-out server.csr \
-subj "/CN=mtls-server.kb-mtls-test.svc.cluster.local"Then create an extension file containing the DNS names of your service:
nano server.extAdd the following content to the file:
subjectAltName=DNS:mtls-server,DNS:mtls-server.kb-mtls-test.svc,DNS:mtls-server.kb-mtls-test.svc.cluster.localSave the changes and close the file (ctrl + x > y > enter).
Then sign the server certificate:
openssl x509 -req \
-in server.csr \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-out server.crt \
-days 365 \
-sha256 \
-extfile server.ext
Step 4
Also create a client certificate for the pod that is allowed to communicate with the service:
openssl req -newkey rsa:2048 -nodes \
-keyout client.key \
-out client.csr \
-subj "/CN=mtls-client"
openssl x509 -req \
-in client.csr \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-out client.crt \
-days 365 \
-sha256
Create the Secrets and workloads
Step 1
First, create a namespace and two Secrets: one for the server and one for the client:
kubectl create namespace kb-mtls-test
kubectl -n kb-mtls-test create secret generic mtls-server \
--from-file=ca.crt=ca.crt \
--from-file=server.crt=server.crt \
--from-file=server.key=server.key
kubectl -n kb-mtls-test create secret generic mtls-client \
--from-file=ca.crt=ca.crt \
--from-file=client.crt=client.crt \
--from-file=client.key=client.keyThe server uses `ca.crt` to verify client certificates. The client uses its own certificate and private key to authenticate with the server.
Step 2
Next, create a manifest for the server, service, and two client pods:
nano mtls-demo.yamlAdd the following configuration to the file:
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-mtls
namespace: kb-mtls-test
data:
default.conf: |
server {
listen 8443 ssl;
server_name mtls-demo;
ssl_certificate /etc/nginx/tls/server.crt;
ssl_certificate_key /etc/nginx/tls/server.key;
ssl_client_certificate /etc/nginx/tls/ca.crt;
ssl_verify_client on;
location / {
default_type text/plain;
return 200 "mtls-ok\n";
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mtls-server
namespace: kb-mtls-test
spec:
replicas: 1
selector:
matchLabels:
app: mtls-server
template:
metadata:
labels:
app: mtls-server
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 8443
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
- name: tls
mountPath: /etc/nginx/tls
readOnly: true
volumes:
- name: config
configMap:
name: nginx-mtls
- name: tls
secret:
secretName: mtls-server
---
apiVersion: v1
kind: Service
metadata:
name: mtls-server
namespace: kb-mtls-test
spec:
selector:
app: mtls-server
ports:
- port: 8443
targetPort: 8443
---
apiVersion: v1
kind: Pod
metadata:
name: mtls-client
namespace: kb-mtls-test
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.12.1
command: ["sh", "-c", "sleep 3600"]
volumeMounts:
- name: client-tls
mountPath: /etc/mtls/client
readOnly: true
volumes:
- name: client-tls
secret:
secretName: mtls-client
---
apiVersion: v1
kind: Pod
metadata:
name: mtls-client-no-cert
namespace: kb-mtls-test
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.12.1
command: ["sh", "-c", "sleep 3600"]Save the changes and close the file (ctrl + x > y > enter).
Step 3
Create the resources:
kubectl apply -f mtls-demo.yaml
kubectl -n kb-mtls-test rollout status deploy/mtls-server
kubectl -n kb-mtls-test wait --for=condition=Ready pod/mtls-client --timeout=180s
kubectl -n kb-mtls-test wait --for=condition=Ready pod/mtls-client-no-cert --timeout=180s
Test successful and rejected connections
Step 1
Test the connection from the client pod with a client certificate:
kubectl -n kb-mtls-test exec mtls-client -- sh -c \
"curl --cacert /etc/mtls/client/ca.crt \
--cert /etc/mtls/client/client.crt \
--key /etc/mtls/client/client.key \
https://mtls-server:8443"If everything is configured correctly, the response is `mtls-ok`.
Step 2
Then test a connection without a client certificate:
kubectl -n kb-mtls-test exec mtls-client -- sh -c \
"curl -k -sv https://mtls-server:8443 2>&1 | tail -n 20"You should see an error such as `400 No required SSL certificate was sent`. This confirms that the service enforces mTLS for internal traffic.