# GCP Service Account Has Admin Role

> Service accounts holding roles/owner, roles/editor, or a predefined *.admin role turn 1 leaked credential into full control of the affected scope. ZopNight's high-severity RC-130 fires on service-account principals whose discovered metadata records has_admin_role=true, telling robots apart from humans by the serviceAccount: prefix on the principal UID.

Source: https://zop.dev/integrations/gcp/recommendations/gcp-service-account-has-admin-role
Updated: 2026-08-19

---

## Machine identities with human-sized power

A service account is credentials plus permissions and nothing else: no MFA prompt, no
login anomaly detection, no person who notices their own account acting strangely. When one
holds an admin-level role, whoever obtains its credential inherits everything the role
grants, silently and at machine speed. The scope depends on the role: basic roles
(`roles/owner`, `roles/editor`) mean project-wide control, while a predefined `*.admin`
role such as `roles/storage.admin` means full control of one service, which in that example is every bucket and
every object in the project. Attackers who land on a VM or in a container
read the metadata server first precisely because the attached service account so often
turns out to be over-privileged.

## How the detection tells robots from people

The discoverer's IAM listing emits both humans and service accounts as `iam-user` rows, so
the rule needs a discriminator: service-account principals keep their `serviceAccount:`
member prefix on the row UID, and only rows with that prefix are evaluated, so a human with
Owner never triggers this rule. The admin verdict itself is the producer's
`has_admin_role=true` flag, read from metadata with a fallback to the legacy tags sink for
rows written before the migration. If neither sink says true, the rule abstains; absence of
evidence is never treated as a finding.

## List the grants worth worrying about

```bash
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members ~ ^serviceAccount: AND bindings.role ~ (roles/owner|roles/editor|admin)" \
  --format="table(bindings.members, bindings.role)"
```

Each line is a machine identity one credential leak away from admin control of its scope.

## Right-sizing a workload's role

The remediation question is never "which admin role is safer" but "which 3 or 4 permissions
does this workload actually use". Cloud Audit Logs answer it empirically: filter for the
service account's calls over a representative window, map the methods to the narrowest
predefined roles, bind those, then remove the admin binding and watch for denials. Uploader
workloads typically land on `roles/storage.objectAdmin` scoped to one bucket rather than
project-wide `roles/storage.admin`; deployers land on service-specific developer roles.
ZopNight only surfaces the finding. Rebinding is yours to do, and the finding closes after
a discovery cycle no longer sees an admin role on the account.
