diff --git a/.github/workflows/gcp-readiness.yml b/.github/workflows/gcp-readiness.yml new file mode 100644 index 0000000..a1a5f18 --- /dev/null +++ b/.github/workflows/gcp-readiness.yml @@ -0,0 +1,98 @@ +name: gcp-readiness + +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + +concurrency: + group: gcp-readiness-${{ github.ref }} + cancel-in-progress: true + +env: + PROJECT_ID: teleo-501523 + WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github + ARTIFACT_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com + +jobs: + readiness: + name: Read-only GCP readiness probe + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.ARTIFACT_SERVICE_ACCOUNT }} + + - uses: google-github-actions/setup-gcloud@v2 + + - name: Capture gcloud identity + shell: bash + run: | + set +e + mkdir -p .gcp-readiness + { + echo "project=${PROJECT_ID}" + echo "service_account=${ARTIFACT_SERVICE_ACCOUNT}" + echo "workflow_ref=${GITHUB_REF}" + echo "revision=${GITHUB_SHA}" + } > .gcp-readiness/context.txt + gcloud auth list --format=json > .gcp-readiness/gcloud-auth-list.json 2> .gcp-readiness/gcloud-auth-list.stderr + echo "$?" > .gcp-readiness/gcloud-auth-list.exitcode + gcloud config list --format=json > .gcp-readiness/gcloud-config-list.json 2> .gcp-readiness/gcloud-config-list.stderr + echo "$?" > .gcp-readiness/gcloud-config-list.exitcode + + - name: Run readiness checker + shell: bash + run: | + set +e + python3 ops/check_gcp_infra_readiness.py \ + > .gcp-readiness/gcp-infra-readiness.json \ + 2> .gcp-readiness/gcp-infra-readiness.stderr + echo "$?" > .gcp-readiness/gcp-infra-readiness.exitcode + python3 - <<'PY' + import json + from pathlib import Path + + base = Path(".gcp-readiness") + report = { + "artifact": "github_wif_gcp_readiness_probe", + "checker_exitcode": (base / "gcp-infra-readiness.exitcode").read_text().strip(), + "stderr_tail": (base / "gcp-infra-readiness.stderr").read_text()[-2000:], + } + try: + payload = json.loads((base / "gcp-infra-readiness.json").read_text() or "{}") + except json.JSONDecodeError as exc: + report["json_parse_error"] = str(exc) + payload = {} + if payload: + report["pass_count"] = payload.get("pass_count") + report["blocked_count"] = payload.get("blocked_count") + report["fail_count"] = payload.get("fail_count") + report["checks"] = [ + { + "name": check.get("name"), + "status": check.get("status"), + "required_tier": check.get("required_tier"), + "current_tier": check.get("current_tier"), + "detail": (check.get("detail") or "")[:500], + } + for check in payload.get("checks", []) + ] + (base / "gcp-readiness-summary.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") + print(json.dumps(report, indent=2, sort_keys=True)) + PY + + - name: Upload readiness artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: gcp-readiness + path: .gcp-readiness/ + if-no-files-found: error diff --git a/docs/gcp-infra-hardening-20260707.md b/docs/gcp-infra-hardening-20260707.md index 471840e..b313164 100644 --- a/docs/gcp-infra-hardening-20260707.md +++ b/docs/gcp-infra-hardening-20260707.md @@ -54,6 +54,17 @@ gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref mai The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact. +For a read-only GCP posture probe through the same Workload Identity path: + +```bash +gh workflow run gcp-readiness.yml --repo living-ip/teleo-infrastructure --ref main +``` + +This workflow runs `ops/check_gcp_infra_readiness.py` from GitHub Actions and +uploads stdout, stderr, exit code, and a summary as the `gcp-readiness` artifact. +It is intentionally non-mutating. If the Artifact Registry service account lacks +broader read permissions, the artifact is still the proof of the exact IAM gap. + For a local/manual Cloud Build proof: ```bash @@ -97,6 +108,7 @@ The check is read-only and prints no secret values. It verifies: - Source SQLite/KB backup/export repeatability. - Whether an approved source KB/Postgres dump or replication credential exists. - Whether source data has actually been restored or replicated into GCP and queried. +- Whether the GitHub WIF readiness workflow exists for non-local readback. ## Current Boundaries diff --git a/ops/check_gcp_infra_readiness.py b/ops/check_gcp_infra_readiness.py index 3ff1517..5ec2be6 100755 --- a/ops/check_gcp_infra_readiness.py +++ b/ops/check_gcp_infra_readiness.py @@ -253,6 +253,44 @@ def check_github_actions_artifact_ci() -> Check: ) +def check_github_actions_readiness_ci() -> Check: + workflow = Path(".github/workflows/gcp-readiness.yml") + if not workflow.exists(): + return Check( + "github_actions_readiness_ci", + "blocked", + "missing .github/workflows/gcp-readiness.yml", + required_tier="T3_live_readonly", + current_tier="T0_spec", + ) + + text = workflow.read_text() + required = [ + "workflow_dispatch", + "google-github-actions/auth@v2", + "ops/check_gcp_infra_readiness.py", + GITHUB_ARTIFACT_SERVICE_ACCOUNT, + "gcp-readiness-summary.json", + "actions/upload-artifact@v4", + ] + absent = [item for item in required if item not in text] + if absent: + return Check( + "github_actions_readiness_ci", + "fail", + f"workflow_missing={absent}", + required_tier="T3_live_readonly", + current_tier="T1_foundation", + ) + return Check( + "github_actions_readiness_ci", + "pass", + "workflow=.github/workflows/gcp-readiness.yml captures readiness stdout/stderr/exitcode through GitHub WIF", + required_tier="T3_live_readonly", + current_tier="T2_runtime", + ) + + def _rule_allows_port(rule: dict, port: str) -> bool: for allowed in rule.get("allowed", []): if allowed.get("IPProtocol") in ("tcp", "all"): @@ -615,6 +653,7 @@ def main() -> int: safe_check("cloud_build_contract", check_cloud_build_contract), safe_check("cloud_build_service_account", check_cloud_build_service_account), safe_check("github_actions_artifact_ci", check_github_actions_artifact_ci), + safe_check("github_actions_readiness_ci", check_github_actions_readiness_ci), safe_check("network_ingress", check_network_ingress), safe_check("compute_runtime_service_accounts", check_compute_runtime_service_accounts), safe_check("compute_disk_snapshots", check_snapshot_policy),