πŸ” TLS Certificates for Homelab Using Cloudflare + Cert-Manager + Let’s Encrypt

With Traefik operational as the ingress controller in a K3s cluster, the next logical step is to provision HTTPS automatically using Let’s Encrypt certificates, Cloudflare DNS validation, and Cert-Manager. This ensures encrypted traffic across services in a self-hosted homelab.

☁️ Prerequisites

  1. A domain managed by Cloudflare
  2. An API token with DNS edit permissions
  3. Traefik installed and configured as the ingress controller
  4. A functional K3s cluster with outbound internet access

πŸ”§ Step 1: Install Cert-Manager

Install cert-manager using Helm

kubectl create namespace cert-manager

helm repo add jetstack https://charts.jetstack.io
helm repo update

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.12.17/cert-manager.crds.yaml

kubectl get  certificates

Create a values.yaml

installCRDs: false
replicaCount: 3
extraArgs:
  - --dns01-recursive-nameservers=1.1.1.1:53,9.9.9.9:53
  - --dns01-recursive-nameservers-only
podDnsPolicy: None
podDnsConfig:
  nameservers:
    - 1.1.1.1
    - 9.9.9.9

Install cert-manager

helm install cert-manager jetstack/cert-manager --namespace cert-manager --values=values.yaml --version v1.12.17

πŸ”‘ Step 2: Create a Cloudflare API Token

Generate an API token via Cloudflare Dashboard:

  • Navigate to My Profile β†’ API Tokens β†’ Create Token

  • Use the Edit zone DNS template

  • Scope the token to either All Zones

  • Save the token securely

🧾 Step 3: Store Cloudflare Token as Kubernetes Secret

Create a secret manifest (secret-cf-token.yaml)

---
apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-token-secret
  namespace: cert-manager
type: Opaque
stringData:
  cloudflare-token: <cloudlare-api-token>
kubectl apply -f secret-cf-token.yaml

πŸ“„ Step 4: Configure a Let’s Encrypt ClusterIssuer (Staging)

Start with Let’s Encrypt staging environment for testing:

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-staging
    solvers:
      - dns01:
          cloudflare:
            email: [email protected]
            apiTokenSecretRef:
              name: cloudflare-token-secret
              key: cloudflare-token
        selector:
          dnsZones:
            - "example.com"
kubectl apply -f letsencrypt-issuer-staging.yaml

πŸ” Step 5: Request a Certificate

Issue a wildcard TLS certificate for the domain

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  secretName: example-com-staging-tls
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
  commonName: "*.example.com"
  dnsNames:
    - "example.com"
    - "*.example.com"
kubectl apply -f example-com-cert.yaml

πŸšͺ Step 6: Enable TLS for Services

Configure Traefik’s IngressRoute to use the issued certificate

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-dashboard
  namespace: traefik
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`traefik-dashboard.example.com`)
      kind: Rule
      middlewares:
        - name: traefik-dashboard-basicauth
      services:
        - name: api@internal
          kind: TraefikService
  tls:
    secretName: example-com-staging-tls
kubectl apply -f ingress.yaml

πŸš€ Switch to Let’s Encrypt Production

Once certificate issuance through staging is successful, switch to the production environment.

Create the production ClusterIssuer

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: [email protected]
    privateKeySecretRef:
      name: letsencrypt-production
    solvers:
      - dns01:
          cloudflare:
            email: [email protected]
            apiTokenSecretRef:
              name: cloudflare-token-secret
              key: cloudflare-token
        selector:
          dnsZones:
            - "example.com"
kubectl apply -f letsencrypt-issuer.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  secretName: example-com-production-tls
  issuerRef:
    name: letsencrypt-production
    kind: ClusterIssuer
  commonName: "*.example.com"
  dnsNames:
    - "example.com"
    - "*.example.com"
kubectl apply -f example-com-cert.yaml

kubectl get certificates #should take a while for certs to be issued

##🌐 Final Step: Update DNS Records in Cloudflare Create a CNAME or A record in Cloudflare pointing traefik-dashboard.example.com to your public IP or create cloudflared tunnels