Self Hosting

Free HTTPS for Kubernetes: Auto-Renewing Let's Encrypt Certificates

December 18, 2025
20 min read
Easy
K8s
Hetzner
Self-host
Fundamentals
Domain
Free HTTPS for Kubernetes: Auto-Renewing Let's Encrypt Certificates

Free HTTPS for Kubernetes: Auto-Renewing Let’s Encrypt Certificates

You have a cluster. You have a domain. Now it’s time to introduce them properly—with encryption, because we’re not savages running plaintext in 2025. This guide wires your domain through Hetzner DNS, points it at your floating IP, and hands off TLS certificate management to cert-manager. The result: automatic HTTPS with zero manual renewal headaches. Forever.

Init

What you need

Before diving in, ensure you have:

  • A running k3s cluster from the Hetzner Terraform setup

  • A domain registered with any provider (we’ll migrate nameservers)

  • kubectl configured against your cluster

  • Terraform with your existing cluster state

  • ping and curl command make remote requests

  • helm to install cert-manager

Apply

Setup DNS provider

Your domain registrar handles registration, but Hetzner will handle resolution. Head to your registrar’s dashboard and update the nameservers to:

  • helium.ns.hetzner.de

  • hydrogen.ns.hetzner.com

  • oxygen.ns.hetzner.com

Propagation Time

DNS propagation can take anywhere from seconds to 48 hours depending on your registrar and TTL settings—grab a coffee, or several

Why Hetzner DNS?

Using Hetzner's nameservers keeps your infrastructure consolidated. More importantly, it enables Terraform to manage DNS records alongside your cluster resources—single source of truth, single terraform apply. The alternative is juggling Cloudflare tokens, Route53 IAM policies, or manual record updates like it's 2010.

Setup DNS in terraform

Pre-require: floating IP output

If you built your cluster through Terraform following Self-host K8s article, this block should already exist in your kube.tf. If not, add it or replace with static IP address—we need the public IP programmatically available for DNS records.

kube.tf

HCL
data "hcloud_floating_ips" "all" {
  depends_on = [module.kube-hetzner]
}

output "floating_ip" {
  description = "Your public IP—point DNS here"
  value       = data.hcloud_floating_ips.all.floating_ips[0].ip_address
}

DNS definition

Now the actual DNS zone and records. We create an A record for the apex domain and a wildcard for subdomains—both pointing at your floating IP.

kube.tf

HCL
Base domain value****

Make sure you export variable

bash

Shell
export TF_VAR_base_domain="your-domain.com"

fish

Shell
# fish
set -xg TF_VAR_base_domain "your-domain.com"

Or define corresponding local value

HCL
locals {
  # ...
  base_domain = "your-domain.com"
}

Run terraform apply and let Hetzner propagate your records.

First check

Time to verify the plumbing. First, grab your floating IP

Shell
terraform output floating_ip

Then confirm DNS resolution points to it

Shell
ping your-domain.com 

You should see something like

Shell
PING your-domain.com (XXX.XX.XXX.XXX) 56(84) bytes of data.
64 bytes from static.XXX.XXX.XXX.XXX.clients.your-server.de (XXX.XX.XXX.XXX): icmp_seq=1 ttl=47 time=69.7 ms
64 bytes from static.XXX.XXX.XXX.XXX.clients.your-server.de (XXX.XX.XXX.XXX): icmp_seq=2 ttl=47 time=77.7 ms
...

The IP in parentheses must match your floating_ip output. If it doesn’t, wait for DNS propagation or double-check your nameserver configuration in your domain provider portal.

Now test HTTP connectivity

Shell
curl http://your-domain.com

Expected response:

HTML
Browser Behavior

Don't bother testing in a browser yet—modern browsers force HTTPS redirects via HSTS preloading, and you'll get a security warning. The curl test confirms your ingress works; TLS comes next.

Real-world Application

Prepare for TLS challenge (HTTP-01)

Congratulations, you now have a domain pointing at a cluster serving unencrypted traffic. Your 2005 PHP forum would be proud. Let’s fix that embarrassment with cert-manager and Let’s Encrypt.

Install cert-manager

Make sure in pod list there are cert-manager namespace, if it’s missing install it through helm command

Shell
helm repo add jetstack https://charts.jetstack.io && \
helm repo update && \
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --version v1.16.0 \
  --set crds.enabled=true

Certificate Issuer

With cert-manager running, define a ClusterIssuer to handle certificate requests across all namespaces. We’re using HTTP-01 challenge through Cilium’s ingress controller.

cluster-issuer.yaml

YAML

Update values and apply it

Shell
kubectl apply -f cluster-issuer.yaml
HTTP-01 vs DNS-01 Challenge: Which Should You Use?

HTTP-01 Challenge (what we're using)

  • How it works: Let's Encrypt asks cert-manager to serve a specific token at http://your-domain.com/.well-known/acme-challenge/<token>. If Let's Encrypt can fetch it, you've proven domain ownership.
  • Requirements: Port 80 must be open and reachable from the internet. Your ingress controller must be working.
  • Pros: Simple setup, works out of the box with any ingress controller, no DNS provider API access needed.
  • Cons: Can't issue wildcard certificates (*.your-domain.com). Won't work for internal-only services that aren't internet-accessible.
  • Best for: Public-facing services, simple setups, getting started quickly.

DNS-01 Challenge

  • How it works: Cert-manager creates a TXT record at _acme-challenge.your-domain.com via your DNS provider's API. Let's Encrypt verifies the record exists.
  • Requirements: API credentials for your DNS provider (Hetzner, Cloudflare, Route53, etc.). More complex cert-manager configuration with secrets.
  • Pros: Supports wildcard certificates. Works for internal services with no public ingress. No port 80 requirement.
  • Cons: Requires DNS provider integration. More moving parts. Propagation delays can cause validation failures.
  • Best for: Wildcard certs, internal services, staging environments, multi-tenant platforms.

The pragmatic choice: Start with HTTP-01. It covers 90% of use cases with minimal configuration. Graduate to DNS-01 when you need wildcards or internal service certificates—we'll cover that setup in a future guide.

Update Ingress

Now update your existing nginx ingress to request a certificate. The annotations tell cert-manager which issuer to use and how to handle the challenge.

nginx-ingress.yaml

YAML

Update values and apply it

Shell
kubectl apply -f nginx-ingress.yaml
Certificate Provisioning

Give cert-manager 30-90 seconds to complete the challenge and provision your certificate. Monitor progress with kubectl describe certificate -n test your-domain-tls or through K9s

Once the certificate status shows Ready, open your browser and navigate to https://your-domain.com. Green padlock. No warnings. You did it.

browser-tls-not-secure-to-secure

Troubleshooting: Certificate Stuck in Pending

Sometimes certificates don’t issue on the first try. Here’s how to diagnose and fix common problems.

Step 1: Check Certificate Status

Shell
kubectl get certificate -n test

If READY shows False, dig deeper:

Shell
kubectl describe certificate -n test your-domain-tls

Look for the Status section and any events at the bottom. Common messages include “Waiting for CertificateRequest” or “Challenge failed.”

Step 2: Inspect the CertificateRequest

Shell
kubectl get certificaterequest -n test
kubectl describe certificaterequest -n test <certificate-request-name>

This shows whether cert-manager successfully created the request and if any validation errors occurred.


<-- Back to Blog <-- Back to Self Hosting