Using Lambda Environment Variables: Secure Configuration with KMS Encryption
Hardcoding a database endpoint directly into your Lambda function code is a deployment anti-pattern that couples configuration to code, breaks multi-environment deployments, and creates a security liability the moment that code touches a repository. Lambda Environment Variables solve this cleanly — and when combined with AWS KMS, they provide encryption at rest for sensitive values like connection strings, API keys, and credentials.
TL;DR
| Concern | Solution | Key Detail |
|---|---|---|
| Avoid hardcoding config | Lambda Environment Variables | Injected at runtime; accessible via process.env / os.environ |
| Encryption at rest | AWS KMS CMK on env vars | Lambda encrypts env vars using KMS before storing; decrypts on cold start |
| Encryption in transit (console) | Encryption helpers (client-side) | Encrypts values before they reach the Lambda service plane |
| Secrets rotation | AWS Secrets Manager (preferred for credentials) | Env vars don't rotate automatically; Secrets Manager does |
| IAM least privilege | Lambda execution role + KMS key policy | Role needs kms:Decrypt on the specific CMK |
How It Works: Architecture & Data Flow
Understanding the lifecycle of an environment variable — from configuration to runtime access — is critical before writing a single line of code.
KMS Key ARN"| B["Lambda Service Plane"] B -->|"kms:Encrypt
(at configuration time)"| C["AWS KMS CMK"] C -->|"Returns Ciphertext"| B B -->|"Stores encrypted
env var values"| D["Lambda Config Store"] D -->|"Cold Start triggered"| E["Lambda Execution Environment"] E -->|"kms:Decrypt
(using execution role)"| C C -->|"Returns Plaintext"| E E -->|"Injects decrypted values
into process space"| F["Function Code"] F -->|"os.environ['DB_ENDPOINT']"| G["Database Endpoint Value"] H["Warm Invocation"] -->|"Reuses existing
execution environment"| F
- Developer/CI sets env vars via the AWS Console, CLI, SAM, or Terraform during function configuration.
- Lambda Service Plane stores the environment variable configuration. If a KMS CMK is specified, Lambda calls KMS to encrypt the values before persisting them.
- KMS encrypts the environment variable values using the specified Customer Managed Key (CMK) and returns the ciphertext to the Lambda service.
- On a cold start, the Lambda execution environment initializes. Lambda calls KMS to decrypt the environment variables using the CMK. This requires the Lambda execution role to have
kms:Decryptpermission on that key. - Decrypted values are injected into the execution environment's process space. Your function code reads them via standard OS environment variable APIs — no SDK calls required for basic usage.
- Warm invocations reuse the already-initialized execution environment, so KMS is not called again until the next cold start.
Analogy: Think of Lambda environment variables like a sealed envelope handed to a courier (the execution environment) at the start of their shift. The envelope is locked (KMS-encrypted at rest). Before the courier starts work, a trusted key holder (KMS) unlocks it. For the rest of the shift, the courier has the information in hand — no need to visit the key holder for every delivery (warm invocations). A new shift (cold start) means a new envelope and another visit to the key holder.
Step 1: Setting Environment Variables (No Encryption)
The simplest case — passing a database endpoint without KMS. Suitable for non-sensitive configuration like hostnames in a private VPC where the sensitivity is low.
Via AWS CLI
aws lambda update-function-configuration \
--function-name my-api-function \
--environment "Variables={DB_ENDPOINT=mydb.cluster-xyz.us-east-1.rds.amazonaws.com,DB_PORT=5432,ENVIRONMENT=production}"
Reading in Your Function Code
Python:
import os
def handler(event, context):
db_endpoint = os.environ['DB_ENDPOINT']
db_port = os.environ['DB_PORT']
# Use db_endpoint to establish connection
print(f"Connecting to {db_endpoint}:{db_port}")
Node.js:
exports.handler = async (event) => {
const dbEndpoint = process.env.DB_ENDPOINT;
const dbPort = process.env.DB_PORT;
// Use dbEndpoint to establish connection
console.log(`Connecting to ${dbEndpoint}:${dbPort}`);
};
Step 2: Encrypting Environment Variables with KMS
For sensitive values (passwords, tokens, connection strings with credentials), you should use a KMS Customer Managed Key (CMK). Lambda uses this key to encrypt your environment variable values at rest.
Prerequisites: Create a KMS CMK
aws kms create-key \
--description "CMK for Lambda environment variable encryption" \
--key-usage ENCRYPT_DECRYPT \
--origin AWS_KMS
Note the KeyId (or ARN) from the response. Create an alias for convenience:
aws kms create-alias \
--alias-name alias/lambda-env-key \
--target-key-id <KeyId-from-above>
Grant the Lambda Execution Role KMS Decrypt Permission
Your Lambda execution role must be able to call kms:Decrypt on this specific key. Attach the following inline or managed policy to the role:
🔽 [Click to expand] IAM Policy: Lambda KMS Decrypt
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowLambdaKMSDecrypt",
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/<your-key-id>"
}
]
}
Also ensure the KMS key policy allows the Lambda execution role to use the key. Add this statement to your KMS key policy:
🔽 [Click to expand] KMS Key Policy Statement
{
"Sid": "AllowLambdaExecutionRoleDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/my-lambda-execution-role"
},
"Action": [
"kms:Decrypt"
],
"Resource": "*"
}
Configure Lambda to Use the CMK
aws lambda update-function-configuration \
--function-name my-api-function \
--kms-key-arn arn:aws:kms:us-east-1:123456789012:key/<your-key-id> \
--environment "Variables={DB_ENDPOINT=mydb.cluster-xyz.us-east-1.rds.amazonaws.com,DB_PASSWORD=supersecretpassword}"
Lambda will now call KMS to encrypt these values before storing them. At cold start, Lambda calls KMS to decrypt them before injecting into the execution environment.
Step 3: Encryption Helpers (Client-Side Encryption in Transit)
By default, environment variables are encrypted at rest using KMS, but they are transmitted in plaintext over TLS to the Lambda service. For highly sensitive values, the Lambda console offers Encryption Helpers which encrypt the value client-side before it ever leaves your browser/CLI, providing an additional layer of protection in transit to the Lambda service plane.
When using encryption helpers, your function code must explicitly call KMS to decrypt the value at runtime, since Lambda cannot auto-decrypt a client-side encrypted value:
🔽 [Click to expand] Python: Manual KMS Decrypt in Function Code
import os
import boto3
import base64
def get_decrypted_secret(env_var_name: str) -> str:
"""Decrypt a client-side encrypted environment variable using KMS."""
kms_client = boto3.client('kms', region_name=os.environ.get('AWS_REGION', 'us-east-1'))
encrypted_value = os.environ[env_var_name]
response = kms_client.decrypt(
CiphertextBlob=base64.b64decode(encrypted_value),
EncryptionContext={
'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']
}
)
return response['Plaintext'].decode('utf-8')
# Cache decrypted value outside handler to benefit from warm starts
DB_PASSWORD = None
def handler(event, context):
global DB_PASSWORD
if DB_PASSWORD is None:
DB_PASSWORD = get_decrypted_secret('DB_PASSWORD')
db_endpoint = os.environ['DB_ENDPOINT']
# Use db_endpoint and DB_PASSWORD
print(f"Connecting to {db_endpoint}")
Note on caching: The DB_PASSWORD global variable pattern above caches the decrypted secret for the lifetime of the execution environment (warm invocations), avoiding a KMS API call on every invocation. This is the recommended pattern.
Full Architecture: Lambda + KMS Encryption Flow
(env vars + KMS key ARN) Lambda->>KMS: kms:GenerateDataKey KMS-->>Lambda: Encrypted ciphertext Lambda->>Lambda: Store encrypted env vars Note over Lambda,Code: Cold Start Lambda->>Exec: Initialize execution environment Exec->>KMS: kms:Decrypt (via execution role) KMS-->>Exec: Plaintext env var values Exec->>Code: Inject into process environment Note over Code: Invocation (Warm or Cold) Code->>Code: os.environ['DB_ENDPOINT'] Code-->>Code: Returns plaintext value Note over Code,KMS: Optional: Client-Side Encryption Path Code->>KMS: kms:Decrypt (explicit SDK call) KMS-->>Code: Decrypted secret value
- Configuration time: The developer sets environment variables with a KMS CMK ARN specified. Lambda calls
kms:GenerateDataKeyto encrypt the values. - Storage: Encrypted ciphertext is stored in the Lambda service configuration. The plaintext value never persists.
- Cold start — Lambda-managed decryption: Lambda's service plane calls
kms:Decryptusing the execution role's credentials. Decrypted values are injected into the process environment. - Function code access: Code reads
os.environ['DB_ENDPOINT']— no SDK call needed for Lambda-managed encryption. - Client-side encryption path (optional): If encryption helpers were used, the function code must call
kms:Decryptexplicitly via the Boto3/SDK client. - Warm invocations: Execution environment is reused; no KMS calls occur unless the function explicitly makes them.
Infrastructure as Code: SAM / CloudFormation
🔽 [Click to expand] AWS SAM Template with KMS-Encrypted Environment Variables
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
LambdaKMSKey:
Type: AWS::KMS::Key
Properties:
Description: CMK for Lambda environment variable encryption
KeyPolicy:
Version: '2012-10-17'
Statement:
- Sid: AllowRootAccountFullAccess
Effect: Allow
Principal:
AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
Action: 'kms:*'
Resource: '*'
- Sid: AllowLambdaExecutionRoleDecrypt
Effect: Allow
Principal:
AWS: !GetAtt MyFunctionRole.Arn
Action: 'kms:Decrypt'
Resource: '*'
MyFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: my-lambda-execution-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: KMSDecryptPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: 'kms:Decrypt'
Resource: !GetAtt LambdaKMSKey.Arn
MyApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: my-api-function
Handler: app.handler
Runtime: python3.12
Role: !GetAtt MyFunctionRole.Arn
KmsKeyArn: !GetAtt LambdaKMSKey.Arn
Environment:
Variables:
DB_ENDPOINT: mydb.cluster-xyz.us-east-1.rds.amazonaws.com
DB_PORT: '5432'
ENVIRONMENT: production
When to Use Secrets Manager Instead
Lambda environment variables with KMS are appropriate for non-rotating configuration values. For database credentials that require automatic rotation, use AWS Secrets Manager. The trade-off:
| Criteria | Lambda Env Vars + KMS | AWS Secrets Manager |
|---|---|---|
| Automatic rotation | ❌ Manual update required | ✅ Native rotation support |
| Audit trail per access | ❌ KMS CloudTrail only at cold start | ✅ CloudTrail per GetSecretValue call |
| Latency | ✅ Zero latency (env var read) | ⚠️ SDK call latency (mitigated by caching) |
| Cost | ✅ KMS key cost only | ⚠️ Per-secret and per-API-call pricing |
| Best for | Endpoints, feature flags, non-rotating config | Passwords, tokens, rotating credentials |
Security Best Practices Checklist
- ✅ Use a CMK, not the AWS-managed Lambda key, so you control key rotation and access policies.
- ✅ Scope the KMS key policy to only the specific Lambda execution role(s) that need decrypt access.
- ✅ Enable KMS key rotation (
aws kms enable-key-rotation --key-id <key-id>) for automatic annual rotation of the key material. - ✅ Never log environment variable values in your function code — CloudWatch Logs are not encrypted by default.
- ✅ Use separate CMKs per environment (dev/staging/prod) to enforce environment isolation at the key policy level.
- ✅ Cache decrypted secrets in the execution environment scope (outside the handler) to minimize KMS API calls and latency.
Glossary
| Term | Definition |
|---|---|
| CMK (Customer Managed Key) | A KMS key you create and control, as opposed to AWS-managed keys. Provides full control over key policy, rotation, and deletion. |
| Cold Start | The initialization phase of a new Lambda execution environment. KMS decryption of environment variables occurs here, not on every invocation. |
| Execution Environment | The isolated runtime sandbox Lambda creates to run your function. Environment variables are injected into this environment's process space. |
| Encryption Helper | A Lambda console feature that encrypts environment variable values client-side before transmission to the Lambda service, requiring explicit SDK-based decryption in function code. |
| Encryption Context | An optional set of key-value pairs passed to KMS during encrypt/decrypt operations. Used for additional authentication and auditing; must match exactly between encrypt and decrypt calls. |
Next Steps
You now have a production-grade pattern for injecting and protecting configuration in Lambda. For credentials requiring rotation, extend this pattern with AWS Secrets Manager rotation. Review the official references below to go deeper:
- AWS Docs: Lambda Environment Variables
- AWS Docs: Encrypting Lambda Environment Variables
- AWS Docs: AWS KMS Concepts
- AWS Docs: AWS Secrets Manager
Comments
Post a Comment