A
?
← Handbook

SOP — GCP service account for Drive access

How the portal's Google credential was created, including the trap. Audience: whoever has to redo, rotate, or revoke this — probably in a year, probably in a hurry.

What it is

Service account atlas-portal-reader in GCP project atlas-portal (org atlasmedia.sg), with domain-wide delegation limited to one scope:

https://www.googleapis.com/auth/drive.readonly

Read-only across the org by construction. Writing into the "Atlas Clients" Shared Drive comes from membership (Contributor role — can add, cannot delete), never from scope. See meeting-notes-pipeline.md.

Creation steps

  1. console.cloud.google.com → project picker → New project → atlas-portal, org atlasmedia.sg. Do not create anything in the old Gemini side-projects.
  2. Search → Google Drive API → Enable.
  3. ☰ → IAM & Admin → Service Accounts → Create: atlas-portal-reader. Skip the optional role screens — it needs no GCP-side roles at all.
  4. The trap: Keys → Add key → JSON will fail with *"Service account key creation is disabled"* — Google enforces iam.disableServiceAccountKeyCreation org-wide by default now. The fix, scoped to this one project so the org-wide protection stays: - Project picker → the organisation → IAM → Grant access → yourself → Organization Policy Administrator → Save. - Picker back to atlas-portal → Organisation policies → filter "service account key" → *Disable service account key creation* → Manage policy → Override parent's policy → Not enforced → Set policy. Wait a minute. - Red herring inside the trap: the console lands you on the *managed* copy of the constraint, which can already read "Not enforced" while keys still fail. The legacy constraint is the one that bites — use the "View legacy constraint" banner button and override THAT one.
  5. Keys → Add key → JSON — downloads now. This file is the credential.
  6. Details tab → copy the Unique ID → admin.google.com → Security → Access and data control → API controls → Domain-wide delegation → Add new → that ID + the scope above → Authorise.
  7. Hand the JSON to the portal (set-secret.sh GOOGLE_SERVICE_ACCOUNT_JSON, value on stdin).

Why a key at all

Google's preferred alternatives (workload identity federation) assume the workload runs in a cloud with an identity provider. The portal runs on a DigitalOcean droplet, which has none, so a key is the correct tool. The mitigations: single read-only scope, the org policy override scoped to one project, and the key rotatable/revocable from the Keys tab in one click.

Where the credential lives

  • GOOGLE_SA_KEY_B64 (the JSON, base64) in /srv/atlas-portal/.env and /srv/atlas-portal-dev/.env on the droplet — set via set-secret.sh, value on stdin.
  • Original JSON: whoever created it downloaded atlas-portal-506101-<key-id>.json; the droplet copies are canonical. Losing every copy is fine — revoke the key and mint a new one, nothing else changes.
  • Delegation client ID 102496074590313147627, authorised in admin.google.com → Security → API controls → Domain-wide delegation, scope drive.readonly only.
  • unauthorized_client on the token exchange means the delegation grant is missing or not yet propagated — it is an Admin-console step, not a GCP one, and the two are easy to conflate.

Rotation / revocation

Revoke: Service Accounts → atlas-portal-reader → Keys → bin icon. The portal loses Drive access and nothing else breaks. Rotate: new JSON key → set-secret.sh again → delete the old key.