Back to Checks
aws

EC2 instance user data contains no secrets

ec2_instance_secrets_user_data
Service: ec2Severity: high
OfficialBuilt-inVerified
Categories:secrets
Resource Type:AwsEc2Instance

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 Cloud
Run
prowler aws --checks ec2_instance_secrets_user_data

Recommendation

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 present

Other

1Open the AWS EC2 console and go to Instances
2Select the affected instance
3Click Actions > Instance settings > Edit user data
4Delete all contents of the user data field
5Click Save

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