Harden Observatory private staging deploy gate (#163)
Some checks are pending
CI / lint-and-test (push) Waiting to run

Merge the T2 readiness and least-privilege staging gate. This does not deploy Cloud Run, mutate Cloud SQL, or change production routing.
This commit is contained in:
twentyOne2x 2026-07-15 09:55:49 +02:00 committed by GitHub
parent 33399bd9ac
commit 36fe0d3cc2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1966 additions and 84 deletions

View file

@ -4,12 +4,13 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
action: action:
description: Build only, or deploy the protected staging service and run live receipts description: Build, preflight, or deploy the protected staging service and run live receipts
required: true required: true
default: build_only default: build_only
type: choice type: choice
options: options:
- build_only - build_only
- preflight_staging
- deploy_staging - deploy_staging
permissions: permissions:
@ -28,10 +29,13 @@ env:
IMAGE_NAME: observatory-read-adapter IMAGE_NAME: observatory-read-adapter
SERVICE_NAME: observatory-read-adapter-staging SERVICE_NAME: observatory-read-adapter-staging
RUNTIME_SERVICE_ACCOUNT: sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com RUNTIME_SERVICE_ACCOUNT: sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com
BUILD_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com
DEPLOY_SERVICE_ACCOUNT: sa-observatory-deployer@teleo-501523.iam.gserviceaccount.com
DB_IAM_USER: sa-observatory-read-adapter@teleo-501523.iam DB_IAM_USER: sa-observatory-read-adapter@teleo-501523.iam
API_KEY_SECRET: observatory-read-api-key-staging API_KEY_SECRET: observatory-read-api-key-staging
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github BUILD_WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
DEPLOY_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com DEPLOY_WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/observatory-read-adapter-main
EXPECTED_WORKFLOW_REF: living-ip/teleo-infrastructure/.github/workflows/gcp-observatory-read-adapter.yml@refs/heads/main
jobs: jobs:
build: build:
@ -57,8 +61,8 @@ jobs:
- id: auth - id: auth
uses: google-github-actions/auth@v3 uses: google-github-actions/auth@v3
with: with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} workload_identity_provider: ${{ env.BUILD_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} service_account: ${{ env.BUILD_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v3 - uses: google-github-actions/setup-gcloud@v3
with: with:
@ -68,28 +72,21 @@ jobs:
run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
- id: image - id: image
name: Build, smoke, and push immutable image name: Build, smoke, and push run-unique immutable image
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
tag="${GITHUB_SHA::12}" tag="${GITHUB_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}" image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}"
digest="$(gcloud artifacts docker images describe "${image}" \
--project="${PROJECT_ID}" \
--format='value(image_summary.digest)' 2>/dev/null || true)"
if [[ -n "${digest}" ]]; then
docker pull "${image}"
else
docker build -f Dockerfile.observatory-read-adapter -t "${image}" . docker build -f Dockerfile.observatory-read-adapter -t "${image}" .
fi
docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \ docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \
"${image}" python -m compileall -q /app/observatory_read_adapter "${image}" python -m compileall -q /app/observatory_read_adapter
docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")' docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")'
if [[ -z "${digest}" ]]; then docker push "${image}"
docker push "${image}" | tee docker-push.log digest="$(gcloud artifacts docker images describe "${image}" \
digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)" --project="${PROJECT_ID}" \
fi --format='value(image_summary.digest)')"
test -n "${digest}" [[ "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]
image_ref="${image}@${digest}" image_ref="${image}@${digest}"
printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}" printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}"
printf 'revision=%s\nimage_ref=%s\n' "${GITHUB_SHA}" "${image_ref}" > observatory-adapter-image.txt printf 'revision=%s\nimage_ref=%s\n' "${GITHUB_SHA}" "${image_ref}" > observatory-adapter-image.txt
@ -100,31 +97,94 @@ jobs:
path: observatory-adapter-image.txt path: observatory-adapter-image.txt
if-no-files-found: error if-no-files-found: error
deploy-staging: preflight-staging:
name: Deploy and prove staging read path name: Verify private staging prerequisites
if: ${{ inputs.action == 'deploy_staging' }} if: ${{ inputs.action != 'build_only' }}
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Enforce reviewed main-only staging path
shell: bash
run: |
set -euo pipefail
[[ "${GITHUB_REF}" == "refs/heads/main" ]]
[[ "${GITHUB_WORKFLOW_REF}" == "${EXPECTED_WORKFLOW_REF}" ]]
[[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]
- id: auth - id: auth
uses: google-github-actions/auth@v3 uses: google-github-actions/auth@v3
with: with:
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} workload_identity_provider: ${{ env.DEPLOY_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v3 - uses: google-github-actions/setup-gcloud@v3
with: with:
project_id: ${{ env.PROJECT_ID }} project_id: ${{ env.PROJECT_ID }}
- name: Deploy bounded GCP staging service - name: Run adapter-specific read-only GCP preflight
shell: bash shell: bash
env: env:
IMAGE_REF: ${{ needs.build.outputs.image_ref }} IMAGE_REF: ${{ needs.build.outputs.image_ref }}
run: | run: |
set -euo pipefail set -euo pipefail
python3 ops/check_observatory_read_adapter_gcp_preflight.py \
--image-ref "${IMAGE_REF}" \
--output observatory-read-adapter-preflight.json
- uses: actions/upload-artifact@v4
if: always()
with:
name: observatory-read-adapter-preflight
path: observatory-read-adapter-preflight.json
if-no-files-found: error
deploy-staging:
name: Deploy and prove staging read path
if: ${{ inputs.action == 'deploy_staging' }}
needs:
- build
- preflight-staging
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Enforce reviewed main-only staging path
shell: bash
run: |
set -euo pipefail
[[ "${GITHUB_REF}" == "refs/heads/main" ]]
[[ "${GITHUB_WORKFLOW_REF}" == "${EXPECTED_WORKFLOW_REF}" ]]
[[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]
- id: auth
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.DEPLOY_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ env.PROJECT_ID }}
- id: deploy
name: Deploy bounded GCP staging service
shell: bash
env:
IMAGE_REF: ${{ needs.build.outputs.image_ref }}
run: |
set -euo pipefail
api_key_version="$(gcloud secrets versions list "${API_KEY_SECRET}" \
--project="${PROJECT_ID}" \
--filter='state=ENABLED' \
--sort-by='~createTime' \
--limit=1 \
--format='value(name)')"
api_key_version="${api_key_version##*/}"
[[ "${api_key_version}" =~ ^[0-9]+$ ]]
gcloud run deploy "${SERVICE_NAME}" \ gcloud run deploy "${SERVICE_NAME}" \
--project="${PROJECT_ID}" \ --project="${PROJECT_ID}" \
--region="${REGION}" \ --region="${REGION}" \
@ -135,39 +195,69 @@ jobs:
--subnet=teleo-staging-europe-west6 \ --subnet=teleo-staging-europe-west6 \
--vpc-egress=private-ranges-only \ --vpc-egress=private-ranges-only \
--ingress=all \ --ingress=all \
--allow-unauthenticated \ --no-invoker-iam-check \
--port=8080 \ --port=8080 \
--cpu=1 \ --cpu=1 \
--memory=512Mi \ --memory=512Mi \
--concurrency=8 \ --concurrency=8 \
--max-instances=2 \ --max-instances=2 \
--timeout=15 \ --timeout=15 \
--set-secrets="OBSERVATORY_API_KEY=${API_KEY_SECRET}:latest" \ --set-secrets="OBSERVATORY_API_KEY=${API_KEY_SECRET}:${api_key_version}" \
--set-env-vars="GCP_PROJECT_ID=${PROJECT_ID},CLOUD_SQL_INSTANCE=${PROJECT_ID}:${REGION}:teleo-pgvector-standby,DB_NAME=teleo_canonical,DB_IAM_USER=${DB_IAM_USER},DB_AUTHORIZATION_ROLE=kb_observatory_read,SERVICE_REVISION=${GITHUB_SHA}" \ --set-env-vars="GCP_PROJECT_ID=${PROJECT_ID},CLOUD_SQL_INSTANCE=${PROJECT_ID}:${REGION}:teleo-pgvector-standby,DB_NAME=teleo_canonical,DB_IAM_USER=${DB_IAM_USER},DB_AUTHORIZATION_ROLE=kb_observatory_read,SERVICE_REVISION=${GITHUB_SHA}" \
--labels="livingip-tier=gcp-staging,livingip-surface=observatory-read-adapter" \ --labels="livingip-tier=gcp-staging,livingip-surface=observatory-read-adapter" \
--quiet --quiet
printf 'api_key_version=%s\n' "${api_key_version}" >> "${GITHUB_OUTPUT}"
- name: Capture positive and negative live receipts - name: Capture positive and negative live receipts
shell: bash shell: bash
env:
API_KEY_VERSION: ${{ steps.deploy.outputs.api_key_version }}
EXPECTED_IMAGE_REF: ${{ needs.build.outputs.image_ref }}
run: | run: |
set -euo pipefail set -euo pipefail
service_url="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.url)')" service_json="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format=json)"
revision="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.latestReadyRevisionName)')" service_url="$(jq -r '.status.url // empty' <<<"${service_json}")"
api_key="$(gcloud secrets versions access latest --secret="${API_KEY_SECRET}" --project="${PROJECT_ID}")" revision="$(jq -r '.status.latestReadyRevisionName // empty' <<<"${service_json}")"
runtime_service_account="$(jq -r '.spec.template.spec.serviceAccountName // .template.serviceAccount // empty' <<<"${service_json}")"
deployed_image="$(jq -r '.spec.template.spec.containers[0].image // .template.containers[0].image // empty' <<<"${service_json}")"
deployed_secret_version="$(jq -r '
[
.spec.template.spec.containers[]?.env[]?
| select(.name == "OBSERVATORY_API_KEY")
| .valueFrom.secretKeyRef.key
][0] // [
.template.containers[]?.env[]?
| select(.name == "OBSERVATORY_API_KEY")
| .valueSource.secretKeyRef.version
][0] // empty
' <<<"${service_json}")"
expected_digest="${EXPECTED_IMAGE_REF##*@}"
api_key="$(gcloud secrets versions access "${API_KEY_VERSION}" --secret="${API_KEY_SECRET}" --project="${PROJECT_ID}")"
test -n "${service_url}" test -n "${service_url}"
test -n "${revision}" test -n "${revision}"
test "${runtime_service_account}" = "${RUNTIME_SERVICE_ACCOUNT}"
test "${deployed_secret_version}" = "${API_KEY_VERSION}"
[[ "${deployed_image}" == "${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}@${expected_digest}" ]]
test -n "${api_key}" test -n "${api_key}"
curl --fail --silent --show-error \ curl --fail --silent --show-error \
--header "X-Api-Key: ${api_key}" \ --header "X-Api-Key: ${api_key}" \
"${service_url}/v1/claims/sample" > positive.json "${service_url}/v1/claims/sample" > positive.json
jq -e ' jq -e \
--arg project_id "${PROJECT_ID}" \
--arg cloud_sql_instance "${PROJECT_ID}:${REGION}:teleo-pgvector-standby" \
--arg db_iam_user "${DB_IAM_USER}" \
--arg git_sha "${GITHUB_SHA}" '
.schema == "livingip.observatory-canonical-claim.v1" .schema == "livingip.observatory-canonical-claim.v1"
and .read_only == true and .read_only == true
and .provenance.gcp_project == $project_id
and .provenance.cloud_sql_instance == $cloud_sql_instance
and .provenance.database == "teleo_canonical" and .provenance.database == "teleo_canonical"
and .provenance.database_principal == $db_iam_user
and .provenance.authorization_role == "kb_observatory_read" and .provenance.authorization_role == "kb_observatory_read"
and .provenance.transaction_read_only == true and .provenance.transaction_read_only == true
and .provenance.write_privileges_denied == true and .provenance.write_privileges_denied == true
and .provenance.service_revision == $git_sha
and (.canonical.evidence | length) > 0 and (.canonical.evidence | length) > 0
and .proposal_ledger.distinct_from_canonical == true and .proposal_ledger.distinct_from_canonical == true
' positive.json >/dev/null ' positive.json >/dev/null
@ -178,12 +268,23 @@ jobs:
--data '{"status":"applied"}' "${service_url}/v1/claims/sample")" --data '{"status":"applied"}' "${service_url}/v1/claims/sample")"
test "${anonymous_status}" = 401 test "${anonymous_status}" = 401
test "${write_status}" = 405 test "${write_status}" = 405
curl --fail --silent --show-error \
--header "X-Api-Key: ${api_key}" \
"${service_url}/v1/claims/sample" > after-write-attempt.json
cmp <(jq -S . positive.json) <(jq -S . after-write-attempt.json)
jq -f ops/redact_observatory_read_adapter_receipt.jq \
positive.json > positive-redacted.json
jq -n \ jq -n \
--arg service_url "${service_url}" \ --arg service_url "${service_url}" \
--arg revision "${revision}" \ --arg revision "${revision}" \
--arg git_sha "${GITHUB_SHA}" \ --arg git_sha "${GITHUB_SHA}" \
--argjson positive "$(cat positive.json)" \ --arg runtime_service_account "${runtime_service_account}" \
--arg database_principal "${DB_IAM_USER}" \
--arg deployed_image "${deployed_image}" \
--arg api_key_secret_version "${API_KEY_VERSION}" \
--argjson positive "$(cat positive-redacted.json)" \
--arg anonymous_status "${anonymous_status}" \ --arg anonymous_status "${anonymous_status}" \
--arg write_status "${write_status}" \ --arg write_status "${write_status}" \
'{ '{
@ -192,14 +293,21 @@ jobs:
service_url: $service_url, service_url: $service_url,
revision: $revision, revision: $revision,
git_sha: $git_sha, git_sha: $git_sha,
runtime_service_account: $runtime_service_account,
database_principal: $database_principal,
deployed_image: $deployed_image,
api_key_secret_version: $api_key_secret_version,
positive: $positive, positive: $positive,
negative: { negative: {
anonymous_http_status: ($anonymous_status | tonumber), anonymous_http_status: ($anonymous_status | tonumber),
authenticated_post_http_status: ($write_status | tonumber) authenticated_post_http_status: ($write_status | tonumber),
canonical_state_unchanged_after_post: true,
database_write_privileges_denied: $positive.provenance.write_privileges_denied
}, },
production_repoint_executed: false production_repoint_executed: false
}' > observatory-read-adapter-live-receipt.json }' > observatory-read-adapter-live-receipt.json
unset api_key unset api_key
rm -f positive.json positive-redacted.json after-write-attempt.json anonymous.json write.json
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:

View file

@ -34,6 +34,13 @@ it does not change Argus or PR #148's Leo runtime files.
- Direct VPC egress: `teleo-staging-net` / `teleo-staging-europe-west6`. - Direct VPC egress: `teleo-staging-net` / `teleo-staging-europe-west6`.
- Cloud SQL: `teleo-501523:europe-west6:teleo-pgvector-standby`, private IP. - Cloud SQL: `teleo-501523:europe-west6:teleo-pgvector-standby`, private IP.
- Database: `teleo_canonical` only. - Database: `teleo_canonical` only.
- Build identity: `sa-artifact-builder@teleo-501523.iam.gserviceaccount.com`;
image build/push only.
- Staging deploy/canary identity:
`sa-observatory-deployer@teleo-501523.iam.gserviceaccount.com`; an exact
workflow/main-only WIF provider, a five-permission Cloud Run custom role,
repository read, exact runtime `actAs`, and exact secret access. Never grant
deploy permissions to the artifact builder.
- Runtime identity: `sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com`. - Runtime identity: `sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com`.
- IAM database user: `sa-observatory-read-adapter@teleo-501523.iam`. - IAM database user: `sa-observatory-read-adapter@teleo-501523.iam`.
- Database authorization role: `kb_observatory_read` (`NOLOGIN`). - Database authorization role: `kb_observatory_read` (`NOLOGIN`).
@ -52,24 +59,103 @@ effective write-denial checks drift.
These are staging mutations. Run them only with an authenticated GCP operator; These are staging mutations. Run them only with an authenticated GCP operator;
they do not route production traffic. they do not route production traffic.
1. Create the dedicated runtime service account and grant only 1. Reauthenticate the project operator, then run the first full direct check:
`roles/cloudsql.client` and `roles/cloudsql.instanceUser` in project
`teleo-501523`. ```bash
2. Add the service account as a Cloud SQL IAM service-account user on gcloud projects describe teleo-501523 --format=value(projectId)
`teleo-pgvector-standby`. python3 ops/check_gcp_infra_readiness.py
3. From the existing private GCP VM database-admin path, run ```
2. Review the exact dry-run packet. It enables only required APIs, creates
separate staging deploy/runtime identities, creates the dedicated
`observatory-read-adapter-main` WIF provider, keeps the artifact builder
narrow, scopes Cloud SQL connect/login to `teleo-pgvector-standby`, and
scopes secret access to `observatory-read-api-key-staging`:
```bash
python3 ops/plan_observatory_read_adapter_gcp.py
python3 ops/plan_observatory_read_adapter_gcp.py --format shell
```
The shell output is unexecuted and uses `set -euo pipefail`. Apply it only
after review with an authenticated `teleo-501523` administrator. The
deployer custom role contains only `run.services.create`,
`run.services.update`, `run.services.get`, `run.services.setIamPolicy`, and
`run.operations.get`; it contains no delete permission. Do not add
`run.admin`, `run.developer`, `cloudsql.admin`, `secretmanager.admin`,
`compute.admin`, Editor, or Owner to either lane identity.
3. Add the dedicated runtime service account as a Cloud SQL IAM
service-account user on `teleo-pgvector-standby`.
4. From the existing private GCP VM database-admin path, run
`ops/observatory_read_role.sql` against `teleo_canonical` with `ops/observatory_read_role.sql` against `teleo_canonical` with
`OBSERVATORY_DB_IAM_USER=sa-observatory-read-adapter@teleo-501523.iam`. `OBSERVATORY_DB_IAM_USER=sa-observatory-read-adapter@teleo-501523.iam`.
Do not read, copy, or retain the administrator password. Do not read, copy, or retain the administrator password.
4. Create `observatory-read-api-key-staging`, add a generated 32-byte-or-longer 5. Create `observatory-read-api-key-staging`, add a generated whitespace-free
value through stdin, and grant that secret's accessor role only to the 32-byte-or-longer value through stdin only when no enabled version exists,
runtime service account and the bounded staging canary identity. and grant that secret's accessor role only to the runtime service account
5. Dispatch `.github/workflows/gcp-observatory-read-adapter.yml` with and the dedicated staging deploy/canary identity. Deployment pins the exact
`action=deploy_staging` from the exact reviewed branch revision. enabled version; it does not inject `latest` into Cloud Run.
6. Dispatch the read-only preflight from merged `main`:
The workflow builds an immutable image, deploys at most two Cloud Run ```bash
gh workflow run gcp-observatory-read-adapter.yml \
--repo living-ip/teleo-infrastructure \
--ref main \
-f action=preflight_staging
```
Require a passing `observatory-read-adapter-preflight` artifact before a
bounded `action=deploy_staging` dispatch. The preflight verifies the exact
WIF condition, custom-role permission sets, project/resource IAM bindings,
Cloud Run service agent, private VPC/subnet relationship, private-only Cloud
SQL network and IAM authentication, trimmed IAM database user, exact secret
policy/value shape, and the run-unique immutable image. It also rejects any
artifact-builder deploy, runtime `actAs`, secret, or service binding.
The workflow always builds a run-unique immutable image, deploys at most two Cloud Run
instances, and retains the positive response plus anonymous `401` and instances, and retains the positive response plus anonymous `401` and
authenticated POST `405` receipts. It does not modify Vercel. authenticated POST `405` receipts. The retained positive receipt contains
identity/provenance, counts, and proposal/live state only; claim text, source
URLs, excerpts, unknown future provenance fields, and secret values are
removed by an explicit tested allowlist. The live gate binds the response to
the exact project, private instance, database principal, Git revision, Cloud
Run image digest, runtime identity, and pinned secret version. It does not
modify Vercel. The app-level `X-Api-Key` remains the staging authentication
boundary, so anonymous Cloud Run invocation reaches the adapter and returns
the required `401` instead of a platform-generated status.
## Current T2 Receipt And Exact T3 Gate
GitHub Actions run `29389090997` proves the existing `living-ip-github` WIF
provider can federate as the artifact builder, run the checker, upload its
artifact, and clean up credentials. The checker reported `8` pass, `10`
blocked, `0` fail. Eight blocks are missing inventory permissions on the
artifact-builder identity; two are missing restore/replication proof. This is
partial OIDC/T2 evidence, not T3 and not authorization to deploy with that
identity.
The exact external action is for `billy@livingip.xyz` to reauthenticate gcloud
and ADC, prove `gcloud projects describe teleo-501523`, run
`python3 ops/check_gcp_infra_readiness.py`, review and execute the planner's
shell output as a project IAM/API administrator, then apply
`ops/observatory_read_role.sql` from the existing private database-admin path.
The general readiness identity and artifact-builder identity are not deployer
substitutes. After those actions, dispatch `preflight_staging`; only a passing
artifact permits `deploy_staging`. A passing preflight remains T2 setup proof:
the fail-closed live claim and negative receipts are what establish T3.
The IAM basis is the Google Cloud deployment contract for Cloud Run (Cloud Run
access, Artifact Registry repository read, and exact runtime `actAs`), Secret
Manager resource-level accessor grants, Direct VPC access through the default
Cloud Run service agent, and conditional Cloud SQL client access to one named
instance:
- https://docs.cloud.google.com/run/docs/deploying
- https://docs.cloud.google.com/run/docs/authenticating/public
- https://docs.cloud.google.com/run/docs/configuring/vpc-direct-vpc
- https://docs.cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines
- https://docs.cloud.google.com/secret-manager/docs/manage-access-to-secrets
- https://docs.cloud.google.com/sql/docs/postgres/iam-conditions
## Unexecuted Cutover Packet ## Unexecuted Cutover Packet

View file

@ -0,0 +1,91 @@
{
"schema": "livingip.observatoryReadAdapterOidcReadinessReceipt.v1",
"status": "partial_oidc_t2",
"required_runtime_tier": "T3_live_gcp_staging_api",
"current_runtime_tier": "T2_authenticated_github_oidc_partial_inventory",
"workflow": {
"run_id": 29389090997,
"url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997",
"revision": "33399bd9acfa09e4e5409873e86c8c46ac694a8d",
"ref": "refs/heads/main",
"identity": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com",
"wif_authentication": "pass",
"gcloud_setup": "pass",
"checker_execution": "pass",
"artifact_upload": "pass",
"credential_cleanup": "pass",
"job_conclusion": "failure_due_to_blocked_readiness_enforcement"
},
"summary": {
"pass_count": 8,
"blocked_count": 10,
"fail_count": 0,
"passed_checks": [
"artifact_repositories",
"cloud_build_contract",
"github_actions_readiness_ci",
"gcp_cloudsql_restore_drill_contract",
"canonical_postgres_restore_contract",
"local_canonical_postgres_restore_preflight",
"gcp_runtime_baseline_contract",
"gcp_service_communications_contract"
],
"permission_or_resource_read_blocks": [
{
"check": "cloud_build_service_account",
"missing_permission": "iam.serviceAccounts.get"
},
{
"check": "github_actions_artifact_ci",
"missing_permission": "iam.workloadIdentityPoolProviders.get"
},
{
"check": "network_ingress",
"missing_permission": "compute.firewalls.list"
},
{
"check": "compute_runtime_service_accounts",
"missing_permission": "compute.instances.list"
},
{
"check": "compute_disk_snapshots",
"missing_permission": "compute.resourcePolicies.get"
},
{
"check": "backup_buckets",
"missing_permission": "bucket metadata read denied; exact permission was not retained by the checker"
},
{
"check": "cloud_sql_standby_target",
"missing_permission": "Cloud SQL instance read denied or target invisible to this identity"
},
{
"check": "kb_source_restore_access",
"missing_permission": "secretmanager.secrets.list"
}
],
"evidence_blocks": [
"local_sqlite_postgres_restore_canary",
"kb_restore_or_replication"
]
},
"security_readback": {
"artifact_builder_deploy_authorized": false,
"cloud_run_service_created": false,
"cloud_sql_queried": false,
"secret_values_logged": false,
"production_or_vercel_routing_changed": false
},
"source_artifact": {
"github_run_url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997",
"artifact_name": "gcp-readiness",
"summary_sha256": "6a1be356c069b8affacc492ac3b1e1ca63b917f92a32d7d886511a226c52925b",
"checker_sha256": "56ce4e8ba655a47e6e620a2c7039ee54fa10b1c53431d1dc6ec4db5189bb17f5",
"local_download_path": "/private/tmp/observatory-read-adapter-oidc-29389090997/gcp-readiness"
},
"proof_paths": [
"docs/reports/observatory-read-adapter/gcp-oidc-readiness-29389090997-redacted.json",
"https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997"
],
"claim_ceiling": "GitHub OIDC and the artifact-only identity work. The receipt does not prove broad GCP inventory, adapter prerequisites, a live Cloud Run service, or a teleo_canonical query."
}

View file

@ -1,19 +1,20 @@
{ {
"schema": "livingip.observatoryReadAdapterCurrentGate.v1", "schema": "livingip.observatoryReadAdapterCurrentGate.v1",
"current_canary": "Authenticated GET /v1/claims/sample in GCP staging returns a provenance-bearing teleo_canonical claim, anonymous GET returns 401, and authenticated POST returns 405.", "current_canary": "Authenticated GET /v1/claims/sample in GCP staging returns a provenance-bearing teleo_canonical claim, anonymous GET returns 401, and authenticated POST returns 405.",
"permission_profile": "approval_policy_never_with_unrestricted_filesystem_and_network", "required_runtime_tier": "T3_live_gcp_staging_api",
"current_runtime_tier": "T2_authenticated_github_oidc_partial_inventory",
"attempted_routes": [ "attempted_routes": [
{ {
"route": "merged GitHub Actions deployment with artifact-builder WIF", "route": "merged GitHub Actions deployment with artifact-builder WIF",
"result": "image build, smoke, and push passed; deploy stopped because run.googleapis.com is disabled" "result": "OIDC, image build, smoke, and push passed; the 29382683335 deploy stopped before resource creation because run.googleapis.com was disabled"
}, },
{ {
"route": "local billy@livingip.xyz gcloud user credential", "route": "GitHub Actions GCP readiness with artifact-builder WIF, run 29389090997",
"result": "token refresh and service enable both stop at noninteractive Google reauthentication" "result": "federation, gcloud setup, checker, artifact upload, and credential cleanup passed; checker result was 8 pass, 10 blocked, 0 fail"
}, },
{ {
"route": "local application-default credential", "route": "local billy@livingip.xyz gcloud user and application-default credentials",
"result": "token refresh stops at noninteractive Google reauthentication" "result": "both require interactive Google reauthentication before project-admin API or IAM changes"
}, },
{ {
"route": "other locally authenticated Google accounts", "route": "other locally authenticated Google accounts",
@ -22,19 +23,30 @@
{ {
"route": "fixed read-only GCP IAP status workflow", "route": "fixed read-only GCP IAP status workflow",
"result": "WIF token exchange failed because the teleo-iap-operator provider target is missing or disabled" "result": "WIF token exchange failed because the teleo-iap-operator provider target is missing or disabled"
},
{
"route": "local browser or computer-use session",
"result": "no browser or computer-use tool is exposed to this task; no foreground takeover was attempted"
} }
], ],
"exact_gate": { "exact_gate": {
"first_infrastructure_gate": "Cloud Run Admin API run.googleapis.com is disabled in teleo-501523.", "non_gui_federation": "Working. sa-artifact-builder can federate through living-ip-github and read/push Artifact Registry resources.",
"current_authorization_gate": "The only local account authorized for teleo-501523, billy@livingip.xyz, requires Google password or MFA reauthentication before project APIs and staging IAM resources can be changed.", "artifact_builder_scope": "Intentionally insufficient for deployment or full inventory. Do not grant it Cloud Run, Cloud SQL, Compute, IAM, or Secret Manager deployment roles.",
"alternate_private_operator_gate": "The read-only teleo-iap-operator WIF provider returns invalid_target and cannot reach its status operation." "last_api_readback": "The last direct deployment reported run.googleapis.com disabled. The current artifact-builder identity cannot verify or enable the required project APIs.",
"t3_bootstrap": "An authenticated teleo-501523 administrator must review and apply ops/plan_observatory_read_adapter_gcp.py, including required APIs, dedicated deploy/runtime identities, the exact workflow/main-only observatory-read-adapter-main WIF provider, five-permission Cloud Run custom role, exact Cloud SQL instance grants, exact secret grants, trimmed IAM database user, and private database role SQL.",
"local_operator": "billy@livingip.xyz requires Google password or MFA reauthentication before that administrator action can run.",
"alternate_private_operator": "The fixed teleo-iap-operator provider is absent or disabled and is not an available substitute."
}, },
"why_autonomous_repair_stops": "Google password or MFA reauthentication is a human-only credential boundary, and no authenticated project-admin browser or computer-use surface is available in this task.", "latest_direct_non_gui_check": {
"clear_CTA": "Run `gcloud auth login billy@livingip.xyz --update-adc`, complete Google password or MFA, and then report `GCP reauth complete`.", "active_account": "billy@livingip.xyz",
"next_non_user_action": "Enable the required staging APIs; create or verify the runtime service account, API-key secret, Cloud SQL IAM database user, and exact read-only role grants; deploy the retained immutable image; capture authenticated GET, anonymous 401, authenticated POST 405, SQL write-denial, exact service/database identity, and cleanup receipts.", "gcloud_projects_describe": "blocked: reauthentication failed and non-interactive execution cannot prompt",
"production_boundary": "Do not change Vercel, production routing, Cloud SQL network exposure, or any write endpoint." "application_default_credentials": "blocked: reauthentication failed and non-interactive execution cannot prompt",
"full_readiness_checker_executed": false,
"why_not": "The required first command, gcloud projects describe teleo-501523, did not pass."
},
"why_t3_stops": "A live staging service requires authorized API, IAM, Secret Manager, Cloud SQL IAM-user, and private database-role mutations. Working artifact-builder federation does not authorize those actions.",
"clear_CTA": "Run `gcloud auth login billy@livingip.xyz --update-adc`, then `gcloud projects describe teleo-501523 --format=value(projectId)` and `python3 ops/check_gcp_infra_readiness.py`; review and apply the exact adapter packet emitted by `python3 ops/plan_observatory_read_adapter_gcp.py --format shell`.",
"next_non_user_action": "Dispatch action=preflight_staging from merged main using sa-observatory-deployer. Only after its redacted preflight artifact passes, dispatch the bounded staging deploy and capture authenticated GET, anonymous 401, authenticated POST 405, database write-denial, exact service/database identity, and cleanup receipts.",
"production_boundary": "Do not change Vercel, production routing, Cloud SQL network exposure, Telegram, x402, or any write endpoint.",
"proof_paths": [
"docs/reports/observatory-read-adapter/gcp-oidc-readiness-29389090997-redacted.json",
"docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json",
"https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997"
]
} }

View file

@ -2,7 +2,7 @@
"schema": "livingip.observatoryReadAdapterStagingReceipt.v1", "schema": "livingip.observatoryReadAdapterStagingReceipt.v1",
"status": "blocked_before_runtime_creation", "status": "blocked_before_runtime_creation",
"required_runtime_tier": "T3_live_gcp_staging_api", "required_runtime_tier": "T3_live_gcp_staging_api",
"current_runtime_tier": "below_T3_build_artifact_only", "current_runtime_tier": "T2_authenticated_github_oidc_partial_inventory",
"project_id": "teleo-501523", "project_id": "teleo-501523",
"region": "europe-west6", "region": "europe-west6",
"service_name": "observatory-read-adapter-staging", "service_name": "observatory-read-adapter-staging",
@ -26,6 +26,18 @@
"failure_detail": "run.googleapis.com is disabled in teleo-501523", "failure_detail": "run.googleapis.com is disabled in teleo-501523",
"authenticated_as": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com" "authenticated_as": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com"
}, },
"readiness_reconciliation": {
"run_id": 29389090997,
"url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997",
"identity": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com",
"wif_authentication": "pass",
"checker_result": {
"pass_count": 8,
"blocked_count": 10,
"fail_count": 0
},
"claim": "working federation with an intentionally narrow artifact identity; not a staging deploy or full inventory"
},
"live_receipts": { "live_receipts": {
"service_url": null, "service_url": null,
"authenticated_get_sample": null, "authenticated_get_sample": null,
@ -47,9 +59,26 @@
"orphan_local_build_processes": 0, "orphan_local_build_processes": 0,
"artifact_registry_image_retained_for_next_deploy": true "artifact_registry_image_retained_for_next_deploy": true
}, },
"proof_paths": [ "source_artifacts": [
"/private/tmp/observatory-read-adapter-run-29382683335/observatory-adapter-image/observatory-adapter-image.txt", {
"/private/tmp/observatory-iap-status-29382844567/gcp-iap-operator-29382844567/result.json" "run_url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29382683335",
"artifact_name": "observatory-adapter-image",
"sha256": "6d958b8d345ec878db2f70aaab5fad4318b106441983bd7f8e9170a429f4e852"
},
{
"run_url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29382844567",
"artifact_name": "gcp-iap-operator-29382844567",
"sha256": "988f77123f7624469c15e3c38bc1e8864a1e9f61e571feb5f6c838e67fe039e9"
},
{
"run_url": "https://github.com/living-ip/teleo-infrastructure/actions/runs/29389090997",
"artifact_name": "gcp-readiness",
"sha256": "6a1be356c069b8affacc492ac3b1e1ca63b917f92a32d7d886511a226c52925b"
}
], ],
"claim_ceiling": "The immutable staging image exists, but no live API or database query receipt exists. T3 is not complete." "proof_paths": [
"docs/reports/observatory-read-adapter/gcp-staging-live-attempt-redacted.json",
"docs/reports/observatory-read-adapter/gcp-oidc-readiness-29389090997-redacted.json"
],
"claim_ceiling": "The immutable staging image and working artifact-only OIDC path exist, but no live API or database query receipt exists. T3 is not complete."
} }

View file

@ -0,0 +1,669 @@
#!/usr/bin/env python3
"""Run the adapter-specific, non-mutating GCP staging preflight."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
from collections.abc import Callable
from dataclasses import asdict, dataclass
from pathlib import Path
PROJECT = "teleo-501523"
PROJECT_NUMBER = "785938879453"
REGION = "europe-west6"
NETWORK = "teleo-staging-net"
SUBNET = "teleo-staging-europe-west6"
INSTANCE = "teleo-pgvector-standby"
SERVICE = "observatory-read-adapter-staging"
DEPLOY_SERVICE_ACCOUNT = f"sa-observatory-deployer@{PROJECT}.iam.gserviceaccount.com"
RUNTIME_SERVICE_ACCOUNT = f"sa-observatory-read-adapter@{PROJECT}.iam.gserviceaccount.com"
BUILD_SERVICE_ACCOUNT = f"sa-artifact-builder@{PROJECT}.iam.gserviceaccount.com"
DB_IAM_USER = f"sa-observatory-read-adapter@{PROJECT}.iam"
SECRET = "observatory-read-api-key-staging"
REPOSITORY = "teleo"
WIF_POOL = "github-actions"
DEPLOY_WIF_PROVIDER = "observatory-read-adapter-main"
DEPLOY_WORKFLOW_REF = (
"living-ip/teleo-infrastructure/.github/workflows/"
"gcp-observatory-read-adapter.yml@refs/heads/main"
)
DEPLOY_WIF_ATTRIBUTE_CONDITION = (
"assertion.repository=='living-ip/teleo-infrastructure' && "
"assertion.ref=='refs/heads/main' && "
f"assertion.workflow_ref=='{DEPLOY_WORKFLOW_REF}'"
)
DEPLOY_WIF_MEMBER = (
f"principal://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/"
f"workloadIdentityPools/{WIF_POOL}/subject/"
"repo:living-ip/teleo-infrastructure:ref:refs/heads/main"
)
PREFLIGHT_ROLE = f"projects/{PROJECT}/roles/observatoryStagingPreflight"
DEPLOY_ROLE = f"projects/{PROJECT}/roles/observatoryStagingDeployer"
PREFLIGHT_PERMISSIONS = {
"artifactregistry.repositories.getIamPolicy",
"cloudsql.instances.get",
"cloudsql.users.list",
"compute.networks.get",
"compute.subnetworks.get",
"iam.serviceAccounts.get",
"iam.serviceAccounts.getIamPolicy",
"iam.roles.get",
"iam.workloadIdentityPoolProviders.get",
"resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy",
"run.operations.get",
"run.services.get",
"run.services.getIamPolicy",
"secretmanager.secrets.getIamPolicy",
"serviceusage.services.get",
"serviceusage.services.use",
}
DEPLOY_PERMISSIONS = {
"run.operations.get",
"run.services.create",
"run.services.get",
"run.services.setIamPolicy",
"run.services.update",
}
REQUIRED_APIS = (
"artifactregistry.googleapis.com",
"compute.googleapis.com",
"iamcredentials.googleapis.com",
"run.googleapis.com",
"secretmanager.googleapis.com",
"sqladmin.googleapis.com",
"sts.googleapis.com",
)
IMAGE_RE = re.compile(
rf"^europe-west6-docker[.]pkg[.]dev/{PROJECT}/teleo/observatory-read-adapter:"
r"[0-9a-f]{40}-[0-9]+-[1-9][0-9]*@sha256:[0-9a-f]{64}$"
)
@dataclass(frozen=True)
class Check:
name: str
status: str
detail: str
def sanitize_error(value: str) -> str:
value = re.sub(r"/[^\s]*/gha-creds-[^\s]+[.]json", "<ephemeral-wif-credential>", value)
return " ".join(value.strip().split())[-600:] or "command_failed_without_stderr"
def run(command: list[str]) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(command, text=True, capture_output=True, timeout=30, check=False)
except subprocess.TimeoutExpired:
return subprocess.CompletedProcess(command, 124, stdout="", stderr="command_timed_out_after_30_seconds")
except OSError as exc:
return subprocess.CompletedProcess(command, 127, stdout="", stderr=f"command_unavailable:{exc}")
def command_check(
name: str,
command: list[str],
validate: Callable[[str], bool],
success_detail: str,
) -> Check:
result = run(command)
if result.returncode != 0:
return Check(name, "blocked", sanitize_error(result.stderr))
try:
valid = validate(result.stdout)
except (AttributeError, IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
return Check(name, "blocked", f"invalid_readback:{type(exc).__name__}")
return Check(name, "pass" if valid else "blocked", success_detail if valid else "unexpected_readback")
def cloud_sql_is_private_and_iam_enabled(value: str) -> bool:
payload = json.loads(value)
flags = {
item.get("name"): str(item.get("value", "")).lower()
for item in payload.get("settings", {}).get("databaseFlags", [])
}
ip_configuration = payload.get("settings", {}).get("ipConfiguration", {})
address_types = {item.get("type") for item in payload.get("ipAddresses", [])}
return (
payload.get("name") == INSTANCE
and payload.get("region") == REGION
and str(payload.get("databaseVersion", "")).startswith("POSTGRES_")
and ip_configuration.get("ipv4Enabled") is False
and str(ip_configuration.get("privateNetwork", "")).endswith(f"/networks/{NETWORK}")
and "PRIVATE" in address_types
and "PRIMARY" not in address_types
and flags.get("cloudsql.iam_authentication") == "on"
)
def cloud_sql_iam_user_exists(value: str) -> bool:
users = json.loads(value)
return any(
user.get("name") == DB_IAM_USER and user.get("type") == "CLOUD_IAM_SERVICE_ACCOUNT" for user in users
)
def subnet_is_exact(value: str) -> bool:
payload = json.loads(value)
return (
payload.get("name") == SUBNET
and str(payload.get("region", "")).endswith(f"/regions/{REGION}")
and str(payload.get("network", "")).endswith(f"/networks/{NETWORK}")
)
def secret_is_valid(value: str) -> bool:
try:
encoded = value.encode("ascii")
except UnicodeEncodeError:
return False
return value == value.strip() and 32 <= len(encoded) <= 512
def normalized(value: str) -> str:
return " ".join(value.split())
def policy_has_binding(
policy: dict[str, object],
role: str,
member: str,
*,
condition_title: str | None = None,
condition_expression: str | None = None,
) -> bool:
bindings = policy.get("bindings", [])
if not isinstance(bindings, list):
return False
for binding in bindings:
if not isinstance(binding, dict) or binding.get("role") != role:
continue
members = binding.get("members", [])
if not isinstance(members, list) or member not in members:
continue
condition = binding.get("condition")
if condition_title is None and condition_expression is None:
return condition in (None, {})
if not isinstance(condition, dict):
continue
if condition_title is not None and condition.get("title") != condition_title:
continue
if condition_expression is not None and normalized(str(condition.get("expression", ""))) != normalized(
condition_expression
):
continue
return True
return False
def member_roles(policy: dict[str, object], member: str) -> set[str]:
roles: set[str] = set()
bindings = policy.get("bindings", [])
if not isinstance(bindings, list):
return roles
for binding in bindings:
if not isinstance(binding, dict):
continue
members = binding.get("members", [])
role = binding.get("role")
if isinstance(members, list) and member in members and isinstance(role, str):
roles.add(role)
return roles
def project_iam_is_exact(value: str) -> bool:
policy = json.loads(value)
deployer = f"serviceAccount:{DEPLOY_SERVICE_ACCOUNT}"
runtime = f"serviceAccount:{RUNTIME_SERVICE_ACCOUNT}"
builder = f"serviceAccount:{BUILD_SERVICE_ACCOUNT}"
service_agent = (
f"serviceAccount:service-{PROJECT_NUMBER}@serverless-robot-prod.iam.gserviceaccount.com"
)
sql_expression = (
f"resource.name == 'projects/{PROJECT}/instances/{INSTANCE}' && "
"resource.service == 'sqladmin.googleapis.com'"
)
forbidden_deployer = {
"roles/artifactregistry.writer",
"roles/cloudsql.admin",
"roles/compute.admin",
"roles/editor",
"roles/owner",
"roles/run.admin",
"roles/run.developer",
"roles/secretmanager.admin",
}
forbidden_builder = forbidden_deployer | {
DEPLOY_ROLE,
PREFLIGHT_ROLE,
"roles/iam.serviceAccountUser",
"roles/secretmanager.secretAccessor",
}
return (
policy_has_binding(policy, PREFLIGHT_ROLE, deployer)
and policy_has_binding(policy, DEPLOY_ROLE, deployer)
and policy_has_binding(
policy,
"roles/cloudsql.client",
runtime,
condition_title="observatory-exact-cloudsql-instance",
condition_expression=sql_expression,
)
and policy_has_binding(
policy,
"roles/cloudsql.instanceUser",
runtime,
condition_title="observatory-exact-cloudsql-instance",
condition_expression=sql_expression,
)
and policy_has_binding(policy, "roles/run.serviceAgent", service_agent)
and not (member_roles(policy, deployer) & forbidden_deployer)
and not (member_roles(policy, builder) & forbidden_builder)
)
def role_has_exact_permissions(value: str, expected: set[str]) -> bool:
payload = json.loads(value)
permissions = payload.get("includedPermissions", [])
return isinstance(permissions, list) and set(permissions) == expected and payload.get("deleted") is not True
def wif_provider_is_exact(value: str) -> bool:
payload = json.loads(value)
mapping = payload.get("attributeMapping", {})
expected_mapping = {
"google.subject": "assertion.sub",
"attribute.repository": "assertion.repository",
"attribute.ref": "assertion.ref",
"attribute.workflow_ref": "assertion.workflow_ref",
}
return (
payload.get("state") == "ACTIVE"
and mapping == expected_mapping
and normalized(str(payload.get("attributeCondition", ""))) == normalized(DEPLOY_WIF_ATTRIBUTE_CONDITION)
)
def deployer_service_account_policy_is_exact(value: str) -> bool:
policy = json.loads(value)
return policy_has_binding(policy, "roles/iam.workloadIdentityUser", DEPLOY_WIF_MEMBER)
def runtime_service_account_policy_is_exact(value: str) -> bool:
policy = json.loads(value)
deployer = f"serviceAccount:{DEPLOY_SERVICE_ACCOUNT}"
builder = f"serviceAccount:{BUILD_SERVICE_ACCOUNT}"
return policy_has_binding(policy, "roles/iam.serviceAccountUser", deployer) and not member_roles(policy, builder)
def artifact_policy_is_exact(value: str) -> bool:
policy = json.loads(value)
deployer = f"serviceAccount:{DEPLOY_SERVICE_ACCOUNT}"
return policy_has_binding(policy, "roles/artifactregistry.reader", deployer)
def secret_policy_is_exact(value: str) -> bool:
policy = json.loads(value)
deployer = f"serviceAccount:{DEPLOY_SERVICE_ACCOUNT}"
runtime = f"serviceAccount:{RUNTIME_SERVICE_ACCOUNT}"
builder = f"serviceAccount:{BUILD_SERVICE_ACCOUNT}"
return (
policy_has_binding(policy, "roles/secretmanager.secretAccessor", deployer)
and policy_has_binding(policy, "roles/secretmanager.secretAccessor", runtime)
and policy_has_binding(policy, "roles/secretmanager.viewer", deployer)
and not member_roles(policy, builder)
)
def api_key_secret_check() -> Check:
version_result = run(
[
"gcloud",
"secrets",
"versions",
"list",
SECRET,
"--project",
PROJECT,
"--filter=state=ENABLED",
"--sort-by=~createTime",
"--limit=1",
"--format=value(name)",
]
)
if version_result.returncode != 0:
return Check("api_key_secret", "blocked", sanitize_error(version_result.stderr))
version = version_result.stdout.strip().rsplit("/", 1)[-1]
if not version.isdigit():
return Check("api_key_secret", "blocked", "enabled_secret_version_missing")
secret_result = run(
[
"gcloud",
"secrets",
"versions",
"access",
version,
"--secret",
SECRET,
"--project",
PROJECT,
]
)
if secret_result.returncode != 0:
return Check("api_key_secret", "blocked", sanitize_error(secret_result.stderr))
valid = secret_is_valid(secret_result.stdout)
detail = f"enabled_version_{version}_valid_length_value_redacted" if valid else "invalid_secret_value"
return Check("api_key_secret", "pass" if valid else "blocked", detail)
def existing_service_policy_check() -> Check:
describe = run(
[
"gcloud",
"run",
"services",
"describe",
SERVICE,
"--project",
PROJECT,
"--region",
REGION,
"--format=value(metadata.name)",
]
)
if describe.returncode != 0:
error = sanitize_error(describe.stderr)
if "not found" in error.lower() or "not_found" in error.lower():
return Check("existing_service_policy", "pass", "service_not_created_yet")
return Check("existing_service_policy", "blocked", error)
if describe.stdout.strip() != SERVICE:
return Check("existing_service_policy", "blocked", "unexpected_service_readback")
policy_result = run(
[
"gcloud",
"run",
"services",
"get-iam-policy",
SERVICE,
"--project",
PROJECT,
"--region",
REGION,
"--format=json",
]
)
if policy_result.returncode != 0:
return Check("existing_service_policy", "blocked", sanitize_error(policy_result.stderr))
try:
policy = json.loads(policy_result.stdout)
builder = f"serviceAccount:{BUILD_SERVICE_ACCOUNT}"
valid = not member_roles(policy, builder)
except (AttributeError, TypeError, ValueError, json.JSONDecodeError) as exc:
return Check("existing_service_policy", "blocked", f"invalid_readback:{type(exc).__name__}")
return Check(
"existing_service_policy",
"pass" if valid else "blocked",
"artifact_builder_absent" if valid else "artifact_builder_has_service_binding",
)
def build_checks(image_ref: str) -> list[Check]:
image_name, image_digest = image_ref.rsplit("@", 1) if "@" in image_ref else (image_ref, "")
digest_ref = f"{image_name.rsplit(':', 1)[0]}@{image_digest}" if image_digest else image_ref
checks = [
command_check(
"active_identity",
["gcloud", "auth", "list", "--filter=status:ACTIVE", "--format=value(account)"],
lambda value: value.strip() == DEPLOY_SERVICE_ACCOUNT,
"dedicated_observatory_deployer",
),
command_check(
"project",
["gcloud", "projects", "describe", PROJECT, "--format=value(projectId)"],
lambda value: value.strip() == PROJECT,
PROJECT,
),
]
checks.extend(
command_check(
f"api_{api.removesuffix('.googleapis.com')}",
["gcloud", "services", "describe", api, "--project", PROJECT, "--format=value(state)"],
lambda value: value.strip() == "ENABLED",
"enabled",
)
for api in REQUIRED_APIS
)
checks.extend(
[
command_check(
"deployer_wif_provider",
[
"gcloud",
"iam",
"workload-identity-pools",
"providers",
"describe",
DEPLOY_WIF_PROVIDER,
"--project",
PROJECT,
"--location",
"global",
"--workload-identity-pool",
WIF_POOL,
"--format=json",
],
wif_provider_is_exact,
"main_and_exact_workflow_only",
),
command_check(
"preflight_custom_role",
[
"gcloud",
"iam",
"roles",
"describe",
"observatoryStagingPreflight",
"--project",
PROJECT,
"--format=json",
],
lambda value: role_has_exact_permissions(value, PREFLIGHT_PERMISSIONS),
"exact_read_only_permissions",
),
command_check(
"deploy_custom_role",
[
"gcloud",
"iam",
"roles",
"describe",
"observatoryStagingDeployer",
"--project",
PROJECT,
"--format=json",
],
lambda value: role_has_exact_permissions(value, DEPLOY_PERMISSIONS),
"five_permissions_no_delete",
),
command_check(
"project_iam",
["gcloud", "projects", "get-iam-policy", PROJECT, "--format=json"],
project_iam_is_exact,
"required_bindings_present_broad_roles_absent",
),
command_check(
"deployer_wif_binding",
[
"gcloud",
"iam",
"service-accounts",
"get-iam-policy",
DEPLOY_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--format=json",
],
deployer_service_account_policy_is_exact,
"exact_main_subject_workload_identity_user",
),
command_check(
"runtime_act_as_policy",
[
"gcloud",
"iam",
"service-accounts",
"get-iam-policy",
RUNTIME_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--format=json",
],
runtime_service_account_policy_is_exact,
"deployer_only_artifact_builder_absent",
),
command_check(
"artifact_repository_policy",
[
"gcloud",
"artifacts",
"repositories",
"get-iam-policy",
REPOSITORY,
"--project",
PROJECT,
"--location",
REGION,
"--format=json",
],
artifact_policy_is_exact,
"deployer_reader",
),
command_check(
"secret_policy",
[
"gcloud",
"secrets",
"get-iam-policy",
SECRET,
"--project",
PROJECT,
"--format=json",
],
secret_policy_is_exact,
"runtime_and_deployer_only_artifact_builder_absent",
),
existing_service_policy_check(),
]
)
checks.extend(
[
command_check(
"runtime_service_account",
[
"gcloud",
"iam",
"service-accounts",
"describe",
RUNTIME_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--format=value(email)",
],
lambda value: value.strip() == RUNTIME_SERVICE_ACCOUNT,
RUNTIME_SERVICE_ACCOUNT,
),
command_check(
"vpc_network",
[
"gcloud",
"compute",
"networks",
"describe",
NETWORK,
"--project",
PROJECT,
"--format=value(name)",
],
lambda value: value.strip() == NETWORK,
NETWORK,
),
command_check(
"vpc_subnet",
[
"gcloud",
"compute",
"networks",
"subnets",
"describe",
SUBNET,
"--project",
PROJECT,
"--region",
REGION,
"--format=json",
],
subnet_is_exact,
f"{NETWORK}/{REGION}/{SUBNET}",
),
command_check(
"cloud_sql_private_iam",
["gcloud", "sql", "instances", "describe", INSTANCE, "--project", PROJECT, "--format=json"],
cloud_sql_is_private_and_iam_enabled,
f"{PROJECT}:{REGION}:{INSTANCE}:private_ip_only:iam_authentication_on",
),
command_check(
"cloud_sql_iam_user",
["gcloud", "sql", "users", "list", "--instance", INSTANCE, "--project", PROJECT, "--format=json"],
cloud_sql_iam_user_exists,
DB_IAM_USER,
),
api_key_secret_check(),
command_check(
"immutable_image",
["gcloud", "artifacts", "docker", "images", "describe", digest_ref, "--project", PROJECT, "--format=json"],
lambda _value: bool(IMAGE_RE.fullmatch(image_ref)),
image_digest or "missing_digest",
),
]
)
return checks
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--image-ref", required=True)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
checks = build_checks(args.image_ref)
pass_count = sum(check.status == "pass" for check in checks)
blocked_count = len(checks) - pass_count
receipt = {
"schema": "livingip.observatory-read-adapter-gcp-preflight.v1",
"project": PROJECT,
"required_tier": "T3_live_readonly",
"current_tier": "T2_authenticated_gcp_preflight",
"ready_for_staging_deploy": blocked_count == 0,
"claim_ceiling": "Preflight only; T3 requires the live private Cloud SQL claim and negative receipts.",
"identity": DEPLOY_SERVICE_ACCOUNT,
"checks": [asdict(check) for check in checks],
"pass_count": pass_count,
"blocked_count": blocked_count,
"secret_values_retained": False,
"database_role_live_verification_deferred_to_fail_closed_canary": True,
"production_repoint_executed": False,
}
rendered = json.dumps(receipt, indent=2, sort_keys=True) + "\n"
if args.output:
args.output.write_text(rendered, encoding="utf-8")
else:
print(rendered, end="")
return 0 if blocked_count == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""Emit the unexecuted least-privilege GCP staging packet for Observatory."""
from __future__ import annotations
import argparse
import json
import shlex
PROJECT = "teleo-501523"
PROJECT_NUMBER = "785938879453"
REGION = "europe-west6"
GITHUB_REPOSITORY = "living-ip/teleo-infrastructure"
WIF_POOL = "github-actions"
DEPLOY_WIF_PROVIDER = "observatory-read-adapter-main"
DEPLOY_WORKFLOW_REF = (
f"{GITHUB_REPOSITORY}/.github/workflows/gcp-observatory-read-adapter.yml@refs/heads/main"
)
DEPLOY_WIF_ATTRIBUTE_MAPPING = (
"google.subject=assertion.sub,attribute.repository=assertion.repository,"
"attribute.ref=assertion.ref,attribute.workflow_ref=assertion.workflow_ref"
)
DEPLOY_WIF_ATTRIBUTE_CONDITION = (
"assertion.repository=='living-ip/teleo-infrastructure' && "
"assertion.ref=='refs/heads/main' && "
f"assertion.workflow_ref=='{DEPLOY_WORKFLOW_REF}'"
)
BUILD_SERVICE_ACCOUNT = f"sa-artifact-builder@{PROJECT}.iam.gserviceaccount.com"
DEPLOY_SERVICE_ACCOUNT_ID = "sa-observatory-deployer"
DEPLOY_SERVICE_ACCOUNT = f"{DEPLOY_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com"
RUNTIME_SERVICE_ACCOUNT_ID = "sa-observatory-read-adapter"
RUNTIME_SERVICE_ACCOUNT = f"{RUNTIME_SERVICE_ACCOUNT_ID}@{PROJECT}.iam.gserviceaccount.com"
DB_IAM_USER = f"{RUNTIME_SERVICE_ACCOUNT_ID}@{PROJECT}.iam"
SERVICE = "observatory-read-adapter-staging"
INSTANCE = "teleo-pgvector-standby"
NETWORK = "teleo-staging-net"
SUBNET = "teleo-staging-europe-west6"
SECRET = "observatory-read-api-key-staging"
REPOSITORY = "teleo"
PREFLIGHT_ROLE_ID = "observatoryStagingPreflight"
PREFLIGHT_ROLE = f"projects/{PROJECT}/roles/{PREFLIGHT_ROLE_ID}"
DEPLOY_ROLE_ID = "observatoryStagingDeployer"
DEPLOY_ROLE = f"projects/{PROJECT}/roles/{DEPLOY_ROLE_ID}"
PREFLIGHT_PERMISSIONS = (
"artifactregistry.repositories.getIamPolicy",
"cloudsql.instances.get",
"cloudsql.users.list",
"compute.networks.get",
"compute.subnetworks.get",
"iam.serviceAccounts.get",
"iam.serviceAccounts.getIamPolicy",
"iam.roles.get",
"iam.workloadIdentityPoolProviders.get",
"resourcemanager.projects.get",
"resourcemanager.projects.getIamPolicy",
"run.operations.get",
"run.services.get",
"run.services.getIamPolicy",
"secretmanager.secrets.getIamPolicy",
"serviceusage.services.get",
"serviceusage.services.use",
)
DEPLOY_PERMISSIONS = (
"run.operations.get",
"run.services.create",
"run.services.get",
"run.services.setIamPolicy",
"run.services.update",
)
REQUIRED_APIS = (
"artifactregistry.googleapis.com",
"compute.googleapis.com",
"iamcredentials.googleapis.com",
"run.googleapis.com",
"secretmanager.googleapis.com",
"sqladmin.googleapis.com",
"sts.googleapis.com",
)
FORBIDDEN_DEPLOYER_ROLES = (
"roles/artifactregistry.writer",
"roles/cloudsql.admin",
"roles/compute.admin",
"roles/editor",
"roles/owner",
"roles/run.admin",
"roles/run.developer",
"roles/secretmanager.admin",
)
def shell_join(parts: list[str]) -> str:
return " ".join(shlex.quote(part) for part in parts)
def github_wif_member() -> str:
return (
f"principal://iam.googleapis.com/projects/{PROJECT_NUMBER}/locations/global/"
f"workloadIdentityPools/{WIF_POOL}/subject/repo:{GITHUB_REPOSITORY}:ref:refs/heads/main"
)
def service_account_member(account: str) -> str:
return f"serviceAccount:{account}"
def project_binding(member: str, role: str, condition: str | None = None) -> str:
command = [
"gcloud",
"projects",
"add-iam-policy-binding",
PROJECT,
"--member",
member,
"--role",
role,
]
if condition:
command.extend(["--condition", condition])
return shell_join(command)
def custom_role_commands(
role_id: str,
title: str,
description: str,
permissions: tuple[str, ...],
) -> list[str]:
common = [
"--project",
PROJECT,
"--title",
title,
"--description",
description,
"--permissions",
",".join(permissions),
"--stage",
"GA",
]
return [
(
f"gcloud iam roles describe {shlex.quote(role_id)} --project {shlex.quote(PROJECT)} "
"|| "
+ shell_join(["gcloud", "iam", "roles", "create", role_id, *common])
),
shell_join(["gcloud", "iam", "roles", "update", role_id, *common]),
]
def build_plan() -> dict[str, object]:
deployer_member = service_account_member(DEPLOY_SERVICE_ACCOUNT)
runtime_member = service_account_member(RUNTIME_SERVICE_ACCOUNT)
cloud_sql_condition = (
"expression=resource.name == "
f"'projects/{PROJECT}/instances/{INSTANCE}' && resource.service == 'sqladmin.googleapis.com',"
"title=observatory-exact-cloudsql-instance"
)
create_accounts = [
(
DEPLOY_SERVICE_ACCOUNT,
DEPLOY_SERVICE_ACCOUNT_ID,
"Observatory staging deploy and canary",
),
(
RUNTIME_SERVICE_ACCOUNT,
RUNTIME_SERVICE_ACCOUNT_ID,
"Observatory private Cloud SQL reader",
),
]
commands = [
shell_join(["gcloud", "projects", "describe", PROJECT, "--format=value(projectId)"]),
shell_join(["gcloud", "services", "enable", *REQUIRED_APIS, "--project", PROJECT]),
project_binding(
f"serviceAccount:service-{PROJECT_NUMBER}@serverless-robot-prod.iam.gserviceaccount.com",
"roles/run.serviceAgent",
),
]
for email, account_id, display_name in create_accounts:
commands.append(
f"gcloud iam service-accounts describe {shlex.quote(email)} --project {shlex.quote(PROJECT)} "
f"|| {shell_join(['gcloud', 'iam', 'service-accounts', 'create', account_id, '--project', PROJECT, '--display-name', display_name])}"
)
commands.extend(
[
(
"gcloud iam workload-identity-pools providers describe "
+ shlex.quote(DEPLOY_WIF_PROVIDER)
+ " --project "
+ shlex.quote(PROJECT)
+ " --location global --workload-identity-pool "
+ shlex.quote(WIF_POOL)
+ " || "
+ shell_join(
[
"gcloud",
"iam",
"workload-identity-pools",
"providers",
"create-oidc",
DEPLOY_WIF_PROVIDER,
"--project",
PROJECT,
"--location",
"global",
"--workload-identity-pool",
WIF_POOL,
"--issuer-uri",
"https://token.actions.githubusercontent.com/",
"--attribute-mapping",
DEPLOY_WIF_ATTRIBUTE_MAPPING,
"--attribute-condition",
DEPLOY_WIF_ATTRIBUTE_CONDITION,
]
)
),
shell_join(
[
"gcloud",
"iam",
"workload-identity-pools",
"providers",
"update-oidc",
DEPLOY_WIF_PROVIDER,
"--project",
PROJECT,
"--location",
"global",
"--workload-identity-pool",
WIF_POOL,
"--attribute-mapping",
DEPLOY_WIF_ATTRIBUTE_MAPPING,
"--attribute-condition",
DEPLOY_WIF_ATTRIBUTE_CONDITION,
]
),
shell_join(
[
"gcloud",
"iam",
"service-accounts",
"add-iam-policy-binding",
DEPLOY_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--member",
github_wif_member(),
"--role",
"roles/iam.workloadIdentityUser",
]
),
]
)
commands.extend(
custom_role_commands(
PREFLIGHT_ROLE_ID,
"Observatory staging preflight",
"Read only the exact resource classes required before the private staging deploy",
PREFLIGHT_PERMISSIONS,
)
)
commands.extend(
custom_role_commands(
DEPLOY_ROLE_ID,
"Observatory staging deployer",
"Create or update Cloud Run without delete or broad infrastructure permissions",
DEPLOY_PERMISSIONS,
)
)
commands.extend(
[
project_binding(deployer_member, PREFLIGHT_ROLE),
project_binding(deployer_member, DEPLOY_ROLE),
shell_join(
[
"gcloud",
"artifacts",
"repositories",
"add-iam-policy-binding",
REPOSITORY,
"--project",
PROJECT,
"--location",
REGION,
"--member",
deployer_member,
"--role",
"roles/artifactregistry.reader",
]
),
shell_join(
[
"gcloud",
"iam",
"service-accounts",
"add-iam-policy-binding",
RUNTIME_SERVICE_ACCOUNT,
"--project",
PROJECT,
"--member",
deployer_member,
"--role",
"roles/iam.serviceAccountUser",
]
),
project_binding(runtime_member, "roles/cloudsql.client", cloud_sql_condition),
project_binding(runtime_member, "roles/cloudsql.instanceUser", cloud_sql_condition),
f"gcloud secrets describe {shlex.quote(SECRET)} --project {shlex.quote(PROJECT)} "
f"|| {shell_join(['gcloud', 'secrets', 'create', SECRET, '--project', PROJECT, '--replication-policy', 'automatic'])}",
]
)
for member in (runtime_member, deployer_member):
commands.append(
shell_join(
[
"gcloud",
"secrets",
"add-iam-policy-binding",
SECRET,
"--project",
PROJECT,
"--member",
member,
"--role",
"roles/secretmanager.secretAccessor",
]
)
)
commands.extend(
[
shell_join(
[
"gcloud",
"secrets",
"add-iam-policy-binding",
SECRET,
"--project",
PROJECT,
"--member",
deployer_member,
"--role",
"roles/secretmanager.viewer",
]
),
(
"if ! gcloud secrets versions list "
+ shlex.quote(SECRET)
+ " --project "
+ shlex.quote(PROJECT)
+ " --filter='state=ENABLED' --limit=1 --format='value(name)' | grep -q .; then "
+ "openssl rand -hex 32 | tr -d '\\n' | gcloud secrets versions add "
+ shlex.quote(SECRET)
+ " --project "
+ shlex.quote(PROJECT)
+ " --data-file=-; fi"
),
(
"if ! gcloud sql users list --instance "
+ shlex.quote(INSTANCE)
+ " --project "
+ shlex.quote(PROJECT)
+ " --filter='type=CLOUD_IAM_SERVICE_ACCOUNT' --format='value(name)' | grep -Fxq "
+ shlex.quote(DB_IAM_USER)
+ "; then "
+ shell_join(
[
"gcloud",
"sql",
"users",
"create",
DB_IAM_USER,
"--instance",
INSTANCE,
"--project",
PROJECT,
"--type",
"CLOUD_IAM_SERVICE_ACCOUNT",
]
)
+ "; fi"
),
]
)
return {
"schema": "livingip.observatory-read-adapter-gcp-plan.v1",
"project": PROJECT,
"required_tier": "T3_live_readonly",
"current_tier": "T2_unexecuted_least_privilege_packet",
"claim_ceiling": "Plan only; no IAM, API, secret, Cloud SQL, Cloud Run, or routing mutation has run.",
"identities": {
"build": BUILD_SERVICE_ACCOUNT,
"deploy_and_canary": DEPLOY_SERVICE_ACCOUNT,
"runtime": RUNTIME_SERVICE_ACCOUNT,
"database_principal": DB_IAM_USER,
},
"kept_narrow": {
"artifact_builder": "image build and push only; never deploy or receive broad infra roles",
"deployer": "workflow-specific main-only WIF plus a five-permission Cloud Run role, repository read, exact runtime actAs, exact secret access",
"runtime": "exact Cloud SQL instance connect/login plus exact secret access",
},
"github_wif_provider": (
f"projects/{PROJECT_NUMBER}/locations/global/workloadIdentityPools/{WIF_POOL}/providers/{DEPLOY_WIF_PROVIDER}"
),
"github_wif_member": github_wif_member(),
"required_apis": list(REQUIRED_APIS),
"preflight_custom_role": {
"name": PREFLIGHT_ROLE,
"permissions": list(PREFLIGHT_PERMISSIONS),
},
"deploy_custom_role": {
"name": DEPLOY_ROLE,
"permissions": list(DEPLOY_PERMISSIONS),
},
"conditions": {
"cloud_sql": cloud_sql_condition,
"deployer_wif": DEPLOY_WIF_ATTRIBUTE_CONDITION,
},
"commands": commands,
"private_database_action": {
"location": "existing authenticated private GCP VM database-admin path",
"command": (
f"OBSERVATORY_DB_IAM_USER={DB_IAM_USER} psql --dbname=teleo_canonical "
"--set=ON_ERROR_STOP=1 --file=ops/observatory_read_role.sql"
),
"required_receipt": (
"database=teleo_canonical, authorization_role=kb_observatory_read, exact database principal, "
"required_reads=true, effective_table_writes_denied=true"
),
},
"cloud_run_service_agent_check": {
"principal": f"service-{PROJECT_NUMBER}@serverless-robot-prod.iam.gserviceaccount.com",
"required_role": "roles/run.serviceAgent",
"reason": "Default Cloud Run service-agent role carries Direct VPC permissions; do not grant them to the runtime identity.",
"empty_readback_action": "Stop. Repair the Google-managed service-agent grant as an administrator before preflight.",
},
"post_apply_checks": [
f"gcloud projects describe {PROJECT} --format=value(projectId)",
"python3 ops/check_gcp_infra_readiness.py",
"Require the adapter preflight to verify the dedicated WIF provider, custom roles, IAM bindings, private Cloud SQL, secret, and immutable image.",
(
"gh workflow run gcp-observatory-read-adapter.yml --repo "
f"{GITHUB_REPOSITORY} --ref main -f action=preflight_staging"
),
"Require a passing observatory-read-adapter-preflight artifact before action=deploy_staging.",
],
"forbidden_deployer_roles": list(FORBIDDEN_DEPLOYER_ROLES),
"forbidden_actions": [
"grant deploy or infra roles to sa-artifact-builder",
"reuse a pre-existing image tag instead of building the current workflow revision",
"add a public Cloud SQL address",
"expose a secret value in Git, logs, or a receipt",
"add a write route",
"change Vercel or production routing",
],
"production_repoint_executed": False,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--format", choices=("json", "shell"), default="json")
args = parser.parse_args()
plan = build_plan()
if args.format == "shell":
print("# Unexecuted staging packet. Review and run only as an authenticated teleo-501523 administrator.")
print("set -euo pipefail")
for command in plan["commands"]:
print(command)
else:
print(json.dumps(plan, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,29 @@
{
schema,
read_only,
provenance: {
gcp_project: .provenance.gcp_project,
cloud_sql_instance: .provenance.cloud_sql_instance,
database: .provenance.database,
database_principal: .provenance.database_principal,
authorization_role: .provenance.authorization_role,
transaction_read_only: .provenance.transaction_read_only,
write_privileges_denied: .provenance.write_privileges_denied,
service_revision: .provenance.service_revision
},
canonical: {
claim: {
id: .canonical.claim.id,
type: .canonical.claim.type,
status: .canonical.claim.status
},
evidence_count: (.canonical.evidence | length),
incoming_edge_count: ([.canonical.edges[] | select(.direction == "incoming")] | length),
outgoing_edge_count: ([.canonical.edges[] | select(.direction == "outgoing")] | length)
},
proposal_ledger: {
distinct_from_canonical: .proposal_ledger.distinct_from_canonical,
status_counts: .proposal_ledger.status_counts,
related_count: (.proposal_ledger.related | length)
}
}

View file

@ -0,0 +1,383 @@
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
import yaml
from ops import check_observatory_read_adapter_gcp_preflight as preflight
ROOT = Path(__file__).resolve().parents[1]
def run_plan(*args: str) -> str:
return subprocess.check_output(
["python3", "ops/plan_observatory_read_adapter_gcp.py", *args],
cwd=ROOT,
text=True,
)
def test_plan_splits_build_deploy_and_runtime_identities() -> None:
plan = json.loads(run_plan())
assert plan["current_tier"] == "T2_unexecuted_least_privilege_packet"
assert plan["identities"] == {
"build": "sa-artifact-builder@teleo-501523.iam.gserviceaccount.com",
"deploy_and_canary": "sa-observatory-deployer@teleo-501523.iam.gserviceaccount.com",
"runtime": "sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com",
"database_principal": "sa-observatory-read-adapter@teleo-501523.iam",
}
commands = "\n".join(plan["commands"])
assert "roles/iam.workloadIdentityUser" in commands
assert "roles/iam.serviceAccountUser" in commands
assert "roles/cloudsql.client" in commands
assert "roles/cloudsql.instanceUser" in commands
assert "roles/artifactregistry.reader" in commands
assert "roles/secretmanager.secretAccessor" in commands
assert "subject/repo:living-ip/teleo-infrastructure:ref:refs/heads/main" in plan["github_wif_member"]
assert plan["conditions"]["deployer_wif"].endswith(
"assertion.workflow_ref=='living-ip/teleo-infrastructure/.github/workflows/"
"gcp-observatory-read-adapter.yml@refs/heads/main'"
)
assert plan["github_wif_provider"].endswith("/providers/observatory-read-adapter-main")
assert "roles/run.serviceAgent" in commands
assert "teleo-pgvector-standby" in plan["conditions"]["cloud_sql"]
assert set(plan["deploy_custom_role"]["permissions"]) == {
"run.operations.get",
"run.services.create",
"run.services.get",
"run.services.setIamPolicy",
"run.services.update",
}
assert "roles/run.admin" not in commands
assert "roles/run.developer" not in commands
assert "sa-artifact-builder" not in commands
assert not set(plan["forbidden_deployer_roles"]) & set(commands.split())
assert plan["production_repoint_executed"] is False
def test_shell_packet_is_unexecuted_and_preserves_private_boundaries() -> None:
shell = run_plan("--format", "shell")
assert shell.startswith("# Unexecuted staging packet")
assert "set -euo pipefail" in shell
assert "run.googleapis.com" in shell
assert "--type CLOUD_IAM_SERVICE_ACCOUNT" in shell
assert "openssl rand -hex 32 | tr -d '\\n'" in shell
assert "--data-file=-" in shell
assert "grep -Fxq sa-observatory-read-adapter@teleo-501523.iam" in shell
assert "users create sa-observatory-read-adapter@teleo-501523.iam" in shell
assert "add-iam-policy-binding teleo" in shell
assert "roles/artifactregistry.writer" not in shell
assert "roles/cloudsql.admin" not in shell
assert "Vercel" not in shell
def test_workflow_requires_dedicated_preflight_before_staging_deploy() -> None:
workflow_path = ROOT / ".github" / "workflows" / "gcp-observatory-read-adapter.yml"
workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
text = workflow_path.read_text(encoding="utf-8")
assert workflow["env"]["BUILD_SERVICE_ACCOUNT"].startswith("sa-artifact-builder@")
assert workflow["env"]["DEPLOY_SERVICE_ACCOUNT"].startswith("sa-observatory-deployer@")
assert workflow["env"]["DEPLOY_WORKLOAD_IDENTITY_PROVIDER"].endswith(
"/providers/observatory-read-adapter-main"
)
assert workflow["jobs"]["deploy-staging"]["needs"] == ["build", "preflight-staging"]
assert "action=preflight_staging" not in text
assert "inputs.action != 'build_only'" in text
assert "EXPECTED_WORKFLOW_REF" in text
assert "${GITHUB_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" in text
assert "docker images describe" in text
assert "2>/dev/null || true" not in text
assert "--no-invoker-iam-check" in text
assert "--allow-unauthenticated" not in text
assert "API_KEY_VERSION" in text
assert "EXPECTED_IMAGE_REF" in text
assert "check_observatory_read_adapter_gcp_preflight.py" in text
assert 'service_account: ${{ env.BUILD_SERVICE_ACCOUNT }}' in text
assert text.count('service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }}') == 2
assert "positive-redacted.json" in text
assert "redact_observatory_read_adapter_receipt.jq" in text
assert "database_principal == $db_iam_user" in text
assert "canonical_state_unchanged_after_post: true" in text
assert "cmp <(jq -S . positive.json) <(jq -S . after-write-attempt.json)" in text
assert "rm -f positive.json" in text
def test_preflight_private_cloud_sql_and_secret_validators() -> None:
instance = {
"name": "teleo-pgvector-standby",
"region": "europe-west6",
"databaseVersion": "POSTGRES_16",
"settings": {
"databaseFlags": [{"name": "cloudsql.iam_authentication", "value": "on"}],
"ipConfiguration": {
"ipv4Enabled": False,
"privateNetwork": "projects/teleo-501523/global/networks/teleo-staging-net",
},
},
"ipAddresses": [{"type": "PRIVATE", "ipAddress": "10.61.0.3"}],
}
assert preflight.cloud_sql_is_private_and_iam_enabled(json.dumps(instance)) is True
instance["settings"]["ipConfiguration"]["ipv4Enabled"] = True
instance["ipAddresses"].append({"type": "PRIMARY", "ipAddress": "203.0.113.1"})
assert preflight.cloud_sql_is_private_and_iam_enabled(json.dumps(instance)) is False
assert preflight.cloud_sql_iam_user_exists(
json.dumps(
[
{
"name": "sa-observatory-read-adapter@teleo-501523.iam",
"type": "CLOUD_IAM_SERVICE_ACCOUNT",
}
]
)
)
assert preflight.subnet_is_exact(
json.dumps(
{
"name": "teleo-staging-europe-west6",
"region": "https://www.googleapis.com/compute/v1/projects/teleo-501523/regions/europe-west6",
"network": "https://www.googleapis.com/compute/v1/projects/teleo-501523/global/networks/teleo-staging-net",
}
)
)
assert preflight.secret_is_valid("a" * 32) is True
assert preflight.secret_is_valid("too-short") is False
assert preflight.secret_is_valid("a" * 32 + "\n") is False
assert preflight.secret_is_valid(" " + "a" * 32) is False
assert "gha-creds" not in preflight.sanitize_error("/tmp/work/gha-creds-deadbeef.json denied")
def test_preflight_end_to_end_snapshot_retains_no_secret(monkeypatch) -> None:
instance = {
"name": "teleo-pgvector-standby",
"region": "europe-west6",
"databaseVersion": "POSTGRES_16",
"settings": {
"databaseFlags": [{"name": "cloudsql.iam_authentication", "value": "on"}],
"ipConfiguration": {
"ipv4Enabled": False,
"privateNetwork": "projects/teleo-501523/global/networks/teleo-staging-net",
},
},
"ipAddresses": [{"type": "PRIVATE", "ipAddress": "10.61.0.3"}],
}
secret_value = "never-retain-this-secret-value-000000000000"
deployer = "serviceAccount:sa-observatory-deployer@teleo-501523.iam.gserviceaccount.com"
runtime = "serviceAccount:sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com"
project_policy = {
"bindings": [
{"role": preflight.PREFLIGHT_ROLE, "members": [deployer]},
{"role": preflight.DEPLOY_ROLE, "members": [deployer]},
{
"role": "roles/cloudsql.client",
"members": [runtime],
"condition": {
"title": "observatory-exact-cloudsql-instance",
"expression": (
"resource.name == 'projects/teleo-501523/instances/teleo-pgvector-standby' "
"&& resource.service == 'sqladmin.googleapis.com'"
),
},
},
{
"role": "roles/cloudsql.instanceUser",
"members": [runtime],
"condition": {
"title": "observatory-exact-cloudsql-instance",
"expression": (
"resource.name == 'projects/teleo-501523/instances/teleo-pgvector-standby' "
"&& resource.service == 'sqladmin.googleapis.com'"
),
},
},
{
"role": "roles/run.serviceAgent",
"members": [
"serviceAccount:service-785938879453@serverless-robot-prod.iam.gserviceaccount.com"
],
},
]
}
deployer_policy = {
"bindings": [
{"role": "roles/iam.workloadIdentityUser", "members": [preflight.DEPLOY_WIF_MEMBER]}
]
}
runtime_policy = {
"bindings": [{"role": "roles/iam.serviceAccountUser", "members": [deployer]}]
}
artifact_policy = {
"bindings": [{"role": "roles/artifactregistry.reader", "members": [deployer]}]
}
secret_policy = {
"bindings": [
{"role": "roles/secretmanager.secretAccessor", "members": [deployer, runtime]},
{"role": "roles/secretmanager.viewer", "members": [deployer]},
]
}
provider = {
"state": "ACTIVE",
"attributeCondition": preflight.DEPLOY_WIF_ATTRIBUTE_CONDITION,
"attributeMapping": {
"google.subject": "assertion.sub",
"attribute.repository": "assertion.repository",
"attribute.ref": "assertion.ref",
"attribute.workflow_ref": "assertion.workflow_ref",
},
}
def fake_run(command: list[str]) -> subprocess.CompletedProcess[str]:
joined = " ".join(command)
stdout = ""
stderr = ""
returncode = 0
if "auth list" in joined:
stdout = "sa-observatory-deployer@teleo-501523.iam.gserviceaccount.com\n"
elif "projects describe" in joined:
stdout = "teleo-501523\n"
elif command[:3] == ["gcloud", "services", "describe"]:
stdout = "ENABLED\n"
elif "workload-identity-pools providers describe" in joined:
stdout = json.dumps(provider)
elif "iam roles describe observatoryStagingPreflight" in joined:
stdout = json.dumps({"includedPermissions": sorted(preflight.PREFLIGHT_PERMISSIONS)})
elif "iam roles describe observatoryStagingDeployer" in joined:
stdout = json.dumps({"includedPermissions": sorted(preflight.DEPLOY_PERMISSIONS)})
elif "projects get-iam-policy" in joined:
stdout = json.dumps(project_policy)
elif f"service-accounts get-iam-policy {preflight.DEPLOY_SERVICE_ACCOUNT}" in joined:
stdout = json.dumps(deployer_policy)
elif f"service-accounts get-iam-policy {preflight.RUNTIME_SERVICE_ACCOUNT}" in joined:
stdout = json.dumps(runtime_policy)
elif "artifacts repositories get-iam-policy" in joined:
stdout = json.dumps(artifact_policy)
elif "secrets get-iam-policy" in joined:
stdout = json.dumps(secret_policy)
elif "run services describe" in joined:
returncode = 1
stderr = "NOT_FOUND: service not found"
elif "service-accounts describe" in joined:
stdout = "sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com\n"
elif "networks subnets describe" in joined:
stdout = json.dumps(
{
"name": "teleo-staging-europe-west6",
"region": "https://www.googleapis.com/compute/v1/projects/teleo-501523/regions/europe-west6",
"network": "https://www.googleapis.com/compute/v1/projects/teleo-501523/global/networks/teleo-staging-net",
}
)
elif "networks describe" in joined:
stdout = "teleo-staging-net\n"
elif "sql instances describe" in joined:
stdout = json.dumps(instance)
elif "sql users list" in joined:
stdout = json.dumps(
[
{
"name": "sa-observatory-read-adapter@teleo-501523.iam",
"type": "CLOUD_IAM_SERVICE_ACCOUNT",
}
]
)
elif "secrets versions list" in joined:
stdout = "7\n"
elif "secrets versions access" in joined:
stdout = secret_value
elif "artifacts docker images describe" in joined:
stdout = "{}"
return subprocess.CompletedProcess(command, returncode, stdout=stdout, stderr=stderr)
monkeypatch.setattr(preflight, "run", fake_run)
image_ref = (
"europe-west6-docker.pkg.dev/teleo-501523/teleo/observatory-read-adapter:"
"33399bd9acfa09e4e5409873e86c8c46ac694a8d-29389090997-1@sha256:" + "a" * 64
)
checks = preflight.build_checks(image_ref)
assert checks
assert {check.status for check in checks} == {"pass"}
assert secret_value not in json.dumps([check.__dict__ for check in checks])
def test_malformed_successful_readback_becomes_blocked_check(monkeypatch) -> None:
monkeypatch.setattr(
preflight,
"run",
lambda command: subprocess.CompletedProcess(command, 0, stdout="[]", stderr=""),
)
check = preflight.command_check(
"malformed",
["gcloud", "sql", "instances", "describe"],
preflight.cloud_sql_is_private_and_iam_enabled,
"unexpected",
)
assert check.status == "blocked"
assert check.detail == "invalid_readback:AttributeError"
@pytest.mark.skipif(shutil.which("jq") is None, reason="jq is required")
def test_receipt_redaction_is_an_explicit_allowlist() -> None:
payload = {
"schema": "livingip.observatory-canonical-claim.v1",
"read_only": True,
"provenance": {
"gcp_project": "teleo-501523",
"cloud_sql_instance": "teleo-501523:europe-west6:teleo-pgvector-standby",
"database": "teleo_canonical",
"database_principal": "sa-observatory-read-adapter@teleo-501523.iam",
"authorization_role": "kb_observatory_read",
"transaction_read_only": True,
"write_privileges_denied": True,
"service_revision": "a" * 40,
"future_sensitive_field": "must-not-survive",
},
"canonical": {
"claim": {"id": "claim-1", "type": "fact", "status": "live", "text": "secret claim text"},
"evidence": [{"excerpt": "secret excerpt", "source_url": "https://private.invalid"}],
"edges": [{"direction": "incoming"}, {"direction": "outgoing"}],
},
"proposal_ledger": {
"distinct_from_canonical": True,
"status_counts": {"approved": 1},
"related": [{"proposal_text": "secret proposal"}],
},
}
result = subprocess.run(
["jq", "-f", str(ROOT / "ops" / "redact_observatory_read_adapter_receipt.jq")],
input=json.dumps(payload),
text=True,
capture_output=True,
check=True,
)
redacted = json.loads(result.stdout)
rendered = json.dumps(redacted)
assert set(redacted["provenance"]) == {
"gcp_project",
"cloud_sql_instance",
"database",
"database_principal",
"authorization_role",
"transaction_read_only",
"write_privileges_denied",
"service_revision",
}
assert redacted["canonical"]["evidence_count"] == 1
assert redacted["proposal_ledger"]["related_count"] == 1
for sensitive in (
"must-not-survive",
"secret claim text",
"secret excerpt",
"https://private.invalid",
"secret proposal",
):
assert sensitive not in rendered

View file

@ -44,6 +44,7 @@ def run(command: list[str], *, input_text: str | None = None, check: bool = True
@pytest.mark.skipif(shutil.which("docker") is None, reason="Docker is required") @pytest.mark.skipif(shutil.which("docker") is None, reason="Docker is required")
def test_observatory_role_is_read_only_and_exactly_allowlisted() -> None: def test_observatory_role_is_read_only_and_exactly_allowlisted() -> None:
container = f"teleo-observatory-role-{uuid.uuid4().hex[:10]}" container = f"teleo-observatory-role-{uuid.uuid4().hex[:10]}"
try:
run( run(
[ [
"docker", "docker",
@ -59,7 +60,6 @@ def test_observatory_role_is_read_only_and_exactly_allowlisted() -> None:
POSTGRES_IMAGE, POSTGRES_IMAGE,
] ]
) )
try:
deadline = time.monotonic() + 25 deadline = time.monotonic() + 25
while time.monotonic() < deadline: while time.monotonic() < deadline:
ready = run( ready = run(