The single edge: Traefik + cert-manager + DNS
All inbound HTTPS enters through one door: Traefik, the Ingress controller bundled with k3s. Nothing else listens on the public 80/443. cert-manager obtains and renews the TLS certificates, and DNS deliberately splits into two planes — public for ingress, private (Tailscale) for the Kubernetes API.
The request path
Section titled “The request path”flowchart LR B([Browser]) -->|DNS lookup| DNS[(public A record)] DNS -->|node public IP| N[node :443] N --> T[Traefik<br/>terminates TLS] T -->|by hostname| Svc[Service] Svc --> P[Pod]
A request to api.<domain> resolves to the node’s public IP, hits
:443, and Traefik terminates TLS and routes by hostname to the matching Service.
Each Ingress declares its host, entrypoint, and the cert-manager issuer:
metadata: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod traefik.ingress.kubernetes.io/router.entrypoints: websecurespec: ingressClassName: traefik tls: - hosts: [api.<domain>] secretName: <app>-api-tlsBackends serve plain HTTP behind Traefik — e.g. Keycloak runs httpEnabled: true
and trusts Traefik’s X-Forwarded-* headers (proxy.headers: xforwarded).
How certificates get issued
Section titled “How certificates get issued”cert-manager solves Let’s Encrypt’s HTTP-01 challenge through Traefik on port 80. For that to work, two things must already be true:
- The public DNS A record for the hostname points at the node’s public IP.
- Port 80 is reachable from the internet (open in the host firewall / VCN).
Let’s Encrypt fetches a token over http://<host>/.well-known/..., confirms control
of the name, and issues the cert into the Secret named in the Ingress
(<app>-api-tls, keycloak-test-tls). cert-manager renews it automatically.
Two DNS planes
Section titled “Two DNS planes”flowchart TD H1[ingress hostnames<br/>api.<domain><br/>auth.<auth-domain><br/>argocd.<auth-domain>] -->|public IP| Edge[node :443 Traefik] H2[ultron<br/>kube API] -->|Tailscale IP| API[kube API :6443]
- Ingress hostnames → the node’s public IP. These are the live endpoints per
app: the Argo CD UI (
argocd.<auth-domain>), the shared auth server (auth.<auth-domain>), and each app’s API (api.<domain>). An app’s web frontend (e.g.<app>.<domain>) may live off-cluster — Penvoice’s, for instance, is on Vercel. - Kubernetes API → the node’s Tailscale IP, never the public
6443. On dev machines,ultronresolves to its Tailscale address. The API server is unreachable from the public internet by design.