observability: Google Play analytics through WIF (read-only play-reports service account, no JSON key) #174

Merged
founder merged 1 commit from claude/kse-0191-play-reports-wif into main 2026-08-22 08:15:25 +00:00
Owner

KSE-0191 follow-up. Google revoked the release@key-d0t-store service-account key on 2026-08-17 (the publish lane already moved to WIF in publish-store-uploads.yml), so store-analytics has been failing its Play leg with invalid_grant: Invalid JWT Signature. Rather than mint a new long-lived key:

  • infra/identity/play-reports.tf: read-only play-reports@key-d0t-store.iam.gserviceaccount.com (no project roles — Play Console attaches permissions to the e-mail), an authoritative roles/iam.workloadIdentityUser binding for the forge-host WIF principal only, playdeveloperreporting.googleapis.com enabled (disable_on_destroy=false). Output google_play_reports_service_account_email. Gated on manage_google_play_reports_service_account (default true).
  • lib/common.cjs googleWorkloadIdentityToken(): external_account file → STS token exchange → generateAccessToken on the target SA.
  • store-analytics.cjs googlePlayCredential(): WIF first (GOOGLE_APPLICATION_CREDENTIALS + GOOGLE_PLAY_IMPERSONATE_SERVICE_ACCOUNT), JSON key only for local use; key_store_store_auth_mode{provider="google-play",mode}.
  • forge: the unit gets /run/google-wif/credentials.json + the target SA and orders after google-wif-credentials; the revoked key is no longer handed to it.

Validation: tofu validate (identity) ok; node --self-test ok; forge toplevel builds (…-26.05.20260822.93478b31, unit env/ordering verified); live on the forge with the real forge-host credential: STS exchange ok, impersonation 404 (SA does not exist yet — expected). cloud-tofu only runs on main, so the identity plan appears after merge (expected: 3 to add).

After merge (founder): 1) cloud-tofu stack identity planapply; 2) Play Console → Users and permissions → invite play-reports@key-d0t-store.iam.gserviceaccount.comstore.key.application → "View app information and download bulk reports" (read-only); 3) deploy-forge-host (switch) — also ships bcbe0659 (ClickHouse readonly=2) already on main; 4) systemctl start store-analytics on the forge or wait for the timer; key_store_store_auth_mode{mode="wif"} = 1 and key_store_store_config_missing{item="play_reports_bucket_access"} disappears once the invite is accepted. STORE_ANALYTICS_PLAY_REPORTS_BUCKET (pubsite_prod_…) still needs to go into /etc/key-store/store-analytics.env for the bulk-report CSVs; the vitals (crash/ANR) leg works without it.

🤖 Generated with Claude Code

KSE-0191 follow-up. Google revoked the `release@key-d0t-store` service-account key on 2026-08-17 (the publish lane already moved to WIF in `publish-store-uploads.yml`), so `store-analytics` has been failing its Play leg with `invalid_grant: Invalid JWT Signature`. Rather than mint a new long-lived key: - **`infra/identity/play-reports.tf`**: read-only `play-reports@key-d0t-store.iam.gserviceaccount.com` (no project roles — Play Console attaches permissions to the e-mail), an *authoritative* `roles/iam.workloadIdentityUser` binding for the forge-host WIF principal only, `playdeveloperreporting.googleapis.com` enabled (`disable_on_destroy=false`). Output `google_play_reports_service_account_email`. Gated on `manage_google_play_reports_service_account` (default true). - **`lib/common.cjs` `googleWorkloadIdentityToken()`**: external_account file → STS token exchange → `generateAccessToken` on the target SA. - **`store-analytics.cjs` `googlePlayCredential()`**: WIF first (`GOOGLE_APPLICATION_CREDENTIALS` + `GOOGLE_PLAY_IMPERSONATE_SERVICE_ACCOUNT`), JSON key only for local use; `key_store_store_auth_mode{provider="google-play",mode}`. - **forge**: the unit gets `/run/google-wif/credentials.json` + the target SA and orders after `google-wif-credentials`; the revoked key is no longer handed to it. Validation: `tofu validate` (identity) ok; `node --self-test` ok; forge toplevel builds (`…-26.05.20260822.93478b31`, unit env/ordering verified); live on the forge with the real forge-host credential: STS exchange **ok**, impersonation **404** (SA does not exist yet — expected). `cloud-tofu` only runs on `main`, so the identity plan appears after merge (expected: 3 to add). After merge (founder): 1) `cloud-tofu` stack `identity` `plan` → `apply`; 2) Play Console → Users and permissions → invite `play-reports@key-d0t-store.iam.gserviceaccount.com` → `store.key.application` → "View app information and download bulk reports" (read-only); 3) `deploy-forge-host` (switch) — also ships bcbe0659 (ClickHouse readonly=2) already on main; 4) `systemctl start store-analytics` on the forge or wait for the timer; `key_store_store_auth_mode{mode="wif"}` = 1 and `key_store_store_config_missing{item="play_reports_bucket_access"}` disappears once the invite is accepted. `STORE_ANALYTICS_PLAY_REPORTS_BUCKET` (`pubsite_prod_…`) still needs to go into `/etc/key-store/store-analytics.env` for the bulk-report CSVs; the vitals (crash/ANR) leg works without it. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Google revoked the release@key-d0t-store service-account key on 2026-08-17
(the publish lane already moved to WIF), so store-analytics could not reach
Play Console reports. Rather than mint a new long-lived key:

- infra/identity/play-reports.tf: a read-only play-reports service account
  (no project roles; Play Console attaches its permissions to the e-mail)
  with an authoritative roles/iam.workloadIdentityUser binding for the
  forge-host WIF principal, plus playdeveloperreporting.googleapis.com.
  Output google_play_reports_service_account_email. Founder step: invite
  that e-mail in Play Console with "View app information and download bulk
  reports".
- lib/common.cjs googleWorkloadIdentityToken(): external_account file →
  STS token exchange → generateAccessToken on the target SA.
- store-analytics.cjs googlePlayCredential(): WIF first
  (GOOGLE_APPLICATION_CREDENTIALS + GOOGLE_PLAY_IMPERSONATE_SERVICE_ACCOUNT),
  JSON key only for local use; key_store_store_auth_mode{mode} gauge.
- forge: the unit gets the WIF credential + target SA and orders after
  google-wif-credentials; the revoked key is no longer handed to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
founder force-pushed claude/kse-0191-play-reports-wif from 93478b3121 to 9a493e3ed4
Some checks failed
Deploy: API / Deploy (API) (push) Failing after 4s
Namespace: Build Images / Refresh macOS runner lane (push) Successful in 4s
Release: If Needed / Check (Release Needed) (push) Successful in 6s
Namespace: Build Images / Refresh macOS Nix snapshot application (push) Failing after 31s
Infra: Cloud OpenTofu / Infra (Cloud OpenTofu) (push) Successful in 57s
Namespace: Build Images / Refresh Linux runner image (push) Successful in 1m8s
Deploy: Web / Deploy (Web) (push) Successful in 2m33s
2026-08-22 08:15:20 +00:00
Compare
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
key-store/key.store!174
No description provided.