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 Secret Rotation Matters for LLM APIs
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.
Key points
- Rotation reduces the window an attacker can misuse a leaked key.
- Serverless containers cache values for performance, which can hide a rotation.
- You need a way to tell the running container, “Hey, the note is stale—refresh it.”
Setting Up Secrets Manager for Claude Tokens
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.
Create the secret
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.
Cost awareness (gotchas)
- Secrets Manager costs $0.40 per secret per month. 100 micro‑services → $40/month minimum.
- Each
GetSecretValuecall is billed. Calling it on every Lambda invocation can add up quickly. - During a rotation window the secret is temporarily unavailable; your code must handle the brief failure.
- If you need to read the secret from another AWS account, you must attach a resource policy to the secret and grant the calling role an IAM policy permission.
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.
Lambda Rotation Function – Code Walkthrough
The rotation function does three things:
- Generate or fetch a new Claude API key (this example uses a placeholder function).
-
Write the new key back to Secrets Manager with
PutSecretValue. - Publish a custom EventBridge event that tells every service Lambda to discard its cache.
Dependencies
npm install @aws-sdk/client-secrets-manager @aws-sdk/client-eventbridge
Rotation Lambda (TypeScript)
// 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
-
SecretsManagerClientandEventBridgeClientare thin wrappers around the AWS SDK that let us talk to the two services. -
PutSecretValueCommandoverwrites the current secret value with a fresh JSON payload. -
PutEventsCommandsends a custom event; any Lambda listening for theClaudeSecretRotateddetail type will receive it. -
generateNewClaudeKeyis 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.
Integrating Rotation with Your Node.js 22 Service
Your production Lambda that calls Claude (via Bedrock) will:
- Load the secret once at cold start.
- Store the API key in a module‑level variable (
cachedKey). - Subscribe to the custom EventBridge event using the Node.js
diagnostics_channelAPI. When the event arrives, it clears the cache so the next request fetches the fresh key.
Why use diagnostics_channel?
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.
Service Lambda (JavaScript)
// 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
-
cachedKeylives at module scope; it survives between Lambda invocations. -
loadKey()callsGetSecretValueCommandonce per container lifecycle, avoiding the per‑invocation cost. -
rotationChannel.subscribelistens for the custom event. When the secret ARN matches, it empties the cache. - The handler checks
if (!cachedKey)before each request; after a rotation event the next request will reload the secret automatically.
Wiring EventBridge to the diagnostics channel
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.
Testing, Monitoring, and Avoiding Common Pitfalls
Local testing
-
Mock Secrets Manager – use the AWS SDK’s
MockClientfromaws-sdk-client-mockto return a fake secret. -
Emit a fake rotation event – call
rotationChannel.publish({ secretArn: "arn:aws:secretsmanager:...:secret:prod/claude/api-key" })and verify thatcachedKeybecomesnull. - Run the handler twice – first invocation should load the key, second should reuse the cache. After publishing the rotation event, a third invocation should reload the key.
// 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 loading)
await serviceHandler({});
// Simulate rotation
const rotationChannel = createChannel("claude-secret-rotation");
rotationChannel.publish({ secretArn: process.env.CLAUDE_SECRET_NAME });
// Third call – should reload
await serviceHandler({});
})();
Monitoring
-
CloudWatch Logs – add
console.logstatements (as in the examples) to track when the key is loaded or the cache is cleared. -
Metrics – publish a custom CloudWatch metric (
SecretCacheMisses) each timeloadKey()runs. A spike indicates frequent rotations or unexpected cache clears. -
Alarms – set a threshold on
4XXErrorfor your service Lambda. If you see a burst of 401s after a rotation, your cache‑clear logic likely failed.
Common gotchas and how to dodge them
| 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 or qualifier 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.
End‑to‑end sanity check
- Deploy the secret, service Lambda, rotation Lambda, and bridge Lambda.
- Run a test request to the service Lambda – you should see “🔑 Loaded fresh Claude key” once.
- Manually invoke the rotation Lambda (or trigger its schedule).
- Observe the bridge Lambda forward the event, the service Lambda log “♻️ Cache cleared”, and the next request log “🔑 Loaded fresh Claude key” again.
- Verify no 401 errors appear after rotation.
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.
The Takeaway
What you should remember after reading this guide
- Rotating LLM API keys is a security best practice, but serverless containers cache secrets in memory, which can cause stale‑token errors.
- Store the Claude key in Secrets Manager, but read it once per container and keep it in a module‑level variable.
- Use a lightweight notification (EventBridge + diagnostics_channel) to tell running Lambdas when the secret changes, so they can drop their cache.
- Pay attention to hidden costs: secret‑manager pricing, per‑call charges, and Lambda features like provisioned concurrency that can surprise your budget.
- Test the cache‑clear path locally, monitor cache misses in CloudWatch, and set alarms on 401 responses.
- Follow the table of common gotchas to avoid pitfalls that often appear when mixing Secrets Manager, Lambda, and VPCs.
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.
Top comments (0)