Installing cert-manager

6 Feb 2022 15:18 kubernetes cert-manager

Up to this point, I’ve been creating and installing certificates manually. Let’s see if cert-manager will make that easier.


$ kubectl apply -f
$ kubectl --namespace cert-manager get all
NAME                                         READY   STATUS    RESTARTS   AGE
pod/cert-manager-6d8d6b5dbb-qfxr5            1/1     Running   0          7m4s
pod/cert-manager-webhook-85fb68c79b-gtj2z    1/1     Running   0          7m4s
pod/cert-manager-cainjector-d6cbc4d9-tw5pl   1/1     Running   0          7m4s

NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/cert-manager           ClusterIP     <none>        9402/TCP   7m5s
service/cert-manager-webhook   ClusterIP   <none>        443/TCP    7m5s

NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/cert-manager              1/1     1            1           7m5s
deployment.apps/cert-manager-webhook      1/1     1            1           7m5s
deployment.apps/cert-manager-cainjector   1/1     1            1           7m5s

NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/cert-manager-6d8d6b5dbb            1         1         1       7m5s
replicaset.apps/cert-manager-webhook-85fb68c79b    1         1         1       7m5s
replicaset.apps/cert-manager-cainjector-d6cbc4d9   1         1         1       7m5s

Certificate Issuer

Before we can issue any certificates, we need to create an Issuer or ClusterIssuer resource. These represent a certificate authority. The former issues certificates for the namespace that it’s installed in. The latter can issue certificates for all namespaces.

Because my cluster isn’t multi-tenant, and everything uses the same root CA, I’m going to create a ClusterIssuer. Because I’m using my own root CA (rather than, say, Let’s Encrypt), I’ll use the CA type.


kind: ClusterIssuer
  name: k3s-ca-cluster-issuer
    secretName: k3s-ca-key-pair
kubectl apply -f k3s-ca-cluster-issuer.yaml

If you take a look at the logs for the cert-manager pod, you’ll see that it’s detected the ClusterIssuer, but can’t find the secret (wrapped for readability):

E0206 15:29:47.347247       1 setup.go:48] cert-manager/clusterissuers/setup
    "msg"="error getting signing CA TLS certificate"
    "error"="secret \"k3s-ca-key-pair\" not found"

By default, ClusterIssuer resources look for the named secret in the cert-manager namespace, so:

kubectl --namespace cert-manager \
    create secret tls k3s-ca-key-pair \
        --cert=k3s-ca.crt \

Issuing a certificate

To request a certificate, create a Certificate resource. The documentation is exhaustive, but a minimal example would look like this:

kind: Certificate
  name: example-k3s-differentpla-net
  namespace: default
  secretName: example-k3s-differentpla-net-tls
    name: k3s-ca-cluster-issuer
    kind: ClusterIssuer

If we apply the manifest:

$ kubectl apply -f example-k3s-differentpla-net-certificate.yaml created

$ kubectl --namespace default get secret
NAME                                        TYPE                                  DATA   AGE
example-k3s-differentpla-net-tls                       3      40s
$ kubectl --namespace default get secret example-k3s-differentpla-net-tls -o yaml
apiVersion: v1
  ca.crt: LS0tLS1...
  tls.crt: LS0tLS1...
  tls.key: LS0tLS1...
kind: Secret
  annotations: example-k3s-differentpla-net "" "" "" ClusterIssuer k3s-ca-cluster-issuer ""
  creationTimestamp: "2022-02-06T15:45:23Z"
  name: example-k3s-differentpla-net-tls
  namespace: default
  resourceVersion: "5999398"
  uid: 7f59a415-1b7c-40f3-9027-0453df5ee4e4

And then, of course, we can reference that secret, either directly in an Ingress (of which more later), an IngressRoute, or in a mount or environment variable.

Inspecting the key/certificate

Let’s take a look at the actual certificate and key, to see what’s in them.

kubectl --namespace default get secret example-k3s-differentpla-net-tls \
    --template="{{index .data \"tls.key\" | base64decode}}" > example-k3s-differentpla-net.key
kubectl --namespace default get secret example-k3s-differentpla-net-tls \
    --template="{{index .data \"tls.crt\" | base64decode}}" > example-k3s-differentpla-net.crt
        Version: 3 (0x2)
        Serial Number:
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = GB, L = London, O =, CN = k3s CA
            Not Before: Feb  6 15:45:23 2022 GMT
            Not After : May  7 15:45:23 2022 GMT
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Basic Constraints: critical
            X509v3 Authority Key Identifier:

            X509v3 Subject Alternative Name: critical
    Signature Algorithm: ecdsa-with-SHA256

Comparing that with the the certificates created by my certs script, I can see a couple of issues:

  • It used an RSA key, rather than my preferred EC key.
  • The Extended Key Usage extension is missing.
  • There’s no subject. Using commonName is deprecated in favour of the subjectAlternativeName extension, but it just seems weird.

Some tweaks to the Certificate manifest can fix all of that:

    algorithm: ECDSA
    - server auth
    - client auth

To recreate the certificate, delete the secret. cert-manager will recreate it. Alternatively, wait for it to expire (default 3 months).


If you’re using Ingress resources, you don’t need to create Certificate resources yourself. cert-manager will automatically issue certificates for them, provided you’ve annotated the Ingress appropriately.

However, it doesn’t seem that you can change the private key algorithm by using annotations. See cert-manager#4769.


Per the documentation:

When using the Traefik Kubernetes CRD Provider, unfortunately Cert-Manager cannot interface directly with the CRDs yet, but this is being worked on by our team.

The workaround seems to be to create a fake Ingress along with the real IngressRoute. I’ve not tried that yet.