Skip to content

sec(ci): GCP cleanup picks the Cloud SQL instance by substring and orphans it in state #1971

Description

@cristim

Summary

The GCP branch of cleanup-staging.yml selects the Cloud SQL instance to delete with gcloud sql instances list --filter="name:cudly-staging" | head -1. gcloud's : operator is a contains match, not equality, so any instance whose name contains cudly-staging is a candidate and head -1 picks whichever sorts first. The delete runs with || true, and the following terraform state rm google_sql_database_instance.main runs unconditionally outside the if [ -n "$INSTANCE" ] block, so a failed or wrong delete still leaves the real instance running, billing, and untracked. This is the exact over-match that scripts/select-owned-name.sh was written to remove for ECR (#1592/#1820) and RDS (#1821); the GCP path never got the guard, and the two regression sweeps (test-ecr-delete-selection.sh, test-rds-deletion-protection-scope.sh) key on aws ecr delete-repository and aws rds modify-db-instance, so they never look at gcloud sql instances delete. The step validates PROJECT carefully at line 396 precisely because a silent skip is unacceptable, but applies no equivalent check to INSTANCE.

Location

.github/workflows/cleanup-staging.yml:401 at 3c0f8ac (selection at 401-402, delete with || true at 405, unconditional terraform state rm at 419-421)

Failure scenario

The GCP project holds cudly-staging-prod-mirror and cudly-staging-5e4d3c2b. The cleanup run lists both, head -1 returns the mirror, and gcloud sql instances delete --quiet destroys it. The staging instance survives, terraform state rm drops it from state anyway, and the next destroy neither sees nor bills for it.

Evidence

INSTANCE=$(gcloud sql instances list --project="$PROJECT" \
  --filter="name:cudly-staging" --format="value(name)" 2>/dev/null | head -1)
if [ -n "$INSTANCE" ]; then
  echo "Deleting Cloud SQL instance $INSTANCE..."
  gcloud sql instances delete "$INSTANCE" --project="$PROJECT" --quiet || true
terraform state rm google_sql_database_instance.main 2>/dev/null || true

Suggested fix

Read the owned instance name from terraform output -json and pipe the gcloud sql instances list output through scripts/select-owned-name.sh, matching the ECR/RDS pattern; drop the || true on the delete and move the terraform state rm lines inside the branch that confirmed the deletion. Add a gcloud sql instances delete case to the existing selection sweep.


Found by the 2026-09-02 codebase audit, finding A14-004, reported by one reviewer and independently confirmed by a second. Full report: docs/audits/codebase-audit-2026-09-02.md.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions