Prevent PII Leaks into AWS CloudWatch Logs
Background
People use Amazon CloudWatch Logs because it provides fast, centralized, real‑time log collection, search, and analysis across AWS services - S3 alone cannot offer, since S3 is only an object store without built‑in querying, live tailing, log insights, or alerting. CloudWatch Logs supports powerful search via Logs Insights, automated alarms, streaming integrations, and native ingestion from EC2, Lambda, containers, CloudTrail, and API Gateway, enabling much quicker troubleshooting and operational visibility than storing raw logs in S3, which lacks these analytics and monitoring features.
Problem
One significant issue with CloudWatch Logs is the risk of unintentionally capturing Personally Identifiable Information (PII). Because many AWS services and applications log far more detail than expected — such as full request payloads, user identifiers, and attributes — sensitive data like emails, birthdates, and driver’s license numbers can be ingested into CloudWatch without developers realizing it, something AWS explicitly acknowledges as a common occurrence.
Risk
- Wider data exposure — Leaked PII becomes indexed and searchable across CloudWatch’s centralized logging platform.
- Propagation to SIEMs — When logs flow into Splunk, Elastic, Datadog, or security data lakes, PII is replicated across multiple systems, increasing who can access it.
- Regulatory non‑compliance — CloudWatch commonly ingests sensitive identifiers like emails, birthdates, and license numbers, creating risks under GDPR, HIPAA, PCI, and SOC2.
- Higher cost and operational burden — Removing PII from CloudWatch, SIEMs, and downstream systems is complex, time‑consuming, and expensive.
- Reputational risk — Even internal PII exposure incidents erode trust and trigger audit scrutiny.
Solution
To prevent PII leaks, we rely on CloudWatch Logs Data Protection policies, which can be implemented using either AWS‑managed data identifiers or custom identifiers tailored to your specific sensitive‑data patterns.
Prerequisite
First, we need to establish a clear and standardized list of PII data elements, as this definition serves as the foundation for securing CloudWatch Logs. Once this list is defined, we can apply a protection policy to automatically redact the specific PII fields we want to safeguard.
Get Rick Yang’s stories in your inbox
Join Medium for free to get updates from this writer.
In this scenario, I want to redact any SourceIp, since it can be considered linkable information that may identify an individual.
Implementation — Lambda Setup
I have a Lambda function that is triggered whenever the API receives a payload. The Lambda’s output is logged to the CloudWatch log group named /aws/lambda/demo-authorizer.
Implementation — Data Protection
To keep the example simple, I’m using a single custom data identifier that redacts the SourceIp value from the Lambda logs.
In this example, I sent a dummy HTTP request from my client environment.
# Dummy Payload
{
##### Skipping Details #####
"domainName": "id.execute-api.us-east-1.amazonaws.com",
"domainPrefix": "id",
"http": {
"method": "POST",
"path": "/path/to/resource",
"protocol": "HTTP/1.1",
"sourceIp": "192.168.0.1", # This should be redacted
"userAgent": "agent"
}
}The example below shows that the payload’s source IP has been successfully redacted. As this log is forwarded to downstream SIEM platforms, the sensitive information remains protected.
Limitation
- Masking sensitive data is supported only for log groups that use the Standard log class. Sensitive data is identified and masked at ingestion time.
- Any log events ingested before a data protection policy is applied will not be masked retroactively.
- Each log group can have only one data protection policy, though that policy can include multiple managed data identifiers for auditing and masking. The maximum size of a data protection policy is 30,720 characters.
Bonus — CloudWatch Data Protection Check Script
import boto3
def list_log_groups_without_data_protection(region_name="us-east-1"):
logs = boto3.client("logs", region_name=region_name)
paginator = logs.get_paginator("describe_log_groups")
has_policy = []
missing_policy = []
for page in paginator.paginate():
for lg in page.get("logGroups", []):
name = lg["logGroupName"]
try:
response = logs.get_data_protection_policy(
logGroupIdentifier=name
)
# If a policyDocument exists, this log group has a policy
if "policyDocument" in response:
has_policy.append(name)
else:
missing_policy.append(name)
except logs.exceptions.ResourceNotFoundException:
# Log group exists but has NO data protection policy
missing_policy.append(name)
except Exception as e:
print(f"Unexpected error on {name}: {e}")
return has_policy, missing_policy
if __name__ == "__main__":
region = "us-east-1"
has_policy, missing_policy = list_log_groups_without_data_protection(region)
print("\n=== Log Groups WITH Data Protection Policy ===")
for lg in has_policy:
print(f"- {lg}")
print("\n=== Log Groups WITHOUT Data Protection Policy ===")
for lg in missing_policy:
print(f"- {lg}")# Run the check script
python3 cw-data-protection-check.py
# Expected output
=== Log Groups WITH Data Protection Policy ===
- /aws/lambda/demo-authorizer
=== Log Groups WITHOUT Data Protection Policy ===
- /aws/apigateway/welcome
- /aws/application-test/dataYou can choose either an AWS Config–based approach or a custom Lambda-based architecture.
# AWS Config Architecture
AWS Config Rule
│
▼
Lambda Function
│
▼
DescribeLogGroups + GetDataProtectionPolicy
│
▼
Return compliance results to AWS Config# Custom Notification Architecture
EventBridge (Daily Schedule)
│
▼
Lambda Function
│
▼
DescribeLogGroups + GetDataProtectionPolicy
│
▼
Send email notification via SNSConclusion
Given the critical need to prevent sensitive data from being written to logs, it is recommended to implement both a manual process — such as reviewing application code — and an automated control using CloudWatch Logs data protection to ensure sensitive information is consistently safeguarded.









