<?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: Antoine</title>
    <description>The latest articles on DEV Community by Antoine (@cracadumi1).</description>
    <link>https://dev.to/cracadumi1</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%2F3886974%2F07895bbb-fcd1-4296-b477-5d9e01233a07.jpeg</url>
      <title>DEV Community: Antoine</title>
      <link>https://dev.to/cracadumi1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cracadumi1"/>
    <language>en</language>
    <item>
      <title>Stop hardcoding API keys in your AI agents — how I built a governance layer in 3 weeks</title>
      <dc:creator>Antoine</dc:creator>
      <pubDate>Sun, 19 Apr 2026 06:36:48 +0000</pubDate>
      <link>https://dev.to/cracadumi1/stop-hardcoding-api-keys-in-your-ai-agents-how-i-built-a-governance-layer-in-3-weeks-233k</link>
      <guid>https://dev.to/cracadumi1/stop-hardcoding-api-keys-in-your-ai-agents-how-i-built-a-governance-layer-in-3-weeks-233k</guid>
      <description>&lt;p&gt;Three weeks ago I got tired of pasting API keys into &lt;code&gt;.env&lt;/code&gt; files every time I spun up a new AI agent. GitHub, Linear, Stripe, Notion, Slack, Vercel — each agent ended up with god-mode credentials to half my stack, with no approval flow, no audit trail, no revocation story.&lt;/p&gt;

&lt;p&gt;I went looking for a tool to fix this. Secrets managers like Vault and 1Password store secrets well but don't model &lt;em&gt;agents&lt;/em&gt;, &lt;em&gt;approvals&lt;/em&gt;, or &lt;em&gt;agent-initiated tool requests&lt;/em&gt;. So I built one. It's called &lt;a href="https://agentkey.dev" rel="noopener noreferrer"&gt;AgentKey&lt;/a&gt;, and it launched today. Here's the interesting stuff under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model: zero-access by default
&lt;/h2&gt;

&lt;p&gt;Every agent starts with &lt;strong&gt;zero access&lt;/strong&gt;. To use a tool, it does this over plain HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/tools
Authorization: Bearer {agent_key}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response lists the catalog and this agent's access status per tool. If it needs something it doesn't have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/tools/{tool_id}/request
{ "reason": "Need to open PRs on behalf of the user" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A human approves once in a dashboard. From that moment, the agent can fetch the credential on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/tools/{tool_id}/credentials
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and only at the moment of fetch. The agent never stores the credential; it's vended fresh each call, rate-limited, and logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encryption: AES-256-GCM with per-record IV
&lt;/h2&gt;

&lt;p&gt;Naive encryption schemes reuse the IV. AgentKey generates a fresh 12-byte IV for every secret using &lt;code&gt;crypto.randomBytes(12)&lt;/code&gt;, appends the 16-byte GCM auth tag, and stores the tuple base64url-encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/src/lib/crypto.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&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;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;base64url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decryption pulls the IV and tag off the front, calls &lt;code&gt;setAuthTag()&lt;/code&gt; before &lt;code&gt;final()&lt;/code&gt;, and fails cleanly on tamper. Every secret in the database has its own IV — compromise of one doesn't weaken the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timing-safe API key verification
&lt;/h2&gt;

&lt;p&gt;Agent API keys are SHA-256 hashed at rest. Verification uses &lt;code&gt;crypto.timingSafeEqual()&lt;/code&gt; to prevent timing-based key recovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyAgentApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;providedHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashAgentApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providedHash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is table-stakes but it's worth saying: if you're comparing hashes with &lt;code&gt;===&lt;/code&gt;, a patient attacker can recover the key one byte at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Append-only audit log
&lt;/h2&gt;

&lt;p&gt;Every action — request submitted, approval granted, credential vended, grant revoked — goes in an audit log that doesn't allow UPDATE or DELETE at the schema level. This is enforced in Drizzle's type system and would need a destructive migration to break. Audit entries link actor (agent, human, system) to target (tool, grant, secret) with signed metadata.&lt;/p&gt;

&lt;p&gt;The trick I like: the &lt;em&gt;fetch&lt;/em&gt; of a credential is an audited event. If an agent's key is compromised and an attacker starts fetching tools, you see it in the log the moment they do — they can't just pull the secret and go silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wild part: agent-driven catalog
&lt;/h2&gt;

&lt;p&gt;This is the one I'm least sure about. When an agent hits a tool that isn't in the catalog, it can submit a suggestion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/tools/suggest
{ "name": "Linear", "url": "https://linear.app", "reason": "..." }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple agents can back the same suggestion. The admin sees &lt;strong&gt;aggregated demand&lt;/strong&gt; — "Onboarding-Agent + 2 others want Linear" — not one-off tickets. You approve once, the whole fleet gets access.&lt;/p&gt;

&lt;p&gt;The bet: in a world where you have 50 agents doing 50 jobs, the catalog should reflect what they actually need, not what an admin guessed they'd need a month ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16&lt;/strong&gt; (App Router, Server Components)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drizzle ORM&lt;/strong&gt; + &lt;strong&gt;Neon Postgres&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upstash Redis&lt;/strong&gt; — 4-tier rate limiting (agent reads, requests, credential fetches, admin ops)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clerk&lt;/strong&gt; — human auth (org members manage the dashboard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — hosting, with AI Gateway for one AI feature below&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM&lt;/strong&gt; via &lt;code&gt;node:crypto&lt;/code&gt;, no third-party crypto lib&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~26,000 lines of TypeScript, 87 commits, single developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one AI feature
&lt;/h2&gt;

&lt;p&gt;I tried hard to not pile AI on AI. One feature earned its keep: paste a product's docs URL, and the admin gets a streaming setup guide — markdown instructions for creating an API key, scopes to select, where to find it. Streamed via SSE, powered by Vercel AI Gateway. Cold-start pain is real when your "tool catalog" is empty, so this makes first-add fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The license choice: BSL 1.1 → Apache 2.0 in 2030
&lt;/h2&gt;

&lt;p&gt;This is the one I debate with myself. I went with &lt;strong&gt;BSL 1.1&lt;/strong&gt; — source-available, self-hostable, permissive for everyone &lt;em&gt;except&lt;/em&gt; someone running it as a competing managed service. On &lt;strong&gt;April 1, 2030&lt;/strong&gt;, it auto-converts to Apache 2.0.&lt;/p&gt;

&lt;p&gt;Why: I want people to trust the code, read it, modify it, self-host it. I don't want AWS launching "Amazon AgentKey" six months in. 2030 gives enough runway to figure out what this is. Then it's truly open.&lt;/p&gt;

&lt;p&gt;I know BSL has vocal critics. I'm open to being wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's rough (honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No pre-seeded integrations.&lt;/strong&gt; You build your catalog from your docs URLs. The AI setup guide helps, but cold start is still real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No RBAC in V1.&lt;/strong&gt; All org members are full admins. Fine for small teams, won't fly enterprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared credential rotation is manual.&lt;/strong&gt; Admin updates the secret, agents fetch the new one on next call. Automatic rotation + secrets-manager integration is on the roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP is not a first-class primitive yet.&lt;/strong&gt; It probably should be.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://agentkey.dev" rel="noopener noreferrer"&gt;agentkey.dev&lt;/a&gt; — free forever managed, or self-host (docker-compose, Neon + Upstash + Clerk marketplace integrations handle the infra).&lt;/p&gt;

&lt;p&gt;If you're building AI agents and you've been ignoring the credential problem, this is the nudge to stop. If you've solved it a different way — Vault pattern, 1Password SDK, custom — I genuinely want to hear how.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Launched on Product Hunt today: &lt;a href="https://www.producthunt.com/products/agentkey-agent-access-management" rel="noopener noreferrer"&gt;AgentKey on PH&lt;/a&gt;. Feedback, roasts, and war stories welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
