Postgres on Google Cloud / Lesson 6 of 8
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.
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.
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.
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.
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.
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.
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.
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.
.gitignore is one git add -f from
failing.ps and shell history see it. Redirect from a file:
… set KEY < key.json.CreateServiceAccountKey means a new key is never silent.
An unexplained one is an incident.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.
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.
# 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.
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.
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).