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.

derived gcp emits CanModifyConfiguration

match (effective permission)

{ "action": "cloudscheduler.jobs.update", "resource_type": "google.cloud.scheduler.v1.Job" }

emit

source typeIdentity
target typeScheduler
source<principal>
target<Cloud Scheduler job (Scheduler node)>
permissionscloudscheduler.jobs.update
conditionsiam_permission service_state
state logicACTIVE 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."
move · open · esc close