CloudWatch Log Group Contains No Secrets in Logs
Overview
This check scans your CloudWatch Log Groups for embedded credentials and sensitive patterns such as API keys, passwords, tokens, and encryption keys. Secrets accidentally logged by applications can be discovered by anyone with read access to those logs, creating a significant security risk.
Risk
When secrets appear in log files:
- Unauthorized access: Attackers who gain log access can reuse exposed credentials to call AWS APIs or access other systems.
- Privilege escalation: Leaked credentials may belong to highly privileged accounts, allowing attackers to elevate their access.
- Data exfiltration: Exposed keys can be used to access databases, storage, or other sensitive resources.
- Amplified exposure: Log subscriptions, exports to S3, or cross-account sharing can spread secrets beyond the original log group.
Remediation Steps
Prerequisites
- AWS Console access with permissions to modify CloudWatch Logs settings
- Ability to identify which log groups contain secrets (Prowler findings will list them)
Required IAM permissions (for administrators)
To apply data protection policies, you need these permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:PutDataProtectionPolicy",
"logs:GetDataProtectionPolicy",
"logs:DeleteDataProtectionPolicy",
"logs:DescribeLogGroups"
],
"Resource": "arn:aws:logs:*:*:log-group:*"
}
]
}
AWS Console Method
The primary remediation is to apply a data protection policy that masks secrets in your logs. This prevents anyone without special permissions from viewing the sensitive data.
-
Open the CloudWatch console in us-east-1.
-
In the left navigation, select Log groups.
-
Find and click on the log group flagged by Prowler.
-
Select the Data protection tab.
-
Click Activate data protection.
-
Under Managed data identifiers, expand Credentials and select the relevant patterns:
- AWS Secret Access Key
- OpenSSH Private Key
- PGP Private Key
- JSON Web Token
- (Select all that apply to your environment)
-
For Audit findings destination, choose at least one:
- CloudWatch Logs (creates a findings log group)
- S3 bucket
- Firehose delivery stream
-
Click Activate data protection.
Important: This policy only masks data in log events ingested after activation. Existing secrets in old log events remain visible.
AWS CLI (optional)
Apply a data protection policy
Create a JSON file named data-protection-policy.json:
{
"Name": "credentials-protection-policy",
"Description": "Mask credentials in CloudWatch Logs",
"Version": "2021-06-01",
"Statement": [
{
"Sid": "audit-policy",
"DataIdentifier": [
"arn:aws:dataprotection::aws:data-identifier/AwsSecretKey",
"arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/JsonWebToken"
],
"Operation": {
"Audit": {
"FindingsDestination": {
"CloudWatchLogs": {
"LogGroup": "/aws/data-protection/audit-findings"
}
}
}
}
},
{
"Sid": "redact-policy",
"DataIdentifier": [
"arn:aws:dataprotection::aws:data-identifier/AwsSecretKey",
"arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/JsonWebToken"
],
"Operation": {
"Deidentify": {
"MaskConfig": {}
}
}
}
]
}
Apply the policy:
aws logs put-data-protection-policy \
--log-group-identifier "<your-log-group-name>" \
--policy-document file://data-protection-policy.json \
--region us-east-1
Verify the policy is applied
aws logs get-data-protection-policy \
--log-group-identifier "<your-log-group-name>" \
--region us-east-1
CloudFormation (optional)
CloudFormation template
This template creates a log group with a data protection policy already configured:
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudWatch Log Group with Data Protection Policy
Parameters:
LogGroupName:
Type: String
Description: Name of the CloudWatch Log Group
Default: /application/my-app
AuditLogGroupName:
Type: String
Description: Log group for audit findings
Default: /aws/data-protection/audit-findings
Resources:
AuditLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref AuditLogGroupName
RetentionInDays: 30
ApplicationLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref LogGroupName
RetentionInDays: 30
DataProtectionPolicy:
Name: credentials-protection-policy
Description: Mask credentials in logs
Version: '2021-06-01'
Statement:
- Sid: audit-policy
DataIdentifier:
- arn:aws:dataprotection::aws:data-identifier/AwsSecretKey
- arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey
- arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey
- arn:aws:dataprotection::aws:data-identifier/JsonWebToken
Operation:
Audit:
FindingsDestination:
CloudWatchLogs:
LogGroup: !Ref AuditLogGroupName
- Sid: redact-policy
DataIdentifier:
- arn:aws:dataprotection::aws:data-identifier/AwsSecretKey
- arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey
- arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey
- arn:aws:dataprotection::aws:data-identifier/JsonWebToken
Operation:
Deidentify:
MaskConfig: {}
Outputs:
LogGroupArn:
Description: ARN of the protected log group
Value: !GetAtt ApplicationLogGroup.Arn
Deploy with:
aws cloudformation deploy \
--template-file log-group-protection.yaml \
--stack-name log-group-data-protection \
--region us-east-1
Terraform (optional)
Terraform configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "log_group_name" {
description = "Name of the CloudWatch Log Group"
type = string
default = "/application/my-app"
}
variable "audit_log_group_name" {
description = "Log group for audit findings"
type = string
default = "/aws/data-protection/audit-findings"
}
# Audit log group for findings
resource "aws_cloudwatch_log_group" "audit" {
name = var.audit_log_group_name
retention_in_days = 30
}
# Application log group with data protection
resource "aws_cloudwatch_log_group" "application" {
name = var.log_group_name
retention_in_days = 30
}
# Data protection policy
resource "aws_cloudwatch_log_data_protection_policy" "credentials" {
log_group_name = aws_cloudwatch_log_group.application.name
policy_document = jsonencode({
Name = "credentials-protection-policy"
Description = "Mask credentials in logs"
Version = "2021-06-01"
Statement = [
{
Sid = "audit-policy"
DataIdentifier = [
"arn:aws:dataprotection::aws:data-identifier/AwsSecretKey",
"arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/JsonWebToken"
]
Operation = {
Audit = {
FindingsDestination = {
CloudWatchLogs = {
LogGroup = aws_cloudwatch_log_group.audit.name
}
}
}
}
},
{
Sid = "redact-policy"
DataIdentifier = [
"arn:aws:dataprotection::aws:data-identifier/AwsSecretKey",
"arn:aws:dataprotection::aws:data-identifier/OpenSshPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/PgpPrivateKey",
"arn:aws:dataprotection::aws:data-identifier/JsonWebToken"
]
Operation = {
Deidentify = {
MaskConfig = {}
}
}
}
]
})
}
output "log_group_arn" {
description = "ARN of the protected log group"
value = aws_cloudwatch_log_group.application.arn
}
Apply with:
terraform init
terraform plan
terraform apply
Verification
After applying the data protection policy:
- Return to the Data protection tab on your log group in the CloudWatch console.
- Confirm Data protection shows as Active.
- Verify the managed data identifiers you selected are listed.
To test that masking works:
- Have your application log a test secret (in a non-production environment).
- View the log event in CloudWatch - the secret should appear as asterisks (
***).
CLI verification commands
Check policy is applied:
aws logs get-data-protection-policy \
--log-group-identifier "<your-log-group-name>" \
--region us-east-1 \
--query 'policyDocument' \
--output text
Re-run Prowler to confirm the finding is resolved:
prowler aws --check cloudwatch_log_group_no_secrets_in_logs --region us-east-1
Additional Resources
- AWS Documentation: Protect sensitive log data with masking
- AWS Documentation: Managed data identifiers
- AWS Documentation: Data protection policy syntax
- Prowler Check Documentation
Notes
-
Existing data is not protected: Data protection policies only mask log events ingested after the policy is applied. Consider reducing retention periods for log groups that contained secrets, or deleting old log streams.
-
Rotate exposed credentials: If Prowler detected actual secrets in your logs, rotate those credentials immediately. The masking policy does not invalidate leaked credentials.
-
Application-level prevention: The best long-term solution is to prevent secrets from being logged in the first place. Review your application logging configuration to sanitize sensitive data before it reaches CloudWatch.
-
logs:Unmaskpermission: Users with this IAM permission can view unmasked data. Audit who has this permission and restrict it to only those who truly need it. -
Performance impact: Data protection scanning adds minimal latency to log ingestion. For most workloads, this is negligible.