Skip to content

feat(ops): sweep-cf-tunnels janitor — orphan tunnels accumulate forever - #2328

Merged
HongmingWang-Rabbit merged 1 commit into
stagingfrom
auto/sweep-cf-tunnel-orphans
Apr 30, 2026
Merged

HongmingWang-Rabbit merged 1 commit into
stagingfrom
auto/sweep-cf-tunnel-orphans

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Summary

CP's tenant-delete cascade removes the DNS record but does not delete the underlying Cloudflare Tunnel. Tunnels named tenant-<slug> accumulate indefinitely.

Same class of leak as the DNS-records issue that drove sweep-cf-orphans (controlplane#239). This PR adds the parallel-shape janitor for Tunnels.

Observed leak

User screenshot of CF dashboard shows dozens of tenant-e2e-canvas-20260... tunnels in Down state with 0 replicas, surviving weeks past their tenant's deletion.

Decision rules

Rule Verdict
Name doesn't match tenant-<slug> keep (don't sweep platform infra)
Tunnel has active connections (status=healthy or connections != []) keep (defense-in-depth — never kill a live tunnel)
Slug ∈ live CP prod+staging orgs keep
Otherwise delete (orphan)

Why a separate workflow vs extending sweep-cf-orphans

  • DNS records live on the zone (/zones/<id>/dns_records)
  • Tunnels live on the account (/accounts/<id>/cfd_tunnel)
  • Different CF API surface, different scopes; existing CF_API_TOKEN may not include account:cloudflare_tunnel:edit
  • Splitting workflows keeps each one's secret-presence gate independent so neither silent-skips when the other's secret is missing
  • Cleaner blast radius — operators can disable one without the other

Verified

  • bash -n shell syntax
  • YAML lint
  • Decide logic offline smoke (7 cases — orphan, live, active-conn, non-tenant-shaped, etc.)
  • End-to-end dry-run smoke with stubbed CP + CF APIs (correctly identified 2 orphans, kept 1 live + 1 non-tenant)

Required secrets

Adds these to the existing repo-secrets set:

Secret Notes
CF_API_TOKEN must include account:cloudflare_tunnel:edit scope (separate from the zone:dns:edit used by sweep-cf-orphans — same token if scope is broad, or a new token if narrowly scoped)
CF_ACCOUNT_ID account that owns the tunnels (visible in dash.cloudflare.com URL path)
CP_PROD_ADMIN_TOKEN reused from sweep-cf-orphans
CP_STAGING_ADMIN_TOKEN reused from sweep-cf-orphans

If the secrets aren't set, the scheduled run fails-loud (red CI) instead of silent-skipping — same hardening as PR #2243 applied to sweep-cf-orphans after the 152/200 zone-records silent-leak incident.

Test plan

  • After merge: run gh workflow run sweep-cf-tunnels.yml -f dry_run=true to confirm secret presence + dry-run output
  • Once dry-run looks right, dispatch with dry_run=false to delete the existing orphan tunnels visible in the dashboard
  • Verify scheduled run picks up new orphans within an hour after a fresh E2E test cycle

Related

  • The CP-side root-cause fix (tenant-delete should cascade to tunnel delete) is in molecule-controlplane and worth filing as a separate issue. This janitor is the operational backstop in the meantime — same pattern as the DNS records sweep.

🤖 Generated with Claude Code

…ccumulate

CP's tenant-delete cascade removes the DNS record (with sweep-cf-orphans
as a backstop) but does NOT delete the underlying Cloudflare Tunnel.
Each E2E provision creates one Tunnel named `tenant-<slug>`; without
cleanup these accumulate indefinitely on the account, consuming the
tunnel quota and cluttering the dashboard.

Observed 2026-04-30: dozens of `tenant-e2e-canvas-*` tunnels in Down
state with zero replicas, weeks past their tenant's deletion. Same
class of bug as the DNS-records leak that drove sweep-cf-orphans
(controlplane#239).

Parallel-shape to sweep-cf-orphans:
  - Same dry-run-by-default + --execute pattern
  - Same MAX_DELETE_PCT safety gate (default 90% — higher than DNS
    sweep's 50% because tenant-shaped tunnels are orphans by design)
  - Same schedule/dispatch hardening (hard-fail on missing secrets
    when scheduled, soft-skip when dispatched)
  - Cron offset to :45 to avoid CF API bursts colliding with the DNS
    sweep at :15

Decision rules (in order):
  1. Name doesn't match `tenant-<slug>` → keep (unknown — never sweep
     tunnels that might belong to platform infra).
  2. Tunnel has active connections (status=healthy or non-empty
     connections array) → keep (defense-in-depth: don't kill a live
     tunnel even if CP forgot the org).
  3. Slug ∈ {prod_slugs ∪ staging_slugs} → keep.
  4. Otherwise → delete (orphan).

Verified by:
  - shell syntax check (bash -n)
  - YAML lint
  - Decide-logic offline smoke (7 cases, all pass)
  - End-to-end dry-run smoke with stubbed CP + CF APIs

Required secrets (added to existing org-secrets):
  CF_API_TOKEN          must include account:cloudflare_tunnel:edit
                        scope (separate from zone:dns:edit used by
                        sweep-cf-orphans — same token if scope is
                        broad, or a new token if narrowly scoped).
  CF_ACCOUNT_ID         account that owns the tunnels (visible in
                        dash.cloudflare.com URL path).
  CP_PROD_ADMIN_TOKEN   reused from sweep-cf-orphans.
  CP_STAGING_ADMIN_TOKEN reused from sweep-cf-orphans.

Note: CP-side root cause (tenant-delete should cascade to tunnel
delete) is in molecule-controlplane and worth fixing separately. This
janitor is the operational backstop in the meantime — same pattern
applied to DNS records when the same root cause was unaddressed.
@HongmingWang-Rabbit
HongmingWang-Rabbit added this pull request to the merge queue Apr 30, 2026
Merged via the queue into staging with commit 1679643 Apr 30, 2026
20 checks passed
@HongmingWang-Rabbit
HongmingWang-Rabbit deleted the auto/sweep-cf-tunnel-orphans branch April 30, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant