Decision. Run the tunnel in-cluster
Alternative. Running it on the router
Why. Its config lives in Git with everything else, it restarts with the cluster, and deployment follows the normal app path.
Publishes services to the internet through an outbound-only tunnel: zero open inbound ports
Cloudflared runs in-cluster and holds outbound connections to the Cloudflare edge. Public traffic for opi5cluster.co.uk enters through the tunnel and lands on Traefik, so the cluster keeps no open inbound ports. It is the only entry-point component that auto-syncs.
Excerpt from argocd-apps/values.yaml in the argocd-apps chart,
with annotations added for this site:
cloudflared:
# true
application: true
# applications despite being infrastructure: it is deployed like an app and restarted freely
project: applications
# true: the one intentionally auto-synced entry point. If it drifts, the tunnel self-heals; being unreachable is worse than a bad sync
autoSync: true
The full cloudflared/values.yaml from the service's
own chart:
image: cloudflare/cloudflared:latest
configFile: config.yml templates/configmap.yaml ConfigMap holding non-sensitive application configuration.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Namespace }}-configmap
namespace: {{ .Release.Namespace }}
data:
config.yml: |
warp-routing:
enabled: true templates/external-secret.yaml ExternalSecret syncing the service credentials from Vault into the namespace.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ .Release.Namespace }}-secret
namespace: {{ .Release.Namespace }}
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: vault-cluster-secret-store
target:
name: {{ .Release.Namespace }}-secret
creationPolicy: Owner
data:
- secretKey: tunnel_token
remoteRef:
key: cloudflare
property: tunnel_token templates/rollout.yaml Argo Rollout workload: container spec, probes, resources and rollout strategy.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: {{ .Release.Namespace }}-rollout
namespace: {{ .Release.Namespace }}
labels:
app: {{ .Release.Namespace }}
spec:
replicas: 1
strategy:
canary:
steps:
- setWeight: 25
- pause: {duration: 30s}
- setWeight: 50
- pause: {duration: 45s}
- setWeight: 100
selector:
matchLabels:
app: {{ .Release.Namespace }}
template:
metadata:
labels:
app: {{ .Release.Namespace }}
spec:
nodeSelector:
kubernetes.io/arch: arm64
containers:
- name: {{ .Release.Namespace }}
image: {{ .Values.image }}
args:
- "tunnel"
- "--no-autoupdate"
- "--config"
- "/app/{{ .Values.configFile }}"
- "run"
env:
- name: TUNNEL_TOKEN
valueFrom:
secretKeyRef:
name: {{ .Release.Namespace }}-secret
key: tunnel_token
volumeMounts:
- name: workingdir
mountPath: "/app/{{ .Values.configFile }}"
subPath: "{{ .Values.configFile }}"
volumes:
- name: workingdir
configMap:
name: {{ .Release.Namespace }}-configmap Decision. Run the tunnel in-cluster
Alternative. Running it on the router
Why. Its config lives in Git with everything else, it restarts with the cluster, and deployment follows the normal app path.
Decision. autoSync on for the tunnel alone
Alternative. Manual sync like every other entry point
Why. Tunnel config is low risk and self-healing; public unreachability is the worst outcome, so recovery speed beats review friction here.