Update (October 2026): I added the two retrieval options AWS now documents for Lambda: the AWS Parameters and Secrets Lambda Extension and the Parameters utility of Powertools for AWS Lambda (TypeScript). The post now has a Secrets Manager example in CDK and explains why CDK’s secretValueFromJson().unsafeUnwrap() in an environment variable leaks the secret. I also fixed the middy example: cacheExpiry is in milliseconds, not seconds.
It is quite common to need a secret value of some kind in a Lambda function. Either for a database connection, a 3-rd party service, or whatever else. But how to securely use secrets in your Lambda?
In this post, I am going to tell you why environment variables are not the tool for the job and what the preferred way looks like.
I will also show you how to do better with AWS Secrets Manager, AWS CDK, and TypeScript.
Why Environment Variables Are Not the Best Solution for Accessing Secrets
I am sure I don’t have to tell you why it is a bad idea to put secrets in plain text into your code base. But why aren’t environment variables very secure either? AWS suggests not using environment variables for your secrets:
To increase security, we recommend that you use AWS Secrets Manager instead of environment variables to store database credentials and other sensitive information like API keys or authorization tokens.
(from Working with Lambda environment variables)
This tweet from Yan Cui (@theburningmonk) sums up the reasons pretty well:
Depends on if you put them as plaintext in env vars - if a function is compromised then env vars is the 1st place attackers would look. There have been multiple instances of compromised NPM packages that steal env vars when initialized. This affects lambda too.
— Yan Cui (@theburningmonk) June 2, 2021
There are malicious packages out there sending your environment variables somewhere else. Just google something like “npm package malicious environment variables” and you will find lists of malicious packages. Often they create packages with almost identical names than widely used packages. If you do a typo while installing a package you could end up sending your secrets to a bad actor. This is not an issue that is specific to AWS Lambda – but the solution I am going to show you is.
In addition to that, secrets stored in environment variables are part of the function configuration. Lambda encrypts
them at rest, but everyone who can read that configuration (in the AWS console or via GetFunctionConfiguration) sees
them in plain text. With other services (I will get to them in the next section) you can restrict who is able to see the
secrets.
CDK’s unsafeUnwrap() Doesn’t Make It Safe
A common pattern in AWS CDK is to store the secret in Secrets Manager and resolve it into an environment variable:
const serviceASecret = Secret.fromSecretNameV2(this, 'ServiceASecret', 'dev/service-a');
new NodejsFunction(this, 'LeakyLambda', { entry: path.join(__dirname, 'lambda', 'index.ts'), environment: { // ❌ the resolved secret ends up in plain text in the function configuration SERVICE_A_API_KEY: serviceASecret.secretValueFromJson('apiKey').unsafeUnwrap(), },});The synthesized template only contains a {{resolve:secretsmanager:...}}
dynamic reference,
so it looks safe. But CloudFormation resolves it during deployment and Lambda stores the resolved value as a regular
environment variable. You are back to where you started – plus, rotating the secret doesn’t reach the function until
the next deployment.
That is exactly why CDK makes you call a method with “unsafe” in its name. The
SecretValue docs name this case
explicitly:
To make sure secret values don’t accidentally end up in readable parts of your infrastructure definition (such as the environment variables of an AWS Lambda Function, where everyone who can read the function definition has access to the secret), using secret values directly is not allowed.
Pass the secret’s name or ARN as an environment variable instead and retrieve the value at runtime.
How to Securely Access Secrets in AWS Lambda?
Now that we know why using environment variables for secrets is a bad idea, how can we do better?
Secrets Manager or Parameter Store?
One solution has already been mentioned in the quote in the previous chapter:
AWS Secrets Manager. Another popular solution is
AWS Systems Manager Parameter Store
with SecureString parameters. Both of them allow you to securely store secrets, restrict access via IAM, and retrieve
them at runtime.
- Secrets Manager is purpose-built for secrets: automatic rotation, cross-account access, and audit logging. It costs $0.40 per secret per month plus $0.05 per 10,000 API calls (pricing).
- Parameter Store standard parameters come at no additional charge. AWS itself recommends Secrets Manager once you need rotation, cross-account access, or fine-grained audit logging.
I will use Secrets Manager for the main example and show Parameter Store with middy at the end.
Malicious packages could theoretically still get access to your secrets somehow but there is now more to it than just
sending process.env somewhere. So this is not an 100% protection, but it is safer and as part of
Defense in Depth a good practice.
It would be even safer to place the Lambda in a VPC and prevent it from connecting to the internet altogether. But that comes with other downsides and isn’t always applicable. (If you do, you need a VPC endpoint for Secrets Manager or Parameter Store.)
Caching: The Trade-off
You have different options on how to retrieve and use secrets – as always, all of them have different trade-offs:
- Retrieve secrets inside of the Lambda handler on every invocation
- Retrieve secrets during Lambda init phase (outside of handler) & cache them forever (= for the time of the Lambda execution context)
- Retrieve secrets once & cache them for a certain period, e.g. 5min
The trade-off you have to make is between performance & price (less API calls are cheaper and faster) and flexibility (you can change secrets immediately if you don’t cache them).

Every execution environment keeps its own cache. So the number of API calls grows with the number of concurrent execution environments, not with the number of invocations. All options below implement option 3.
Secrets Manager + Lambda Example With AWS CDK
Let’s create a secret first. A JSON secret lets you keep related values together:
aws secretsmanager create-secret --name "dev/service-a" --secret-string '{"apiKey":"my-super-secret-api-key"}'Note: I like to use a naming convention like [stage]/[service] in order to keep everything nice and organized and to
be able to easily construct and access secrets for different environments.
In CDK, import the secret, pass its name (not its value) to the function, and grant read access:
export class LambdaRetrieveSecretsStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const serviceASecret = Secret.fromSecretNameV2(this, 'ServiceASecret', 'dev/service-a');
const secretsExampleLambda = new NodejsFunction(this, 'SecretsExampleLambda', { entry: path.join(__dirname, 'lambda', 'index.ts'), runtime: Runtime.NODEJS_24_X, environment: { SERVICE_A_SECRET_NAME: serviceASecret.secretName, }, });
serviceASecret.grantRead(secretsExampleLambda); }}grantRead allows secretsmanager:GetSecretValue (and DescribeSecret) on that secret. If the secret is encrypted
with a customer managed KMS key, the function’s role also needs kms:Decrypt on that key.
Now there are two ways AWS recommends to retrieve the secret in the function. Both cache the value.
Option 1: Powertools for AWS Lambda Parameters Utility
The Parameters utility of Powertools for AWS Lambda (TypeScript) retrieves values from Secrets Manager, Parameter Store, AppConfig, or DynamoDB and caches them in memory:
npm install @aws-lambda-powertools/parameters @aws-sdk/client-secrets-managerThe handler:
import { getSecret } from '@aws-lambda-powertools/parameters/secrets';
type ServiceASecret = { apiKey: string };
const { SERVICE_A_SECRET_NAME } = process.env;
if (!SERVICE_A_SECRET_NAME) { throw new Error('SERVICE_A_SECRET_NAME environment variable is not set');}
export const handler = async () => { const secret = await getSecret<ServiceASecret>(SERVICE_A_SECRET_NAME, { transform: 'json', maxAge: 300, // seconds });
if (!secret) { throw new Error(`Secret ${SERVICE_A_SECRET_NAME} not found`); }
// use secret.apiKey – but never log it};The default cache duration is only 5 seconds. Set maxAge per call or the POWERTOOLS_PARAMETERS_MAX_AGE
environment variable for all calls. transform: 'json' parses the JSON secret for you.
This is my default for TypeScript functions: it is just a library, it is easy to unit test, and the same API works for
Parameter Store (getParameter from @aws-lambda-powertools/parameters/ssm).
Option 2: AWS Parameters and Secrets Lambda Extension
The
AWS Parameters and Secrets Lambda Extension
is an AWS-managed Lambda layer. It runs a local HTTP server (port 2773 by default) next to your function, fetches
secrets and parameters using the function’s role, and caches them. It works with every runtime and doesn’t add a
dependency to your code.
CDK has built-in support for it via the paramsAndSecrets property, which picks the right layer for your region and
architecture:
const secretsExampleLambda = new NodejsFunction(this, 'SecretsExampleLambda', { entry: path.join(__dirname, 'lambda', 'index.ts'), runtime: Runtime.NODEJS_24_X, environment: { SERVICE_A_SECRET_NAME: serviceASecret.secretName, }, paramsAndSecrets: ParamsAndSecretsLayerVersion.fromVersion(ParamsAndSecretsVersions.V1_0_103, { secretsManagerTtl: Duration.minutes(5), }),});
serviceASecret.grantRead(secretsExampleLambda);The handler requests the secret from localhost. The X-Aws-Parameters-Secrets-Token header is required and must contain
the function’s session token:
const { SERVICE_A_SECRET_NAME, AWS_SESSION_TOKEN } = process.env;
const getSecretFromExtension = async (secretId: string): Promise<string> => { const response = await fetch( `http://localhost:2773/secretsmanager/get?secretId=${encodeURIComponent(secretId)}`, { headers: { 'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN! } }, );
if (!response.ok) { throw new Error(`Failed to retrieve secret: ${response.status}`); }
const { SecretString } = (await response.json()) as { SecretString: string }; return SecretString;};
export const handler = async () => { const { apiKey } = JSON.parse(await getSecretFromExtension(SERVICE_A_SECRET_NAME!)); // use apiKey};A few things to know:
- The default and maximum TTL is 300 seconds (
SECRETS_MANAGER_TTL,SSM_PARAMETER_STORE_TTL). The extension doesn’t detect changes before the TTL expires. - For Parameter Store, use
/systemsmanager/parameters/get?name=...&withDecryption=true. AWS_SESSION_TOKENisn’t set in every initialization mode, e.g. with SnapStart. AWS recommends reading the token from the SDK’s credential provider chain inside the handler in that case.
Option 3: middy With Parameter Store
The original version of this post used middy with Parameter Store. That still works. You can find the code in my JannikWempe/lambda-retrieve-secrets repository.
Create the parameter:
aws ssm put-parameter --name "/dev/service-a/secret-key" --type "SecureString" --value "my-super-secret-secret"In CDK, reference it with StringParameter.fromSecureStringParameterAttributes(this, 'ServiceASecretKey', { parameterName: '/dev/service-a/secret-key' }),
pass parameterName as an environment variable, and call grantRead on it – just like with the secret above.
The ssm middleware retrieves and caches the decrypted parameter:
import middy from '@middy/core';import ssm from '@middy/ssm';
const { SERVICE_A_SECRET_KEY_PARAMETER_NAME } = process.env;
if (!SERVICE_A_SECRET_KEY_PARAMETER_NAME) { throw new Error('SERVICE_A_SECRET_KEY_PARAMETER_NAME environment variable is not set');}
export const handler = middy() .use( ssm({ fetchData: { SERVICE_A_SECRET_KEY: SERVICE_A_SECRET_KEY_PARAMETER_NAME, }, cacheKey: 'service-a-secret-key', cacheExpiry: 5 * 60 * 1000, // 5min – in milliseconds! setToContext: true, }), ) .handler(async (_event, context) => { // use context.SERVICE_A_SECRET_KEY – but never log it });fetchData maps the name under which the value is made available to the parameter name. setToContext: true puts the
secret on the Lambda context – it will not end up in the environment variables. middy also has a
secrets-manager middleware.
Conclusion
There are safer ways to use secrets in your AWS Lambda than by using environment variables – and resolving a secret
into an environment variable with CDK’s unsafeUnwrap() doesn’t change that. Store secrets in Secrets Manager (or
Parameter Store), pass only the name to your function, and retrieve the value at runtime with Powertools, the Parameters
and Secrets Lambda Extension, or middy. Cache it to keep latency and cost low. You can easily rotate secrets in a central
place and configure permissions only showing the secrets to authorized people. From now on, you should not use
environment variables for secrets.