SHART.CLOUD | DESKTOP | LABROULETTE | GCP-LEAST-PRIVILEGE-BIGQUERY.YAML
01 - Labroulette - Least-Privilege Service Accounts for a BigQuery Pipeline
| shart.cloud / labroulette / gcp-least-privilege-bigquery |
GCP *** ~120 min $1-5 reviewed 6/29/2026

Least-Privilege Service Accounts for a BigQuery Pipeline

Re-cut the ETL pipeline so each stage runs as its own service account with only the permissions that stage needs — read this bucket, write that dataset, nothing more — and prove a stolen key from one stage can't touch the rest.

#iam#data#security
Lab Hoarderly, an analytics startup that wires every job to one "editor-on-everything" service account because it was faster, until a leaked key turned that single account into the keys to the entire data warehouse. all labs
02 - Actions
score -- - -- votes
-- completed
State loading
03 - Scenario

Hoarderly, an analytics startup that wires every job to one "editor-on-everything" service account because it was faster, until a leaked key turned that single account into the keys to the entire data warehouse.

Re-cut the ETL pipeline so each stage runs as its own service account with only the permissions that stage needs — read this bucket, write that dataset, nothing more — and prove a stolen key from one stage can't touch the rest.

Constraints

  • No primitive roles (roles/editor, roles/owner) anywhere in the pipeline
  • Each pipeline stage gets a distinct service account scoped to its job
  • Dataset and bucket access granted at the resource level, not the project
  • Everything in Terraform, with a written blast-radius analysis per account
Scenario GCP - advanced
05 - Steps
  1. STEP_01

    One service account per stage

    Create a distinct service account for each pipeline stage (ingest, transform, publish). Name them for their job so an audit log entry tells you exactly which stage acted.

    Terraform docs

  2. STEP_02

    Read access where reads happen

    Grant the ingest account object-read on only its source bucket, scoped to that bucket rather than the project. The transform and publish accounts get nothing here.

    Hint: Resource-level bindings beat project-level every time — a project-wide storage.objectViewer can read every bucket you'll ever create, forever.

    Terraform docs

  3. STEP_03

    Write access where writes happen

    Give the transform account dataEditor on the staging dataset and the publish account dataEditor on the published dataset — each on its own dataset, not the project. Reads and writes never overlap.

    Terraform docs

  4. STEP_05

    Prove the blast radius

    For each service account, write down what a stolen key could reach and what it could not. Confirm by impersonating each account and attempting an action it should be denied — the deny is the deliverable.

    Terraform docs

Steps 5 tasks
06 - Deliverables
  • A Terraform module with three service accounts and resource-scoped bindings
  • A blast-radius table — per account, what it can and cannot touch
  • Output from a denied cross-stage action proving isolation holds
Deliverables 3 required
07 - Rubric
No primitive roles used; every grant is a predefined or custom role 25%
Each stage runs as its own account with access scoped to its resources 30%
A custom role is defined where predefined roles were too broad 20%
Blast-radius analysis is documented and a cross-stage denial is demonstrated 25%
Rubric self-assessed