Blog
May 4, 2017 Marie H.

Lambda Environment Variables and KMS Encryption

Lambda Environment Variables and KMS Encryption

Photo by <a href="https://unsplash.com/@bel2000a?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Belinda Fewings</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

Lambda environment variables are the quick-and-easy way to get config into your function without baking it into the deployment package. I use them constantly, but there's a meaningful difference between the default encryption behavior and actually encrypting sensitive values in transit with KMS. Let me show you both, and how to decrypt properly in your handler.

Setting Environment Variables

Via the Console: In the Lambda configuration page, scroll down to "Environment variables" and add key/value pairs. Simple enough.

Via the AWS CLI:

aws lambda update-function-configuration \
  --function-name my-function \
  --environment "Variables={SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00/B00/xxx,APP_ENV=prod}"

To check what's currently set:

aws lambda get-function-configuration \
  --function-name my-function \
  --query 'Environment'

Accessing Them in Python

Standard os.environ — nothing surprising here:

import os

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
APP_ENV = os.environ.get('APP_ENV', 'dev')  # with a default

def handler(event, context):
    print(f"Running in {APP_ENV}")
    # do stuff with SLACK_WEBHOOK_URL

Encrypted at Rest vs Encrypted in Transit

Here's the part that confused me initially. Lambda always encrypts environment variables at rest using a service-managed KMS key. That's the default, you get it for free, no action required.

What you don't get by default is encryption during the deployment pipeline — when the variable travels from the console or CLI to Lambda's servers. To cover that gap, you bring your own KMS key and encrypt the value before setting it. Lambda stores the ciphertext, and your function decrypts it at runtime using the KMS API.

This matters if you have compliance requirements around data in transit, or if you just don't want plaintext secrets visible in the Lambda console to anyone with read access to the function config.

Setting Up KMS Encryption

First, encrypt your secret locally:

aws kms encrypt \
  --key-id alias/my-lambda-key \
  --plaintext "https://hooks.slack.com/services/T00/B00/actualsecret" \
  --query CiphertextBlob \
  --output text

This gives you a base64-encoded ciphertext blob. Set that as your environment variable value. Now the Lambda console only ever sees ciphertext.

Decrypting in the Lambda Handler

The KMS client returns bytes on decrypt, so you need to decode:

import os
import boto3
from base64 import b64decode

kms = boto3.client('kms', region_name='us-east-1')

def decrypt_env_var(var_name):
    encrypted = os.environ[var_name]
    decrypted = kms.decrypt(
        CiphertextBlob=b64decode(encrypted)
    )
    return decrypted['Plaintext'].decode('utf-8')

# Decrypt once at cold-start, reuse on warm invocations
SLACK_WEBHOOK_URL = decrypt_env_var('SLACK_WEBHOOK_URL')

def handler(event, context):
    # SLACK_WEBHOOK_URL is ready to use
    send_slack_notification(SLACK_WEBHOOK_URL, event)
    return {'statusCode': 200}

def send_slack_notification(webhook_url, event):
    import urllib.request
    import json
    data = json.dumps({'text': str(event)}).encode()
    req = urllib.request.Request(webhook_url, data=data)
    urllib.request.urlopen(req)

IAM Permissions Required

Your Lambda execution role needs permission to call kms:Decrypt on your key:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
    }
  ]
}

Without this, you'll get a very unhelpful AccessDeniedException at runtime. Ask me how I know.

Practical Example: DB Password

Same pattern works for a database password. Encrypt with KMS, set the ciphertext as DB_PASSWORD, decrypt in your handler. The plaintext never hits the wire, and anyone who can see your Lambda config just sees an opaque blob.

DB_PASSWORD = decrypt_env_var('DB_PASSWORD')
DB_HOST = os.environ['DB_HOST']  # non-sensitive, no KMS needed

Wrapping Up

Default encryption at rest is fine for non-sensitive config. For anything you'd be embarrassed to see in plain text in the console — Slack webhooks, database passwords, API tokens — encrypt with KMS before setting it. The decryption code is six lines and you only write it once. Worth it.