<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cloudsentinel.dev</title>
    <description>The latest articles on DEV Community by Cloudsentinel.dev (@cloudsentinel_official).</description>
    <link>https://dev.to/cloudsentinel_official</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2310330%2F8f345a16-61c8-4964-a336-2b2c1ad92732.png</url>
      <title>DEV Community: Cloudsentinel.dev</title>
      <link>https://dev.to/cloudsentinel_official</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudsentinel_official"/>
    <language>en</language>
    <item>
      <title>GCP Has No Automatic Kill Switch for Leaked API Keys. Here's What I Built.</title>
      <dc:creator>Cloudsentinel.dev</dc:creator>
      <pubDate>Thu, 30 Apr 2026 09:56:56 +0000</pubDate>
      <link>https://dev.to/cloudsentinel_official/gcp-has-no-automatic-kill-switch-for-leaked-api-keys-heres-what-i-built-3680</link>
      <guid>https://dev.to/cloudsentinel_official/gcp-has-no-automatic-kill-switch-for-leaked-api-keys-heres-what-i-built-3680</guid>
      <description>&lt;p&gt;&lt;em&gt;And what you can do right now to protect yourself — whether you use my tool or not.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I kept seeing posts like this on Reddit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Woke up to a $128,000 Google Cloud bill. Key was compromised overnight. Google denied the adjustment request."&lt;/p&gt;

&lt;p&gt;"3-person startup. Gemini API key silently reauthorized. Normal monthly spend was $180. Bill: $82,314 in 48 hours."&lt;/p&gt;

&lt;p&gt;"Student. Pushed API key to a private GitHub repo that was accidentally public. Was on summer break. Never saw the alerts. $55,444."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm a developer building on GCP myself. After reading enough of these, I realized I had zero automatic protection. If one of my keys got leaked tonight, the only thing standing between me and a five-figure bill was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hoping I'd see a budget alert email in time&lt;/li&gt;
&lt;li&gt;Being awake&lt;/li&gt;
&lt;li&gt;Logging in fast enough&lt;/li&gt;
&lt;li&gt;Finding the right key&lt;/li&gt;
&lt;li&gt;Deleting it manually&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a terrible safety net.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — an automatic kill switch for GCP API keys. But this article isn't a pitch. It's an honest walkthrough of the problem, why GCP's existing tools fall short, and what you can do about it — including building your own solution if you prefer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why GCP's Built-In Tools Aren't Enough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Budget alerts lag 4-12 hours
&lt;/h3&gt;

&lt;p&gt;GCP billing data is not real-time. It's aggregated and delivered with a delay. By the time a budget alert fires, the damage is often already done.&lt;/p&gt;

&lt;p&gt;Here's the timeline of a typical incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00:00 — API key leaks (committed to GitHub, exposed in frontend, etc.)
00:03 — Automated scanners find the key
00:05 — Attacker starts making requests
04:00 — GCP billing data updates
04:30 — Budget alert email arrives
04:31 — You're asleep
08:00 — You wake up and see the email
08:05 — You log in and delete the key
08:05 — Damage: 8 hours of uncontrolled API usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Budget alerts are better than nothing. But they depend on someone being awake, seeing the email, and acting fast enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spend Caps are a blunt instrument
&lt;/h3&gt;

&lt;p&gt;Google recently announced Spend Caps for some services (Gemini, Cloud Run, Maps). Good first step. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They pause &lt;strong&gt;all&lt;/strong&gt; traffic in your project — not just the abused key&lt;/li&gt;
&lt;li&gt;Your other keys and services stop working too&lt;/li&gt;
&lt;li&gt;They're spend-based, not request-based — they trigger after money is already spent&lt;/li&gt;
&lt;li&gt;They don't cover every GCP API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have 5 API keys in a project and one gets compromised, Spend Caps kill all 5. Your app goes down. CloudSentinel revokes only the specific key that crossed the threshold — everything else keeps running.&lt;/p&gt;

&lt;h3&gt;
  
  
  API key restrictions help but don't stop abuse
&lt;/h3&gt;

&lt;p&gt;You should restrict your API keys to specific APIs and referrers. This is good practice. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP restrictions don't help if the attacker uses the same IP range as your legitimate traffic&lt;/li&gt;
&lt;li&gt;Referrer restrictions can be spoofed&lt;/li&gt;
&lt;li&gt;Restrictions don't stop someone who already has the key and knows what APIs it's authorized for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Restrictions reduce the blast radius. They don't eliminate the risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Works: Request Volume Monitoring
&lt;/h2&gt;

&lt;p&gt;The GCP billing system lags. But the &lt;strong&gt;GCP Cloud Monitoring API&lt;/strong&gt; doesn't.&lt;/p&gt;

&lt;p&gt;Specifically, the &lt;code&gt;serviceruntime.googleapis.com/api/request_count&lt;/code&gt; metric gives you near-real-time request counts per API key. This is what GCP uses internally. And it's accessible via the &lt;code&gt;timeSeries.list&lt;/code&gt; API with just a &lt;code&gt;monitoring.timeSeries.list&lt;/code&gt; permission.&lt;/p&gt;

&lt;p&gt;Here's what a simple poll looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified — real implementation handles auth via service account JWT&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://monitoring.googleapis.com/v3/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gcpProjectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/timeSeries?`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`filter=metric.type="serviceruntime.googleapis.com/api/request_count"`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;amp;interval.startTime=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;interval.endTime=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// data.timeSeries contains request counts per credential_id (API key UID)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;metric.labels.credential_id&lt;/code&gt; — the unique identifier of the API key. You can map this back to specific keys and compare against thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; GCP returns cumulative counts over the requested time window. If you poll every minute with a 24-hour window, you get the total requests for that key today. When that number crosses your threshold, you revoke the key.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Build Your Own (DIY Version)
&lt;/h2&gt;

&lt;p&gt;If you want to build this yourself, here's the minimal architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a minimal IAM role
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam roles create my_api_monitor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"API Key Monitor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apikeys.keys.delete,apikeys.keys.get,apikeys.keys.list,monitoring.timeSeries.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all you need. No owner role. No billing access. No editor permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Poll the monitoring API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_request_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;monitoring&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;timeSeries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metric.type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serviceruntime.googleapis.com/api/request_count&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;interval.startTime&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;interval.endTime&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aggregation.alignmentPeriod&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;86400s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aggregation.perSeriesAligner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALIGN_SUM&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timeSeries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credential_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;credential_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;int64Value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Evaluate thresholds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_and_revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monitored_keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;apikeys_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apikeys&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;monitored_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credential_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;threshold&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Revoke the key
&lt;/span&gt;            &lt;span class="n"&gt;apikeys_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resource_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revoked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;display_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; requests &amp;gt; threshold &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Send alert (email, Slack, etc.)
&lt;/span&gt;            &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run it on a schedule
&lt;/h3&gt;

&lt;p&gt;On a Cloud Run job, a cron on a cheap VM, or any scheduler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cloud Scheduler → Cloud Run job every minute&lt;/span&gt;
&lt;span class="c"&gt;# Or a simple cron:&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; python3 /opt/api-monitor/check_keys.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the core of it. The full implementation adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deduplication (don't revoke the same key twice)&lt;/li&gt;
&lt;li&gt;Retry logic for GCP API failures&lt;/li&gt;
&lt;li&gt;Proper JWT auth for service accounts&lt;/li&gt;
&lt;li&gt;Storage for monitored keys and thresholds&lt;/li&gt;
&lt;li&gt;Email notifications&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Gotchas I Hit Building This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Permission casing matters
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;monitoring.timeSeries.list&lt;/code&gt; — capital &lt;strong&gt;S&lt;/strong&gt; in timeSeries. I spent an embarrassing amount of time getting 403 errors because I wrote &lt;code&gt;monitoring.timeseries.list&lt;/code&gt;. GCP is case-sensitive here.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. GCP doesn't return zero-count keys
&lt;/h3&gt;

&lt;p&gt;If a key has zero requests in the time window, it doesn't appear in the timeSeries response at all. Your polling logic needs to handle this — keys with no data have zero requests, not missing data.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Billing vs monitoring delay
&lt;/h3&gt;

&lt;p&gt;Billing data lags 4-12 hours. Monitoring data lags 3-5 minutes. If you're using Cloud Monitoring (not billing), your detection is much faster. Always use the monitoring API, not billing data, for real-time detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cumulative vs delta counts
&lt;/h3&gt;

&lt;p&gt;The API returns cumulative counts over the requested time window. Don't subtract consecutive polls to get deltas — just compare the total against your daily/hourly threshold directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. GCP alerting policies add too much latency
&lt;/h3&gt;

&lt;p&gt;I initially tried using GCP alerting policies + pub/sub + Cloud Functions. The additional latency from alerting policy evaluation added 15-25 minutes on top of the 3-5 minute metric delay. Direct polling is faster and simpler.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Built
&lt;/h2&gt;

&lt;p&gt;After going through all of the above myself, I packaged it into &lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — a hosted version with a dashboard, per-key thresholds, and automatic revocation.&lt;/p&gt;

&lt;p&gt;The setup is one gcloud command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project YOUR_PROJECT &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;apikeys.googleapis.com monitoring.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud iam roles create cloudsentinel_XXXXXXXX &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CloudSentinel Monitor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apikeys.keys.delete,apikeys.keys.get,apikeys.keys.list,monitoring.timeSeries.list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud projects add-iam-policy-binding YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:cloudsentinel@cloudsentinel-dev.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"projects/YOUR_PROJECT/roles/cloudsentinel_XXXXXXXX"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;None &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's early — I launched last week. 14-day free trial, no credit card. If you'd rather build your own using the code above, that's completely fine too — that's exactly what this article is for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Should Do Right Now
&lt;/h2&gt;

&lt;p&gt;Whether you use CloudSentinel or build your own, here's the minimum you should have in place for any GCP project with API keys:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Restrict your API keys&lt;/strong&gt;&lt;br&gt;
In GCP Console → APIs &amp;amp; Services → Credentials → edit each key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict to specific APIs only&lt;/li&gt;
&lt;li&gt;Add referrer/IP restrictions where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Set a budget alert&lt;/strong&gt;&lt;br&gt;
Not a replacement for monitoring, but a backstop. Set it at 50% of your expected monthly spend so you get early warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never commit API keys to code&lt;/strong&gt;&lt;br&gt;
Use Secret Manager or environment variables. Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. Use &lt;code&gt;git-secrets&lt;/code&gt; or &lt;code&gt;gitleaks&lt;/code&gt; as a pre-commit hook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Audit who has access to your GCP project&lt;/strong&gt;&lt;br&gt;
Go to IAM → check every member and role. Remove anything you don't recognize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitor request volume&lt;/strong&gt;&lt;br&gt;
Either build the script above, use CloudSentinel, or set up a Cloud Monitoring alert on &lt;code&gt;serviceruntime.googleapis.com/api/request_count&lt;/code&gt;. Any of these is better than nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The developers who lost $55K, $82K, $128K weren't careless. They were building, shipping, and learning. API key leaks happen to good engineers. The gap is that GCP gives you alerts but no automatic response.&lt;/p&gt;

&lt;p&gt;A budget alert tells you your house is on fire. CloudSentinel (or your own version of it) calls the fire department automatically.&lt;/p&gt;

&lt;p&gt;Set up some form of automatic protection before you need it. The setup takes 5 minutes. The regret takes longer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm David, one of the founders of CloudSentinel. If you have questions about the GCP monitoring API, the IAM setup, or anything in this article — drop a comment. Happy to help whether or not you use our tool.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — automatic GCP API key revocation. 14-day free trial, no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>googlecloud</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>GCP Has No Automatic Kill Switch for Leaked API Keys. Here's What I Built.</title>
      <dc:creator>Cloudsentinel.dev</dc:creator>
      <pubDate>Thu, 30 Apr 2026 09:51:43 +0000</pubDate>
      <link>https://dev.to/cloudsentinel_official/gcp-has-no-automatic-kill-switch-for-leaked-api-keys-heres-what-i-built-44nh</link>
      <guid>https://dev.to/cloudsentinel_official/gcp-has-no-automatic-kill-switch-for-leaked-api-keys-heres-what-i-built-44nh</guid>
      <description>&lt;p&gt;&lt;em&gt;And what you can do right now to protect yourself — whether you use my tool or not.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I kept seeing posts like this on Reddit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Woke up to a $128,000 Google Cloud bill. Key was compromised overnight. Google denied the adjustment request."&lt;/p&gt;

&lt;p&gt;"3-person startup. Gemini API key silently reauthorized. Normal monthly spend was $180. Bill: $82,314 in 48 hours."&lt;/p&gt;

&lt;p&gt;"Student. Pushed API key to a private GitHub repo that was accidentally public. Was on summer break. Never saw the alerts. $55,444."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm a developer building on GCP myself. After reading enough of these, I realized I had zero automatic protection. If one of my keys got leaked tonight, the only thing standing between me and a five-figure bill was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hoping I'd see a budget alert email in time&lt;/li&gt;
&lt;li&gt;Being awake&lt;/li&gt;
&lt;li&gt;Logging in fast enough&lt;/li&gt;
&lt;li&gt;Finding the right key&lt;/li&gt;
&lt;li&gt;Deleting it manually&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's a terrible safety net.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — an automatic kill switch for GCP API keys. But this article isn't a pitch. It's an honest walkthrough of the problem, why GCP's existing tools fall short, and what you can do about it — including building your own solution if you prefer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why GCP's Built-In Tools Aren't Enough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Budget alerts lag 4-12 hours
&lt;/h3&gt;

&lt;p&gt;GCP billing data is not real-time. It's aggregated and delivered with a delay. By the time a budget alert fires, the damage is often already done.&lt;/p&gt;

&lt;p&gt;Here's the timeline of a typical incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00:00 — API key leaks (committed to GitHub, exposed in frontend, etc.)
00:03 — Automated scanners find the key
00:05 — Attacker starts making requests
04:00 — GCP billing data updates
04:30 — Budget alert email arrives
04:31 — You're asleep
08:00 — You wake up and see the email
08:05 — You log in and delete the key
08:05 — Damage: 8 hours of uncontrolled API usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Budget alerts are better than nothing. But they depend on someone being awake, seeing the email, and acting fast enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spend Caps are a blunt instrument
&lt;/h3&gt;

&lt;p&gt;Google recently announced Spend Caps for some services (Gemini, Cloud Run, Maps). Good first step. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They pause &lt;strong&gt;all&lt;/strong&gt; traffic in your project — not just the abused key&lt;/li&gt;
&lt;li&gt;Your other keys and services stop working too&lt;/li&gt;
&lt;li&gt;They're spend-based, not request-based — they trigger after money is already spent&lt;/li&gt;
&lt;li&gt;They don't cover every GCP API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have 5 API keys in a project and one gets compromised, Spend Caps kill all 5. Your app goes down. CloudSentinel revokes only the specific key that crossed the threshold — everything else keeps running.&lt;/p&gt;

&lt;h3&gt;
  
  
  API key restrictions help but don't stop abuse
&lt;/h3&gt;

&lt;p&gt;You should restrict your API keys to specific APIs and referrers. This is good practice. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP restrictions don't help if the attacker uses the same IP range as your legitimate traffic&lt;/li&gt;
&lt;li&gt;Referrer restrictions can be spoofed&lt;/li&gt;
&lt;li&gt;Restrictions don't stop someone who already has the key and knows what APIs it's authorized for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Restrictions reduce the blast radius. They don't eliminate the risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Works: Request Volume Monitoring
&lt;/h2&gt;

&lt;p&gt;The GCP billing system lags. But the &lt;strong&gt;GCP Cloud Monitoring API&lt;/strong&gt; doesn't.&lt;/p&gt;

&lt;p&gt;Specifically, the &lt;code&gt;serviceruntime.googleapis.com/api/request_count&lt;/code&gt; metric gives you near-real-time request counts per API key. This is what GCP uses internally. And it's accessible via the &lt;code&gt;timeSeries.list&lt;/code&gt; API with just a &lt;code&gt;monitoring.timeSeries.list&lt;/code&gt; permission.&lt;/p&gt;

&lt;p&gt;Here's what a simple poll looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified — real implementation handles auth via service account JWT&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`https://monitoring.googleapis.com/v3/projects/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gcpProjectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/timeSeries?`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`filter=metric.type="serviceruntime.googleapis.com/api/request_count"`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;`&amp;amp;interval.startTime=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;interval.endTime=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// data.timeSeries contains request counts per credential_id (API key UID)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes &lt;code&gt;metric.labels.credential_id&lt;/code&gt; — the unique identifier of the API key. You can map this back to specific keys and compare against thresholds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; GCP returns cumulative counts over the requested time window. If you poll every minute with a 24-hour window, you get the total requests for that key today. When that number crosses your threshold, you revoke the key.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Build Your Own (DIY Version)
&lt;/h2&gt;

&lt;p&gt;If you want to build this yourself, here's the minimal architecture:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create a minimal IAM role
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam roles create my_api_monitor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"API Key Monitor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apikeys.keys.delete,apikeys.keys.get,apikeys.keys.list,monitoring.timeSeries.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all you need. No owner role. No billing access. No editor permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Poll the monitoring API
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_request_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;monitoring&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;timeSeries&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metric.type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serviceruntime.googleapis.com/api/request_count&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;interval.startTime&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;interval.endTime&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aggregation.alignmentPeriod&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;86400s&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aggregation.perSeriesAligner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ALIGN_SUM&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timeSeries&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;labels&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credential_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;credential_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;int64Value&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Evaluate thresholds
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_and_revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monitored_keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;apikeys_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apikeys&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;monitored_keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;credential_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credential_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;threshold&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credential_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Revoke the key
&lt;/span&gt;            &lt;span class="n"&gt;apikeys_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resource_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Revoked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;display_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; requests &amp;gt; threshold &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Send alert (email, Slack, etc.)
&lt;/span&gt;            &lt;span class="nf"&gt;send_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run it on a schedule
&lt;/h3&gt;

&lt;p&gt;On a Cloud Run job, a cron on a cheap VM, or any scheduler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cloud Scheduler → Cloud Run job every minute&lt;/span&gt;
&lt;span class="c"&gt;# Or a simple cron:&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; python3 /opt/api-monitor/check_keys.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the core of it. The full implementation adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deduplication (don't revoke the same key twice)&lt;/li&gt;
&lt;li&gt;Retry logic for GCP API failures&lt;/li&gt;
&lt;li&gt;Proper JWT auth for service accounts&lt;/li&gt;
&lt;li&gt;Storage for monitored keys and thresholds&lt;/li&gt;
&lt;li&gt;Email notifications&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Gotchas I Hit Building This
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Permission casing matters
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;monitoring.timeSeries.list&lt;/code&gt; — capital &lt;strong&gt;S&lt;/strong&gt; in timeSeries. I spent an embarrassing amount of time getting 403 errors because I wrote &lt;code&gt;monitoring.timeseries.list&lt;/code&gt;. GCP is case-sensitive here.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. GCP doesn't return zero-count keys
&lt;/h3&gt;

&lt;p&gt;If a key has zero requests in the time window, it doesn't appear in the timeSeries response at all. Your polling logic needs to handle this — keys with no data have zero requests, not missing data.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Billing vs monitoring delay
&lt;/h3&gt;

&lt;p&gt;Billing data lags 4-12 hours. Monitoring data lags 3-5 minutes. If you're using Cloud Monitoring (not billing), your detection is much faster. Always use the monitoring API, not billing data, for real-time detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cumulative vs delta counts
&lt;/h3&gt;

&lt;p&gt;The API returns cumulative counts over the requested time window. Don't subtract consecutive polls to get deltas — just compare the total against your daily/hourly threshold directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. GCP alerting policies add too much latency
&lt;/h3&gt;

&lt;p&gt;I initially tried using GCP alerting policies + pub/sub + Cloud Functions. The additional latency from alerting policy evaluation added 15-25 minutes on top of the 3-5 minute metric delay. Direct polling is faster and simpler.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Built
&lt;/h2&gt;

&lt;p&gt;After going through all of the above myself, I packaged it into &lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — a hosted version with a dashboard, per-key thresholds, and automatic revocation.&lt;/p&gt;

&lt;p&gt;The setup is one gcloud command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project YOUR_PROJECT &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud services &lt;span class="nb"&gt;enable &lt;/span&gt;apikeys.googleapis.com monitoring.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud iam roles create cloudsentinel_XXXXXXXX &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CloudSentinel Monitor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--permissions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apikeys.keys.delete,apikeys.keys.get,apikeys.keys.list,monitoring.timeSeries.list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
gcloud projects add-iam-policy-binding YOUR_PROJECT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:cloudsentinel@cloudsentinel-dev.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"projects/YOUR_PROJECT/roles/cloudsentinel_XXXXXXXX"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;None &lt;span class="nt"&gt;--quiet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's early — I launched last week. 14-day free trial, no credit card. If you'd rather build your own using the code above, that's completely fine too — that's exactly what this article is for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Should Do Right Now
&lt;/h2&gt;

&lt;p&gt;Whether you use CloudSentinel or build your own, here's the minimum you should have in place for any GCP project with API keys:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Restrict your API keys&lt;/strong&gt;&lt;br&gt;
In GCP Console → APIs &amp;amp; Services → Credentials → edit each key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Restrict to specific APIs only&lt;/li&gt;
&lt;li&gt;Add referrer/IP restrictions where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Set a budget alert&lt;/strong&gt;&lt;br&gt;
Not a replacement for monitoring, but a backstop. Set it at 50% of your expected monthly spend so you get early warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never commit API keys to code&lt;/strong&gt;&lt;br&gt;
Use Secret Manager or environment variables. Add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. Use &lt;code&gt;git-secrets&lt;/code&gt; or &lt;code&gt;gitleaks&lt;/code&gt; as a pre-commit hook.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Audit who has access to your GCP project&lt;/strong&gt;&lt;br&gt;
Go to IAM → check every member and role. Remove anything you don't recognize.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Monitor request volume&lt;/strong&gt;&lt;br&gt;
Either build the script above, use CloudSentinel, or set up a Cloud Monitoring alert on &lt;code&gt;serviceruntime.googleapis.com/api/request_count&lt;/code&gt;. Any of these is better than nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The developers who lost $55K, $82K, $128K weren't careless. They were building, shipping, and learning. API key leaks happen to good engineers. The gap is that GCP gives you alerts but no automatic response.&lt;/p&gt;

&lt;p&gt;A budget alert tells you your house is on fire. CloudSentinel (or your own version of it) calls the fire department automatically.&lt;/p&gt;

&lt;p&gt;Set up some form of automatic protection before you need it. The setup takes 5 minutes. The regret takes longer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm David, one of the founders of CloudSentinel. If you have questions about the GCP monitoring API, the IAM setup, or anything in this article — drop a comment. Happy to help whether or not you use our tool.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://cloudsentinel.dev" rel="noopener noreferrer"&gt;CloudSentinel&lt;/a&gt; — automatic GCP API key revocation. 14-day free trial, no credit card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>googlecloud</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
