Back to Checks
aws

EC2 launch template user data contains no secrets in any version

ec2_launch_template_no_secrets
Service: ec2Severity: high
OfficialBuilt-inVerified
Categories:secrets
Resource Type:AwsEc2LaunchTemplate

EC2 launch template user data is analyzed across versions to identify embedded secrets-hard-coded passwords, tokens, API keys, or private keys-within the startup scripts or configuration supplied to instances.

Risk

Secrets in user data can be read by identities able to view launch templates, eroding confidentiality.

Exposed credentials enable unauthorized API actions, data exfiltration, and lateral movement. Past template versions retain leaked values, complicating rotation and recovery.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks ec2_launch_template_no_secrets

Recommendation

Keep user data free of secrets. Retrieve sensitive values at runtime from AWS Secrets Manager or SSM Parameter Store SecureString using instance roles.

Enforce least privilege, rotate to short-lived credentials, and review template history; if exposure occurred, rotate affected secrets.

Remediation

Terraform

# Terraform: Launch Template with empty user data
resource "aws_launch_template" "<example_resource_name>" {
  name      = "<example_resource_name>"
  user_data = ""  # Critical: empty user data prevents secrets from being stored
}

NativeIaC

# CloudFormation: Launch Template without user data secrets
Resources:
  <example_resource_name>:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        UserData: ""  # Critical: empty user data ensures no secrets are stored in any new version

Other

1In the AWS Console, go to EC2 > Launch Templates
2Select the launch template and click Create new version
3In Advanced details, clear the User data field so it is blank
4Save and set this clean version as the Default version
5Back in the Versions tab, select all versions that contain secrets and click Actions > Delete versions
6Ensure only versions with blank (or non-secret) user data remain

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_launch_template_no_secrets(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)
        templates = list(ec2_client.launch_templates)

        # Track versions whose User Data cannot be decoded so the template is
        # surfaced (MANUAL) instead of silently claiming no secrets were found.
        undecodable_versions = {}

        # Collect the decoded User Data of every (template, version) and scan it
        # all in batched Kingfisher invocations instead of one subprocess per
        # version. Versions whose User Data cannot be decoded are recorded above.
        def payloads():
            for template_index, template in enumerate(templates):
                for version_index, version in enumerate(template.versions):
                    if not version.template_data.user_data:
                        continue
                    user_data = b64decode(version.template_data.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"{template.region} -- Unable to decode User Data in EC2 Launch Template {template.name} version {version.version_number}: {error}"
                        )
                        undecodable_versions.setdefault(template_index, []).append(
                            version.version_number
                        )
                        continue
                    except Exception as error:
                        logger.error(
                            f"{template.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
                        )
                        undecodable_versions.setdefault(template_index, []).append(
                            version.version_number
                        )
                        continue
                    yield (template_index, version_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 template_index, template in enumerate(templates):
            report = Check_Report_AWS(metadata=self.metadata(), resource=template)

            if scan_error and any(
                version.template_data.user_data for version in template.versions
            ):
                report.status = "MANUAL"
                report.status_extended = (
                    f"Could not scan EC2 Launch Template {template.name} User Data "
                    f"for secrets: {scan_error}; manual review is required."
                )
                findings.append(report)
                continue

            versions_with_secrets = []
            all_secrets = []

            for version_index, version in enumerate(template.versions):
                version_secrets = batch_results.get((template_index, version_index))
                if version_secrets:
                    all_secrets.extend(version_secrets)
                    secrets_string = ", ".join(
                        [
                            f"{secret['type']} on line {secret['line_number']}"
                            for secret in version_secrets
                        ]
                    )
                    versions_with_secrets.append(
                        f"Version {version.version_number}: {secrets_string}"
                    )

            undecodable = undecodable_versions.get(template_index, [])
            if len(versions_with_secrets) > 0:
                report.status = "FAIL"
                report.status_extended = f"Potential secret found in User Data for EC2 Launch Template {template.name} in template versions: {', '.join(versions_with_secrets)}."
                annotate_verified_secrets(report, all_secrets)
            elif undecodable:
                report.status = "MANUAL"
                report.status_extended = f"Could not decode User Data for EC2 Launch Template {template.name} versions: {', '.join(str(version_number) for version_number in undecodable)}; manual review is required to scan for secrets."
            else:
                report.status = "PASS"
                report.status_extended = f"No secrets found in User Data of any version for EC2 Launch Template {template.name}."

            findings.append(report)

        return findings