Service Accounts and Workload Identity on GCP

A pipeline cannot sign in with a password, so on Google Cloud it runs as a service account. This guide explains the kinds of service account, the five ways a workload proves it is one, why key files are the last resort, and how Workload Identity Federation lets GitHub Actions and GKE pods authenticate with no key.

GloryTecks Team

Training & Placement Faculty

6 May 2026 12 min read

Last updated: 2 October 2026

Share:

A pipeline that loads files into BigQuery at two in the morning cannot type a password or answer a phone prompt. It still has to be somebody, because Google Cloud checks every request against an identity. For a workload, that identity is a service account. How safe the arrangement is depends less on the account than on how the workload proves it is that account. This guide covers GCP service accounts and the five ways to authenticate as one, from the method to prefer to the one to avoid.

What a service account is

A service account is an account used by a workload, not by a person. It is identified by an email address, has no password and cannot sign in through a browser (Google Cloud: service accounts overview). One that you create in a project has an address of the form service-account-name@project-id.iam.gserviceaccount.com.

It has a dual nature, and most of the security advice follows from it:

  • It is a principal. You grant it roles on resources, as you would a user: read access to a bucket, write access to a dataset.
  • It is a resource. It has its own allow policy, which states who may attach it, impersonate it or create keys for it.

So every service account raises two questions. What can it do? And who can become it? A narrowly scoped account that every developer in the company can impersonate is not narrow. Roles and policies themselves are covered in GCP IAM explained.

Three kinds you will meet

KindCreated byWhat to do with it
User-managedYou, one per workloadUse these
DefaultGoogle Cloud, when you enable or use certain services such as Compute EngineReplace with your own
Service agentGoogle Cloud, so its services can act on your behalfLeave alone

Default service accounts deserve attention. Depending on organization policy, a default account might automatically be granted the Editor role on the project. Google strongly recommends disabling that automatic grant through an organization policy constraint, and enforces the constraint by default for organizations created after 3 May 2024 (Google Cloud: types of service accounts). In an older project, check. A virtual machine running as a default account with Editor can change nearly everything in the project, and so can anyone able to run code on that machine.

Service agents appear in allow policies and audit logs with Google-owned addresses. They are what let, for example, a logging sink write to a storage bucket. They are not created in your project and you cannot use them directly.

Five ways to prove identity

MethodWhere the workload runsSecret you must store
Attached service accountOn Google Cloud: a VM, a serverless service, a managed jobNone
Workload Identity Federation for GKEPods on Google Kubernetes EngineNone
ImpersonationA developer's machine, or one account acting as anotherNone
Workload Identity FederationOutside Google Cloud, where another identity provider vouches for the workloadNone on the Google side
Service account keyAnywhereA private key file

The first four produce short-lived tokens. Only the last involves a long-lived secret, which is why it sits at the bottom.

Client libraries find credentials through Application Default Credentials, which looks in three places in order: the GOOGLE_APPLICATION_CREDENTIALS environment variable, a file created by gcloud auth application-default login, and the attached service account supplied by the metadata server (Google Cloud: Application Default Credentials). Code written this way runs unchanged on a laptop and in production.

Attached service accounts

For code that runs on Google Cloud, attach a user-managed account to the resource. The code then authenticates as that account with no configuration at all.

Attaching requires the Service Account User role on the account. That role is the gate: whoever can attach an account to a resource can run code as it. The commands below create an account, state what it can do, and state who can use it (Google Cloud: create service accounts).

bash
# One account for one workload
gcloud iam service-accounts create nightly-loader \
  --description="Loads landing files into the analytics dataset" \
  --display-name="Nightly loader"

# What it can do: read one bucket, run jobs in one project
gcloud storage buckets add-iam-policy-binding gs://example-landing \
  --member="serviceAccount:nightly-loader@analytics-prod.iam.gserviceaccount.com" \
  --role="roles/storage.objectViewer"
gcloud projects add-iam-policy-binding analytics-prod \
  --member="serviceAccount:nightly-loader@analytics-prod.iam.gserviceaccount.com" \
  --role="roles/bigquery.jobUser"

# Who can use it: one group may attach it to a resource
gcloud iam service-accounts add-iam-policy-binding \
  nightly-loader@analytics-prod.iam.gserviceaccount.com \
  --member="group:data-platform@example.com" \
  --role="roles/iam.serviceAccountUser"

Note where the last grant is made: on the account itself, not on the project. The same role granted on the project would let the group act as every service account in it.

Impersonation

Impersonation involves two identities: an authenticated principal, and the service account it borrows. A principal holding the Service Account Token Creator role on an account can obtain a short-lived token for it. Google describes this as similar to AssumeRole on AWS (Google Cloud: service account impersonation), which is covered in AWS IAM explained.

bash
gcloud storage ls gs://example-landing \
  --impersonate-service-account=nightly-loader@analytics-prod.iam.gserviceaccount.com

Three things make it the right tool for people:

  • No key file reaches a laptop. The credential lasts a few hours or less.
  • The trail names the person. Most audit logs record both the caller and the impersonated account. With a key, or on a shared machine, the logs show only the service account.
  • It tests permissions honestly. Running a pipeline locally as its own account shows whether the account's roles are sufficient, before deployment.

Why keys are the last resort

A service account key is the private half of an RSA key pair. Holding it is similar to knowing a user's password, with one difference that makes it worse: a stolen password often fails at a second verification step, while a stolen key simply works, because service accounts have no additional sign-in checks (Google Cloud: managing service account keys).

Google lists the threats: credential leakage, privilege escalation, information disclosure and non-repudiation. The last means that actions taken with a key cannot be traced to a person. Keys leak through ordinary places: source repositories, public buckets, email, file shares, backups and temporary directories.

A key created in IAM has no expiry time by default and stays valid until deleted. Google's guidance is to avoid user-managed keys wherever another method exists. Where a tool truly accepts nothing else, restrict who can create keys, rotate them, and keep the account's roles minimal.

Workload Identity Federation

A workload outside Google Cloud usually has an identity already: an AWS role, a Microsoft Entra ID application, a GitHub Actions workflow. Workload Identity Federation lets Google Cloud trust that identity directly. The workload presents the token it already has, Google's Security Token Service checks it and returns a short-lived federated token, and no Google key exists to leak (Google Cloud: Workload Identity Federation).

Four pieces make it work:

  • A workload identity pool: a container for external identities. Google recommends one per external environment.
  • A provider: the trust relationship with one identity provider, named by its issuer.
  • An attribute mapping: which claims in the external token become Google attributes. google.subject is required; custom attributes are optional.
  • An attribute condition: an expression that must be true, or the credential is rejected.

For GitHub Actions the setup is three commands (Google Cloud: federation with deployment pipelines):

bash
gcloud iam workload-identity-pools create ci-pool \
  --location="global" \
  --display-name="CI pipelines"

gcloud iam workload-identity-pools providers create-oidc github \
  --location="global" \
  --workload-identity-pool="ci-pool" \
  --issuer-uri="https://token.actions.githubusercontent.com/" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository_id=assertion.repository_id,attribute.repository_owner_id=assertion.repository_owner_id" \
  --attribute-condition="assertion.repository_owner_id=='1001'"

gcloud storage buckets add-iam-policy-binding gs://example-landing \
  --role="roles/storage.objectViewer" \
  --member="principalSet://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/ci-pool/attribute.repository_id/5001"

The condition is not optional in practice. GitHub uses one issuer for every organization on the platform, so a provider without a condition would accept a valid token from any repository anywhere. Google's documentation also advises conditions on the numeric id claims, not on names, because a deleted organization's name can be registered again by someone else.

What the condition and the binding each do

The script below models the flow for four tokens. It is a model written for this article, not the Security Token Service, and the claims are sample values. The subject follows the format GitHub documents for a branch, repo:OWNER/REPOSITORY:ref:refs/heads/BRANCH (GitHub: OpenID Connect reference).

python
# A model of Workload Identity Federation for a deployment pipeline: check the issuer and the
# attribute condition, map the token's claims, build the principal identifiers, then look for a
# role binding. It is not Google's Security Token Service; the claims are sample values.
POOL = "projects/123456789012/locations/global/workloadIdentityPools/ci-pool"
ISSUER = "https://token.actions.githubusercontent.com"

MAPPING = {                                    # target attribute <- claim in the pipeline's token
    "google.subject": "sub",
    "attribute.repository_id": "repository_id",
    "attribute.repository_owner_id": "repository_owner_id",
}
ALLOWED_OWNER_ID = "1001"                      # condition: assertion.repository_owner_id == '1001'

BUCKET_BINDINGS = {                            # principal -> role granted on one bucket
    f"principalSet://iam.googleapis.com/{POOL}/attribute.repository_id/5001": "roles/storage.objectViewer",
}


def exchange(claims):
    """Return the mapped attributes, or None with the reason the credential is rejected."""
    if claims["iss"] != ISSUER:
        return None, "rejected: the issuer is not this provider's"
    if claims["repository_owner_id"] != ALLOWED_OWNER_ID:
        return None, "rejected: the attribute condition is false"
    return {target: claims[source] for target, source in MAPPING.items()}, "accepted"


def roles_on_bucket(mapped):
    principals = [f"principal://iam.googleapis.com/{POOL}/subject/{mapped['google.subject']}"]
    principals += [f"principalSet://iam.googleapis.com/{POOL}/{name}/{value}"
                   for name, value in mapped.items() if name.startswith("attribute.")]
    return [BUCKET_BINDINGS[p] for p in principals if p in BUCKET_BINDINGS]


TOKENS = [
    ("pipelines repository", {"iss": ISSUER, "sub": "repo:example-org/data-pipelines:ref:refs/heads/main",
                              "repository_id": "5001", "repository_owner_id": "1001"}),
    ("another repository, same owner", {"iss": ISSUER, "sub": "repo:example-org/website:ref:refs/heads/main",
                                        "repository_id": "5002", "repository_owner_id": "1001"}),
    ("look-alike owner name", {"iss": ISSUER, "sub": "repo:examp1e-org/data-pipelines:ref:refs/heads/main",
                               "repository_id": "9001", "repository_owner_id": "2002"}),
    ("token from another issuer", {"iss": "https://gitlab.com",
                                   "sub": "project_path:example-org/data-pipelines:ref_type:branch:ref:main"}),
]

for label, claims in TOKENS:
    mapped, verdict = exchange(claims)
    if mapped is None:
        print(f"{label:31} {verdict}")
        continue
    roles = roles_on_bucket(mapped)
    print(f"{label:31} {verdict}; " + (f"holds {roles[0]} on the bucket" if roles else "no role on the bucket, so no access"))
text
pipelines repository            accepted; holds roles/storage.objectViewer on the bucket
another repository, same owner  accepted; no role on the bucket, so no access
look-alike owner name           rejected: the attribute condition is false
token from another issuer       rejected: the issuer is not this provider's
  • The pipelines repository is accepted and authorised. Its owner satisfies the condition, and its repository id matches the binding on the bucket.
  • Another repository of the same owner is accepted and gets nothing. The pool recognises it; no role binding names it. Authentication is not authorisation, and the two are configured in different places.
  • The look-alike owner is rejected. The name differs by one character, which a hurried reviewer might miss. The condition compares the numeric id, so the resemblance does not matter.
  • A token from another issuer never reaches the condition.

Granting roles straight to the federated principal, as here, is what Google recommends. Where an API does not accept federated tokens, the external identity is instead allowed to impersonate a service account.

Workload Identity Federation for GKE

Pods on Google Kubernetes Engine get the same treatment with less setup. When the feature is enabled, GKE creates a fixed pool named PROJECT_ID.svc.id.goog, registers the cluster as an identity provider and runs a metadata server on every node. When a pod asks for credentials, that server exchanges the pod's Kubernetes ServiceAccount token for a short-lived federated token (Google Cloud: Workload Identity Federation for GKE).

bash
gcloud container clusters update analytics-cluster \
  --location=asia-south1 \
  --workload-pool=analytics-prod.svc.id.goog

gcloud storage buckets add-iam-policy-binding gs://example-landing \
  --role="roles/storage.objectViewer" \
  --member="principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/analytics-prod.svc.id.goog/subject/ns/pipelines/sa/loader"

Two points catch teams out. Enabling the feature grants nothing by itself; each Kubernetes ServiceAccount still needs roles. And the principal is identified by namespace and ServiceAccount name, not by cluster. Two clusters in one project that both have a loader account in a pipelines namespace are the same identity to IAM. Google's advice is to put clusters you do not equally trust in separate projects.

Choosing a method

Google's own guidance reduces to four questions (Google Cloud: using service accounts securely):

  1. Is this a developer's own environment? Use your user credentials, or impersonate a service account when the work needs one.
  2. Does the code run on Google Cloud? On GKE, use Workload Identity Federation for GKE. Elsewhere, attach a service account.
  3. Does it run elsewhere with an identity provider that supports federation? Configure Workload Identity Federation.
  4. None of these? Only then create a key.

The same idea appears on other clouds under other names; managed identities in Azure Data Factory are the Azure counterpart of an attached account.

Habits that hold up

  • One account per workload. Shared accounts accumulate permissions and can never be safely deleted.
  • No basic roles. Remove Editor from default accounts and grant specific roles on specific resources, such as one Cloud Storage bucket or one BigQuery dataset.
  • Grant access to an account on the account, never at project or folder level.
  • Do not let anyone impersonate an account more privileged than they are. That is privilege escalation by configuration.
  • Block key creation by organization policy, and make exceptions deliberately.
  • Limit shell access to machines that have a privileged account attached.
  • Disable an unused account before deleting it, so a mistake can be undone.

What is the difference between a service account and a user account on Google Cloud?

A user account belongs to a person and signs in with a password. A service account is used by a workload, has no password and is managed as a resource in a project. Both are principals that can be granted roles.

Are service account keys safe to use?

They are a risk. A key is a long-lived private key that works for anyone who holds it. Google recommends avoiding user-managed keys whenever another method is available.

Do GitHub Actions need a service account key to deploy to Google Cloud?

No. With Workload Identity Federation, the workflow exchanges its own GitHub token for a short-lived Google token. An attribute condition must restrict the provider to your organization.

Does enabling Workload Identity Federation for GKE give pods access to Google Cloud?

No. It gives each Kubernetes ServiceAccount an identity that IAM understands. You still grant roles to that identity on the resources it needs.

Google Cloud, pipelines and governance are part of the Data Engineering course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master GCP?

Join GloryTecks’ expert-led Data Engineering training in Hyderabad with real-time projects and 100% placement support.

Explore course