EC2 instance user data contains no secrets
EC2 instance User Data is inspected for secret-like values (credentials, tokens, keys). Both plain and compressed content are parsed, honoring configured exclusions, to identify patterns that resemble sensitive material within initialization scripts.
Risk
Secrets embedded in User Data undermine confidentiality and integrity. Anyone with instance or build-system access can read them, reuse credentials to call services, exfiltrate data, or move laterally. Exposure may persist in AMIs, snapshots, and backups, increasing blast radius over time.
Run this check with Prowler
Run in Prowler Cloudprowler aws --checks ec2_instance_secrets_user_dataRecommendation
Avoid placing secrets in User Data. Store them in a managed secret service and fetch at runtime via a least-privilege instance role. Prefer short-lived credentials with regular rotation. Limit who can view or edit User Data and apply defense in depth with automated secret scanning in build pipelines.
Remediation
CLI
aws ec2 modify-instance-attribute --instance-id <INSTANCE_ID> --user-data "Value="Terraform
resource "aws_instance" "<example_resource_name>" {
ami = "<AMI_ID>"
instance_type = "t3.micro"
user_data = "" # Critical: empty user data so no secrets are stored
}NativeIaC
# CloudFormation: EC2 instance with empty user data
Resources:
<example_resource_name>:
Type: AWS::EC2::Instance
Properties:
ImageId: <AMI_ID>
InstanceType: t3.micro
UserData: !Base64 "" # Critical: empty user data ensures no secrets are presentOther
Check Code
import zlib
from base64 import b64decode
from prowler.config.config import encoding_format_utf_8
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.lib.logger import logger
from prowler.lib.utils.utils import (
SecretsScanError,
annotate_verified_secrets,
detect_secrets_scan_batch,
)
from prowler.providers.aws.services.ec2.ec2_client import ec2_client
class ec2_instance_secrets_user_data(Check):
def execute(self):
findings = []
secrets_ignore_patterns = ec2_client.audit_config.get(
"secrets_ignore_patterns", []
)
validate = ec2_client.audit_config.get("secrets_validate", False)
instances = list(ec2_client.instances)
# Collect the decoded User Data of each non-terminated instance and scan
# it all in batched Kingfisher invocations instead of one subprocess each.
# Instances whose User Data cannot be decoded are undecodable (no report),
# matching the original per-resource behavior.
undecodable = set()
def payloads():
for index, instance in enumerate(instances):
if instance.state == "terminated" or not instance.user_data:
continue
user_data = b64decode(instance.user_data)
try:
if user_data[0:2] == b"\x1f\x8b": # GZIP magic number
user_data = zlib.decompress(
user_data, zlib.MAX_WBITS | 32
).decode(encoding_format_utf_8)
else:
user_data = user_data.decode(encoding_format_utf_8)
except UnicodeDecodeError as error:
logger.warning(
f"{instance.region} -- Unable to decode user data in EC2 instance {instance.id}: {error}"
)
undecodable.add(index)
continue
except Exception as error:
logger.error(
f"{instance.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
undecodable.add(index)
continue
yield index, user_data
scan_error = None
try:
batch_results = detect_secrets_scan_batch(
payloads(), excluded_secrets=secrets_ignore_patterns, validate=validate
)
except SecretsScanError as error:
batch_results = {}
scan_error = error
for index, instance in enumerate(instances):
if instance.state == "terminated":
continue
report = Check_Report_AWS(metadata=self.metadata(), resource=instance)
if scan_error and instance.user_data:
report.status = "MANUAL"
report.status_extended = (
f"Could not scan EC2 instance {instance.id} User Data for "
f"secrets: {scan_error}; manual review is required."
)
findings.append(report)
continue
if index in undecodable:
report.status = "MANUAL"
report.status_extended = f"Could not decode User Data for EC2 instance {instance.id}; manual review is required to scan for secrets."
elif instance.user_data:
detect_secrets_output = batch_results.get(index)
if detect_secrets_output:
secrets_string = ", ".join(
[
f"{secret['type']} on line {secret['line_number']}"
for secret in detect_secrets_output
]
)
report.status = "FAIL"
report.status_extended = f"Potential secret found in EC2 instance {instance.id} User Data -> {secrets_string}."
annotate_verified_secrets(report, detect_secrets_output)
else:
report.status = "PASS"
report.status_extended = (
f"No secrets found in EC2 instance {instance.id} User Data."
)
else:
report.status = "PASS"
report.status_extended = f"No secrets found in EC2 instance {instance.id} since User Data is empty."
findings.append(report)
return findings