cd /news/ai-tools/how-to-rotate-claude-api-keys-with-s… · home › topics › ai-tools › article
[ARTICLE · art-151207] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

How to Rotate Claude API Keys with Secrets Manager and Lambda Without Getting Locked Out

A developer has published a guide on rotating Claude API keys safely using AWS Secrets Manager and Lambda, addressing the common failure where serverless functions keep serving a stale cached secret and return 401 Unauthorized errors. The approach stores the key in Secrets Manager, uses a rotation Lambda to call PutSecretValue, and emits an EventBridge event named "ClaudeSecretRotated" so downstream services can invalidate their in-process caches.

by read9 min views1 publishedOct 16, 2026

Developers rush to rotate API keys for security, only to discover their AI‑powered apps suddenly start throwing 401 errors. The hidden culprit? A stale token cached in your Node.js Lambda. This guide shows exactly how to rotate Claude secrets safely, so your code keeps talking to the model without a hiccup.

Why rotate?

Any credential that lets your code talk to an LLM (large language model) is a doorway to a costly service. If a key leaks, an attacker can run expensive prompts on your bill. Rotating the key regularly closes that door behind a potential intruder.

Why is rotation tricky with serverless code?

Serverless functions like AWS Lambda keep a small amount of memory alive between invocations. Many developers store a secret in a module‑level variable to avoid a network call on every request. That works—until you change the secret. The function continues to read the old value from its in‑process cache, and the LLM replies with 401 Unauthorized. Because the cache lives only inside the container, the problem disappears only when AWS discards the container (a “cold start”).

In plain English: Think of the cached secret as a sticky note on your desk. If you change the password, the note doesn’t automatically update; you have to throw it away and write a new one.

What is Secrets Manager?

AWS Secrets Manager is a fully managed vault that stores secrets (like passwords, API keys, or database credentials) and can rotate them automatically. It offers a simple API (GetSecretValue, PutSecretValue) to read and write secrets.

aws secretsmanager create-secret \
  --name prod/claude/api-key \
  --description "Claude API key for production" \
  --secret-string '{"apiKey":"YOUR_INITIAL_KEY"}'

apiKey is the field that our code will look for. You could store additional metadata (e.g., rotation schedule) in the same JSON object.

GetSecretValue call is billed. Calling it on every Lambda invocation can add up quickly. Tip: Store the secret in a dedicated “secrets” account and give each micro‑service read‑only access via cross‑account roles. This isolates billing and reduces blast radius.

The rotation function does three things:

PutSecretValue.

npm install @aws-sdk/client-secrets-manager @aws-sdk/client-eventbridge
js
// rotationLambda.ts
import {
  SecretsManagerClient,
  PutSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import {
  EventBridgeClient,
  PutEventsCommand,
} from "@aws-sdk/client-eventbridge";

// The ARN of the secret we are rotating
const SECRET_ARN = process.env.CLAUDE_SECRET_ARN!;

// Clients are created outside the handler so they are reused across invocations
const smClient = new SecretsManagerClient({});
const ebClient = new EventBridgeClient({});

export const handler = async (): Promise<void> => {
  // 1️⃣ Get a brand‑new Claude API key.
  // In real life you would call Claude's key‑generation endpoint or a secure vault.
  const newApiKey = await generateNewClaudeKey();

  // 2️⃣ Write the fresh key back to Secrets Manager.
  // The secret value is a JSON string; we keep the same shape as the original.
  const putCmd = new PutSecretValueCommand({
    SecretId: SECRET_ARN,
    SecretString: JSON.stringify({ apiKey: newApiKey }),
  });
  await smClient.send(putCmd);
  console.log("✅ Updated secret with new Claude key");

  // 3️⃣ Tell the rest of the world that the secret changed.
  // We emit a custom EventBridge event named "ClaudeSecretRotated".
  const eventCmd = new PutEventsCommand({
    Entries: [
      {
        Source: "custom.claude.rotation",
        DetailType: "ClaudeSecretRotated",
        Detail: JSON.stringify({ secretArn: SECRET_ARN }),
        EventBusName: "default", // could be a dedicated bus
      },
    ],
  });
  await ebClient.send(eventCmd);
  console.log("🔔 Published rotation event");
};

/**
 * Placeholder for whatever process creates a new Claude key.
 * Replace with real API call or manual step.
 */
async function generateNewClaudeKey(): Promise<string> {
  // Simulate network latency
  await new Promise((res) => setTimeout(res, 200));
  // Return a dummy key – never commit real keys to source control!
  return `claude-${Date.now()}`;
}

Explanation of key lines

SecretsManagerClient and EventBridgeClient are thin wrappers around the AWS SDK that let us talk to the two services.PutSecretValueCommand overwrites the current secret value with a fresh JSON payload.PutEventsCommand sends a custom event; any Lambda listening for the ClaudeSecretRotated detail type will receive it.generateNewClaudeKey is a stub; in production you would replace it with a secure call to Claude’s key‑generation endpoint. Key takeaway: The rotation Lambda does not need to know about your service’s cache; it simply announces “the secret changed.” The service Lambdas decide how to react.

Your production Lambda that calls Claude (via Bedrock) will:

cachedKey). diagnostics_channel API. When the event arrives, it clears the cache so the next request fetches the fresh key. diagnostics_channel is a built‑in Node.js feature that lets different parts of a process talk without adding a heavy library. It works even when the Lambda is running in a VPC, where network‑based pub/sub could be slower.

// serviceLambda.js
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import { BedrockClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
import { createChannel } from "diagnostics_channel";

// The name of the secret that holds the Claude key
const SECRET_NAME = process.env.CLAUDE_SECRET_NAME;

// Clients are reused across invocations for performance
const smClient = new SecretsManagerClient({});
const bedrock = new BedrockClient({});

// Module‑level cache: stays alive as long as the container lives
let cachedKey = null;

/**
 * Pull the API key from Secrets Manager.
 * We call this only when the cache is empty or has been cleared.
 */
async function loadKey() {
  const cmd = new GetSecretValueCommand({ SecretId: SECRET_NAME });
  const resp = await smClient.send(cmd);
  // Secrets Manager returns a JSON string; we parse to get the field we need.
  const secret = JSON.parse(resp.SecretString);
  cachedKey = secret.apiKey;
  console.log("🔑 Loaded fresh Claude key");
}

/**
 * Clear the in‑process cache. The next request will trigger loadKey().
 */
function refreshCache() {
  cachedKey = null;
  console.log("♻️ Cache cleared – will reload on next call");
}

/**
 * Subscribe to the custom EventBridge event that signals rotation.
 * The EventBridge rule forwards events to this Lambda’s /internal channel.
 */
const rotationChannel = createChannel("claude-secret-rotation");
rotationChannel.subscribe((msg) => {
  // Message shape matches what the rotation Lambda published.
  if (msg.secretArn && msg.secretArn.includes(SECRET_NAME)) {
    refreshCache();
  }
});

/**
 * The Lambda handler that receives API Gateway (or other) requests.
 * It ensures we have a fresh key, then calls Claude via Bedrock.
 */
export const handler = async (event) => {
  // If we have no key, fetch it now.
  if (!cachedKey) {
    await loadKey();
  }

  // Build the request payload for Claude (simplified example)
  const prompt = JSON.stringify({ text: "Hello Claude!" });

  const invokeCmd = new InvokeModelCommand({
    ModelId: "anthropic.claude-v2", // example model identifier
    ContentType: "application/json",
    Accept: "application/json",
    Body: Buffer.from(prompt),
    // Pass the API key via custom header if required by your setup.
    // Some Bedrock integrations use the key implicitly; adjust as needed.
    // Here we illustrate adding it as an HTTP header.
    // Note: Bedrock SDK handles signing; this is just an illustration.
    // headers: { "x-api-key": cachedKey },
  });

  const response = await bedrock.send(invokeCmd);
  const result = JSON.parse(Buffer.from(response.Body).toString());

  return {
    statusCode: 200,
    body: JSON.stringify(result),
  };
};

Line‑by‑line notes

loadKey() calls GetSecretValueCommand rotationChannel.subscribe listens for the custom event. When the secret ARN matches, it empties the cache.if (!cachedKey) before each request; after a rotation event the next request will reload the secret automatically. EventBridge can’t talk directly to diagnostics_channel. Instead we create a bridge Lambda that receives the EventBridge event and forwards it to the same process via the channel. Because both the bridge and the service run in the same container (using the same Lambda version), the forward works instantly.

// bridgeLambda.ts
import { createChannel } from "diagnostics_channel";

const rotationChannel = createChannel("claude-secret-rotation");

// This Lambda is triggered by the EventBridge rule that the rotation function emits.
export const handler = async (event: any) => {
  // Forward the event payload to the in‑process channel.
  rotationChannel.publish(event.detail);
  console.log("🚀 Forwarded rotation event to service Lambda");
};

Deploy the bridge Lambda in the same function version (or use Lambda Layers to share the channel code) so that the publish call reaches the service’s subscriber.

Tip: If you prefer not to run a bridge Lambda, you can configure the EventBridge rule to invoke the service Lambda directly with a payload that includes "action":"refreshCache". The service Lambda then checks the payload and clears its cache. The channel approach keeps the logic inside the process and avoids an extra network hop.

MockClient from aws-sdk-client-mock to return a fake secret.rotationChannel.publish({ secretArn: "arn:aws:secretsmanager:...:secret:prod/claude/api-key" }) and verify that null.

// simple test script (run with node)
import { handler as serviceHandler } from "./serviceLambda.js";
import { createChannel } from "diagnostics_channel";

(async () => {
  // First call – loads key
  await serviceHandler({});
  // Second call – uses cache (no log about )
  await serviceHandler({});

  // Simulate rotation
  const rotationChannel = createChannel("claude-secret-rotation");
  rotationChannel.publish({ secretArn: process.env.CLAUDE_SECRET_NAME });

  // Third call – should reload
  await serviceHandler({});
})();

console.log statements (as in the examples) to track when the key is loaded or the cache is cleared.SecretCacheMisses) each time 4XXError for your service Lambda. If you see a burst of 401s after a rotation, your cache‑clear logic likely failed.

Gotcha Why it hurts Fix
Calling GetSecretValue on every request Each call costs money; you can easily exceed a tiny budget. Cache the value in a module variable and refresh only on rotation events.
Forgetting cross‑account resource policy The service Lambda gets AccessDenied even though the IAM role allows it. Add a resource policy to the secret that lists the external account’s role ARN.
EventBridge rule pointing to the wrong version New code never receives the rotation signal, so cache stays stale. Pin the rule to the alias orqualifier that maps to the version you’re deploying.
SnapStart + VPC disables the cold‑start speedup You think you saved money on latency, but the function still pays VPC attachment time. Disable SnapStart for Lambdas that must attach to a VPC, or move the VPC resources to a private subnet with a NAT gateway.
Provisioned Concurrency left on for idle functions You pay for reserved capacity even when traffic is zero. Use on‑demand concurrency and let CloudWatch Auto Scaling adjust based on traffic patterns.
Lambda@Edge response size >1 MB Edge functions truncate the response, causing mysterious errors. Keep responses under 1 MB or shift the logic to a regional Lambda.

In plain English: Think of each gotcha as a pothole on the road to smooth secret rotation. Spotting them early keeps your journey (and your bill) safe.

If you see a 401, double‑check that the service Lambda actually cleared its cache and that the new key was stored correctly in Secrets Manager.

What you should remember after reading this guide

By wiring rotation events directly into your Lambda’s runtime, you keep the “sticky note” fresh without needing a cold start. Your Claude‑powered applications stay secure and stay online. Happy coding!

Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-10-16 · Primary focus: SecretsManager

All code blocks are intended to be correct and runnable, but please verify them

against the official docs for the tools mentioned before using in production.

Find an error? Drop a comment — corrections are always welcome.

── more in #ai-tools 4 stories · sorted by recency
── more on @claude 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-to-rotate-claude…] indexed:0 read:9min 2026-10-16 · —