gcp-ci-dwd-sa-delegates-user
Service account with Domain-Wide Delegation can act as any active Workspace user in the domain for its configured OAuth2 scopes, without the user's knowledge or consent - a tenant-wide identity takeover primitive.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
where
node_type(?sa) ==
ServiceAccount
?dwd_binding.type == 'DWDScopeBinding' # the SA's OAuth2 client ID appears in the Workspace Admin console DWD list
?dwd_binding.scopes is non-empty # at least one authorized OAuth2 scope
?sa.suspended == false OR ?sa.suspended is unknown # skip suspended SAs
emit
| source type | Identity |
|---|---|
| target type | Identity |
| source | ?sa |
| target | <any active HumanIdentity in the domain> |
| conditions | service_state |
| state logic | CONDITIONAL(service_state) if the SA has at least one active service account key (iam.serviceAccountKeys.list is non-empty) — DWD requires the SA to sign a JWT with its private key to call the OAuth2 token endpoint. If the SA has no active keys, the edge is BLOCKED and DWD cannot be exploited. BLOCKED if the DWD grant has been revoked (client ID removed from delegated clients list). Note: delegation is scoped to the authorized OAuth2 scopes — a narrow-scope DWD SA (e.g. gmail.readonly only) cannot call admin APIs, but can still read email for every user. The node target is HumanIdentity(domain=*), not one user. Key existence is the gating precondition, checked via iam.serviceAccountKeys.list, not iam.serviceAccountKeys.create (which creates new keys, not exercises delegation). |
Narrative
{sa.name} has been granted Domain-Wide Delegation (client_id: {sa.client_id}) with authorized scopes {dwd_binding.scopes}. If {sa.name} has an active private key, any code running as {sa.name} can call Google Workspace APIs as ANY active user in the domain - reading email, accessing Drive files, and (if admin.directory.* scopes are present) performing admin operations - without the targeted user's knowledge or consent.
Raw rule rules/derived/gcp/cloudidentity.yaml
id: gcp-ci-dwd-sa-delegates-user
emits: CanDelegate
description: "Service account with Domain-Wide Delegation can act as any active Workspace user in the\
\ domain for its configured OAuth2 scopes, without the user's knowledge or consent \u2014 a tenant-wide\
\ identity takeover primitive."
match:
- - sa: null
- HasPolicy
- dwd_binding: null
where:
- node_type(?sa) == ServiceAccount
- '?dwd_binding.type == ''DWDScopeBinding'' # the SA''s OAuth2 client ID appears in the Workspace Admin
console DWD list'
- '?dwd_binding.scopes is non-empty # at least one authorized OAuth2 scope'
- '?sa.suspended == false OR ?sa.suspended is unknown # skip suspended SAs'
emit:
source_type: Identity
target_type: Identity
source: ?sa
target: <any active HumanIdentity in the domain>
permissions: []
conditions:
- service_state
state_logic: "CONDITIONAL(service_state) if the SA has at least one active service account key (iam.serviceAccountKeys.list\
\ is non-empty) \u2014 DWD requires the SA to sign a JWT with its private key to call the OAuth2 token\
\ endpoint. If the SA has no active keys, the edge is BLOCKED and DWD cannot be exploited. BLOCKED\
\ if the DWD grant has been revoked (client ID removed from delegated clients list). Note: delegation\
\ is scoped to the authorized OAuth2 scopes \u2014 a narrow-scope DWD SA (e.g. gmail.readonly only)\
\ cannot call admin APIs, but can still read email for every user. The node target is HumanIdentity(domain=*),\
\ not one user. Key existence is the gating precondition, checked via iam.serviceAccountKeys.list,\
\ not iam.serviceAccountKeys.create (which creates new keys, not exercises delegation)."
confidence: 0.97
derived_from:
- 'DWD scope binding: SA client_id authorized in Workspace Admin console with scopes ?dwd_binding.scopes'
false_positive_note: "DWD grant is a Workspace directory admin capability, not a GCP IAM permission.\
\ The key existence check (iam.serviceAccountKeys.list is non-empty) is a precondition on state_logic,\
\ not a permission \u2014 SA must have an active private key to sign DWD JWTs. Verify iam.serviceAccountKeys.list\
\ is non-empty before asserting ACTIVE or CONDITIONAL(service_state). Do not confuse iam.serviceAccountKeys.create\
\ (which creates new keys) with the key-existence check. Scope matters: a DWD SA authorized only for\
\ gmail.readonly cannot make Admin SDK calls or reset passwords \u2014 do not inflate capability beyond\
\ the configured scopes. Model scope-specific capabilities separately if needed (e.g. CanReadData\
\ for Gmail scope, CanAdminister only if admin.directory.* scopes are present). Suspended users (suspended=true\
\ in Directory API) cannot have tokens minted for them via DWD \u2014 exclude them from the target\
\ set. Some Workspace Enterprise Plus configurations restrict DWD per organizational unit; if collected,\
\ narrow the target set accordingly. Do NOT emit for SAs whose DWD grant has been deleted or whose\
\ OAuth2 client ID no longer appears in the delegated clients list. Edge is CanDelegate (not CanImpersonate)\
\ because the DWD target is a HumanIdentity and CanImpersonate's target set excludes HumanIdentity\
\ in the schema."
narrative: "{sa.name} has been granted Domain-Wide Delegation (client_id: {sa.client_id}) with authorized\
\ scopes {dwd_binding.scopes}. If {sa.name} has an active private key, any code running as {sa.name}\
\ can call Google Workspace APIs as ANY active user in the domain \u2014 reading email, accessing\
\ Drive files, and (if admin.directory.* scopes are present) performing admin operations \u2014 without\
\ the targeted user's knowledge or consent."