ADR 016: Zero Trust Access with Cloudflare Tunnels
Context
Internal platform services like ArgoCD should not be exposed through inbound home-router NAT rules or directly published Ingress endpoints.
Our current state has two distinct service classes:
- Public product surfaces (for example
lab.northlift.net) that are intentionally internet-accessible. - Internal operator surfaces (for example
argocd.northlift.net) that should require stronger access controls and should not depend on inbound router exposure.
With Cloudflare DNS automation already established, we need a complementary zero-trust connectivity model that:
- Uses outbound-only encrypted tunnels from cluster to Cloudflare edge.
- Enforces identity-aware access controls before any request reaches internal services.
- Preserves GitOps reconciliation and repeatable bootstrap.
We evaluated these options:
- Keep direct ingress exposure with IP allowlists.
- Add VPN-only access for operators.
- Use Cloudflare Tunnel with Cloudflare Access (GitHub OAuth) for internal hostnames.
Decision
We adopt Cloudflare Tunnel for internal service access and enforce Cloudflare Access policies using GitHub OAuth.
Implementation details:
- Deploy tunnel connector via ArgoCD Application
cloudflare-tunnelusing the official Cloudflare Helm chart. - Run
replicaCount: 2for high availability. - Configure ingress routing in chart values:
argocd.northlift.net -> http://argocd-server.argocd.svc.cluster.local:80- fallback behavior remains
http_status:404via chart-generated ConfigMap rule. - Store tunnel credentials as a SealedSecret in
gitops/secretsand reference it withcloudflare.secretName. - Disable direct ArgoCD ingress in the ArgoCD Helm values to enforce tunnel-only exposure.
Service Classification
| Class | Exposure Model | Examples | Control Boundary |
|---|---|---|---|
| Public | Public DNS + Kubernetes Ingress | lab.northlift.net (status-api) |
TLS + application auth |
| Tunnel-only | Cloudflare Tunnel hostname + Access policy | argocd.northlift.net |
Cloudflare Access (GitHub OAuth) + internal service |
| Internal-only | No public DNS and no external route | Redis, cert-manager, sealed-secrets | Cluster network + Kubernetes RBAC |
Operational Workflow
Cloudflare setup (Dashboard or CLI)
- Create tunnel
lab-internal-services. - Add hostname route for
argocd.northlift.nettohttp://argocd-server.argocd.svc.cluster.local:80. - Configure Cloudflare Access application for
argocd.northlift.net. - Add an allow policy constrained to approved GitHub identities.
CLI reference flow:
cloudflared tunnel login
cloudflared tunnel create lab-internal-services
cloudflared tunnel route dns lab-internal-services argocd.northlift.net
Then configure the Access application and GitHub OAuth policy in Cloudflare Zero Trust (Dashboard), or via API/Terraform if your account automation is already in place.
Seal credentials for GitOps
Generate a namespace-scoped SealedSecret for the tunnel credentials JSON:
kubectl create secret generic cloudflare-tunnel-credentials \
--namespace cloudflare-tunnel \
--from-file=credentials.json=./credentials.json \
--dry-run=client -o json | kubeseal \
--controller-name sealed-secrets \
--controller-namespace kube-system \
--format yaml > gitops/secrets/cloudflare-tunnel-credentials-sealedsecret.yaml
Commit only the resulting SealedSecret manifest. Do not commit plaintext credentials.
Consequences
Positive
- Removes dependency on inbound router exposure for internal operator endpoints.
- Adds identity-aware access checks before traffic reaches ArgoCD.
- Keeps connectivity and access controls declarative alongside existing GitOps assets.
Negative
- Introduces a new control-plane dependency on Cloudflare Tunnel and Access availability.
- Requires secure lifecycle management for tunnel credentials and rotation procedures.
- Misconfigured Access policy can either overexpose or unintentionally block operator access.
Status
Accepted and implemented in Phase 10 with Cloudflare Tunnel and Access controls for internal hostnames.