Security — posture drift #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Security posture drift check (KAN-173). | |
| # | |
| # Closes the detection half of KAN-170. The exposure there — flask-backend | |
| # running with run.googleapis.com/invoker-iam-disabled=true — persisted for | |
| # roughly 4.6 months and was found by a human following a trail, not by any | |
| # control. It was invisible in both artifacts a reviewer would naturally check: | |
| # cloudbuild.yaml said --no-allow-unauthenticated, and the IAM policy showed no | |
| # allUsers binding. Only the live service annotation revealed it. | |
| # | |
| # So this asks the one question neither the repo nor the IAM policy can answer: | |
| # is production still in the posture we left it in? | |
| # | |
| # The check itself is scripts/gcloud/kan170_verify.sh, which is read-only and | |
| # GET-only. It never POSTs to /api/generate* — those endpoints complete and bill | |
| # Gemini/Imagen even for an unauthenticated caller. | |
| # | |
| # The paired pr-gate job `Security — posture check can fail` runs the script's | |
| # --self-test on every PR, so a detector that has quietly stopped detecting gets | |
| # caught at review time rather than by a green run here. | |
| name: Security — posture drift | |
| on: | |
| schedule: | |
| # Daily, 15:20 UTC — offset from release-train's 15:00 so the two do not | |
| # contend for the same runner window. | |
| - cron: '20 15 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| id-token: write # required for Workload Identity Federation | |
| jobs: | |
| posture: | |
| name: Cloud Run posture unchanged | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| # Deliberately NOT the GCP_WORKLOAD_IDENTITY_PROVIDER / GCP_SERVICE_ACCOUNT | |
| # pair used by gc-build-deploy.yml. That workflow treats a non-empty value | |
| # in those two secrets as "deployment is configured" and changes behaviour | |
| # accordingly, so pointing them at this read-only identity would make the | |
| # deploy path believe it is wired up while holding an SA that cannot | |
| # deploy. Separate names keep a read-only detector from re-configuring a | |
| # write path. | |
| - uses: google-github-actions/auth@v3 | |
| with: | |
| workload_identity_provider: ${{ secrets.GCP_POSTURE_WIF_PROVIDER }} | |
| service_account: ${{ secrets.GCP_POSTURE_SERVICE_ACCOUNT }} | |
| - uses: google-github-actions/setup-gcloud@v3 | |
| # Fail loudly rather than skip. A posture check that silently no-ops when | |
| # credentials are missing is worse than no check: the run goes green and | |
| # the drift it exists to catch stays invisible. | |
| - name: Verify posture | |
| env: | |
| PROJECT_ID: ${{ vars.GCP_PROJECT_ID || secrets.GCP_PROJECT_ID }} | |
| REGION: ${{ vars.GCP_REGION || 'us-central1' }} | |
| # Two guards, required, since the KAN-176 cutover on 2026-08-28: | |
| # invoker IAM enforced AND ingress restricted. Until that day only one | |
| # existed, and the check asked for one — asking for two earlier would | |
| # have made this job permanently red, and a job that is always red is | |
| # a job nobody reads. | |
| # | |
| # Now that both are real, one guard is a REGRESSION and must fail. The | |
| # single-guard state is exactly what KAN-170 was: reachable from the | |
| # internet, refused only by IAM, and one `--invoker-iam-check` removal | |
| # away from being open again. | |
| REQUIRED_FLASK_GUARDS: '2' | |
| run: bash scripts/gcloud/kan170_verify.sh |