Skip to content

Cilium PR1: cf-ssh-bastion Teardown — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Cleanly retire the Cloudflared-tunneled SSH bastion (cf-ssh-bastion) and all its supporting Terraform, Kubernetes, Velero, and documentation references.

Architecture: Single PR with multiple commits. Removes the ArgoCD Application (whose finalizer cascades deletion of namespace + child resources), the Cloudflare DNS record + Access app + tunnel ingress route (via Terraform), the Vault k8s auth role + policy (via Terraform), the Velero backup-schedule namespace inclusion, and the one live documentation reference in docs/operations/cilium.md. Spec docs and migration history are preserved verbatim.

Tech Stack: ArgoCD (GitOps), Cilium (CNI/policy), Cloudflare (tunnel + Zero Trust Access via Terraform), Vault (Kubernetes auth + KV v2 via Terraform), HCP Terraform (TF execution), Velero (backup), bd (issue tracker).

Spec: docs/engineering/specs/2026-05-12-cilium-post-migration-hardening-design.md §“PR1: Bastion Teardown”.


PathActionPurpose
argocd/app-configs/cf-ssh-bastion/ (entire directory, 8 files)DELETEArgoCD-managed bastion manifests (Deployment, Service, RBAC, ExternalSecret, CNP, namespace, SSH client config, kustomization)
argocd/cluster-app/templates/cf-ssh-bastion.yamlDELETEArgoCD Application template (has finalizer → deletion cascades to all child resources)
tf/cloudflare/ssh-bastion.tfDELETEcloudflare_zero_trust_access_application.ssh_bastion (browser-rendered SSH terminal at ssh.fzymgc.net)
tf/cloudflare/variables.tfMODIFYEmpty ssh_services default map (cascades removal of cloudflare_dns_record.ssh_services["ssh"])
tf/vault/k8s-cf-ssh-bastion.tfDELETEvault_kubernetes_auth_backend_role.cf_ssh_bastion
tf/vault/policy-cf-ssh-bastion.tfDELETEvault_policy.cf_ssh_bastion (read access to bastion secret)
argocd/app-configs/velero/backup-schedule.yamlMODIFYRemove cf-ssh-bastion from includedNamespaces in both backup schedules (lines 43, 97)
docs/operations/cilium.mdMODIFYRemove the single live reference (line 86) under “NetworkPolicy enforcement”
docs/operations/cilium-migration.mdLEAVE AS-ISHistorical record of the migration; references are part of the narrative
docs/engineering/specs/* + docs/engineering/plans/*LEAVE AS-ISHistorical design/plan docs
images/cf-ssh-bastion/DockerfileLEAVE AS-ISSource for the published GHCR image; deletion deferred to a follow-up bead
Vault secret secret/data/fzymgc-house/cluster/cf-ssh-bastionLEAVE AS-ISDocumented as orphaned; deletion deferred to a follow-up bead

Task 0: Verify Pre-State and Capture Baseline

Section titled “Task 0: Verify Pre-State and Capture Baseline”

Files: none (verification only).

  • Step 0.1: Confirm you are in the correct workspace

Run:

Terminal window
jj --no-pager workspace list
pwd

Expected: cilium-hardening workspace exists, pwd ends with .worktrees/cilium-hardening.

  • Step 0.2: Confirm bastion currently exists in-cluster

Run:

Terminal window
kubectl get ns cf-ssh-bastion -o jsonpath='{.metadata.name}{"\n"}'
kubectl get application cf-ssh-bastion -n argocd -o jsonpath='{.metadata.finalizers}{"\n"}'
kubectl get pod -n cf-ssh-bastion -o jsonpath='{.items[*].metadata.name}{"\n"}'
kubectl get cnp -n cf-ssh-bastion -o jsonpath='{.items[*].metadata.name}{"\n"}'

Expected output (literal):

  • Line 1: cf-ssh-bastion
  • Line 2: ["resources-finalizer.argocd.argoproj.io"]
  • Line 3: one or more cf-ssh-bastion-* pod names
  • Line 4: six CNP names: bastion-ingress-cloudflared bastion-allow-kube-dns bastion-allow-k8s-api bastion-allow-node-ssh bastion-allow-icmp bastion-allow-external-https

If finalizer is missing on line 2, STOP — Application deletion will not cascade. File an issue and abort PR1.

  • Step 0.3: Confirm hl-7h0 is still in_progress

Run:

Terminal window
bd show hl-7h0 | head -5

Expected: shows IN_PROGRESS status. We will close this at the end of PR1.

  • Step 0.4: Confirm Cloudflare resources are live

Run:

Terminal window
dig +short ssh.fzymgc.net CNAME

Expected: returns a cfargotunnel.com CNAME (tunnel is live). Record the exact value so we can verify it is gone post-merge.

  • Step 0.5: Commit the baseline state to your memory (do NOT commit to git)

No commit. This task is read-only.


Task 1: Delete ArgoCD-Managed Bastion Manifests

Section titled “Task 1: Delete ArgoCD-Managed Bastion Manifests”

Files:

  • Delete: argocd/cluster-app/templates/cf-ssh-bastion.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/deployment.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/external-secret.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/kustomization.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/namespace.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/rbac.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/service.yaml
  • Delete: argocd/app-configs/cf-ssh-bastion/ssh-client-config.yaml
  • Step 1.1: Delete the ArgoCD Application template

Run:

Terminal window
rm argocd/cluster-app/templates/cf-ssh-bastion.yaml
  • Step 1.2: Delete the entire bastion app-configs directory

Run:

Terminal window
rm -r argocd/app-configs/cf-ssh-bastion/
  • Step 1.3: Verify deletions

Run:

Terminal window
ls argocd/cluster-app/templates/cf-ssh-bastion.yaml argocd/app-configs/cf-ssh-bastion/ 2>&1

Expected: both paths report No such file or directory.

  • Step 1.4: Render cluster-app Helm template locally to catch regressions

Run (per memory cluster-app-helm-template-trap):

Terminal window
helm template argocd/cluster-app --name-template cluster-app --namespace argocd >/tmp/cluster-app-render.yaml 2>&1
echo "exit: $?"
grep -c "^kind:" /tmp/cluster-app-render.yaml
grep "cf-ssh-bastion" /tmp/cluster-app-render.yaml | head -5

Expected: exit 0; positive number of kind: matches; no cf-ssh-bastion matches.

  • Step 1.5: Run yamllint on changed paths

Run:

Terminal window
yamllint argocd/cluster-app/ argocd/app-configs/ 2>&1 | tail -20
echo "exit: $?"

Expected: exit 0 (deletions do not introduce lint errors).

  • Step 1.6: Stage and describe the change in jj

Run:

Terminal window
jj --no-pager st
jj --no-pager describe -m "feat(cf-ssh-bastion): remove ArgoCD manifests and cluster-app template
Cloudflared-tunneled SSH bastion is retired. The ArgoCD Application
template carries resources-finalizer.argocd.argoproj.io so deletion
cascades to the namespace and all child resources (Deployment, Service,
ExternalSecret, CiliumNetworkPolicy, ServiceAccount, RBAC).
Vault secret secret/data/fzymgc-house/cluster/cf-ssh-bastion is left
in place; documented as orphaned, removal deferred to a follow-up bead.
Refs spec: docs/engineering/specs/2026-05-12-cilium-post-migration-hardening-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
jj --no-pager log -r '@-..@'

Expected: jj st shows 9 deleted files; jj describe updates the working-copy commit’s message; jj log confirms description on @.


Task 2: Remove Cloudflare DNS, Access App, and Tunnel Ingress Route

Section titled “Task 2: Remove Cloudflare DNS, Access App, and Tunnel Ingress Route”

Files:

  • Delete: tf/cloudflare/ssh-bastion.tf
  • Modify: tf/cloudflare/variables.tf:144-160 (the ssh_services variable block)
  • Step 2.1: Delete the Access app file

Run:

Terminal window
rm tf/cloudflare/ssh-bastion.tf
  • Step 2.2: Empty the ssh_services default map

Edit tf/cloudflare/variables.tf. Find the ssh_services variable (currently around line 144). Replace its default block.

Before:

variable "ssh_services" {
description = "SSH services exposed via tunnel on fzymgc.net domain with Cloudflare Access browser-rendered terminal"
type = map(object({
service_url = string
}))
default = {
ssh = {
service_url = "ssh://bastion-ssh.cf-ssh-bastion.svc.cluster.local:2222"
}
}
validation {
condition = alltrue([for k, v in var.ssh_services : can(regex("^ssh://", v.service_url))])
error_message = "All SSH service URLs must start with ssh://."
}
}

After:

variable "ssh_services" {
description = "SSH services exposed via tunnel on fzymgc.net domain with Cloudflare Access browser-rendered terminal"
type = map(object({
service_url = string
}))
default = {}
validation {
condition = alltrue([for k, v in var.ssh_services : can(regex("^ssh://", v.service_url))])
error_message = "All SSH service URLs must start with ssh://."
}
}

Rationale: cloudflare_dns_record.ssh_services in tunnel.tf uses for_each = var.ssh_services. Emptying the map removes the ssh.fzymgc.net CNAME and the tunnel ingress rule it backs, without removing the variable definition (so future SSH services can be re-added by setting the variable).

  • Step 2.3: Validate Terraform formatting and syntax

Run:

Terminal window
cd tf/cloudflare && terraform fmt -check -diff && terraform validate 2>&1; cd -

Expected: exit 0, no fmt diff, validate succeeds.

If terraform validate errors with provider-init failure, run terraform init -backend=false first.

  • Step 2.4: Render a local plan-style summary (no HCP run needed)

Run:

Terminal window
cd tf/cloudflare && terraform fmt 2>&1 && grep -n "ssh_bastion\|ssh_services" *.tf | head -20; cd -

Expected: no remaining ssh_bastion resource references; ssh_services variable still present with empty default; for_each in tunnel.tf still references var.ssh_services (which is fine — empty map => zero resources).

  • Step 2.5: Describe a new commit

Run:

Terminal window
jj --no-pager new -m "feat(tf/cloudflare): remove SSH bastion DNS, Access app, and tunnel route
Empties var.ssh_services default map (removes the cloudflare_dns_record.ssh_services\[\"ssh\"\]
entry via for_each) and removes the cloudflare_zero_trust_access_application.ssh_bastion
that fronted ssh.fzymgc.net with Cloudflare's browser-rendered xterm.
The ssh_services variable itself is retained so future SSH services can be
re-added without re-introducing the variable block.
HCP Terraform will plan + apply on PR merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
jj --no-pager st

Expected: working copy moves to a new change stl ... with the message above. (jj new -m creates a new commit on top of @ with the given description, then advances @ to it. Files for this commit are added in the next step.)

  • Step 2.6: Verify staged file changes are the intended ones

Run:

Terminal window
jj --no-pager diff --summary

Expected: shows D tf/cloudflare/ssh-bastion.tf and M tf/cloudflare/variables.tf and nothing else.

If anything else appears, run jj --no-pager restore <path> for each unintended path to remove it from this commit.


Task 3: Remove Vault k8s Auth Role and Policy

Section titled “Task 3: Remove Vault k8s Auth Role and Policy”

Files:

  • Delete: tf/vault/k8s-cf-ssh-bastion.tf
  • Delete: tf/vault/policy-cf-ssh-bastion.tf
  • Step 3.1: Start a new jj change for the Vault commit

Run:

Terminal window
jj --no-pager new -m "feat(tf/vault): remove cf-ssh-bastion auth role and policy
Drops vault_kubernetes_auth_backend_role.cf_ssh_bastion and
vault_policy.cf_ssh_bastion. The KV-v2 secret at
secret/data/fzymgc-house/cluster/cf-ssh-bastion is left orphaned
(not managed by Terraform); cleanup deferred to a follow-up bead.
HCP Terraform will plan + apply on PR merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  • Step 3.2: Delete the two TF files

Run:

Terminal window
rm tf/vault/k8s-cf-ssh-bastion.tf tf/vault/policy-cf-ssh-bastion.tf
  • Step 3.3: Verify no remaining references

Run:

Terminal window
grep -rn "cf_ssh_bastion\|cf-ssh-bastion" tf/vault/ 2>&1 | head -10

Expected: no matches (or only matches in markdown comments — none expected).

  • Step 3.4: Validate Terraform

Run:

Terminal window
cd tf/vault && terraform fmt -check -diff && terraform validate 2>&1; cd -

Expected: exit 0.

  • Step 3.5: Verify the commit has only the intended changes

Run:

Terminal window
jj --no-pager diff --summary

Expected: only D tf/vault/k8s-cf-ssh-bastion.tf and D tf/vault/policy-cf-ssh-bastion.tf.


Task 4: Remove cf-ssh-bastion from Velero Backup Schedules

Section titled “Task 4: Remove cf-ssh-bastion from Velero Backup Schedules”

Files:

  • Modify: argocd/app-configs/velero/backup-schedule.yaml (two occurrences at lines 43 and 97)

  • Step 4.1: Start a new jj change

Run:

Terminal window
jj --no-pager new -m "chore(velero): remove cf-ssh-bastion from backup schedules
Drops the retired bastion namespace from both backup-schedule
includedNamespaces lists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  • Step 4.2: Read the affected lines and surrounding comment

Run:

Terminal window
grep -n -B1 "cf-ssh-bastion" argocd/app-configs/velero/backup-schedule.yaml

Expected: two occurrences, each preceded by a # Bastion - stateless, secrets from Vault comment line.

  • Step 4.3: Remove both occurrences (the namespace line AND its # Bastion - comment)

Use Edit on argocd/app-configs/velero/backup-schedule.yaml. There are two locations to update. For each, find this block:

Before:

- cloudflared
# Bastion - stateless, secrets from Vault
- cf-ssh-bastion
includedResources:

After:

- cloudflared
includedResources:

Note: the YAML appears twice in the file (two separate Schedule resources), with slightly different includedResources lists. Apply the deletion at both locations. Do NOT use a single replace_all if the surrounding context differs — verify each replacement individually.

  • Step 4.4: Verify both occurrences are gone

Run:

Terminal window
grep -c "cf-ssh-bastion" argocd/app-configs/velero/backup-schedule.yaml

Expected: 0.

  • Step 4.5: yamllint the file

Run:

Terminal window
yamllint argocd/app-configs/velero/backup-schedule.yaml
echo "exit: $?"

Expected: exit 0.

  • Step 4.6: Confirm the commit’s diff is minimal

Run:

Terminal window
jj --no-pager diff

Expected: shows two two-line deletions (each: the # Bastion - comment line and the - cf-ssh-bastion line) and nothing else.


Task 5: Remove Live Bastion Reference from cilium.md

Section titled “Task 5: Remove Live Bastion Reference from cilium.md”

Files:

  • Modify: docs/operations/cilium.md:80-95 (the “NetworkPolicy enforcement” section)

  • Step 5.1: Start a new jj change

Run:

Terminal window
jj --no-pager new -m "docs(cilium): remove retired cf-ssh-bastion NP reference
Updates the NetworkPolicy enforcement section to reflect that
cf-ssh-bastion has been retired. Migration history docs are kept
verbatim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
  • Step 5.2: Read the current section

Run:

Terminal window
sed -n '80,95p' docs/operations/cilium.md

Confirm content matches what’s described in Step 5.3 “Before”.

  • Step 5.3: Update the section

Use Edit on docs/operations/cilium.md.

Before:

## NetworkPolicy enforcement
Cluster-wide `policyEnforcementMode: default` — pods without matching CiliumNetworkPolicy are allowed by default; pods with matching policies are restricted to what the policies permit.
Currently enforced policies live in:
- `argocd/app-configs/cf-ssh-bastion/cilium-network-policy.yaml`
## Known follow-ups

After:

## NetworkPolicy enforcement
Cluster-wide `policyEnforcementMode: default` — pods without matching CiliumNetworkPolicy are allowed by default; pods with matching policies are restricted to what the policies permit.
No CiliumNetworkPolicies are currently in force. Crown-jewel namespace coverage is tracked under the post-migration hardening initiative (see `docs/engineering/specs/2026-05-12-cilium-post-migration-hardening-design.md`).
## Known follow-ups
  • Step 5.4: Run rumdl on the doc

Run:

Terminal window
rumdl check docs/operations/cilium.md
echo "exit: $?"

Expected: exit 0. If failures, fix them inline before continuing.

  • Step 5.5: Verify mkdocs renders the page

Per memory mkdocs-nav-curated, this file is already in the nav (we are not adding new docs in PR1). Run:

Terminal window
uvx --with mkdocs-material mkdocs build --strict 2>&1 | tail -20
echo "exit: $?"

Expected: exit 0, no broken-link warnings about cf-ssh-bastion paths.

  • Step 5.6: Confirm commit diff

Run:

Terminal window
jj --no-pager diff --summary

Expected: only M docs/operations/cilium.md.


Verification, Push, and PR (commit 6 if any cleanup needed)

Section titled “Verification, Push, and PR (commit 6 if any cleanup needed)”

Task 6: Cluster-Side Pre-Push Sanity Checks

Section titled “Task 6: Cluster-Side Pre-Push Sanity Checks”

Files: none (live cluster checks).

  • Step 6.1: Review the full PR contents

Run:

Terminal window
jj --no-pager log -r 'trunk()..@'
jj --no-pager diff -r 'trunk()..@' --summary

Expected:

  • 5 commits (Task 1 through Task 5).
  • File summary:
    • D argocd/app-configs/cf-ssh-bastion/{cilium-network-policy,deployment,external-secret,kustomization,namespace,rbac,service,ssh-client-config}.yaml (8 files)
    • D argocd/cluster-app/templates/cf-ssh-bastion.yaml
    • D tf/cloudflare/ssh-bastion.tf
    • M tf/cloudflare/variables.tf
    • D tf/vault/k8s-cf-ssh-bastion.tf
    • D tf/vault/policy-cf-ssh-bastion.tf
    • M argocd/app-configs/velero/backup-schedule.yaml
    • M docs/operations/cilium.md

If anything else appears in the summary, investigate before proceeding.

  • Step 6.2: Confirm cluster bastion is still healthy (we have not deployed yet)

Run:

Terminal window
kubectl get pod -n cf-ssh-bastion -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.phase}{"\n"}{end}'

Expected: cf-ssh-bastion-* Running (single pod, Running). This is the “no premature damage” check before push.

  • Step 6.3: Run global lint pre-flight

Run:

Terminal window
lefthook run pre-commit 2>&1 | tail -30
echo "exit: $?"

Expected: exit 0. lefthook chains yamllint, rumdl, cocogitto.


Task 7: Push, Open PR, and Watch the Rollout

Section titled “Task 7: Push, Open PR, and Watch the Rollout”

Files: none (push + PR + verification).

  • Step 7.1: Create the bookmark and push

Run:

Terminal window
jj --no-pager bookmark create feat/cilium-pr1-bastion-teardown -r @
jj --no-pager git push -b feat/cilium-pr1-bastion-teardown

Expected: bookmark created at @; remote accepts the push; output ends with a Create a pull request for ... URL.

If push is rejected with branch-protection error, STOP; you are pushing to main — re-check the bookmark and jj log.

  • Step 7.2: Open the PR

Run:

Terminal window
gh pr create --head feat/cilium-pr1-bastion-teardown --title "feat(cf-ssh-bastion): retire bastion and supporting infra (PR1/5)" --body "$(cat <<'EOF'
## Summary
First PR of the cilium-post-migration-hardening initiative. Retires the Cloudflared-tunneled SSH bastion now that the tunnel path is no longer used.
Removes, in five small commits:
1. **ArgoCD manifests** — Application template (carries resources-finalizer so deletion cascades) + `app-configs/cf-ssh-bastion/`.
2. **Cloudflare TF** — `cloudflare_zero_trust_access_application.ssh_bastion`, empties `var.ssh_services` (removes DNS + tunnel ingress route via `for_each`).
3. **Vault TF** — `vault_kubernetes_auth_backend_role.cf_ssh_bastion` + `vault_policy.cf_ssh_bastion`.
4. **Velero** — Removes `cf-ssh-bastion` from both backup-schedule `includedNamespaces` lists.
5. **Docs** — Removes the one live reference in `docs/operations/cilium.md`. Migration history docs left verbatim.
## Out of scope (follow-up beads)
- Delete `images/cf-ssh-bastion/Dockerfile` source.
- Delete the orphaned Vault secret `secret/data/fzymgc-house/cluster/cf-ssh-bastion`.
## Test plan
- [ ] HCP Terraform Cloudflare workspace plan shows: delete `cloudflare_zero_trust_access_application.ssh_bastion`, delete `cloudflare_dns_record.ssh_services["ssh"]`, delete tunnel ingress route to `bastion-ssh.cf-ssh-bastion.svc.cluster.local:2222`.
- [ ] HCP Terraform Vault workspace plan shows: delete role + policy, no other changes.
- [ ] Post-merge: `kubectl get application cf-ssh-bastion -n argocd` returns NotFound.
- [ ] Post-merge: `kubectl get ns cf-ssh-bastion` returns NotFound.
- [ ] Post-merge: `kubectl get pod,cnp -A | grep -i bastion` returns nothing.
- [ ] Post-merge: `dig +short ssh.fzymgc.net` no longer returns a cfargotunnel.com record.
- [ ] Close `hl-7h0` (stale; PR4/PR5 of the Calico→Cilium migration are merged).
Refs spec: `docs/engineering/specs/2026-05-12-cilium-post-migration-hardening-design.md`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"

Expected: PR URL printed. Open the URL in a browser and verify the diff matches Step 6.1’s summary.

  • Step 7.3: Inspect HCP Terraform plans on the PR

Wait for HCP Terraform to comment on the PR (typically <2 min). Inspect:

Terminal window
gh pr view --json comments --jq '.comments[] | select(.body | contains("Terraform")) | .body' | head -100

Expected: two plans — one for cloudflare workspace, one for vault workspace.

Verify the Cloudflare plan shows:

  • Destroy cloudflare_zero_trust_access_application.ssh_bastion
  • Destroy cloudflare_dns_record.ssh_services["ssh"]
  • An update on the tunnel ingress config that removes the SSH-services route (exact resource name depends on how tunnel.tf composes ingress rules; verify only cf-ssh-bastion / 2222 / ssh.fzymgc.net references are removed)

Verify the Vault plan shows:

  • Destroy vault_kubernetes_auth_backend_role.cf_ssh_bastion
  • Destroy vault_policy.cf_ssh_bastion
  • NO other destroys (especially no vault_kv_secret_v2 — the bastion secret must remain).

If either plan shows additional unexpected destroys, STOP; ask before merging.

  • Step 7.4: Merge

Once review + plans are clean:

Terminal window
gh pr merge --squash
  • Step 7.5: Watch ArgoCD reconcile + cascade

Run in a loop until the namespace is gone (use Monitor for state-change emission per memory cluster-reconciliation-monitoring):

Terminal window
while true; do
APP=$(kubectl get application cf-ssh-bastion -n argocd -o jsonpath='{.status.sync.status}' 2>&1)
NS=$(kubectl get ns cf-ssh-bastion -o jsonpath='{.status.phase}' 2>&1)
PODS=$(kubectl get pod -n cf-ssh-bastion --no-headers 2>&1 | wc -l)
echo "$(date +%H:%M:%S) app=$APP ns=$NS pods=$PODS"
[[ "$APP" == *NotFound* && "$NS" == *NotFound* ]] && break
sleep 10
done

Expected: within ~2 min, both app and ns report NotFound.

  • Step 7.6: Verify the post-state checklist

Run all of these and capture exit codes:

Terminal window
kubectl get application cf-ssh-bastion -n argocd 2>&1 | grep -i notfound
kubectl get ns cf-ssh-bastion 2>&1 | grep -i notfound
kubectl get pod,cnp -A 2>&1 | grep -i bastion | wc -l # expected 0
dig +short ssh.fzymgc.net # expected empty

Expected: all four match (three notfound lines, fourth empty). Any miss = investigate before closing the PR conversation.

  • Step 7.7: Close stale bead hl-7h0

Run:

Terminal window
bd close hl-7h0 --message "Stale. PR4 of the Calico→Cilium migration merged as #1018 and PR5 as #1019. Closing as part of PR1 of the post-migration hardening initiative (#$(gh pr view --json number --jq .number))."
bd show hl-7h0 | head -5

Expected: bead transitions to closed.

  • Step 7.8: File follow-up beads

Run:

Terminal window
bd create --title "Remove orphaned Vault secret secret/data/fzymgc-house/cluster/cf-ssh-bastion" --description "Bastion was retired in PR #<this-pr>. The KV-v2 secret remains in Vault. Delete via 'vault kv metadata delete secret/fzymgc-house/cluster/cf-ssh-bastion' once verified unused for 30 days." --priority 3 --type chore
bd create --title "Remove cf-ssh-bastion Dockerfile source from images/" --description "The bastion was retired in PR #<this-pr>. Source for the GHCR image at images/cf-ssh-bastion/ is no longer needed. Delete after confirming no consumers reference the published image tag." --priority 3 --type chore

Replace <this-pr> with the merged PR number. Capture the new bead IDs and add them to the PR description as a comment.


This implementation plan was checked against the spec on 2026-05-12:

  1. Spec coverage — All “Repo changes” items in spec §“PR1: Bastion Teardown” map to tasks above:

    • argocd/app-configs/cf-ssh-bastion/ deletion → Task 1
    • argocd/cluster-app/templates/cf-ssh-bastion.yaml deletion → Task 1
    • Cloudflared tunnel route audit + removal → Task 2 (resolved to tf/cloudflare/ not argocd/app-configs/cloudflared-main/; Cloudflared k8s deployment does NOT reference the bastion)
    • Vault path (KV-v2) left intact → Task 7.8 (follow-up bead)
    • hl-7h0 closure → Step 7.7
    • All “Verification” items in spec §“PR1: Bastion Teardown” map to Step 7.6.
  2. Surface area beyond the spec — three additions surfaced during file-structure audit:

    • tf/vault/k8s-cf-ssh-bastion.tf + tf/vault/policy-cf-ssh-bastion.tf (Task 3) — Vault auth role + policy were not enumerated in the spec but exist and must be removed.
    • argocd/app-configs/velero/backup-schedule.yaml (Task 4) — Velero backup schedule references the bastion namespace; removed.
    • docs/operations/cilium.md (Task 5) — single live doc reference (not migration history) updated. These are noted as “Surface area beyond the spec” rather than spec gaps; they are direct consequences of removing the workload and were missed in spec because they are TF / non-manifest references.
  3. Placeholder scan — no TBDs, no “implement later”, no “similar to Task N” without inline code, no vague verification (“ensure it works”). All steps have exact commands + expected output.

  4. Type/name consistency — Confirmed:

    • Bookmark name feat/cilium-pr1-bastion-teardown used consistently in Task 7.1 and 7.2.
    • PR title prefix feat(cf-ssh-bastion): matches conventional-commits scope.
    • HCP Terraform workspace names are not hardcoded in this plan; the reviewer reads them from the PR comment Step 7.3.