Runbook: grant log-read access to the deployer service account
Who runs this: a human with resourcemanager.projects.setIamPolicy on
ordermatic-prod. Agents cannot grant themselves IAM.
github-deployer@ordermatic-prod.iam.gserviceaccount.com is the account to
impersonate for gcloud work here β user credentials expire constantly and the
deployer is stable. It already covers Compute and IAP, which is why SSH-to-VM
and deploy flows work.
It does not cover Cloud Logging. Every attempt to read logs as the deployer
fails with a logging.logEntries.list permission error. Since Coolify was
retired and the platform moved to Cloud Run, Cloud Logging is where all service
logs live β so this gap blocks routine diagnosis, and it gets rediscovered from
scratch each time as βthe deployer is broken.β
The grant
Section titled βThe grantβroles/logging.viewer is the right role: it grants logging.logEntries.list
(plus logging.logs.list and log-metric reads) β enough to read normal service
logs from the _Default and _Required buckets. Do not use
roles/logging.admin (adds sink/bucket mutation) or roles/viewer
(project-wide read of everything).
It does not grant access to private or Data Access audit logs; those need
roles/logging.privateLogViewer. Donβt add that unless something specifically
needs it β Cloud Run service logs donβt.
You need resourcemanager.projects.getIamPolicy and setIamPolicy to run the
grant (e.g. roles/resourcemanager.projectIamAdmin). If you donβt have them,
this is an ask for whoever owns the projectβs IAM.
Two more prerequisites, both of which fail in ways that look like the grant didnβt work:
- You need
iam.serviceAccounts.getAccessTokenon the deployer SA (roles/iam.serviceAccountTokenCreator) to run the impersonated verification below. Without it the verify step fails on impersonation, not on logging. - The deployer SA needs
serviceusage.services.use(roles/serviceusage.serviceUsageConsumer) on the project forgcloud logging readto resolve a quota project. Without it the read fails even withroles/logging.viewercorrectly bound.
Read the error text rather than assuming the logging grant is at fault: each of these names a different permission.
gcloud projects add-iam-policy-binding ordermatic-prod \ --member="serviceAccount:github-deployer@ordermatic-prod.iam.gserviceaccount.com" \ --role="roles/logging.viewer" \ --condition=None--condition=None explicitly requests an unconditioned binding. The flag is
optional and omitting it also creates an unconditioned binding; passing it
states the intent, and keeps the grant and the rollback below referring to the
same binding.
If custom log views are in use, roles/logging.viewAccessor on the specific
view is the narrower alternative β prefer it if it covers the logs you need.
(roles/logging.privateLogViewer is a different thing again: private logs such
as Data Access audit logs.)
Read a log entry as the deployer, not as yourself. Impersonation is the whole point of the check:
gcloud logging read \ 'resource.type="cloud_run_revision"' \ --project=ordermatic-prod --limit=1 --freshness=7d \ --impersonate-service-account=github-deployer@ordermatic-prod.iam.gserviceaccount.com--freshness matters: it defaults to 1d, so a quiet service returns zero
entries on a working grant and reads as failure. Distinguish the two by the
error, not the row count β PERMISSION_DENIED naming logging.logEntries.list
is the failure; empty output with no error is success against a quiet service.
If it does deny, confirm the binding actually landed:
gcloud projects get-iam-policy ordermatic-prod \ --flatten="bindings[].members" \ --filter="bindings.members=serviceAccount:github-deployer@ordermatic-prod.iam.gserviceaccount.com AND bindings.role=roles/logging.viewer" \ --format="value(bindings.role)"Use = (exact match), not : (pattern match) β a : filter can match a
similarly-named principal or role and report a binding you donβt have.
Empty output means no binding, regardless of what add-iam-policy-binding
printed. IAM changes usually propagate within ~2 minutes but can take 7
minutes or more; if the policy shows the role and reads still fail, wait
before re-granting.
Rollback
Section titled βRollbackβOnly if the grant created the binding. Removal is unconditional β it strips the member/role pair regardless of who added it or when, so running it against a binding that predated you revokes access someone else depends on. Record the pre-grant state before step 1 by running the policy check from Verify: empty output then means the binding was absent and this rollback is safe.
gcloud projects remove-iam-policy-binding ordermatic-prod \ --member="serviceAccount:github-deployer@ordermatic-prod.iam.gserviceaccount.com" \ --role="roles/logging.viewer" \ --condition=NoneScope note
Section titled βScope noteβTwo things are true at once here, and the second is the one to weigh.
This is a read-only grant to a service account that already holds broader deploy and compute rights on the same project, so it barely moves the blast radius of a compromised deployer key. Rotation of that key is covered by the secret rotation playbook, separately.
But roles/logging.viewer at project scope grants read access to all
project logs, and logs can carry sensitive field values. The narrower grant is
roles/logging.viewAccessor on a specific custom log view β use it when it
covers the logs you actually need. Project scope is used here because the
diagnostic need is unscoped: the whole point is reading logs from whichever
Cloud Run service is misbehaving, which is not known in advance. If a fixed set
of services ever covers it, replace this with a per-view grant.