Networking & TLS

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.ai is a single proxied CNAME at the Cloudflare DNS level pointing at the in-cluster tunnel’s hostname. Every subdomain flows through ingress-nginx and gets routed by Host header.
  • git.skypalette.ai is 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-tls

cert-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: nginx

Renewals 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.