gcp-cloudscheduler-update-config
cloudscheduler.jobs.update lets a principal change an existing job's URI, headers, body, schedule, or OIDC/OAuth2 SA. This is the configuration-change primitive; URI-retarget (no new actAs needed) and SA-swap (actAs required) are the two attack patterns.
match (effective permission)
{
"action": "cloudscheduler.jobs.update",
"resource_type": "google.cloud.scheduler.v1.Job"
}
emit
| source type | Identity |
|---|---|
| target type | Scheduler |
| source | <principal> |
| target | <Cloud Scheduler job (Scheduler node)> |
| permissions | cloudscheduler.jobs.update |
| conditions | iam_permission service_state |
| state logic | ACTIVE when the principal holds cloudscheduler.jobs.update on the job AND the job exists and is not in UPDATE_FAILED state. CONDITIONAL(service_state) if the job is PAUSED or DISABLED (update succeeds but the job must be re-enabled to fire). BLOCKED by an IAM deny or org policy denying cloudscheduler.jobs.update. |
Narrative
{principal.name} can update {job.name} (cloudscheduler.jobs.update), changing its HTTP target URI, payload, headers, or OIDC/OAuth2 service account - enabling URI retargeting to receive the existing SA's token, or swapping the SA (with actAs) to pivot the execution identity.
Raw rule rules/derived/gcp/cloudscheduler.yaml
id: gcp-cloudscheduler-update-config
emits: CanModifyConfiguration
description: cloudscheduler.jobs.update lets a principal change an existing job's URI, headers, body,
schedule, or OIDC/OAuth2 SA. This is the configuration-change primitive; URI-retarget (no new actAs
needed) and SA-swap (actAs required) are the two attack patterns.
match_effective_permission:
action: cloudscheduler.jobs.update
resource_type: google.cloud.scheduler.v1.Job
emit:
source_type: Identity
target_type: Scheduler
source: <principal>
target: <Cloud Scheduler job (Scheduler node)>
permissions:
- cloudscheduler.jobs.update
derived_from:
- 'effective permission: cloudscheduler.jobs.update on the job resource'
conditions:
- iam_permission
- service_state
state_logic: ACTIVE when the principal holds cloudscheduler.jobs.update on the job AND the job exists
and is not in UPDATE_FAILED state. CONDITIONAL(service_state) if the job is PAUSED or DISABLED (update
succeeds but the job must be re-enabled to fire). BLOCKED by an IAM deny or org policy denying cloudscheduler.jobs.update.
false_positive_note: "cloudscheduler.jobs.update is a project-level permission; the update applies to\
\ any job in the project within the effective scope. Two distinct attack patterns branch from this\
\ edge: (a) URI-only retarget: attacker changes httpTarget.uri to their endpoint. The\n existing\
\ SA's OIDC/OAuth2 token continues to be sent \u2014 NO new actAs required.\n This yields CanRetrieveToken\
\ (the attacker endpoint receives the SA's bearer\n token on the next scheduled invocation) but\
\ NOT CanPassIdentity on a new SA.\n(b) SA-swap: attacker changes oidcToken.serviceAccountEmail to\
\ a new (more privileged)\n SA they can actAs. This requires CanPassIdentity on the new SA and\
\ yields\n CanExecuteAs via can-execute-as execute-as-via-config-identity-swap.\nModel both branches;\
\ the actAs gate differentiates them. A PAUSED/DISABLED job that is updated does not fire until re-enabled\
\ (re-enable requires cloudscheduler.jobs.enable or cloudscheduler.jobs.pause to be called; admin/update\
\ is sufficient in practice via the API state field)."
narrative: "{principal.name} can update {job.name} (cloudscheduler.jobs.update), changing its HTTP target\
\ URI, payload, headers, or OIDC/OAuth2 service account \u2014 enabling URI retargeting to receive\
\ the existing SA's token, or swapping the SA (with actAs) to pivot the execution identity."