← Back to Articles
· kubernetes

Free HTTPS for Your Homelab Kubernetes Cluster with cert-manager and Let's Encrypt

A step-by-step companion to Episode 6 of the Kubernetes on Raspberry Pi series. Covers cert-manager, ClusterIssuers, DNS-01 wildcard certificates with DNSimple, and forcing HTTPS in Traefik.

Watch the Video kubernetescert-managerletsencrypttlshttpstraefikraspberry-pihomelabdns

Every service in the cluster is reachable by hostname now, but they're all running over HTTP. Browsers warn about insecure connections, and any credential sent to Gitea or Grafana crosses the network unencrypted. In Episode 5 we built the routing layer. Now we add free, auto-renewing TLS certificates: one wildcard that covers everything.

This is the companion article to Episode 6 of the Kubernetes on Raspberry Pi series.

All configs are in the kubernetes-series GitHub repo under video-06-tls-cert-manager/.

How It Works

Three pieces work together. cert-manager watches for Certificate resources, talks to Let's Encrypt, stores the resulting cert as a Kubernetes Secret, and renews it automatically before expiry. Let's Encrypt is the free Certificate Authority that issues the certs. The DNS-01 challenge is how we prove domain ownership without exposing any ports publicly, which makes it the right approach for internal services and wildcard certs.

Because we own spatacoli.xyz, cert-manager can get a wildcard cert (*.spatacoli.xyz) that covers every service in the cluster, even though none of them are publicly reachable.

Installing cert-manager

# cert-manager-values.yaml
crds:
  enabled: true
extraArgs:
  - --dns01-recursive-nameservers=8.8.8.8:53,1.1.1.1:53
  - --dns01-recursive-nameservers-only

Why those flags? By default cert-manager uses your cluster's internal DNS to verify DNS-01 challenge records. If you're running BIND9 split-horizon DNS (as we set up in Episode 5), cert-manager will query it and get nothing back, even if the public DNS record exists. These flags force cert-manager to use Google and Cloudflare instead.

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --values cert-manager-values.yaml

Verify all three cert-manager components are running:

kubectl get pods -n cert-manager
# cert-manager, cert-manager-cainjector, cert-manager-webhook -- all Running

Creating the DNSimple API Secret

Generate an API token in your DNSimple account under Account > API Access > New Access Token. Store it as a Kubernetes Secret so cert-manager can read it:

kubectl create secret generic dnsimple-api-token \
  --from-literal=api-token=<your-token> \
  --namespace cert-manager

Creating a ClusterIssuer

A ClusterIssuer defines how cert-manager talks to Let's Encrypt. We use staging first to avoid rate limits. Let's Encrypt has strict limits on production certificates, and you can burn through them quickly while getting things working.

# cluster-issuer-staging.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: your@email.com
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - dns01:
          dnsimple:
            apiTokenSecretRef:
              name: dnsimple-api-token
              key: api-token
kubectl apply -f cluster-issuer-staging.yaml
kubectl get clusterissuer
# READY should show True

Requesting a Wildcard Certificate

The dnsNames field includes both the apex domain and the wildcard, letting you use the same cert for both spatacoli.xyz and gitea.spatacoli.xyz:

# wildcard-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: spatacoli-xyz-wildcard
  namespace: traefik
spec:
  secretName: spatacoli-xyz-wildcard-tls
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
  dnsNames:
    - "spatacoli.xyz"
    - "*.spatacoli.xyz"
kubectl apply -f wildcard-cert.yaml
kubectl get certificate -n traefik -w
# Watch READY column -- will flip to True once the DNS-01 challenge completes

DNS propagation takes 1-5 minutes. You can describe the Certificate resource to see challenge status while you wait.

Switching Ingress Resources to HTTPS

Once the certificate is ready, update your Ingress resources to use the websecure entrypoint and reference the TLS secret. The key changes are the Traefik annotations and the tls block:

# gitea-ingress.yaml (updated)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gitea-ingress
  namespace: gitea
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
spec:
  ingressClassName: traefik
  tls:
    - secretName: spatacoli-xyz-wildcard-tls
  rules:
    - host: gitea.spatacoli.xyz
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: gitea
                port:
                  number: 3000

Also update Gitea's ROOT_URL to use https://:

- name: GITEA__server__ROOT_URL
  value: https://gitea.spatacoli.xyz/

Repeat the Ingress update for Grafana, updating its root_url as well.

Switching to Production Certificates

Staging certificates are signed by an untrusted CA, so browsers will still warn. That's fine for verification purposes. Once you've confirmed there are no errors in the cert-manager logs and the challenge process worked correctly, create the production ClusterIssuer:

# cluster-issuer-prod.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your@email.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - dns01:
          dnsimple:
            apiTokenSecretRef:
              name: dnsimple-api-token
              key: api-token

Update the Certificate resource to reference letsencrypt-prod and re-apply. The cert will be reissued and the browser will show a trusted padlock. cert-manager renews it automatically before expiry, so you never need to think about certificates again.

What's Next

The cluster now has free, auto-renewing HTTPS for every service. In Episode 7 we connect the cluster to a GPU: bridging Kubernetes with a Windows PC running Ollama for local AI inference.

← Back to Articles