Claude API Keys rotieren, ohne dass Anwendungen aussteigen
Nach dem Rotieren von API Keys werfen KI-Anwendungen plötzlich 401-Fehler, weil im Node.js Lambda noch ein abgelaufener Token im Cache hängt. Ein Guide zeigt, wie sich Claude Secrets sauber rotieren lassen.

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"}' Enter fullscreen mode Exit fullscreen mode apiKey is the field that our code will look for. You could store additional metadata (e.g., rotation schedule) in the same JSON object.
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:
npm install @aws-sdk/client-secrets-manager @aws-sdk/client-eventbridge Enter fullscreen mode Exit fullscreen mode 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 ()} `; } Enter fullscreen mode Exit fullscreen mode Explanation of key lines
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:
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 ), }; }; Enter fullscreen mode Exit fullscreen mode Line‑by‑line notes
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 " ); }; Enter fullscreen mode Exit fullscreen mode 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.
// 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 ({}); })(); Enter fullscreen mode Exit fullscreen mode Monitoring CloudWatch Logs – add console.log statements (as in the examples) to track when the key is loaded or the cache is cleared. Metrics – publish a custom CloudWatch metric ( SecretCacheMisses ) each time loadKey() runs.
A spike indicates frequent rotations or unexpected cache clears. Alarms – set a threshold on 4XXError for 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.
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!
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.