gcp-pubsub-push-sa-exposes-credential
A Pub/Sub push subscription delivers a Google-signed OIDC token for its pushConfig.oidcToken.serviceAccountEmail to the push endpoint on every delivery; code at the endpoint can extract and replay the token, exposing the push SA's identity.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'match_record': {'resource_type': 'google.pubsub.Subscription', 'field': 'pushConfig.oidcToken.serviceAccountEmail'}, 'where': ['pushConfig.oidcToken.serviceAccountEmail is non-empty (push OIDC auth configured)', '?subscription.state is ACTIVE']}
where
node_type(?subscription) ==
Queue
emit
| source type | Messaging |
|---|---|
| target type | ServiceAccount |
| source | <Pub/Sub subscription node> |
| target | <push SA (ServiceAccount node for oidcToken.serviceAccountEmail)> |
| conditions | trigger_exists service_state |
| state logic | CONDITIONAL(trigger_exists): the OIDC token is only delivered when messages are being actively pushed (subscription ACTIVE + messages in the topic + push endpoint reachable). ACTIVE when the subscription is confirmed ACTIVE and push delivery is ongoing (subscription has recent delivery metrics). BLOCKED when the subscription is PAUSED or the push SA's actAs is revoked (Pub/Sub Service Agent no longer holds actAs on the push SA). |
Narrative
Pub/Sub push subscription {subscription.name} delivers a Google-signed OIDC token for {push_sa.name} to {push_endpoint} on every message POST; code running at the push endpoint can extract the token from the Authorization header and authenticate to GCP APIs as {push_sa.name}.
Raw rule rules/derived/gcp/pubsub.yaml
id: gcp-pubsub-push-sa-exposes-credential
emits: ExposesCredential
description: A Pub/Sub push subscription delivers a Google-signed OIDC token for its pushConfig.oidcToken.serviceAccountEmail
to the push endpoint on every delivery; code at the endpoint can extract and replay the token, exposing
the push SA's identity.
match:
- match_record:
resource_type: google.pubsub.Subscription
field: pushConfig.oidcToken.serviceAccountEmail
where:
- pushConfig.oidcToken.serviceAccountEmail is non-empty (push OIDC auth configured)
- ?subscription.state is ACTIVE
where:
- node_type(?subscription) == Queue
emit:
source_type: Messaging
target_type: ServiceAccount
source: <Pub/Sub subscription node>
target: <push SA (ServiceAccount node for oidcToken.serviceAccountEmail)>
permissions: []
conditions:
- trigger_exists
- service_state
state_logic: 'CONDITIONAL(trigger_exists): the OIDC token is only delivered when messages are being
actively pushed (subscription ACTIVE + messages in the topic + push endpoint reachable). ACTIVE when
the subscription is confirmed ACTIVE and push delivery is ongoing (subscription has recent delivery
metrics). BLOCKED when the subscription is PAUSED or the push SA''s actAs is revoked (Pub/Sub Service
Agent no longer holds actAs on the push SA).'
confidence: 0.85
derived_from:
- pubsub.subscriptions.get[pushConfig.oidcToken.serviceAccountEmail, state]
false_positive_note: "The OIDC token is a SHORT-LIVED bearer token (1-hour expiry by default). The push\
\ endpoint receives it as an Authorization header and may or may not log/store it \u2014 the ExposesCredential\
\ fact is about the delivery mechanism, not a confirmed theft. Actual exploitation requires code at\
\ the push endpoint to extract the token from the request header and use it to call GCP APIs as the\
\ push SA. The push SA is a DIFFERENT identity from the consumer's runtime SA (Cloud Run / GCF runtime\
\ SA) \u2014 do NOT conflate them. A push SA with minimal roles (e.g. only run.invoker on the target\
\ service) is a low-value exposure; a push SA with broad project roles is high-value. The ExposesCredential\
\ edge feeds credential-chains only if CredentialsFor(token \u2192 push SA) is also emitted."
narrative: Pub/Sub push subscription {subscription.name} delivers a Google-signed OIDC token for {push_sa.name}
to {push_endpoint} on every message POST; code running at the push endpoint can extract the token
from the Authorization header and authenticate to GCP APIs as {push_sa.name}.