Skip to content

Security — posture drift #36

Security — posture drift

Security — posture drift #36

# 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