Photo by Alex Duffy on Unsplash
To get a secret value with AWS SDK v3, create a SecretsManagerClient and send GetSecretValueCommand with the secret’s name or ARN as SecretId. The value comes back in SecretString, or in SecretBinary as a Uint8Array for binary secrets. Parse JSON yourself, cache the result, and grant secretsmanager:GetSecretValue plus kms:Decrypt for customer managed keys.
This guide is for Node.js and TypeScript developers who need to get a secret value from AWS Secrets Manager with AWS SDK v3, whether in a long-running service, a script or a Lambda function. You’ll end up with a small typed helper that handles both value fields, validates JSON secrets, caches values in memory and turns errors into messages that say what to fix.
If your code still calls new AWS.SecretsManager().getSecretValue(...).promise() from v2, follow the steps to migrate a Node.js app from AWS SDK v2 to v3, or convert the file with the free AWS SDK v2 to v3 converter and then come back here.
What does GetSecretValueCommand return?
By default GetSecretValue returns the version labeled AWSCURRENT. Pass VersionStage: "AWSPREVIOUS" for the version before the last rotation, or a specific VersionId. The fields you’ll use:
| Field | Type in SDK v3 | When it’s set |
|---|---|---|
SecretString |
string |
The secret was stored as text, or created in the console. Console-created secrets are JSON key/value pairs. |
SecretBinary |
Uint8Array |
The secret was stored as binary. The SDK decodes it for you; only the HTTP API, the Python SDK and the AWS CLI return it Base64-encoded. |
ARN, Name |
string |
Always. Useful in logs instead of the value. |
VersionId, VersionStages |
string, string[] |
Always. Tells you which version you got, which matters during rotation. |
The API limits each value field to a length of 65,536. SecretId accepts a name or an ARN, and a secret in another account must be addressed by its full ARN. Secrets Manager writes a CloudTrail entry for every call but leaves the value out of it, so the one place a secret can leak into logs is your own code.
Prerequisites
- Node.js 20 or later, with TypeScript and
tsxfor running the examples. - The
@aws-sdk/client-secrets-managerpackage; the Lambda extension example also uses@aws-sdk/credential-providers. - A secret in the same Region as your client, and credentials that the SDK can find. If you’re unsure which ones it picks up, the guide to AWS SDK v3 credentials providers such as fromIni and fromSSO walks through the order.
How to get a secret value with AWS SDK v3, step by step
- Create one client per processBuild
SecretsManagerClientat module level so connections and credentials are reused. Leaveregionempty to take it fromAWS_REGIONor the profile. - Send GetSecretValueCommandPass the name or full ARN as
SecretId. Use the ARN for cross-account secrets. - Read whichever field is set
SecretStringfor text and JSON,SecretBinaryfor bytes. Handle both so a secret created by another tool doesn’t break you. - Parse and validate JSONCheck that the keys you need exist, and never put the raw value in an error message.
- Cache with a TTLFetch once per few minutes per secret instead of once per request. AWS recommends client-side caching to cut latency and cost.
- Refresh on authentication failureAfter a rotation, drop the cached value and fetch again when the database or API rejects the old credentials.
Example: a typed helper with an in-memory cache
// secrets.ts
// Read a Secrets Manager secret with AWS SDK v3, parse JSON, and cache it in memory with a TTL.
import {
SecretsManagerClient,
GetSecretValueCommand,
ResourceNotFoundException,
DecryptionFailure,
InvalidRequestException,
} from "@aws-sdk/client-secrets-manager";
// One client per process, created outside any handler so it's reused.
const client = new SecretsManagerClient({});
/** Returns the secret as a string, whichever field it was stored in. */
export async function getSecretString(secretId: string): Promise<string> {
const out = await client.send(new GetSecretValueCommand({ SecretId: secretId }));
if (out.SecretString !== undefined) return out.SecretString;
if (out.SecretBinary !== undefined) return Buffer.from(out.SecretBinary).toString("utf8");
throw new Error(`Secret ${secretId} has no SecretString or SecretBinary`);
}
/** Parses a JSON secret and checks that the keys you need are there. */
export async function getJsonSecret<K extends string>(
secretId: string,
requiredKeys: readonly K[],
): Promise<Record<K, string>> {
const raw = await getSecretString(secretId);
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
// Never include `raw` in the message: it is the secret.
throw new Error(`Secret ${secretId} is not valid JSON`);
}
if (typeof parsed !== "object" || parsed === null) {
throw new Error(`Secret ${secretId} is not a JSON object`);
}
const obj = parsed as Record<string, unknown>;
const missing = requiredKeys.filter((k) => obj[k] === undefined);
if (missing.length > 0) throw new Error(`Secret ${secretId} is missing keys: ${missing.join(", ")}`);
return Object.fromEntries(requiredKeys.map((k) => [k, String(obj[k])])) as Record<K, string>;
}
type CacheEntry = { value: Promise<string>; expires: number };
const cache = new Map<string, CacheEntry>();
/** In-memory cache: one GetSecretValue call per secret per TTL, shared by concurrent callers. */
export function getCachedSecret(secretId: string, ttlMs = 5 * 60_000): Promise<string> {
const hit = cache.get(secretId);
if (hit && hit.expires > Date.now()) return hit.value;
const value = getSecretString(secretId);
cache.set(secretId, { value, expires: Date.now() + ttlMs });
value.catch(() => cache.delete(secretId)); // don't cache failures
return value;
}
/** Drop a cached value, for example after the database rejects the password. */
export function invalidateSecret(secretId: string): void {
cache.delete(secretId);
}
/** Maps the errors you'll actually see to messages that say what to fix. */
export function explainSecretError(err: unknown, secretId: string): string {
if (err instanceof ResourceNotFoundException) return `${secretId}: not found in this Region or account`;
if (err instanceof DecryptionFailure) return `${secretId}: KMS key can't decrypt it (check kms:Decrypt)`;
if (err instanceof InvalidRequestException) return `${secretId}: invalid state, e.g. scheduled for deletion`;
if (err instanceof Error && err.name === "AccessDeniedException") return `${secretId}: IAM denied GetSecretValue`;
return `${secretId}: ${err instanceof Error ? err.name : String(err)}`;
}
Two design choices matter. The cache stores the promise, not the value, so 50 concurrent requests on a cold start produce one API call, not 50. And failed lookups are removed from the cache, so a transient error doesn’t stick for five minutes. Using the helper looks like this:
// get-db-config.ts
import { getJsonSecret, explainSecretError } from "./secrets.js";
const secretId = process.env.DB_SECRET_ID ?? "prod/orders/db";
try {
const db = await getJsonSecret(secretId, ["username", "password", "host", "port"] as const);
console.log(`Connecting to ${db.host}:${db.port} as ${db.username}`);
} catch (err) {
console.error(explainSecretError(err, secretId));
process.exitCode = 1;
}
npm install @aws-sdk/client-secrets-manager
npm install --save-dev tsx typescript
AWS_PROFILE=dev AWS_REGION=eu-west-1 DB_SECRET_ID=prod/orders/db npx tsx get-db-config.ts
# Connecting to orders.cluster-abc123.eu-west-1.rds.amazonaws.com:5432 as app_user
If you need several secrets at start-up, BatchGetSecretValueCommand fetches up to 20 in one call by name or by filter. Per-secret failures come back in its Errors list instead of failing the whole call, so check it.
Should you cache secrets in Lambda?
Yes. A Lambda execution environment is reused across invocations, so the module-level cache above already works there: the first invocation pays for the call, later ones read memory until the TTL expires. The SDK retries throttling and transient errors on its own; to tune that, see how to configure retry and timeout settings in AWS SDK for JavaScript v3.
The alternative is the AWS Parameters and Secrets Lambda Extension, a layer that runs a local HTTP cache next to your function. It serves Parameter Store values too, as the guide to read SSM Parameter Store values with AWS SDK v3 shows. Its defaults: port 2773, a cache of up to 1,000 secrets, and a TTL of 300 seconds set by SECRETS_MANAGER_TTL. Each request must carry the function’s session token in the X-Aws-Parameters-Secrets-Token header:
// secret-extension.ts
// Lambda handler that reads a secret through the AWS Parameters and Secrets Lambda Extension.
import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
const credentials = fromNodeProviderChain();
const port = process.env.PARAMETERS_SECRETS_EXTENSION_HTTP_PORT ?? "2773";
async function getSecretFromExtension(secretId: string): Promise<string> {
const { sessionToken } = await credentials();
if (!sessionToken) throw new Error("No session token; is this running in Lambda?");
const url = `http://localhost:${port}/secretsmanager/get?secretId=${encodeURIComponent(secretId)}`;
const res = await fetch(url, { headers: { "X-Aws-Parameters-Secrets-Token": sessionToken } });
if (!res.ok) throw new Error(`Extension returned HTTP ${res.status} for ${secretId}`);
const body = (await res.json()) as { SecretString?: string };
if (body.SecretString === undefined) throw new Error(`${secretId} has no SecretString`);
return body.SecretString;
}
export const handler = async (): Promise<{ statusCode: number }> => {
const secretId = process.env.DB_SECRET_ID;
if (!secretId) throw new Error("Set DB_SECRET_ID");
const { username } = JSON.parse(await getSecretFromExtension(secretId)) as { username: string };
console.log(`Loaded credentials for ${username}`);
return { statusCode: 200 };
};
The session token is resolved inside the handler, as the Lambda documentation recommends, so it stays valid across initialization modes. Choose the extension when several runtimes in one team should share one approach; keep the SDK helper when you want typed errors and no extra layer. Either way, add the execution role permissions below. For the calling side, see how to invoke a Lambda function with AWS SDK v3 in TypeScript, and when a secret lookup fails in production, how to investigate Lambda errors with CloudWatch.
Which permissions does GetSecretValue need?
secretsmanager:GetSecretValue on the secret’s ARN. If the secret is encrypted with a customer managed KMS key rather than the AWS managed aws/secretsmanager key, add kms:Decrypt on that key. Secret ARNs end in a six-character random suffix, hence the -?????? wildcard.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOneSecret",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/orders/db-??????"
},
{
"Sid": "DecryptWithCustomerManagedKey",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:eu-west-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab",
"Condition": {
"StringEquals": { "kms:ViaService": "secretsmanager.eu-west-1.amazonaws.com" }
}
}
]
}
BatchGetSecretValue needs secretsmanager:BatchGetSecretValue as well as GetSecretValue on each secret, plus secretsmanager:ListSecrets if you use filters. To pull the action list from your own code, use the guide to find the IAM actions your AWS SDK for JavaScript code needs or the IAM policy generator for TypeScript code.
Troubleshooting GetSecretValue errors
ResourceNotFoundException. Usually the wrong Region or account, not a missing secret. Log the resolved Region and pass the full ARN for cross-account secrets.AccessDeniedException. The identity lacksGetSecretValue, or a resource policy on the secret denies it. The steps to troubleshoot AWS IAM access denied errors go through each policy layer.DecryptionFailure. Secrets Manager can’t decrypt with the KMS key: addkms:Decrypt, and check that the key policy allows your role and that the key is enabled. A key disabled during a cleanup, such as one that follows the script to find unused customer managed KMS keys, fails exactly this way.InvalidRequestException. Most often the secret is scheduled for deletion. Restore it or point at its replacement.- The value parses as a string, not an object. The secret was stored as plain text. Either store JSON or skip the parse for that secret.
- Old credentials after a rotation. Your cache or the extension’s TTL is still serving the previous version. Call
invalidateSecretwhen authentication fails, or shorten the TTL.
Limits and common mistakes
- Logging the value. The most common leak is a
console.log(out)during debugging. LogNameandVersionIdonly. The OWASP Secrets Management Cheat Sheet covers logging, memory handling and rotation in more depth. - Secrets in environment variables. Copying the value into a Lambda environment variable at deploy time bypasses rotation. Store the secret ID there and fetch the value at runtime. The scripts to find secrets in Lambda environment variables and find plaintext secrets in ECS task definitions show where values were copied in anyway.
- Hard-coding before migrating. Scripts that still contain keys are exactly what a converter’s secret check is for; see whether it’s safe to paste AWS code into an AI converter.
- A cache is not a fallback. If Secrets Manager is unreachable on a cold start, the helper throws. Decide whether your service should fail fast or keep a last-known value.
To see what’s stored in an account without writing code, ChatWithCloud can answer “Which Secrets Manager secrets haven’t been rotated in 90 days?” from a read-only profile. It runs the AWS calls on your machine, but the results go to the AI model for the answer, so don’t ask it to print secret values; the ChatWithCloud security model lists what is sent. For the opposite question, secrets nobody reads anymore, the script to find unused Secrets Manager secrets by last access date reports them without reading a value.
Frequently asked questions
How do I get a secret value from Secrets Manager in Node.js?
Install @aws-sdk/client-secrets-manager, then await client.send(new GetSecretValueCommand({ SecretId: "name-or-arn" })) and read SecretString.
Why is SecretString undefined?
The secret was stored as binary. Read SecretBinary, a Uint8Array in SDK v3, and convert it with Buffer.from(...).
How do I get the previous version of a secret?
Pass VersionStage: "AWSPREVIOUS" to GetSecretValueCommand.
How long should I cache a secret?
Minutes, not hours. The Lambda extension defaults to 300 seconds; go shorter if you rotate often, and refetch on authentication errors.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud