Postgres on Google Cloud / Lesson 6 of 8

Giving a workload an identity

Your laptop has a Google login. A server does not. This is the ladder — and you climb down it only as far as you must.

≈ 18 minutes · decisions and commands · the security-sensitive lesson

Everything in lesson 5 assumed the application already had a Google identity. On your laptop it does, because you logged in. A server cannot open a browser.

Google's own guidance is a ranked list, and the ranking is the lesson. Take the highest rung that applies to you.

1
Attached service account — the workload runs on Google Cloud
Compute Engine, GKE, Cloud Run, Cloud Functions. You attach a service account to the resource and the credential arrives through the metadata server. No key exists at all. Nothing to rotate, nothing to leak.
2
Workload identity federation — the workload runs elsewhere
AWS, Azure, GitHub Actions, any Kubernetes with an OIDC issuer. The platform's own identity token is exchanged for a short-lived Google token. Still no key.
3
Impersonation — a human acting as a service account
For testing what an application can actually do. You keep your own login and borrow the service account's permissions.
4
A service account key — last resort
A JSON file containing a private key. Long-lived, portable, and a bearer credential. Only when nothing above applies.

Rung 1 — attached service accounts

If your workload runs on Google Cloud, you are done in two commands and there is no credential to manage:

# create the identity
gcloud iam service-accounts create app-db \
  --display-name="Application database access"

# attach it to a VM at creation time
gcloud compute instances create my-vm \
  --service-account=app-db@PROJECT_ID.iam.gserviceaccount.com \
  --scopes=https://www.googleapis.com/auth/cloud-platform

Then grant that service account the two roles from layer 1, add it as an IAM database user for layer 2, and grant it in PostgreSQL for layer 3. The application code from lesson 5 does not change by one character — Connector() finds the identity through the metadata server.

On GKE the equivalent is GKE Workload Identity, which maps a Kubernetes service account to a Google one. Same property: no key.

Rung 2 — workload identity federation

Your workload runs on another cloud, or in CI. If that platform issues OIDC identity tokens — AWS, Azure, GitHub Actions, most managed Kubernetes — you can trade them for Google tokens without a key ever existing.

gcloud iam workload-identity-pools create my-pool \
  --location=global --display-name="External workloads"

gcloud iam workload-identity-pools providers create-oidc my-provider \
  --location=global --workload-identity-pool=my-pool \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub"

Then allow the external identity to impersonate a service account, and the same connector code works.

The question that decides rung 2 versus rung 4

Does the platform expose an OIDC issuer? That is the whole test. Managed Kubernetes services generally do. Plain virtual machines from a hosting provider generally do not. Ask before you assume — teams have created keys for workloads that could have been federated, because nobody checked.

Rung 3 — impersonation, for testing

The most useful thing in this lesson for day-to-day work. It lets you connect as your application and find out what it can really do:

cloud-sql-proxy --port 5434 --auto-iam-authn \
  --impersonate-service-account=app-db@PROJECT_ID.iam.gserviceaccount.com \
  PROJECT_ID:REGION:INSTANCE_NAME

psql -h 127.0.0.1 -p 5434 -U "app-db@PROJECT_ID.iam" -d appdb

Note the username drops .gserviceaccount.com, exactly as in layer 2.

Owner is not enough, and that is deliberate

Impersonation needs roles/iam.serviceAccountTokenCreator granted on the service account itself. Even a project owner is refused without it. Grant it on the one service account, never at project level — a project-level binding covers every service account that exists now or is created later, for any reason.

gcloud iam service-accounts add-iam-policy-binding \
  app-db@PROJECT_ID.iam.gserviceaccount.com \
  --member="group:db-users@example.com" \
  --role="roles/iam.serviceAccountTokenCreator"

Be aware of what you are doing: statements you run this way are attributed to the application in the audit log, not to you. That is a reasonable trade for a test on a non-production environment and a poor one for production, where it routes around whatever break-glass process exists.

Rung 4 — a key, when there is genuinely no alternative

Google is blunt about why this is last. From the best-practices guide: anyone who possesses a service account key can use it, and there is no reliable way to tell who used the key.

A key is a file. Files get copied into images, snapshots, backups, chat messages and support tickets. It does not expire. Nothing tells you when it is used by the wrong person.

You may also simply not be allowed to create one. The organization policy constraint iam.disableServiceAccountKeyCreation blocks it, and Google notes that for organizations created on or after 3 May 2024 these constraints are enforced by default. Lifting it is an organization-level decision, not a project-level one.

If you must, these six rules make it survivable

Prefer environment over filesystem

Most connectors accept an explicit credentials object, so the key can be parsed from an environment variable and never written to disk at all:

import json, os
from google.oauth2 import service_account
from google.cloud.sql.connector import Connector

credentials = service_account.Credentials.from_service_account_info(
    json.loads(os.environ["GCP_SA_KEY"]),
    scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
connector = Connector(credentials=credentials)
# getconn() and the engine are exactly as in lesson 5

Nothing in /etc, nothing in a snapshot, nothing to forget to delete. If a tool demands an actual file path — psql, pg_dump, third-party binaries — write it to tmpfs (/dev/shm) with mode 600 so it dies with the machine, and remove it when the task ends.

This trades one long-lived secret for another

Fetching the key from a secrets manager at runtime means the host now holds a secrets-manager token instead of a GCP key. That is a genuine improvement — it is centrally revocable, scoped to one config, and rotation becomes one update instead of a visit to every host — but the long-lived credential did not disappear. It moved. Scope that token read-only, to one config, and treat it as the thing an attacker wants.

Rotate on a calendar

# 1. create the new key first — never leave a gap with no working credential
gcloud iam service-accounts keys create new.json \
  --iam-account=app-db@PROJECT_ID.iam.gserviceaccount.com

# 2. update the secret, restart the service, verify it works

# 3. only then list and delete the old key, by ID
gcloud iam service-accounts keys list \
  --iam-account=app-db@PROJECT_ID.iam.gserviceaccount.com \
  --managed-by=user

gcloud iam service-accounts keys delete KEY_ID \
  --iam-account=app-db@PROJECT_ID.iam.gserviceaccount.com

--managed-by=user matters twice. Every service account also carries system-managed keys, which are Google's own signing keys, are not downloadable, and are not yours to touch. And after a rotation, this command returning more than one row is the check that catches the step everyone skips — an undeleted old key is a working credential nobody is watching.

A zero here proves less than you think

Listing zero user-managed keys tells you none has been created. It says nothing about whether one can be — an organization policy may forbid it. An inventory is not a capability test. If your plan depends on creating a key on cutover day, create one and delete it again in advance.

Check yourself

Your service runs on managed Kubernetes at another cloud provider. Which rung?

Managed Kubernetes generally exposes an OIDC issuer, which is exactly what workload identity federation needs. Being outside Google Cloud rules out rung 1, not rung 2. Creating a key here would be an avoidable long-lived secret.

A project owner runs --impersonate-service-account and gets 403. Why?

roles/owner does not carry iam.serviceAccounts.getAccessToken. It has to be granted deliberately on the target service account — which is the point, since impersonation borrows an identity that is not yours.

Why never create a service account key in Terraform?

Terraform state holds resource attributes in the clear, and state buckets are versioned. Read access to the bucket becomes equivalent to the key, for every version that ever contained it. Create keys out of band.

From memory: the four rungs, highest first.

1. Attached service account (on Google Cloud). 2. Workload identity federation (elsewhere, with an OIDC issuer). 3. Impersonation (a human testing). 4. A service account key (last resort).