Skip to content

sec: supply chain hardening — AR migration, Binary Auth, SHA pins, SLSA#128

Merged
Pyronewbic merged 2 commits into
mainfrom
dev
May 20, 2026
Merged

sec: supply chain hardening — AR migration, Binary Auth, SHA pins, SLSA#128
Pyronewbic merged 2 commits into
mainfrom
dev

Conversation

@Pyronewbic
Copy link
Copy Markdown
Owner

Summary

  • SHA-pin all 16 GitHub Actions across 4 workflows (immutable commit refs, Dependabot handles updates)
  • Migrate container images from deprecated GCR to Artifact Registry (API + base image)
  • Add KMS-backed Binary Auth attestor with deploy pipeline attestation creation
  • Replace hand-rolled SLSA provenance with actions/attest-build-provenance v2
  • Fix cosign verify (was || true, now fails on bad signature)
  • Deduplicate CI triggers (push only on main, not dev)
  • Terraform: AR repos with cleanup policies, KMS key ring + EC P256 signing key, attestor IAM bindings

Rollout steps after merge

  1. Terraform auto-applies (AR repos + KMS key + attestor created)
  2. Manually trigger base-image.yml to push node24 to Artifact Registry
  3. Push to main triggers deploy with AR paths + Binary Auth attestation
  4. After first successful attested deploy: update binary-auth.tf to REQUIRE_ATTESTATION (separate PR)

Test plan

  • CI passes (unit + codeql)
  • Terraform plan shows expected new resources (2 AR repos, KMS key ring, attestor, IAM bindings)
  • After merge: terraform apply succeeds
  • After merge: base-image.yml manual trigger pushes to AR
  • After merge: deploy succeeds with new pipeline (sign + verify + attest + SBOM + provenance)
  • Verify: gcloud container binauthz attestations list --attestor=deploy-attestor

Pin all 16 actions across 4 workflows to commit SHAs to prevent
upstream tag compromise. Remove dev from CI push triggers to
eliminate duplicate runs on PRs.
…ce, cosign verify

- Migrate API + base image from deprecated GCR to Artifact Registry
- Add KMS-backed Binary Auth attestor with deploy pipeline attestation
- Replace hand-rolled SLSA provenance with actions/attest-build-provenance
- Fix cosign verify (remove || true that made it a no-op)
- Terraform: AR repos with cleanup policies, KMS key ring, attestor IAM
- Update docs to reflect AR paths and new deploy pipeline
@github-actions
Copy link
Copy Markdown

Terraform Plan

Acquiring state lock. This may take a few moments...
google_project_service.scheduler: Refreshing state... [id=casecomp-495718/cloudscheduler.googleapis.com]
google_project_service.containeranalysis: Refreshing state... [id=casecomp-495718/containeranalysis.googleapis.com]
google_project_service.firestore: Refreshing state... [id=casecomp-495718/firestore.googleapis.com]
data.google_project.current: Reading...
google_logging_metric.api_errors: Refreshing state... [id=cardscrapebot-errors]
google_storage_bucket.site: Refreshing state... [id=casecomp-site]
google_project_service.binaryauthorization: Refreshing state... [id=casecomp-495718/binaryauthorization.googleapis.com]
data.google_secret_manager_secret_version.api_key: Reading...
google_compute_managed_ssl_certificate.api_cert: Refreshing state... [id=projects/casecomp-495718/global/sslCertificates/cardscrapebot-cert-v2]
google_project_service.compute: Refreshing state... [id=casecomp-495718/compute.googleapis.com]
google_project_service.cloudbuild: Refreshing state... [id=casecomp-495718/cloudbuild.googleapis.com]
google_project_service.run: Refreshing state... [id=casecomp-495718/run.googleapis.com]
data.google_secret_manager_secret_version.api_key: Read complete after 1s [id=projects/129850122606/secrets/CASECOMP_API_KEY/versions/1]
google_compute_managed_ssl_certificate.site_cert: Refreshing state... [id=projects/casecomp-495718/global/sslCertificates/casecomp-site-cert]
google_project_service.monitoring: Refreshing state... [id=casecomp-495718/monitoring.googleapis.com]
data.google_project.current: Read complete after 1s [id=projects/casecomp-495718]
google_project_service.secretmanager: Refreshing state... [id=casecomp-495718/secretmanager.googleapis.com]
google_storage_bucket_iam_member.site_public: Refreshing state... [id=b/casecomp-site/roles/storage.objectViewer/allUsers]
google_monitoring_notification_channel.email: Refreshing state... [id=projects/casecomp-495718/notificationChannels/3431772178774051140]
google_compute_global_address.api_ip: Refreshing state... [id=projects/casecomp-495718/global/addresses/cardscrapebot-ip]
google_cloud_scheduler_job.check_alerts: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/jobs/casecomp-check-alerts]
google_secret_manager_secret.api_secrets["CASECOMP_ADMIN_SUB"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_ADMIN_SUB]
google_secret_manager_secret.api_secrets["CASECOMP_JWT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_JWT_SECRET]
google_secret_manager_secret.api_secrets["TOGETHER_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/TOGETHER_API_KEY]
google_secret_manager_secret.api_secrets["PSA_AUTH_TOKEN"]: Refreshing state... [id=projects/casecomp-495718/secrets/PSA_AUTH_TOKEN]
google_secret_manager_secret.api_secrets["EBAY_CLIENT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_SECRET]
google_secret_manager_secret.api_secrets["CASECOMP_SANDBOX_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_SANDBOX_KEY]
google_secret_manager_secret.api_secrets["ANTHROPIC_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/ANTHROPIC_API_KEY]
google_secret_manager_secret.api_secrets["EBAY_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_ID]
google_firestore_database.default: Refreshing state... [id=projects/casecomp-495718/databases/(default)]
google_secret_manager_secret.api_secrets["GOOGLE_OAUTH_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/GOOGLE_OAUTH_CLIENT_ID]
google_secret_manager_secret.api_secrets["RESEND_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/RESEND_API_KEY]
google_secret_manager_secret.api_secrets["CASECOMP_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_API_KEY]
google_binary_authorization_policy.default: Refreshing state... [id=projects/casecomp-495718]
google_monitoring_uptime_check_config.api_uptime: Refreshing state... [id=projects/casecomp-495718/uptimeCheckConfigs/casecomp-api-health-lQkUaC0Vzb8]
google_cloud_scheduler_job.track_prices: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/jobs/casecomp-track-prices]
google_monitoring_alert_policy.api_error_alert: Refreshing state... [id=projects/casecomp-495718/alertPolicies/16365448047387079183]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_SANDBOX_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_SANDBOX_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["GOOGLE_OAUTH_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/GOOGLE_OAUTH_CLIENT_ID/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["RESEND_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/RESEND_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["TOGETHER_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/TOGETHER_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_ADMIN_SUB"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_ADMIN_SUB/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["ANTHROPIC_API_KEY"]: Refreshing state... [id=projects/casecomp-495718/secrets/ANTHROPIC_API_KEY/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["CASECOMP_JWT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/CASECOMP_JWT_SECRET/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["EBAY_CLIENT_ID"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_ID/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["PSA_AUTH_TOKEN"]: Refreshing state... [id=projects/casecomp-495718/secrets/PSA_AUTH_TOKEN/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_secret_manager_secret_iam_member.cloud_run_access["EBAY_CLIENT_SECRET"]: Refreshing state... [id=projects/casecomp-495718/secrets/EBAY_CLIENT_SECRET/roles/secretmanager.secretAccessor/serviceAccount:129850122606-compute@developer.gserviceaccount.com]
google_monitoring_alert_policy.api_uptime_alert: Refreshing state... [id=projects/casecomp-495718/alertPolicies/14098674883088940398]
google_cloud_run_v2_service.site["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-site]
google_cloud_run_v2_service.site["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-site]
google_cloud_run_v2_service.api["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-api]
google_cloud_run_v2_service.api["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-api]
google_firestore_index.composite["grade-logs_userId_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJim14AK]
google_firestore_index.composite["api-analytics_userId_ts"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-analytics/indexes/CICAgJjF9oIK]
google_firestore_index.composite["price-history_cardKey_recordedAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/price-history/indexes/CICAgOjXh4EK]
google_firestore_index.composite["grade-logs_source_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/grade-logs/indexes/CICAgJj7z4EJ]
google_firestore_index.composite["api-keys_ownerId_createdAt"]: Refreshing state... [id=projects/casecomp-495718/databases/(default)/collectionGroups/api-keys/indexes/CICAgJiUpoMK]
google_cloud_run_v2_service_iam_member.site_public["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-site/roles/run.invoker/allUsers]
google_cloud_run_v2_service_iam_member.site_public["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-site/roles/run.invoker/allUsers]
google_compute_region_network_endpoint_group.site_neg["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/regions/asia-south1/networkEndpointGroups/casecomp-site-neg]
google_compute_region_network_endpoint_group.site_neg["us-central1"]: Refreshing state... [id=projects/casecomp-495718/regions/us-central1/networkEndpointGroups/casecomp-site-neg-us-central1]
google_compute_region_network_endpoint_group.api_neg["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/regions/asia-south1/networkEndpointGroups/casecomp-api-neg]
google_compute_region_network_endpoint_group.api_neg["us-central1"]: Refreshing state... [id=projects/casecomp-495718/regions/us-central1/networkEndpointGroups/casecomp-api-neg-us-central1]
google_cloud_run_v2_service_iam_member.public["us-central1"]: Refreshing state... [id=projects/casecomp-495718/locations/us-central1/services/casecomp-api/roles/run.invoker/allUsers]
google_cloud_run_v2_service_iam_member.public["asia-south1"]: Refreshing state... [id=projects/casecomp-495718/locations/asia-south1/services/casecomp-api/roles/run.invoker/allUsers]
google_compute_backend_service.site_backend: Refreshing state... [id=projects/casecomp-495718/global/backendServices/casecomp-site-backend]
google_compute_backend_service.api_backend: Refreshing state... [id=projects/casecomp-495718/global/backendServices/cardscrapebot-backend]
google_compute_url_map.api_urlmap: Refreshing state... [id=projects/casecomp-495718/global/urlMaps/cardscrapebot-urlmap]
google_compute_target_https_proxy.api_proxy: Refreshing state... [id=projects/casecomp-495718/global/targetHttpsProxies/cardscrapebot-https-proxy]
google_compute_global_forwarding_rule.api_https: Refreshing state... [id=projects/casecomp-495718/global/forwardingRules/cardscrapebot-https-rule]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.google_kms_crypto_key_version.attestor will be read during apply
  # (config refers to values not yet known)
 <= data "google_kms_crypto_key_version" "attestor" {
      + algorithm        = (known after apply)
      + crypto_key       = (known after apply)
      + id               = (known after apply)
      + name             = (known after apply)
      + protection_level = (known after apply)
      + public_key       = (known after apply)
      + state            = (known after apply)
    }

  # google_artifact_registry_repository.casecomp_api will be created
  + resource "google_artifact_registry_repository" "casecomp_api" {
      + create_time      = (known after apply)
      + description      = "casecomp API container images"
      + effective_labels = (known after apply)
      + format           = "DOCKER"
      + id               = (known after apply)
      + location         = "us"
      + mode             = "STANDARD_REPOSITORY"
      + name             = (known after apply)
      + project          = "casecomp-495718"
      + repository_id    = "casecomp-api"
      + terraform_labels = (known after apply)
      + update_time      = (known after apply)

      + cleanup_policies {
          + action = "KEEP"
          + id     = "keep-recent"

          + most_recent_versions {
              + keep_count            = 20
              + package_name_prefixes = []
            }
        }
    }

  # google_artifact_registry_repository.casecomp_node24 will be created
  + resource "google_artifact_registry_repository" "casecomp_node24" {
      + create_time      = (known after apply)
      + description      = "casecomp Node.js 24 base image"
      + effective_labels = (known after apply)
      + format           = "DOCKER"
      + id               = (known after apply)
      + location         = "us"
      + mode             = "STANDARD_REPOSITORY"
      + name             = (known after apply)
      + project          = "casecomp-495718"
      + repository_id    = "casecomp-node24"
      + terraform_labels = (known after apply)
      + update_time      = (known after apply)

      + cleanup_policies {
          + action = "KEEP"
          + id     = "keep-recent"

          + most_recent_versions {
              + keep_count            = 5
              + package_name_prefixes = []
            }
        }
    }

  # google_artifact_registry_repository_iam_member.api_cloudbuild will be created
  + resource "google_artifact_registry_repository_iam_member" "api_cloudbuild" {
      + etag       = (known after apply)
      + id         = (known after apply)
      + location   = "us"
      + member     = "serviceAccount:129850122606@cloudbuild.gserviceaccount.com"
      + project    = (known after apply)
      + repository = (known after apply)
      + role       = "roles/artifactregistry.writer"
    }

  # google_artifact_registry_repository_iam_member.api_deploy will be created
  + resource "google_artifact_registry_repository_iam_member" "api_deploy" {
      + etag       = (known after apply)
      + id         = (known after apply)
      + location   = "us"
      + member     = "serviceAccount:casecomp-deploy@casecomp-495718.iam.gserviceaccount.com"
      + project    = (known after apply)
      + repository = (known after apply)
      + role       = "roles/artifactregistry.writer"
    }

  # google_artifact_registry_repository_iam_member.node24_deploy will be created
  + resource "google_artifact_registry_repository_iam_member" "node24_deploy" {
      + etag       = (known after apply)
      + id         = (known after apply)
      + location   = "us"
      + member     = "serviceAccount:casecomp-deploy@casecomp-495718.iam.gserviceaccount.com"
      + project    = (known after apply)
      + repository = (known after apply)
      + role       = "roles/artifactregistry.writer"
    }

  # google_binary_authorization_attestor.deploy will be created
  + resource "google_binary_authorization_attestor" "deploy" {
      + id      = (known after apply)
      + name    = "deploy-attestor"
      + project = "casecomp-495718"

      + attestation_authority_note {
          + delegation_service_account_email = (known after apply)
          + note_reference                   = "deploy-attestor"

          + public_keys {
              + id = (known after apply)

              + pkix_public_key {
                  + public_key_pem      = (known after apply)
                  + signature_algorithm = "ECDSA_P256_SHA256"
                }
            }
        }
    }

  # google_container_analysis_note.deploy_attestor will be created
  + resource "google_container_analysis_note" "deploy_attestor" {
      + create_time = (known after apply)
      + id          = (known after apply)
      + kind        = (known after apply)
      + name        = "deploy-attestor"
      + project     = "casecomp-495718"
      + update_time = (known after apply)

      + attestation_authority {
          + hint {
              + human_readable_name = "Deploy pipeline attestor"
            }
        }
    }

  # google_kms_crypto_key.attestor_key will be created
  + resource "google_kms_crypto_key" "attestor_key" {
      + crypto_key_backend         = (known after apply)
      + destroy_scheduled_duration = (known after apply)
      + effective_labels           = (known after apply)
      + id                         = (known after apply)
      + import_only                = (known after apply)
      + key_ring                   = (known after apply)
      + name                       = "attestor-key"
      + primary                    = (known after apply)
      + purpose                    = "ASYMMETRIC_SIGN"
      + terraform_labels           = (known after apply)

      + version_template {
          + algorithm        = "EC_SIGN_P256_SHA256"
          + protection_level = "SOFTWARE"
        }
    }

  # google_kms_crypto_key_iam_member.deploy_sa_signer will be created
  + resource "google_kms_crypto_key_iam_member" "deploy_sa_signer" {
      + crypto_key_id = (known after apply)
      + etag          = (known after apply)
      + id            = (known after apply)
      + member        = "serviceAccount:casecomp-deploy@casecomp-495718.iam.gserviceaccount.com"
      + role          = "roles/cloudkms.signerVerifier"
    }

  # google_kms_key_ring.binary_auth will be created
  + resource "google_kms_key_ring" "binary_auth" {
      + id       = (known after apply)
      + location = "global"
      + name     = "binary-auth"
      + project  = "casecomp-495718"
    }

  # google_project_iam_member.deploy_sa_note_attacher will be created
  + resource "google_project_iam_member" "deploy_sa_note_attacher" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:casecomp-deploy@casecomp-495718.iam.gserviceaccount.com"
      + project = "casecomp-495718"
      + role    = "roles/containeranalysis.notes.attacher"
    }

  # google_project_iam_member.deploy_sa_occurrence_editor will be created
  + resource "google_project_iam_member" "deploy_sa_occurrence_editor" {
      + etag    = (known after apply)
      + id      = (known after apply)
      + member  = "serviceAccount:casecomp-deploy@casecomp-495718.iam.gserviceaccount.com"
      + project = "casecomp-495718"
      + role    = "roles/containeranalysis.occurrences.editor"
    }

  # google_project_service.artifactregistry will be created
  + resource "google_project_service" "artifactregistry" {
      + disable_on_destroy = false
      + id                 = (known after apply)
      + project            = "casecomp-495718"
      + service            = "artifactregistry.googleapis.com"
    }

  # google_project_service.cloudkms will be created
  + resource "google_project_service" "cloudkms" {
      + disable_on_destroy = false
      + id                 = (known after apply)
      + project            = "casecomp-495718"
      + service            = "cloudkms.googleapis.com"
    }

Plan: 14 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

Merge to main to apply.

@Pyronewbic Pyronewbic merged commit fac047c into main May 20, 2026
12 checks passed
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