Internet
→ pai.pericak.com → Cloudflare Access (One-time PIN auth) → Cloudflare Tunnel → Traefik → service
→ wh.pericak.com → Cloudflare Tunnel (no Access) → Traefik → webhook handler
pai-m1), two public hostnames.Allowed users (pai.pericak.com): [email protected], [email protected], [email protected]
| Hostname | Access protection | Purpose |
|---|---|---|
pai.pericak.com |
pericak-family Allow policy (One-time PIN) |
ArgoCD UI, OpenObserve, and other authenticated services |
wh.pericak.com |
None — open to internet | Webhooks; application-level secret auth required |
If this is the first time using Zero Trust on this account, Cloudflare will show an onboarding screen.
<team>.cloudflareaccess.com (the auth portal URL). Enter pericak → click NextThis onboarding only runs once.
pai-m1 → click Save tunnelsudo cloudflared service install eyJ...
The actual token is everything after install (just the JWT, starting with eyJ).exports.sh as CLOUDFLARE_TUNNEL_TOKEN (JWT only, no command prefix).Add two routes — both point to Traefik:
Route 1 — authenticated services:
| Field | Value |
|---|---|
| Subdomain | pai |
| Domain | pericak.com |
| Type | HTTP |
| URL | traefik.kube-system.svc.cluster.local:80 |
Route 2 — webhooks (no Access protection):
| Field | Value |
|---|---|
| Subdomain | wh |
| Domain | pericak.com |
| Type | HTTP |
| URL | traefik.kube-system.svc.cluster.local:80 |
Traefik handles hostname + path routing internally. The tunnel always points to Traefik for both hostnames.
Click Save hostname after each, then Done.
One-time PIN is built-in to Cloudflare Access — no external IdP setup needed. It sends a numeric code to the user's email address. The email does not need to be a Gmail address.
paipai, domain pericak.com → full domain: pai.pericak.compericak-family[email protected][email protected][email protected]
(Type each and press Enter to add as a chip)pericak-family → click ConfirmThe Cloudflare free plan does not support path-scoped Access policies. You cannot restrict a Bypass policy to /webhooks/* only — it would bypass auth for all traffic to the application.
Solution: Use a second tunnel hostname (wh.pericak.com) with no Access application. Webhook senders call wh.pericak.com; Traefik routes by hostname. No Cloudflare Access challenge is triggered for wh.pericak.com because there is no Access application protecting it.
Security: wh.pericak.com is open to the internet. Webhook handlers must verify a shared secret at the application level (e.g., verify X-Hub-Signature-256 for GitHub webhooks).
The webhooks-bypass policy (Action: Bypass, Everyone) exists as a reusable policy but is not assigned to any application — it's kept as a reference in case of a paid plan upgrade.
If on a paid plan and want to consolidate to a single hostname with path-scoped bypass:
webhooks-bypass → Confirm/webhooks/*K3s ships Traefik as a built-in component in the kube-system namespace. No separate Traefik deployment is needed — it is already running and already has the ingressroutes.traefik.io CRDs installed.
Key details:
traefik.kube-system.svc.cluster.local:80The Cloudflare tunnel points to traefik.kube-system.svc.cluster.local:80. IngressRoutes in any namespace are picked up automatically.
# After unlocking Vault and sourcing exports.sh:
source apps/blog/exports.sh
# Then run store-secrets.sh — it prompts for CLOUDFLARE_TUNNEL_TOKEN
infra/ai-agents/bin/store-secrets.sh
# Or pass as env var:
CLOUDFLARE_TUNNEL_TOKEN="$CLOUDFLARE_TUNNEL_TOKEN" infra/ai-agents/bin/store-secrets.sh
The token is the JWT from the Cloudflare dashboard (Zero Trust → Networks → Connectors → pai-m1). It starts with eyJ. Do not include the sudo cloudflared service install prefix.
bootstrap.sh reads the token from Vault and creates the cloudflared-token K8s secret automatically. Re-run bootstrap.sh after storing the token:
infra/ai-agents/bin/bootstrap.sh
The infra/ai-agents/argocd/cloudflared.yaml ApplicationSet deploys cloudflared to clusters labeled cloudflare-tunnel=true. This label is applied by bootstrap.sh during cluster registration.
The local chart is at infra/ai-agents/cloudflared/helm/. It runs cloudflare/cloudflared:latest with 2 replicas, reading the tunnel token from the cloudflared-token K8s secret.
IngressRoutes are managed by ArgoCD via infra/ai-agents/argocd/traefik-routes.yaml, which deploys infra/ai-agents/traefik/argocd-ingress.yaml from the main branch.
The ArgoCD IngressRoute exposes ArgoCD at pai.pericak.com/argocd. ArgoCD is configured with server.rootpath: /argocd (set in infra/ai-agents/helmfile.yaml via configs.params.server\.rootpath) so it serves the UI correctly under the path prefix. Do not use a strip-prefix middleware — ArgoCD expects to receive the full /argocd path.
Traefik Middleware gotcha: If an IngressRoute references a Middleware CRD that doesn't exist in the same namespace, Traefik silently drops the entire route with no 404 or error logged. Always verify the Middleware resource exists before referencing it.
Authenticated service (user-facing, requires login):
IngressRoute matching Host(pai.pericak.com) && PathPrefix(/your-path) in the target namespacepai.pericak.com Access application (pericak-family Allow policy)Webhook endpoint (machine-to-machine, no Cloudflare Access):
IngressRoute matching Host(wh.pericak.com) && PathPrefix(/your-webhook) in the target namespaceX-Hub-Signature-256 for GitHub)# Tunnel should show as healthy in Cloudflare dashboard
# Zero Trust → Networks → Connectors → pai-m1 → status: Active
# Verify K3s Traefik is up (in kube-system, not a separate namespace)
kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik
# Verify cloudflared is connected
kubectl get pods -n cloudflared
kubectl logs -n cloudflared deploy/cloudflared | grep -i "connection"
# Check IngressRoute is loaded
kubectl get ingressroute -n argocd
# Hit ArgoCD remotely — should 302 to Cloudflare Access login
curl -I https://pai.pericak.com/argocd
# ArgoCD admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d && echo