Kubernetes
TLS
Ingress
cert-manager
ClusterIssuer

Kubernetes TLS Ingress route with cert-manager and SelfSigned ClusterIssuer not working

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When a TLS Ingress route with cert-manager and a SelfSigned ClusterIssuer is not working, the issue is typically one of three things: the ClusterIssuer is not in a Ready state, the Ingress annotations are wrong, or the Certificate/Secret is not being created in the correct namespace. Self-signed certificates add an extra layer of complexity because browsers and clients reject them by default. This guide walks through the correct configuration and the most common failure points.

Correct Setup

Step 1: Create the SelfSigned ClusterIssuer

yaml
1# clusterissuer.yaml
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5  name: selfsigned-issuer
6spec:
7  selfSigned: {}
bash
1kubectl apply -f clusterissuer.yaml
2kubectl get clusterissuer selfsigned-issuer
3# NAME                READY   AGE
4# selfsigned-issuer   True    10s

The READY column must show True. If it shows False, cert-manager cannot issue certificates.

Step 2: Create the Ingress with TLS

yaml
1# ingress.yaml
2apiVersion: networking.k8s.io/v1
3kind: Ingress
4metadata:
5  name: my-app-ingress
6  annotations:
7    cert-manager.io/cluster-issuer: "selfsigned-issuer"
8spec:
9  ingressClassName: nginx
10  tls:
11    - hosts:
12        - app.example.com
13      secretName: my-app-tls
14  rules:
15    - host: app.example.com
16      http:
17        paths:
18          - path: /
19            pathType: Prefix
20            backend:
21              service:
22                name: my-app-service
23                port:
24                  number: 80

Key annotations:

  • cert-manager.io/cluster-issuer: References the ClusterIssuer (not cert-manager.io/issuer, which references a namespaced Issuer)
  • secretName in tls: Name of the Secret where cert-manager stores the certificate

Step 3: Verify the Certificate

bash
1# Check if Certificate was created
2kubectl get certificate -n default
3# NAME         READY   SECRET       AGE
4# my-app-tls   True    my-app-tls   30s
5
6# Check certificate details
7kubectl describe certificate my-app-tls
8
9# Check the secret
10kubectl get secret my-app-tls
11# NAME         TYPE                DATA   AGE
12# my-app-tls   kubernetes.io/tls   3      30s

Debugging: Certificate Not Ready

bash
1# Step 1: Check ClusterIssuer status
2kubectl describe clusterissuer selfsigned-issuer
3# Look for: Status → Conditions → Ready = True
4
5# Step 2: Check Certificate status
6kubectl describe certificate my-app-tls -n default
7# Look for: Status → Conditions → Ready
8# Look for: Events — shows success or failure reasons
9
10# Step 3: Check CertificateRequest
11kubectl get certificaterequest -n default
12kubectl describe certificaterequest <name> -n default
13
14# Step 4: Check cert-manager logs
15kubectl logs -n cert-manager deployment/cert-manager -f

Common Issue 1: Wrong Annotation Name

yaml
1# WRONG — old annotation format (cert-manager v0.x)
2annotations:
3  certmanager.k8s.io/cluster-issuer: "selfsigned-issuer"
4
5# WRONG — using issuer annotation for ClusterIssuer
6annotations:
7  cert-manager.io/issuer: "selfsigned-issuer"
8
9# CORRECT — cert-manager v1.x annotation for ClusterIssuer
10annotations:
11  cert-manager.io/cluster-issuer: "selfsigned-issuer"

Common Issue 2: Namespace Mismatch

A ClusterIssuer is cluster-scoped, but Issuer is namespace-scoped. If you created an Issuer instead of a ClusterIssuer, the Ingress in a different namespace cannot find it:

yaml
1# This is a namespaced Issuer — only works in the same namespace
2apiVersion: cert-manager.io/v1
3kind: Issuer          # NOT ClusterIssuer
4metadata:
5  name: selfsigned-issuer
6  namespace: default  # Only accessible in 'default' namespace
7
8# Use ClusterIssuer for cross-namespace access
9apiVersion: cert-manager.io/v1
10kind: ClusterIssuer   # Works in ALL namespaces
11metadata:
12  name: selfsigned-issuer

Common Issue 3: IngressClass Not Set

yaml
1# WRONG — missing ingressClassName (Kubernetes 1.22+)
2apiVersion: networking.k8s.io/v1
3kind: Ingress
4metadata:
5  name: my-app-ingress
6spec:
7  # No ingressClassName — no controller picks it up
8  tls:
9    - hosts:
10        - app.example.com
11      secretName: my-app-tls
12
13# CORRECT
14spec:
15  ingressClassName: nginx  # Or traefik, haproxy, etc.

Without ingressClassName, no Ingress controller processes the Ingress resource, and TLS termination never happens.

Common Issue 4: Self-Signed CA for Multiple Certificates

A basic SelfSigned issuer creates a new self-signed certificate each time. For a proper CA setup, create a self-signed root CA, then use it to sign other certificates:

yaml
1# Step 1: Self-signed issuer to create the CA certificate
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5  name: selfsigned-bootstrap
6spec:
7  selfSigned: {}
8---
9# Step 2: CA certificate (signed by selfsigned-bootstrap)
10apiVersion: cert-manager.io/v1
11kind: Certificate
12metadata:
13  name: my-ca
14  namespace: cert-manager
15spec:
16  isCA: true
17  commonName: my-ca
18  secretName: my-ca-secret
19  issuerRef:
20    name: selfsigned-bootstrap
21    kind: ClusterIssuer
22---
23# Step 3: CA issuer that uses the CA certificate
24apiVersion: cert-manager.io/v1
25kind: ClusterIssuer
26metadata:
27  name: my-ca-issuer
28spec:
29  ca:
30    secretName: my-ca-secret

Then reference my-ca-issuer in your Ingress annotations instead of the self-signed issuer directly.

Testing the TLS Connection

bash
1# Verify with curl (skip certificate verification for self-signed)
2curl -k https://app.example.com
3
4# Inspect the certificate
5openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null | openssl x509 -noout -text
6
7# Check from inside the cluster
8kubectl run tmp --image=curlimages/curl --rm -it -- curl -k https://my-app-service.default.svc.cluster.local

Common Pitfalls

  • Using cert-manager.io/issuer instead of cert-manager.io/cluster-issuer: These are different annotations. issuer looks for a namespaced Issuer resource; cluster-issuer looks for a ClusterIssuer. Using the wrong one causes cert-manager to silently fail to find the issuer.
  • cert-manager not installed or CRDs missing: If cert-manager is not installed, the annotations are ignored and no Certificate is created. Verify with kubectl get pods -n cert-manager and kubectl get crd | grep cert-manager.
  • Secret not in the same namespace as the Ingress: The TLS secret must exist in the same namespace as the Ingress. cert-manager creates the secret in the Ingress namespace automatically, but if you created it manually in the wrong namespace, the Ingress controller cannot find it.
  • Browsers rejecting self-signed certificates: Self-signed certificates are not trusted by browsers or HTTP clients by default. For development, use curl -k or add the CA to your system trust store. For production, use Let's Encrypt with an ACME ClusterIssuer instead.
  • DNS not pointing to the Ingress controller: Even with a valid certificate, if app.example.com does not resolve to the Ingress controller's external IP, TLS connections fail. Verify with nslookup app.example.com and kubectl get service -n ingress-nginx.

Summary

  • Create a ClusterIssuer with selfSigned: {} and verify it shows READY: True
  • Use the annotation cert-manager.io/cluster-issuer (not cert-manager.io/issuer) on the Ingress
  • Set ingressClassName in the Ingress spec (required in Kubernetes 1.22+)
  • Check Certificate, CertificateRequest, and cert-manager logs when debugging
  • For proper CA hierarchy, create a self-signed root CA and use a CA ClusterIssuer to sign app certificates
  • Self-signed certificates are for development only — use Let's Encrypt ACME for production

Course illustration
Course illustration

All Rights Reserved.