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
- console.cloud.google.com → project picker → New project →
atlas-portal, orgatlasmedia.sg. Do not create anything in the old Gemini side-projects. - Search → Google Drive API → Enable.
- ☰ → IAM & Admin → Service Accounts → Create:
atlas-portal-reader. Skip the optional role screens — it needs no GCP-side roles at all. - The trap: Keys → Add key → JSON will fail with *"Service account key creation is disabled"* — Google enforces
iam.disableServiceAccountKeyCreationorg-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. - Keys → Add key → JSON — downloads now. This file is the credential.
- 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.
- 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/.envand/srv/atlas-portal-dev/.envon the droplet — set viaset-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, scopedrive.readonlyonly. unauthorized_clienton 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.