Skip to content

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.

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: websecure
spec:
ingressClassName: traefik
tls:
- hosts: [api.<domain>]
secretName: <app>-api-tls

Backends serve plain HTTP behind Traefik — e.g. Keycloak runs httpEnabled: true and trusts Traefik’s X-Forwarded-* headers (proxy.headers: xforwarded).

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:

  1. The public DNS A record for the hostname points at the node’s public IP.
  2. 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.

flowchart TD
  H1[ingress hostnames<br/>api.&lt;domain&gt;<br/>auth.&lt;auth-domain&gt;<br/>argocd.&lt;auth-domain&gt;] -->|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, ultron resolves to its Tailscale address. The API server is unreachable from the public internet by design.