cert-manager is one of the standard ways to manage TLS certificates in Kubernetes. It can request, issue, renew, and rotate certificates automatically, and it integrates well with Ingress controllers, gateways, and applications running inside the cluster.
In public environments, the most common choice is Let’s Encrypt. In a home lab, though, a private Certificate Authority (CA) is often more practical: it works without exposing services to the Internet, it does not depend on ACME challenges, and it gives you fast, repeatable certificate issuance for internal hostnames.
This is an updated version of my original post. The idea is the same, but the manifests, API versions, and recommendations have been refreshed for current Kubernetes and cert-manager releases.
In this article, I will use
mkcert to create a local CA, import that CA into cert-manager, and issue a test certificate from a ClusterIssuer.
What this setup is good for
This approach is a good fit when:
- you run a home lab or internal Kubernetes cluster
- you want trusted HTTPS for internal services
- you control the client machines that will trust the CA
- you do not want to depend on public DNS or public ACME challenges
It is not the right approach for public Internet-facing services. For that, use an ACME issuer such as Let’s Encrypt.
Install cert-manager
Start by installing cert-manager in your cluster. The official release manifest is the simplest option for a lab environment because it includes the CRDs and the controller components.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
Then verify that the pods are running:
kubectl -n cert-manager get pods
NAME READY STATUS RESTARTS AGE
cert-manager-webhook-578954cdd-wwzgg 1/1 Running 0 14d
cert-manager-86548b886-cflh5 1/1 Running 1 14d
cert-manager-cainjector-6d59c8d4f7-lgtv4 1/1 Running 1 14d
Once cert-manager is ready, the next step is to give it a CA that it can use to sign certificates.
Create a local CA with mkcert
For this example, I will use mkcert, a very convenient tool for generating locally trusted development certificates.
Download the binary for your platform from the mkcert releases page, create a working directory, and initialize your CA there.
The example below uses Linux on amd64. If you are on macOS, installing mkcert with Homebrew is usually easier, but the overall workflow is the same.
mkdir internalCA && cd internalCA
wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64
chmod +x mkcert-v1.4.3-linux-amd64
CAROOT=. ./mkcert-v1.4.3-linux-amd64 -install
This creates the CA certificate and private key in the local directory and also installs the root CA in your machine’s trust store.
On Linux, mkcert usually copies the root CA into /usr/local/share/ca-certificates/. On macOS and Windows, it updates the native system trust store instead.
root@zbox:/tmp/internalCA# ls -l
total 4700
-rwxr-xr-x 1 root root 4803796 Nov 25 14:17 mkcert-v1.4.3-linux-amd64
-r-------- 1 root root 2484 Dec 26 00:54 rootCA-key.pem
-rw-r--r-- 1 root root 1594 Dec 26 00:54 rootCA.pem
root@zbox:/tmp/internalCA# ls -l /usr/local/share/ca-certificates/
total 4
-rw-r--r-- 1 root root 1594 Dec 26 00:48 mkcert_development_CA_75165785500873602433925991678959123844.crt
From here, cert-manager needs access to that CA certificate and private key.
Store the CA key pair in Kubernetes
Create a TLS secret in the cert-manager namespace:
kubectl create secret tls ca-key-pair \
--cert=rootCA.pem \
--key=rootCA-key.pem \
--namespace=cert-manager
Verify that the secret was created correctly:
root@zbox:/tmp/internalCA# kubectl -n cert-manager describe secret ca-key-pair
Name: ca-key-pair
Namespace: cert-manager
Labels: <none>
Annotations: <none>
Type: kubernetes.io/tls
Data
====
tls.crt: 1688 bytes
tls.key: 2488 bytes
Create a ClusterIssuer
cert-manager supports two issuer scopes:
Issuer, which is namespacedClusterIssuer, which can be used from any namespace
For a home lab, ClusterIssuer is usually more convenient, especially if several services in different namespaces will request certificates.
Create localCAissuer.yaml and point it to the secret you just created:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: ca-issuer
spec:
ca:
secretName: ca-key-pair
Apply it and inspect its status:
kubectl apply -f localCAissuer.yaml
kubectl describe clusterissuer ca-issuer
If everything is correct, the issuer should become Ready.
Example output:
Name: ca-issuer
Namespace:
Labels: <none>
Annotations: <none>
API Version: cert-manager.io/v1
Kind: ClusterIssuer
Metadata:
Creation Timestamp: 2020-11-29T19:01:59Z
Generation: 1
Managed Fields:
API Version: cert-manager.io/v1
Fields Type: FieldsV1
fieldsV1:
f:status:
f:conditions:
Manager: controller
Operation: Update
Time: 2020-11-29T19:01:59Z
API Version: cert-manager.io/v1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:kubectl.kubernetes.io/last-applied-configuration:
f:spec:
.:
f:ca:
.:
f:secretName:
Manager: kubectl-client-side-apply
Operation: Update
Time: 2020-11-29T19:01:59Z
Resource Version: 1072
Self Link: /apis/cert-manager.io/v1/clusterissuers/ca-issuer
UID: 4c8396be-a038-4170-b754-3107cb39425c
Spec:
Ca:
Secret Name: ca-key-pair
Status:
Conditions:
Last Transition Time: 2020-11-29T19:01:59Z
Message: Signing CA verified
Reason: KeyPairVerified
Status: True
Type: Ready
Events: <none>
Request a certificate
Now that the CA is available through cert-manager, create your first Certificate resource.
For this example I will use nip.io in the hostname, because it is convenient in small labs and does not require dedicated DNS management for every test service.
Create testcert.yaml with these values:
- kind: Certificate
- namespace: default
- issuer: ClusterIssuer ca-issuer
- dnsNames: test.192.168.1.222.nip.io
Replace 192.168.1.222 with the IP address that makes sense in your own environment. If you use another wildcard DNS helper, just adapt the hostname.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: test-cert
namespace: default
spec:
secretName: test-cert-tls
issuerRef:
name: ca-issuer
kind: ClusterIssuer
commonName: test.192.168.1.222.nip.io
organizations:
- ZBOX CA
dnsNames:
- test.192.168.1.222.nip.io
Apply the manifest:
kubectl apply -f testcert.yaml
If the request succeeds, the certificate should become ready:
root@zbox:/tmp/internalCA# kubectl get certificate test-cert
NAME READY SECRET AGE
test-cert True test-cert-tls 6d
You can then inspect the generated secret:
root@zbox:/tmp/internalCA# kubectl describe secret test-cert-tls
Name: test-cert-tls
Namespace: default
Labels: <none>
Annotations: cert-manager.io/alt-names: test.192.168.1.222.nip.io
cert-manager.io/certificate-name: test-cert
cert-manager.io/common-name: test.192.168.1.222.nip.io
cert-manager.io/ip-sans:
cert-manager.io/issuer-group:
cert-manager.io/issuer-kind: ClusterIssuer
cert-manager.io/issuer-name: ca-issuer
cert-manager.io/uri-sans:
Type: kubernetes.io/tls
Data
====
ca.crt: 1688 bytes
tls.crt: 1489 bytes
tls.key: 1679 bytes
At this point, cert-manager has signed the certificate using the CA generated by mkcert, and stored the result in a Kubernetes TLS secret containing ca.crt, tls.crt, and tls.key.
You can now reference that secret from an Ingress controller, a Gateway, or any application that expects a standard Kubernetes TLS secret. In my case, I originally used it with Traefik for local HTTPS endpoints.
Practical notes
- Use
cert-manager.io/v1resources. Older API versions such asv1alpha2are long deprecated. - A private CA created with
mkcertis excellent for development and home labs, but it is not a public trust solution. - Every client that should trust certificates issued by this CA must also trust the mkcert root CA.
- Keep the CA private key safe. Anyone with access to it can issue certificates trusted by your lab clients.
- If you want certificates that browsers trust by default on the public Internet, use an ACME issuer such as Let’s Encrypt instead.
Why this still works well in 2026
Even with all the changes in Kubernetes over the last few years, this pattern is still extremely useful in internal environments:
- cert-manager handles issuance and renewal automatically
- workloads consume certificates through normal Kubernetes secrets
- you avoid manual certificate generation for every service
- you get a setup that is simple, predictable, and easy to reproduce
For a home lab, that is usually exactly what you want.
If you later decide to expose services publicly, the migration path is straightforward: keep cert-manager, replace the private CA issuer with an ACME issuer, and request publicly trusted certificates with almost the same workflow.