Networking & TLS
External traffic to the homelab is routed through two Cloudflare tunnels — one inside the Kubernetes cluster, one inside the Forgejo Docker Compose stack. There’s no port forwarding on the home router, no public IP exposure, and no static IP requirement.
Two tunnels, different scopes
*.skypalette.ai git.skypalette.ai
│ │
▼ ▼
in-cluster cloudflared Forgejo-stack cloudflared
│ │
▼ ▼
ingress-nginx (k8s) forgejo container (docker)Each tunnel runs as a separate cloudflared connector with its own
credentials in Cloudflare. The in-cluster one is two replicas for HA;
Forgejo’s is one container in the compose stack.
*.skypalette.aiis a single proxied CNAME at the Cloudflare DNS level pointing at the in-cluster tunnel’s hostname. Every subdomain flows throughingress-nginxand gets routed byHostheader.git.skypalette.aiis a separate proxied CNAME pointing at the Forgejo-side tunnel.
The tunnel tokens are stored encrypted (SOPS for the in-cluster one,
~/homelab-git/.env for the Forgejo one). Both tokens are
base64-encoded JSON containing {a, t, s} = {AccountTag, TunnelID, TunnelSecret}.
ingress-nginx
Standard upstream ingress-nginx-controller chart, deployed as one
replica. Every app’s Ingress is a normal resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
rules:
- host: web-app.skypalette.ai
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: web-app, port: { number: 80 } }
tls:
- hosts: [web-app.skypalette.ai]
secretName: web-app-tlscert-manager + Let’s Encrypt
cert-manager watches Ingress objects for the
cert-manager.io/cluster-issuer annotation, then requests a certificate
via the HTTP-01 challenge. The challenge succeeds because cloudflared
will forward /.well-known/acme-challenge/... like any other path.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef: { name: letsencrypt-prod-account-key }
solvers:
- http01:
ingress:
ingressClassName: nginxRenewals are automatic 30 days before expiry. If a cert is within 14 days,
CertificateExpiringSoon fires (homelab-alerts PrometheusRule).
Wildcard DNS, not per-subdomain
*.skypalette.ai is a single proxied CNAME at Cloudflare. Adding a new
subdomain requires zero DNS changes — just create an Ingress with the new
Host, and cert-manager + ingress-nginx do the rest.
DNS-01, hybrid mode
For wildcard certs (which HTTP-01 can’t issue), cert-manager can also do
DNS-01 via the Cloudflare API. That key is in ~/homelab-git/.env
(CF_API_TOKEN) and a ClusterIssuer scoped to DNS-01 is provisioned.
Today it’s used for *.skypalette.ai as a fallback to per-host certs;
each app still gets a host-specific Let’s Encrypt cert by default.