DEV Community

Cover image for AWS Lambda PII Handling in Production: DynamoDB Field Encryption with KMS

AWS Lambda PII Handling in Production: DynamoDB Field Encryption with KMS

Handling Personally Identifiable Information (or PII for short) in AWS Lambda-backed systems is not difficult because AWS lacks security primitives. It is difficult because the default patterns in backend development encourage storing sensitive data in places where access control is only enforced at the infrastructure boundary — DynamoDB encryption at rest protects disks, but not application data, and once PII enters any of the database solutions inside the system, every principal with read access becomes a major threat.

This article demonstrates a production-tested pattern — encrypting sensitive data categorized as PII (user’s home address, email, name, surname, date of birth etc.) in AWS Lambda before they are stored in DynamoDB, using customer-managed KMS keys and infrastructure entirely defined in Python AWS CDK. The goal of this pattern is to ensure that if ever a data breach happens to your application, that the data is encrypted security inside DynamoDB, and that only chosen parts of the system can decrypt the data if necessary. This pattern can be adjusted to be used with any other database solution available on AWS.

This post will have most important snippets of the whole pattern, but the full code, where the Lambda handles all CRUD operations with DynamoDB, is available here.

Why are we going over this pattern?

There are many examples of existing serverless systems built on AWS Lambda and DynamoDB where some of the data, which gets stored inside the database, qualifies as PII and must not be stored in a human-readable format. As previously mentioned, the infrastructure in this pattern is defined using AWS CDK in Python, with a Lambda runtime in Python as well. Encryption keys are stored in AWS Key Management Service (KMS) and are customer-managed with explicit key policies, together with a strictly defined Lambda execution role.

This design does not attempt to solve encrypted search or complex querying over sensitive fields, as those problems require different tradeoffs. The scope here is to deliberately limit access to certain fields which are categorized as PII and could give you a legal headache in case of a data breach.

Moreover, DynamoDB server-side encryption is a must in any environment, but insufficient for PII or any field which needs protection by encryption. It protects against physical and low-level threats, but it does nothing to prevent plaintext access through IAM — any resource which has IAM access to the table and can scan or query the data, which can be a weakness in your infrastructure.

The only way to tip the scales in your favor here is to make sure that the sensitive fields are never stored in plaintext anywhere in your infrastructure. Once encryption happens in the application layer, any database solution becomes a persistence layer for ciphertext, and access to the data is no longer enough to expose PII. Data decryption process then becomes an explicit operation, which only authorized compute resources with correct IAM permissions can decrypt the data.

From this point forward, you are responsible for key rotation, correct IAM scoping and making sure that the application doesn’t leak any plain text information.

Architecture Overview

At a high level, the flow is simple:

  1. User’s payload enter a Lambda function from API Gateway
  2. Sensitive fields are encrypted immediately using AWS KMS
  3. Encrypted data is written to DynamoDB

When data must be read and interpreted, the Lambda function explicitly decrypts the fields using the same KMS key from the encryption context.

Basically, there are 3 crucial operations:

  1. Lambda calling KMS to encrypt and decrypt
  2. Lambda reading and writing encrypted attributes in DynamoDB
  3. IAM enforcing which principals are allowed to perform those operations

Architecture Diagram

KMS Key Design in AWS CDK

In this pattern, the symmetric KMS key is the foundation. If its policy is overly permissive, field-level encryption becomes meaningless. The following CDK code is an example of creating a single customer-managed key dedicated for encryption and decryption processes for one service, with key rotation enabled and environment separation handled outside the snippet.

# Creation of a symmetric-key in KMS
pii_key = kms.Key(
    self,
    "PiiEncryptionKey",
    alias="pii-encryption-key",
    description="KMS key for encrypting PII data (latitude/longitude)",
    enable_key_rotation=True,
    removal_policy=RemovalPolicy.DESTROY,  # RETAIN for prod
)
Enter fullscreen mode Exit fullscreen mode

At this layer, the key policy should be strict. Humans should not have decrypt permissions by default, and the Lambda execution role should be the primary and only consumer. CDK makes it easy to wire this correctly, but it also makes it easy to accidentally grant too much access if you are not careful enough.

Enabling Lambda IAM Access via Least Privilege

With the key in place, the next constraint is IAM. Encryption only works if decryption is tightly controlled. The Lambda execution role should have permission to encrypt and decrypt using exactly one KMS key and to read and write only the DynamoDB table it needs.

The following CDK code shows the Lambda function definition and the explicit grants for DynamoDB and KMS. There are no wildcards and no implicit permissions.

# CDK code for the Lambda + IAM Least Privilege Access
pii_handler = _lambda.Function(
    self,
    "PiiHandler",
    runtime=_lambda.Runtime.PYTHON_3_12,
    handler="handler.handler",
    code=_lambda.Code.from_asset(
        os.path.join(os.path.dirname(__file__), "PIIHandler"),
    ),
    layers=[powertools_layer],
    timeout=Duration.seconds(15),
    memory_size=256,
    environment={
        "POWERTOOLS_SERVICE_NAME": "pii-service",
        "POWERTOOLS_LOG_LEVEL": "INFO",
        "PII_TABLE_NAME": pii_table.table_name,
        "KMS_KEY_ARN": pii_key.key_arn,
        "ALLOWED_ORIGIN": "*",
    }
)

# Enable the PIIHandler Lambda access to DynamoDB table
# and the KMS key to encrypt/decrypt the data
pii_table.grant_read_write_data(pii_handler)
pii_key.grant_encrypt_decrypt(pii_handler)
Enter fullscreen mode Exit fullscreen mode

How to encrypt and decrypt data inside the AWS Lambda?

All encryption and decryption logic lives in the Lambda function, close to where data enters and leaves the system. The key rule is that plaintext PII should exist only in memory and only for as long as necessary.

The following Python Lambda code uses the KMS client to directly to encrypt and decrypt fields which are sensitive.

import json
import os
import base64

from boto3 import client, resource

# Environment variables, AWS Client and AWS resources
TABLE_NAME = os.environ.get("PII_TABLE_NAME")
KMS_KEY_ARN = os.environ.get("KMS_KEY_ARN")

KMS_CLIENT = client('kms')
DDB_RESOURCE = resource('dynamodb')
TABLE = DDB_RESOURCE.Table(TABLE_NAME)

def encrypt_data(plaintext: str) -> str:
        """Encrypt data using KMS and return base64-encoded ciphertext."""
    response = KMS_CLIENT.encrypt(
        KeyId=KMS_KEY_ARN,
        Plaintext=plaintext.encode("utf-8"),
        EncryptionContext={
            "purpose": "pii-location-encryption",
            "service": "pii-service",
        },
    )
    return base64.b64encode(response["CiphertextBlob"]).decode("utf-8")

def decrypt_data(encrypted_data: str) -> str:
        """Decrypt base64-encoded ciphertext using KMS."""
    ciphertext = base64.b64decode(encrypted_data)
    response = KMS_CLIENT.decrypt(
        CiphertextBlob=ciphertext,
        KeyId=KMS_KEY_ARN,
        EncryptionContext={
            "purpose": "pii-location-encryption",
            "service": "pii-service",
        },
    )
    return response["Plaintext"].decode("utf-8")
Enter fullscreen mode Exit fullscreen mode

Writing Encrypted Data to DynamoDB

Encryption happens before saving data anywhere in the system and only the PIIHandler Lambda has access to encrypt and decrypt the data, meaning that DynamoDB never sees plaintext latitude or longitude values, only base64-encoded cipher text.

The following code snippet shows you the usage of the encrypt_data method I’ve shown above and the process of saving the sensitive information inside the DynamoDB table:

def handle_create_location(event) -> dict:
        """Handle POST /locations - Store encrypted PII location data."""
    event_body = json.loads(event.get('body'))

    user_id = event_body["user_id"]
    latitude = event_body["latitude"]
    longitude = event_body["longitude"]

    # ..

    # Encrypt PII fields (latitude and longitude)
    coordinates = json.dumps({"latitude": latitude, "longitude": longitude})
    encrypted_coordinates = encrypt_data(coordinates)

    item = {
        "pk": f"USER#{user_id}",
        "sk": f"LOCATION#{timestamp}",
        "encrypted_coordinates": encrypted_coordinates
    }

    TABLE.put_item(Item=item)

    # ...
Enter fullscreen mode Exit fullscreen mode

AWS KMS Operational Considerations and Pricing

Now, to talk about some of the considerations. This approach does introduce some latency and cost because of the KMS calls, and fetching encrypted data inside the DynamoDB will increase item size overall. However, these are normal and predictable tradeoffs, which are rarely a limiting factor unless you have a system which is already operating at a very high scale.

More importantly, this pattern makes PII access, or any access to sensitive fields for that matter, explicit. Every decrypt operation is visible in CloudTrail, and access can be revoked centrally by modifying the KMS key policy. That property is far more valuable in production than marginal performance gains.

Regarding pricing, when writing this in February 2026, the price for one KMS key is $1USD per month (prorated hourly) and operations cost only $0.03USD per 10 000 requests, more information about pricing can be found by clicking on the link here. It’s worth mentioning that KMS operations are free for 20 000 operations in every AWS region which has KMS.

This pattern has key rotation enabled, meaning that every 365 days a new key will be generated and used for encrypting new data, which increases your cost per month by $1 USD. You don’t have to worry about the older data encrypted with the old key — KMS will save your older key which gets replaced and it will still be used to decrypt your data.

Conclusion

Field-level encryption in Lambda isn't advanced security — it should be a baseline for any system handling PII information.

The pattern above is made simple on purpose, but the constraints it enforces are fundamental: DynamoDB read access no longer implies an insight into sensitive data and any compliance audits shift straight to concrete log analysis, making your job easier to do.

As we’ve already discussed, the tradeoffs are real but manageable — KMS client calls add tens of milliseconds per operation and are relatively cheap (with a very generous Free tier as well!), meaning that this overhead is totally worth it because of the risk mitigation it provides.

The real question in a production environment isn’t whether encryption exists, but whether which computing resources have access to your KMS key and which resources are authorized to decrypt the sensitive data. By using this pattern, your future self (and your legal team if you have it in your company!) will thank you when the inevitable security questionnaire arrives.

Top comments (0)