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.

graph TD A["Developer / CI Pipeline"] -->|"Sets env vars +
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
  1. Developer/CI sets env vars via the AWS Console, CLI, SAM, or Terraform during function configuration.
  2. Lambda Service Plane stores the environment variable configuration. If a KMS CMK is specified, Lambda calls KMS to encrypt the values before persisting them.
  3. KMS encrypts the environment variable values using the specified Customer Managed Key (CMK) and returns the ciphertext to the Lambda service.
  4. 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:Decrypt permission on that key.
  5. 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.
  6. 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

sequenceDiagram participant Dev as "Developer / CI" participant Lambda as "Lambda Service" participant KMS as "AWS KMS CMK" participant Exec as "Execution Environment" participant Code as "Function Code" Note over Dev,KMS: Configuration Time Dev->>Lambda: update-function-configuration
(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
  1. Configuration time: The developer sets environment variables with a KMS CMK ARN specified. Lambda calls kms:GenerateDataKey to encrypt the values.
  2. Storage: Encrypted ciphertext is stored in the Lambda service configuration. The plaintext value never persists.
  3. Cold start — Lambda-managed decryption: Lambda's service plane calls kms:Decrypt using the execution role's credentials. Decrypted values are injected into the process environment.
  4. Function code access: Code reads os.environ['DB_ENDPOINT'] — no SDK call needed for Lambda-managed encryption.
  5. Client-side encryption path (optional): If encryption helpers were used, the function code must call kms:Decrypt explicitly via the Boto3/SDK client.
  6. 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:

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Fix Internet Gateway and Route Table

EC2 SSH Connection Timeout: Which Security Group Rules to Check

Difference Between IAM User and IAM Role: Which One Should Your EC2 Use?