---
title: Applying Cluster Wide mTLS Certificactes in Kubernetes (Ingress Nginx)
date: 2025-11-29
tags:
  - tlm
  - kubernetes
  - ssl
  - security
url: https://blog.paradoxis.nl/applying-cluster-wide-mtls-certificactes-in-kubernetes-ingress-nginx-0daee4104b19
author:
  name: Luke Paris
  bio: OSCP & OSWE certified Dutch security researcher specializing in active directory, malware development and all things devops. Currently work for Merlon Security in The Hague, and previously worked in Fox-IT's Red Team for 6 years.
  links:
    - type: linkedin
      url: https://www.linkedin.com/in/paradoxis
    - type: github
      url: https://github.com/paradoxis
    - type: unsplash
      url: https://unsplash.com/@paradoxis
---

# Applying Cluster Wide mTLS Certificactes in Kubernetes (Ingress Nginx)

*I’m deviating from my regular offensive-security related blog activities today to stray a little towards more sysadmin related stuff, if you’re not into system hardening, or Kubernetes at all, feel free to click off.*

*This also might not be the smartest thing to do if your cluster gets used by anonymous end-users, as, well, they don’t have certificates to begin with. This is a solution to a problem I created for myself because* ***muh’ hardening***

---

A couple of weeks ago I looked over at the old hardware I had laying around for ages which I had the plan to build a home lab with, but never got around to. But for some reason I just woke up that day and said, screw it. Let’s build a Kubernetes cluster in my home for fun, why not?

![Credit /u/datamattsson on Reddit](https://blog.paradoxis.nl/assets/img/figure-01-6b9551c4-1653.webp "Credit /u/datamattsson on Reddit")

Even before I began though, I had already planted the seed in my mind that, whatever happens, my load balancer should, must and **will be protected** with my own layer of mTLS authentication.

For those not in the know, mTLS (Mutual TLS) allows you to force clients to authenticate, not using a password, but using an SSL certificate with corresponding private key. Whenever you visit a web application, it will prompt you to pick a certificate for authentication. If you lack the required cryptographic materials, you get a kind message to go away and take your business elsewhere.

![Like this](https://blog.paradoxis.nl/assets/img/figure-02-6306df62-385.webp "Like this")

This basically means that even if you have a 0day in my vulnerable webapp, you still need to authenticate at the transport level before you can even reach it (a.k.a.: good luck getting in without compromising my workstation first, at which point, you might as well steal the cluster admin credentials, right?).

## Okay, so what? Just configure it then?

Well, I’m not a Kubernetes rockstar, and I just couldn’t find anyone with my specific use case and even Mr GPT (and no, I refuse to pay for AI slop) sucked at this and got confused by my question to apply mTLS authentication **cluster wide**.

Moreover, the only issue here is that all blogs or tutorials online I could find were instances where they show you how to configure mTLS at the namespace/deployment level (like [this](https://medium.com/@badawekoo/apply-mutual-tls-over-kubernetes-nginx-ingress-controller-4ea203bce3e0), no hate though, I still learned a lot ❤). And in my eyes, this has two downsides:

![OUR certificate communism meme](https://blog.paradoxis.nl/assets/img/figure-03-f475a103-348.webp)

The first is pure overhead. Say you have 10 applications, and 10 users. This means you’d need to either sign 100 certificates/keys (one cert/key per application per user), or just re-use the same mTLS keys everywhere, at which point you might as well move mTLS to the front of the load balancer anyway, as you’re effectively just applying mTLS cluster wide, with extra steps.

The second downside is that of security. Say your application is hosted in namespace A, and your colleagues’s in namespace B. Your colleague has a service account with slightly too many privileges, and left `automountServiceAccountToken` as `true` (the default, please change this to `false` if you don’t use this, it doesn’t hurt, leaving it on is just free attack surface) in their deployment, and the attacker now has full control over all secrets in namespace B.

If you re-use the same mTLS certificate between namespace A and namespace B, the attacker can just read the mTLS key and now perform nasty attacks with the certificate.

> I’m not implying they can just access stuff in namespace A with a server-side mTLS cert, that would require some more catastrophic issues with your setup. Think more like them already being on your network and MITM’ing stuff, and relaying network traffic. Lot’s of fun scenario’s to think about here

## Cluster Wide Authentication!

So, if you’re going to re-use cryptographic material anyway.. why not just place it in front of the entire cluster? Not only does it ensure that nothing gets through without an mTLS cert (you could see that as a good, but also a terrible thing, depending on who uses your app), but it ensures that even if namespace B (from the last example) is compromised, they won’t have enough rights to steal the mTLS keys because:

![Meme of Patrick star moving things showing how Kubernetes certs are moved to the nginx-ingress namespace](https://blog.paradoxis.nl/assets/img/figure-04-f46facd2-499.webp)

We can now move the keys to a single location where an attacker won’t have access to if they compromise a namespace entirely. Namely, the namespace of whatever ingress software you’re using.

> I know I’m using Ingress Nginx which [just got deprecated](https://kubernetes.io/blog/2025/11/11/ingress-nginx-retirement/), that’s a whole other can of worms I don’t want to go in to. The same principle applies to other ingress servers as well. Granted you can apply custom configs, or if they support cluster-wide mTLS.

## Configuring Ingress Nginx

This is the final solution I ended up with. By no means should my OpenSSL configurations be used as a sane secure default. I’m just providing them here so you can play around with them. I am by no means a cryptographic expert, nor should I ever be trusted with running a real Certificate Authority.

I began by creating my own CA (with a 100 year expiry date because I love breaking my own rules):

```ini
[ req ]
default_bits       = 4096
prompt             = no
default_md         = sha256
distinguished_name = dn
x509_extensions    = v3_ca

[ dn ]
C  = Enter
ST = Distinguised
L  = Name
O  = Stuff
OU = Here

[ v3_ca ]
basicConstraints = critical,CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
```

```bash
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 36500 -out ca.crt -config ca.cnf
```

With our newly created CA public/private key pair, I created the server-side certificates and keys:

```ini
[ req ]
default_bits       = 4096
prompt             = no
default_md         = sha256
req_extensions     = req_ext
distinguished_name = dn

[ dn ]
C  = Enter
ST = Distinguised
L  = Name
O  = Stuff
OU = Here

[ req_ext ]
subjectAltName = @alt_names
extendedKeyUsage = serverAuth

[ alt_names ]
DNS.1 = example.local
DNS.2 = *.example.local
```

```bash
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr -config server.cnf
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36500 -sha256 -extensions req_ext -extfile server.cnf
```

And lastly, the client certificate:

```ini
[ req ]
default_bits       = 4096
prompt             = no
default_md         = sha256
req_extensions     = req_ext
distinguished_name = dn

[ dn ]
C  = Enter
ST = Distinguised
L  = Name
O  = Stuff
CN = Here

[ req_ext ]
extendedKeyUsage = clientAuth
```

```bash
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr -config client.cnf
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 36500 -sha256 -extensions req_ext -extfile client.cnf
PASSWORD=$(uuidgen) && echo "Export password: $PASSWORD" && echo -n $PASSWORD > client.pass && openssl pkcs12 -export -inkey client.key -in client.crt -name "Client RSA" -out client.p12 -passout pass:"$PASSWORD"
```

With all of these certificates, we can now start applying the mTLS certificate across our entire cluster. Please be smart and don’t randomly do this against a production cluster on a Friday evening. You’ll ruin your weekend.

```bash
# Create the default TLS certificate in the `ingress-nginx` namespace
kubectl create secret tls default-tls --cert=server.crt --key=server.key -n ingress-nginx

# Tell ingress nginx to use this newly applied certificate as the default
kubectl patch deployment ingress-nginx-controller -n ingress-nginx --type='json' -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/args/-",
    "value": "--default-ssl-certificate=ingress-nginx/default-tls"
  }
]'

# Create a new secret containing the ca.crt (doesn't need to be secret, but whatever)
kubectl create secret generic mtls-ca --from-file=ca.crt=ca.crt -n ingress-nginx

# Create a volume and a volume mount for the CA
kubectl patch deployment ingress-nginx-controller -n ingress-nginx --type='json' -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/volumeMounts/-",
    "value": {
      "name": "mtls-ca",
      "mountPath": "/etc/nginx/secrets",
      "readOnly": true
    }
  },
  {
    "op": "add",
    "path": "/spec/template/spec/volumes/-",
    "value": {
      "name": "mtls-ca",
      "secret": { "secretName": "mtls-ca" }
    }
  }
]'

# The magic sauce: update the config of `ingress-nginx` with a server-snippet containing the
# `ssl_client_certificate` and `ssl_verify_client`
kubectl patch configmap ingress-nginx-controller -n ingress-nginx --type merge -p '{
  "data": {
    "force-ssl-redirect": "true",
    "server-snippet": "ssl_client_certificate /etc/nginx/secrets/ca.crt;\nssl_verify_client on;"
  }
}'
```

Lastly, all you need to do is import the CA certificate onto your machine and add it as a trusted root, as well as importing the `.p12` bundle containing the mTLS key/cert and you’re good to go!

Whenever I now visit any of my web applications hosted in my cluster, I’m prompted to provide authentication!

![If everything goes well, Chrome should spit out this popup upon visiting](https://blog.paradoxis.nl/assets/img/figure-05-0782b427-546.webp "If everything goes well, Chrome should spit out this popup upon visiting")
