CoursesGCP securityService accounts & impersonation

Service accounts & impersonation

Killing keys and scoping the escalation path.

Expert35 min · lesson 3 of 15

Someone on your team pushes a file called sa-key.json to a public GitHub repo. A bot scrapes it within minutes, and by the afternoon a stranger in another country is running crypto miners billed to your project. This happens constantly, and the cause barely varies: a downloaded service-account key. That file is a house key that never changes the lock and never expires. Copy it once and the copy works forever.

Back up to what a service account actually is. Your programs, your build pipelines and your virtual machines are not people, but Google still makes them prove who they are before it will hand over the contents of a storage bucket or let them write a database row. A service account (SA), the identity a piece of software signs in as, is how that happens. Treat it as a work badge issued to a job rather than to a person. The nightly export job carries one badge; the web server carries another. Each badge has an email-shaped name like [email protected], and you attach permissions to it (IAM roles, from Identity and Access Management, Google's system for deciding who may call which API, the door software knocks on instead of a web page) the same way you would for a human user.

There are two ways to put that badge in a machine's hand, and the gap between them is the whole lesson. Option one: photocopy the badge and let the contractor keep the copy for life. That is a downloaded key file. Option two: have them sign the real badge out at the front desk for one errand, with their name in the logbook, and the badge stops working an hour later. That is impersonation. The first is a liability you carry until someone finds it. The second expires on its own and leaves a trail.

Make the badge boring before you hand it out

Before any of the borrowing matters, keep the badge itself dull. Build a service account for one job and give it only the roles that job needs, in the one project it needs them in. A reporting account that reads tables should read tables and nothing else. Then the day it gets borrowed by the wrong person, the worst outcome is 'someone read a few BigQuery tables', not 'someone deleted production'. Least privilege on the account is the ceiling on the damage from every mistake that comes after it.

create a scoped reporting service account
$ gcloud iam service-accounts create reporter \
--display-name="Nightly BigQuery export" \
--project=payments-prod
Created service account [reporter].
$ gcloud projects add-iam-policy-binding payments-prod \
--member="serviceAccount:[email protected]" \
--role="roles/bigquery.dataViewer"
Updated IAM policy for project [payments-prod].
bindings:
- members:
- serviceAccount:[email protected]
role: roles/bigquery.dataViewer
etag: BwYX3nL2p9c=
version: 1

Now the part most teams have to unlearn. A downloaded service-account key is a small text file (JSON, a plain-text format for structured data) with a private key inside, and it is a bearer credential: whoever holds the file gets treated as the service account, no questions asked. No password prompt. No second factor. No expiry date. Commit it to git once and it stays burned even after you delete the commit, because the repository history keeps a copy. The safest number of key files in your whole organization is zero, and Google Cloud lets you make that a rule with an organization policy (a constraint set high in the resource hierarchy that every project underneath inherits), so nobody can create one even by accident.

org policy: no downloadable keys, ever
# key-policy.yaml
name: organizations/561209481324/policies/iam.disableServiceAccountKeyCreation
spec:
rules:
- enforce: true
$ gcloud org-policies set-policy key-policy.yaml
name: organizations/561209481324/policies/iam.disableServiceAccountKeyCreation
spec:
etag: CNiP2r0GENDp3IsB
rules:
- enforce: true
updateTime: '2026-07-16T02:01:14Z'
# Someone tries to export a key anyway:
$ gcloud iam service-accounts keys create key.json \
ERROR: (gcloud.iam.service-accounts.keys.create) FAILED_PRECONDITION: Key creation
is not allowed on this service account because it violates constraint
constraints/iam.disableServiceAccountKeyCreation on the resource hierarchy.

With that constraint enforced, the leak path is shut at the door. A project Owner cannot create a key. A well-meaning deploy script that used to export one now fails loudly instead of quietly producing a time bomb somebody finds three years later. So how does anyone act as the service account when there is no file to hand out? They borrow it.

Impersonation: sign the badge out, for one job, in the logbook

Impersonation means asking Google for a fresh, short-lived token that lets you act as a service account without ever touching a key file. The permission that allows it is roles/iam.serviceAccountTokenCreator, and you grant it on one named service account. Once someone holds it, they add a single flag to almost any gcloud command, --impersonate-service-account, and Google's IAM Credentials API (the service software calls when it needs a token) hands back an access token good for about an hour by default. That token is the badge signed out at the front desk. It covers the one job and then dies by itself. Nothing to rotate, nothing to clean up.

grant borrow rights on one SA, then borrow it
$ gcloud iam service-accounts add-iam-policy-binding \
--member="user:[email protected]" \
--role="roles/iam.serviceAccountTokenCreator"
Updated IAM policy for serviceAccount [[email protected]].
bindings:
- members:
role: roles/iam.serviceAccountTokenCreator
# Dana never downloads a key. She borrows the SA for one command:
$ gcloud storage ls gs://payments-prod-exports \
--impersonate-service-account=reporter@payments-prod.iam.gserviceaccount.com
WARNING: This command is using service account impersonation. All API calls will
be executed as [[email protected]].
gs://payments-prod-exports/2026-07-15/
gs://payments-prod-exports/2026-07-16/
# Or mint just the raw token (valid ~1 hour) for a script or another tool:
$ gcloud auth print-access-token \
--impersonate-service-account=reporter@payments-prod.iam.gserviceaccount.com
ya29.c.c0ASRK0GY0m...QpZ3Xv9

Behind that flag sits a GenerateAccessToken call to the IAM Credentials API. Google checks that the caller really does hold Token Creator on the target account, mints the token, and the command runs as the service account. The security payoff is in what gets written down. The audit log captures both identities in one entry: the human email that made the request, and the service account that was borrowed. Nobody can quietly act as the account and then blame 'the machine', because the log always names the person, or the CI runner (the automated server that runs your build and deploy pipeline), that did the borrowing.

How an impersonated call is minted and traced
1you or a CI runner
signed in as a low-privilege human or SA
2call IAM Credentials API
GenerateAccessToken on the target SA
3short-lived token minted
access token, ~60 min, no key file
4API call runs as the target SA
gcloud/gsutil use the borrowed identity
5both identities logged
caller + impersonated SA in Cloud Audit Logs
The token is the badge signed out at the desk. It covers one job and dies on its own, so there is nothing to revoke. Every hop records who did the borrowing.

Where this quietly turns into an escalation path

Here is why 'on one named service account' carries so much weight. Grant Token Creator at the project level instead and you have handed out the master key to the whole badge cabinet. That principal can now borrow every service account in the project, including any carrying Owner or Editor. A user with almost no permissions of their own is one command away from acting as your most powerful identity. Google's legacy default Compute Engine and App Engine service accounts made this worse for years by shipping with the Editor role attached, so anything running as a default account could change almost anything. Replace those defaults with purpose-built, least-privilege accounts, and keep Token Creator pinned to individual accounts.

There is a quieter version of the same problem. Impersonation chains. If account A can borrow B, and B can borrow C, then whoever can act as A can walk that line all the way to C, and the --impersonate-service-account flag even accepts a comma-separated delegation chain to do it in a single call. Reviewing direct grants is not enough. You have to follow the whole graph of who-can-borrow-whom. What saves you is that every borrow is recorded. Turn on Data Access audit logs for the IAM Credentials API and each token-minting call lands in the log with the caller's name attached, so you can chase the entire chain from the log itself.

who borrowed which SA, straight from the audit trail
$ gcloud logging read \
'protoPayload.methodName="GenerateAccessToken"' \
--project=payments-prod --limit=1 --format=json
[
{
"protoPayload": {
"authenticationInfo": { "principalEmail": "[email protected]" },
"methodName": "GenerateAccessToken",
"request": {
"name": "projects/-/serviceAccounts/[email protected]"
},
"serviceName": "iamcredentials.googleapis.com"
},
"resource": { "type": "service_account" },
"timestamp": "2026-07-16T02:00:07Z"
}
]
A Token Creator grant is worth exactly what the account it points at is worth
Teams nod serviceAccountTokenCreator requests through as harmless because 'they're only borrowing it'. But borrowing an account hands you every role that account holds. Grant Token Creator on an account that has Editor, and you have handed that person Editor. Review each request against the target account's roles, not against the friendly-sounding word 'impersonate'. And alert on any Token Creator binding that shows up at the project or folder level, because there it becomes a standing route into every identity underneath.

A default compute service account carrying Editor is a gift to whoever gets a shell on that virtual machine. Give every workload its own account, grant it the roles that workload needs and no others, and attach it at create time. Take the identity from the metadata server, or from Workload Identity Federation (which lets an outside system, say a GitHub Actions job, trade its own signed identity for a Google token), rather than from a JSON file on disk. When a person or a pipeline has to act as that account, put roles/iam.serviceAccountTokenCreator on a short list of principals and borrow the account for one command, not for a week.

Audit for key files the way you audit for Owner. Walk the projects on a schedule and list the keys. Anything user-managed and older than your rotation window gets revoked. Anything that still genuinely needs a key gets a ticket explaining why federation and attachment both fail for that case, and those tickets should be rare enough that a security engineer still reads them properly.

Try this

Take one project. List its service accounts, confirm that key creation is blocked by org policy, then get yourself an hour of access by impersonation instead of downloading anything.

terminal
gcloud iam service-accounts list --project=payments-prod
gcloud iam service-accounts keys list \
--project=payments-prod
gcloud org-policies describe constraints/iam.disableServiceAccountKeyCreation \
--project=payments-prod
# short-lived access without a key file:
gcloud auth print-access-token \
--impersonate-service-account=reporter@payments-prod.iam.gserviceaccount.com
output
EMAIL DISABLED
Listed 0 items.
constraint: constraints/iam.disableServiceAccountKeyCreation
listPolicy:
allValues: DENY
ya29.c.b0Aaekm1... # ~1 hour token; no sa-key.json on disk

Takeaway

A service-account key is a password that never expires and never asks who is holding it. Attach identities to workloads, borrow them by impersonation when a human or a pipeline needs one, block key creation with org policy, and treat every user-managed key still sitting in your estate as an incident that has not been reported yet.

Next you will fence the network those workloads sit on: VPC (virtual private cloud, the private network your machines live in) firewall rules, private Google access, and network policy set at the org level, so a stolen token has fewer doors left to try.

Quick check
01Key creation is blocked across your whole organization. A data engineer needs to run the nightly export as the reporter service account from their laptop. How do you give them that access?
Incorrect — Still a downloadable credential with no expiry, and the org policy blocks key creation anyway. This is the exact door you closed.
Correct — Scoped to one account, a fresh short-lived token per run, and the log names the human behind every call. This is what replaces keys.
Incorrect — Project-wide Token Creator lets them borrow every account in the project, privileged ones included. That is an escalation path wearing a convenience costume.
Incorrect — Owner on the account is far broader than they need and still gives them no way to act as it. Nowhere near least privilege.
02Dana holds Token Creator on the reporter service account and runs a command with the --impersonate-service-account flag. Whose identity does Cloud Audit Logs record for that call?
Correct — The entry names the person who signed the badge out and the account she borrowed, so the action cannot be pinned on 'the machine'.
Incorrect — No. That blind spot is exactly what impersonation exists to remove; the caller is captured as well.
Incorrect — No. The borrowed service account is written into the same entry, not left out of it.
Incorrect — No. Impersonation is logged precisely because it goes through the IAM Credentials API, with no key involved anywhere.
03An access request lands on your desk: give a developer roles/iam.serviceAccountTokenCreator on one named service account. Tight scope, one account. That account already holds roles/editor on the project. What have you actually granted?
Incorrect — It does both. Minting a token means acting as the account, and the account brings all of its roles along with it.
Correct — A Token Creator grant is worth whatever the target account is worth, so review it against that account's roles rather than the word 'impersonate'.
Incorrect — No. Impersonation hands over the account's real permissions, not a view of them.
Incorrect — No. They can mint a fresh token whenever they like, so the access stands until you remove the binding.

Short-lived, logged identities settle the question of who is acting on a resource. They say nothing about where the call is allowed to come from. A borrowed badge does an attacker no good if they cannot get near the building, and deciding who can reach the door at all is the job of VPC firewall rules, the network walls around your cloud, plus private access. That is where we go next.

Related