Lambda Environment Variables: Inject Config Safely and Encrypt with KMS

Hardcoding a database endpoint inside your Lambda function is a deployment anti-pattern — it couples your code to infrastructure, breaks multi-environment workflows, and creates a security liability. Lambda Environment Variables solve this cleanly, and AWS KMS adds an encryption layer for sensitive values like connection strings or API keys.

TL;DR

Concern Solution Key Detail
Inject DB endpoint without hardcoding Lambda Environment Variable Set DB_ENDPOINT at deploy time; read via os.environ
Protect sensitive values at rest KMS Customer Managed Key (CMK) Lambda encrypts env vars using your CMK; decrypts on cold start
Minimum IAM for KMS decryption kms:Decrypt on the CMK ARN Attach to the Lambda execution role only
Avoid secret sprawl Combine with SSM Parameter Store or Secrets Manager For rotation needs, env vars alone are insufficient

How Lambda Environment Variables Work Internally

Lambda stores environment variables as key-value pairs attached to a function version. At the infrastructure level, AWS encrypts all environment variables at rest by default using an AWS-managed key (aws/lambda). When you specify a Customer Managed Key (CMK), Lambda uses that key instead, giving you full audit control via AWS CloudTrail.

The analogy: Think of environment variables as a sealed envelope handed to your function when it wakes up. By default, AWS seals it with their master lock. When you bring your own KMS key, you're using your own padlock — only your Lambda execution role has the key to open it.

The decryption happens before your handler code runs, during the Lambda execution environment initialization (cold start). Your code reads plain-text values from os.environ — it never sees the encrypted form.

Data Flow: From Deployment to Runtime

flowchart TD subgraph Deploy_Time ["Phase 1: Deployment"] A["Developer / CI Pipeline"] -->|"aws lambda update-function-configuration\n--environment Variables={DB_ENDPOINT=...}"| B["Lambda Service API"] B -->|"Encrypt env vars with CMK"| C["KMS CMK"] C -->|"Ciphertext stored"| D["Lambda Function Config\n(Versioned)"] end subgraph Runtime ["Phase 2: Cold Start"] E["Lambda Execution Environment Init"] -->|"kms:Decrypt call"| C C -->|"Plaintext env vars"| F["Process Environment\n(os.environ)"] F --> G["Handler Code\nos.environ['DB_ENDPOINT']"] end subgraph IAM ["IAM Boundary"] H["Lambda Execution Role"] -.->|"Must have kms:Decrypt\non CMK ARN"| C end D --> E

Step-by-Step Implementation

Phase 1: Create a KMS Customer Managed Key

  • Create a CMK and note its ARN — this will be referenced in both the Lambda config and the IAM policy.
aws kms create-key \
  --description "Lambda env var encryption key" \
  --key-usage ENCRYPT_DECRYPT \
  --query 'KeyMetadata.Arn' \
  --output text

# Optionally create a human-readable alias
aws kms create-alias \
  --alias-name alias/lambda-env-key \
  --target-key-id <KEY_ID>

Phase 2: Grant the Lambda Execution Role kms:Decrypt

  • Apply least-privilege: the execution role needs only kms:Decrypt on this specific CMK ARN.
  • Do not grant kms:Encrypt or kms:GenerateDataKey to the Lambda role — those are only needed by the Lambda service during deployment.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowLambdaDecryptEnvVars",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/<KEY_ID>"
    }
  ]
}
aws iam put-role-policy \
  --role-name my-lambda-execution-role \
  --policy-name LambdaKMSDecrypt \
  --policy-document file://kms-decrypt-policy.json

Phase 3: Deploy the Lambda with Environment Variables and CMK

  • Pass the --kms-key-arn flag to instruct Lambda to use your CMK instead of the default AWS-managed key.
  • The DB_ENDPOINT value is encrypted in transit (TLS) and at rest (KMS) automatically.
aws lambda update-function-configuration \
  --function-name my-function \
  --kms-key-arn arn:aws:kms:us-east-1:123456789012:key/<KEY_ID> \
  --environment "Variables={DB_ENDPOINT=mydb.cluster-xyz.us-east-1.rds.amazonaws.com,DB_PORT=5432}"

Phase 4: Read the Variable in Your Handler

  • Lambda decrypts the values before your handler is invoked — your code reads plain text directly from the process environment.
  • No SDK calls to KMS are needed in your application code; the Lambda service handles decryption transparently.
import os
import psycopg2

DB_ENDPOINT = os.environ['DB_ENDPOINT']
DB_PORT = int(os.environ.get('DB_PORT', 5432))

def handler(event, context):
    conn = psycopg2.connect(
        host=DB_ENDPOINT,
        port=DB_PORT,
        database="mydb",
        user=os.environ['DB_USER'],
        password=os.environ['DB_PASSWORD']
    )
    # ... query logic
    return {"statusCode": 200}

Terraform Alternative

  • Infrastructure-as-code is the preferred approach for reproducible deployments across environments.
resource "aws_lambda_function" "my_function" {
  function_name = "my-function"
  role          = aws_iam_role.lambda_exec.arn
  handler       = "app.handler"
  runtime       = "python3.12"
  filename      = "function.zip"
  kms_key_arn   = aws_kms_key.lambda_env_key.arn

  environment {
    variables = {
      DB_ENDPOINT = var.db_endpoint
      DB_PORT     = "5432"
    }
  }
}

When Environment Variables Are Not Enough

  • Secret rotation: Env vars are static per deployment. If your DB password rotates, you must redeploy. Use AWS Secrets Manager with the SDK for automatic rotation support.
  • Secrets larger than 4 KB: Lambda env var total size is capped at 4 KB. Use SSM Parameter Store (SecureString) or Secrets Manager for larger payloads.
  • Cross-function sharing: Env vars are scoped to a single function. SSM Parameter Store is better for shared config across multiple Lambdas.

Cost Impact

  • Default AWS-managed key (aws/lambda): Free. No additional KMS charges.
  • Customer Managed Key (CMK): $1.00/month per key + $0.03 per 10,000 API calls. Each Lambda cold start triggers one kms:Decrypt API call. At moderate invocation rates with warm containers, actual API call costs are minimal.
  • Secrets Manager: $0.40/secret/month + $0.05 per 10,000 API calls — justified only when rotation is required.

IAM Permissions Summary

Principal Required Permission Reason
Lambda Execution Role kms:Decrypt Decrypt env vars at cold start
Deploying IAM User/Role (CI/CD) kms:Encrypt, kms:DescribeKey Encrypt values when setting function config
Lambda Service Principal Granted via KMS Key Policy Allow Lambda service to use the key on behalf of the function

Glossary

  • Customer Managed Key (CMK): A KMS encryption key you create and control, enabling custom key policies, rotation schedules, and CloudTrail audit logs.
  • Lambda Execution Role: The IAM role assumed by the Lambda service when running your function, defining what AWS resources the function can access.
  • Cold Start: The initialization phase of a new Lambda execution environment, where the runtime is bootstrapped and environment variables are decrypted before your handler runs.
  • Least Privilege: The IAM principle of granting only the minimum permissions required for a task — nothing more.
  • SSM Parameter Store: An AWS Systems Manager feature for storing configuration data and secrets as key-value pairs, with optional KMS encryption via SecureString type.

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

Lambda Infinite Loop with S3: How to Break the Recursive Trigger Cycle

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User