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)
kubectlconfigured against your clusterTerraform with your existing cluster state
pingandcurlcommand make remote requestshelmto installcert-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.dehydrogen.ns.hetzner.comoxygen.ns.hetzner.com
DNS propagation can take anywhere from seconds to 48 hours depending on your registrar and TTL settings—grab a coffee, or several
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
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
# === INPUT VARIABLES ===
locals {
# ...
base_domain = "***"
}
variable "base_domain" {
description = "Base domain for the cluster, e.g. example.com"
}
# === DNS ZONE & RECORDS ===
resource "hcloud_zone" "my_domain" {
name = var.base_domain != "" ? var.base_domain : local.base_domain
mode = "primary"
ttl = 3600
}
resource "hcloud_zone_rrset" "root" {
zone = hcloud_zone.my_domain.id
name = "@" # main domain
type = "A"
ttl = 300
records = [
{
value = data.hcloud_floating_ips.all.floating_ips[0].ip_address
}
]
}
resource "hcloud_zone_rrset" "wildcard" {
zone = hcloud_zone.my_domain.id
name = "*" # all sub-domains
type = "A"
ttl = 300
records = [
{
value = data.hcloud_floating_ips.all.floating_ips[0].ip_address
}
]
}
Base domain value****
Make sure you export variable
bash
export TF_VAR_base_domain="your-domain.com"
fish
# fish
set -xg TF_VAR_base_domain "your-domain.com"
Or define corresponding local value
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
terraform output floating_ip
Then confirm DNS resolution points to it
ping your-domain.com
You should see something like
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
curl http://your-domain.com
Expected response:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
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
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