Public names are proxied CNAMEs to the Cloudflare Tunnel since PR #156 and work from the LAN, so they should resolve publicly there too. Replace the address=/${BASE_DOMAIN}/ wildcard with explicit entries: the LAN-only names (derived from every controller:none HTTPRoute), gitea/nextcloud (off Cloudflare's 100 MB / 100 s limits), keycloak/auth, and the L4 VIPs. Add L0 check pihole-lan-names, mutation-tested. Correct docs that still described public names as CNAME to wan. and the cloudflared entrypoint comment. Co-Authored-By: Claude Opus 5.5 <[email protected]>
152 lines
6.3 KiB
YAML
152 lines
6.3 KiB
YAML
---
|
|
# 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
|
|
# fsGroup makes the kubelet GROUP-own the Secret volume's files to 65532. Without it they
|
|
# are root:root, and defaultMode 0440 (below) is unreadable to uid 65532 — every replica
|
|
# CrashLooped on "couldn't read tunnel credentials ... permission denied" (2026-11-08),
|
|
# which cloudflared then misreports as "Error locating origin cert". Same lesson as
|
|
# platform/keycloak/client-secret-reconciler.yaml. fsGroup + 0440 keeps the credential
|
|
# unreadable to any other uid rather than relaxing it to world-readable 0444.
|
|
# Pinned by the L0 check cloudflared-ingress-rules, assertion (7).
|
|
fsGroup: 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` and its CMD is `version` (read from the pinned
|
|
# digest's image config, 2026-11-09); these args replace that CMD. `no-autoupdate`, 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
|
|
# 0440, owner root, group fsGroup 65532 (pod securityContext) — readable by the
|
|
# container user through its group, by nobody else.
|
|
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
|