Cloudflared

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.

ArgoCD configuration

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

Chart values

The full cloudflared/values.yaml from the service's own chart:

image: cloudflare/cloudflared:latest
configFile: config.yml

Manifests & templates

templates/configmap.yaml

ConfigMap holding non-sensitive application configuration.

Show manifest
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.

Show manifest
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.

Show manifest
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

Trade-offs

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.

← Back to Platform & Infrastructure · All service groups