--- # cloudflared — the Cloudflare Tunnel connector. Outbound-only: it dials the Cloudflare edge on # 7844 (QUIC, falling back to HTTP/2 over TCP) and carries requests for the tunnel's hostnames # back to the shared Gateway. Nothing listens on the LAN and no router port is opened. # # HA: 2 replicas, hard-spread across nodes. Each replica registers its own set of edge # connections for the SAME tunnel, so Cloudflare load-balances between them and losing one node # (a kured reboot, a drain) loses no traffic. The PDB in this directory keeps a drain from taking # both at once. apiVersion: apps/v1 kind: Deployment metadata: name: cloudflared namespace: cloudflared labels: app.kubernetes.io/name: cloudflared spec: replicas: 2 revisionHistoryLimit: 3 strategy: type: RollingUpdate rollingUpdate: # Never drop below the running count during a rollout; with 3 nodes and a hard spread the # surge pod always has a node to land on. maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app.kubernetes.io/name: cloudflared template: metadata: labels: app.kubernetes.io/name: cloudflared # internet, NEVER vpn. The vpn class steers egress through the PIA tunnel (ADR-0015), which # would put the edge connections behind a third party's exit and its kill-switch — the # tunnel would drop whenever PIA does. This namespace is outside the app-namespace scope # of require-egress-class-label, so the label is declared for the steering, not to pass # admission; the L0 check cloudflared-ingress-rules pins it. homelab.keithjackson.dev/egress-class: internet spec: # cloudflared never talks to the Kubernetes API. No token mounted, and the egress policy # does not admit the kube-apiserver entity either. automountServiceAccountToken: false enableServiceLinks: false securityContext: runAsNonRoot: true # The image is distroless `nonroot` (65532). runAsUser: 65532 runAsGroup: 65532 seccompProfile: type: RuntimeDefault topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app.kubernetes.io/name: cloudflared # cloudflared drains in-flight requests for up to its own 30s grace-period on SIGTERM. terminationGracePeriodSeconds: 45 containers: - name: cloudflared # Tag + digest: Renovate's `kubernetes` manager (renovate.json5, platform/**) bumps both. image: docker.io/cloudflare/cloudflared:2026.10.7@sha256:4955524aeaa379e154023d5a027c250f7b35041a2e7e950cf13f144bc9b9c5ba # The image ENTRYPOINT is `cloudflared --no-autoupdate`; these replace its CMD. The tunnel # UUID, credentials path, metrics address and ingress rules all come from the file. args: - tunnel - --config - /etc/cloudflared/config/config.yaml - run ports: - name: metrics containerPort: 2000 protocol: TCP # /ready is 200 only while this replica holds at least one live edge connection, so an # unenrolled or edge-isolated replica is NotReady and the Deployment says so. readinessProbe: httpGet: path: /ready port: metrics initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 # Liveness is deliberately NOT /ready: an edge outage would otherwise restart-loop every # replica, which reconnects nothing faster and churns the pods. /healthcheck answers # whenever the process is serving. livenessProbe: httpGet: path: /healthcheck port: metrics initialDelaySeconds: 10 periodSeconds: 20 failureThreshold: 3 resources: requests: cpu: 10m memory: 64Mi limits: cpu: 500m memory: 256Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL volumeMounts: - name: config mountPath: /etc/cloudflared/config readOnly: true - name: creds mountPath: /etc/cloudflared/creds readOnly: true volumes: - name: config configMap: # Rewritten to the content-hashed name by kustomize (configMapGenerator). name: cloudflared-config - name: creds secret: # cloudflared-credentials.sops.yaml — the tunnel credentials JSON, by name only. secretName: cloudflared-credentials defaultMode: 0440 --- # One replica may be disrupted at a time — enough for kured's one-node-at-a-time drain to # proceed, never enough to take the tunnel down. minAvailable 1 (not 2) on purpose: a 0-disruption # PDB wedges a drain (platform/kured/alerts.yaml, KuredRebootPending). apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: cloudflared namespace: cloudflared labels: app.kubernetes.io/name: cloudflared spec: minAvailable: 1 selector: matchLabels: app.kubernetes.io/name: cloudflared