Postgres on Google Cloud / Lesson 4 of 8
This is the mental model the rest of the course runs on. If you keep one page, keep this one.
"I can't connect to the database" is three completely different problems wearing the same sentence. They live in different systems, are fixed by different people, and fail at different moments.
Once you can name which layer an error belongs to, this stops being mysterious and becomes a lookup.
roles/cloudsql.client
(open a connection) and roles/cloudsql.instanceUser (log in with
IAM authentication). Managed by whoever administers the cloud project, usually
in Terraform. Fails when the proxy starts.GRANT statements. Nothing to do with
Google. Run by someone with SQL access. Fails on your first
query.The gift of this design is that each layer fails at a different moment as well as with a different message. The moment alone narrows it to one:
| When it breaks | Message | Layer |
|---|---|---|
| Proxy will not start | 403 … connectSettings |
1 — no cloudsql.client |
Proxy runs, psql refuses |
password authentication failed for user "…" |
2 — not an IAM database user |
| Connected, first query | permission denied for table … |
3 — no grants |
Connected, CREATE TABLE |
permission denied for schema public |
3 — and usually intentional, see lesson 7 |
Layer 1 fails before the proxy works. Layer 2 fails after the proxy works. Layer 3 fails after you are connected. That sentence resolves most tickets before anyone opens a console.
| Role | Key permission | Without it |
|---|---|---|
roles/cloudsql.client |
cloudsql.instances.connect |
The proxy cannot fetch instance metadata or an ephemeral certificate. It dies on startup. |
roles/cloudsql.instanceUser |
cloudsql.instances.login |
The tunnel opens; IAM login is refused. |
Google's documentation describes the second as a predefined role that
contains the necessary Cloud SQL IAM
(Cloud SQL
docs). Grant both, together, to a group:cloudsql.instances.login
permission. You need this permission to login to a database instance with IAM
database authentication
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="group:db-users@example.com" \
--role="roles/cloudsql.client"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="group:db-users@example.com" \
--role="roles/cloudsql.instanceUser"
Cloud SQL has no per-instance IAM policy. A binding on the project applies to every instance in it. The usual way to make a production boundary real is therefore to put production in its own project. If production shares a project with development, granting a developer layer 1 on development grants it on production too — and only layers 2 and 3 are then standing between them and production data.
IAM changes propagate. A call that returns 403 immediately
after a binding is created may succeed 40 seconds later. Wait before changing
anything. Re-granting a binding that was already correct just spends the time
it would have taken to propagate.
Holding the roles does not make you a database user. That is a separate record on the instance, and there are three kinds:
| Type | Username in PostgreSQL | Notes |
|---|---|---|
| User | you@example.com |
The full email address. |
| Service account | app@PROJECT_ID.iam |
The .gserviceaccount.com suffix is dropped.
Google: for a service account, this is the service account's email without
the . |
| Group | db-users@example.com |
Every member inherits login and the group's privileges. Members are not added individually. |
# a person
gcloud sql users create you@example.com \
--instance=INSTANCE_NAME --type=cloud_iam_user
# a service account — full email here, suffix and all
gcloud sql users create app@PROJECT_ID.iam.gserviceaccount.com \
--instance=INSTANCE_NAME --type=cloud_iam_service_account
# a group — the one to reach for
gcloud sql users create db-users@example.com \
--instance=INSTANCE_NAME --type=cloud_iam_group
When you create the user, you pass the full service account email
including .gserviceaccount.com. When you connect, you use
the form without it. Getting this backwards produces
password authentication failed, which points at layer 2 and tells
you nothing about why.
With group authentication, Google's documentation notes that a member gains
the ability to log in to the Cloud SQL instance because the user or service
account belongs to the group
and automatically inherits any database
privileges that have been granted to the group
(IAM
authentication). Removing them from the group removes both. No Terraform run,
no SQL, no instance change.
One constraint worth knowing before you design around it: you cannot add an individual user to an instance and add a group they belong to. Pick one model per instance.
A fresh IAM database user has nothing. From the docs: when an IAM user is
added to a database, by default the new database user has no privileges to any
database
. So the third layer is plain SQL, run by someone who already has
it:
-- as a role that can grant
GRANT app_rw TO "you@example.com";
That grants membership of a group role which carries the actual privileges, rather than granting tables directly. Why that is the right shape — and how it goes wrong on an instance holding more than one database — is lesson 7.
A GRANT … ON ALL TABLES IN SCHEMA applies to the tables that
exist at that moment. Every table created afterwards — by a migration,
by a restore — is not covered. Nothing reports this. You find out when someone
reads a new table and cannot. The durable fix is
ALTER DEFAULT PRIVILEGES; the interim one is re-running your grants
script after every migration and every restore.
SELECT current_user return what
you expect? No → you are on a different connection than you think.The proxy starts fine. psql says
password authentication failed. Which layer?
The proxy starting proves layer 1. The failure is at login, which is
layer 2. For a service account, suspect the .gserviceaccount.com
suffix before anything else.
A migration added tables. Users who could read everything
yesterday get permission denied on the new ones. Why?
GRANT … ON ALL TABLES is a snapshot, not a standing rule.
New tables are not covered. Re-run the grants, and set
ALTER DEFAULT PRIVILEGES so it stops happening.
Why does granting a developer database access on a shared project weaken a production boundary?
There is no per-instance IAM policy. A project-level binding covers every instance in the project, which is why production is usually given a project of its own. Layers 2 and 3 still stand in the way — but layer 1 has stopped contributing.
From memory: the three layers, and the moment each one fails.
1. Project IAM — cloudsql.client plus
cloudsql.instanceUser — fails before the proxy works.
2. IAM database user — fails at login, after the proxy
works. 3. PostgreSQL grants — fails on the first query.