Trusted local TLS certificates with mkcert and Kubernetes

Something you might often do when deploying and testing your application on a local or a test development Kubernetes cluster is to hit the Ingress and test access. You’re almost certainly configuring TLS, and with that comes the question of what to do with x509 certificates. Typically you’d let the venerable cert-manager handle this for you, but what if you don’t want to battle with self-signed certificates and your browser (thisisunsafe) or local client moaning everytime you connect? Turns out there’s an easy solution to this by making use of a criminally overlooked project called mkcert and leveraging that in combination with cert-manager to provide a very simple solution.

I’m going to go with the assumption that you have a local Kubernetes cluster installed along with cert-manager plus the NGINX Ingress controller, but that you’ve not yet defined any Issuers or ClusterIssuers. The first step then is to install mkcert on your local machine, and then use that to generate and install the CA certificate and keys:

mkcert -install

This is a one-time thing, and now your browser plus other clients on your local machine will trust any certificates generated via this CA. Now let’s create a Secret in Kubernetes with this CA’s certificate and Key:

kubectl create secret tls mkcert-ca-key-pair \
--key "$(mkcert -CAROOT)"/rootCA-key.pem \
--cert "$(mkcert -CAROOT)"/rootCA.pem -n cert-manager

And now for the bit that tells cert-manager to make use of this:

kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: mkcert-issuer
  namespace: cert-manager
spec:
  ca:
    secretName: mkcert-ca-key-pair
EOF

And that’s it. Now when you define your Ingress, you just need to make sure the cert-manager related annotation refers to this ClusterIssuer we defined (mkcert-issuer) and it’ll generate the appropriate certificate and key for you. Let’s do a quick test:

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-deployment
  labels:
    app: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - args:
        - netexec
        image: registry.k8s.io/e2e-test-images/agnhost:2.40
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: demo-service
  labels:
    app: demo
spec:
  selector:
    app: demo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: mkcert-issuer
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - hello.192.168.106.4.nip.io
    secretName: hello-ingress-cert
  rules:
  - host: hello.192.168.106.4.nip.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo-service
            port:
              number: 80
EOF

Season to taste (i.e change the IP address) and all being well your browser should trust the HTTPS site you’re connecting to:

Well Done