Secrets Found in EC2 Launch Template User Data
Overview
This check scans all versions of your EC2 launch templates for secrets embedded in User Data. User Data is a startup script that runs when an instance launches. If passwords, API keys, tokens, or private keys are hardcoded there, anyone who can view the launch template can see those credentials.
Risk
Hardcoded secrets in User Data create serious security vulnerabilities:
- Anyone with EC2 read access can see them - Launch template User Data is visible to anyone who can describe launch templates
- Historical versions retain secrets - Even after creating a new version, old versions with secrets remain accessible
- Attackers gain lateral movement - Exposed API keys let attackers access other AWS services or accounts
- Secrets get logged - Bootstrap scripts often appear in logs, exposing credentials
- Incident recovery is harder - Multiple template versions with leaked secrets complicate credential rotation
Remediation Steps
Prerequisites
You need:
- AWS Console access with permissions to view and modify EC2 launch templates
- A secrets management solution (AWS Secrets Manager or SSM Parameter Store)
AWS Console Method
-
Find the affected launch template
- Open the EC2 Console
- In the left menu, click Launch Templates
- Locate the flagged launch template
-
Review all versions for secrets
- Select the launch template
- Click the Versions tab
- For each version, click Actions > View details
- Scroll to User data and check for hardcoded secrets (passwords, API keys, tokens, connection strings)
-
Move secrets to AWS Secrets Manager
- Open Secrets Manager
- Click Store a new secret
- Select Other type of secret and enter your key-value pairs
- Name the secret (e.g.,
myapp/database-credentials) - Complete the wizard
-
Create an IAM role for instances to access secrets
- Open the IAM Console
- Click Roles > Create role
- Select AWS service and choose EC2
- Attach a policy that grants
secretsmanager:GetSecretValuefor your secret - Name the role (e.g.,
EC2SecretsAccessRole)
-
Create a new launch template version without secrets
- Back in the EC2 Console, select your launch template
- Click Actions > Modify template (Create new version)
- Under Advanced details, find User data
- Remove hardcoded secrets and replace them with code that fetches secrets at runtime (see example below)
- Under Advanced details > IAM instance profile, select the role you created
- Click Create template version
-
Set the new version as default
- Select the launch template
- Click Actions > Set default version
- Choose your new secure version
-
Delete old versions containing secrets
- On the Versions tab, select each version that contained secrets
- Click Actions > Delete template version
- Confirm deletion
-
Rotate the exposed secrets
- Treat any secrets that were in User Data as compromised
- Generate new credentials and update them in Secrets Manager
- Update any systems using the old credentials
Example: Fetching secrets at runtime in User Data
Instead of:
#!/bin/bash
export DB_PASSWORD="my-secret-password"
Use:
#!/bin/bash
DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id myapp/database-credentials \
--query SecretString --output text \
--region us-east-1 | jq -r '.password')
export DB_PASSWORD
AWS CLI Method
Step 1: List all launch templates
aws ec2 describe-launch-templates \
--region us-east-1 \
--query 'LaunchTemplates[*].[LaunchTemplateId,LaunchTemplateName,LatestVersionNumber]' \
--output table
Step 2: Check all versions of a launch template for User Data
# List all versions
aws ec2 describe-launch-template-versions \
--launch-template-name <your-launch-template-name> \
--region us-east-1 \
--query 'LaunchTemplateVersions[*].[VersionNumber,LaunchTemplateData.UserData]' \
--output table
Step 3: Decode and inspect User Data for a specific version
# Get User Data (base64 encoded) and decode it
aws ec2 describe-launch-template-versions \
--launch-template-name <your-launch-template-name> \
--versions 1 \
--region us-east-1 \
--query 'LaunchTemplateVersions[0].LaunchTemplateData.UserData' \
--output text | base64 --decode
Step 4: Store secrets in Secrets Manager
aws secretsmanager create-secret \
--name myapp/database-credentials \
--secret-string '{"username":"admin","password":"new-secure-password"}' \
--region us-east-1
Step 5: Create IAM role for instances to access secrets
# Create trust policy file
cat > /tmp/trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
EOF
# Create the role
aws iam create-role \
--role-name EC2SecretsAccessRole \
--assume-role-policy-document file:///tmp/trust-policy.json
# Attach policy for Secrets Manager access
aws iam put-role-policy \
--role-name EC2SecretsAccessRole \
--policy-name SecretsManagerReadAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:us-east-1:*:secret:myapp/*"
}]
}'
# Create instance profile
aws iam create-instance-profile \
--instance-profile-name EC2SecretsAccessProfile
# Add role to profile
aws iam add-role-to-instance-profile \
--instance-profile-name EC2SecretsAccessProfile \
--role-name EC2SecretsAccessRole
Step 6: Create new launch template version without secrets
# Prepare secure User Data script (base64 encoded)
USER_DATA=$(cat << 'USERDATA' | base64 -w 0
#!/bin/bash
# Fetch secrets at runtime instead of hardcoding them
DB_CREDS=$(aws secretsmanager get-secret-value \
--secret-id myapp/database-credentials \
--query SecretString --output text \
--region us-east-1)
export DB_USERNAME=$(echo $DB_CREDS | jq -r '.username')
export DB_PASSWORD=$(echo $DB_CREDS | jq -r '.password')
# Continue with application startup...
USERDATA
)
# Create new version with secure User Data
aws ec2 create-launch-template-version \
--launch-template-name <your-launch-template-name> \
--source-version 1 \
--launch-template-data "{
\"UserData\": \"$USER_DATA\",
\"IamInstanceProfile\": {\"Name\": \"EC2SecretsAccessProfile\"}
}" \
--region us-east-1
Step 7: Set new version as default
aws ec2 modify-launch-template \
--launch-template-name <your-launch-template-name> \
--default-version <new-version-number> \
--region us-east-1
Step 8: Delete old versions containing secrets
# Delete a specific version
aws ec2 delete-launch-template-versions \
--launch-template-name <your-launch-template-name> \
--versions 1 2 3 \
--region us-east-1
CloudFormation Template
This template creates a secure launch template that fetches secrets from Secrets Manager at runtime.
AWSTemplateFormatVersion: '2010-09-09'
Description: Secure EC2 Launch Template without hardcoded secrets
Parameters:
AmiId:
Type: AWS::EC2::Image::Id
Description: AMI ID for the EC2 instances
InstanceType:
Type: String
Default: t3.medium
Description: EC2 instance type
SecretArn:
Type: String
Description: ARN of the secret in Secrets Manager
Resources:
# IAM Role for EC2 instances to access Secrets Manager
EC2SecretsRole:
Type: AWS::IAM::Role
Properties:
RoleName: EC2SecretsAccessRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: SecretsManagerReadAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref SecretArn
# Instance Profile
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: EC2SecretsAccessProfile
Roles:
- !Ref EC2SecretsRole
# Secure Launch Template
SecureLaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: my-app-launch-template-secure
LaunchTemplateData:
ImageId: !Ref AmiId
InstanceType: !Ref InstanceType
IamInstanceProfile:
Arn: !GetAtt EC2InstanceProfile.Arn
UserData:
Fn::Base64: !Sub |
#!/bin/bash
# Fetch secrets at runtime - no hardcoded credentials
DB_CREDS=$(aws secretsmanager get-secret-value \
--secret-id ${SecretArn} \
--query SecretString --output text \
--region ${AWS::Region})
export DB_USERNAME=$(echo $DB_CREDS | jq -r '.username')
export DB_PASSWORD=$(echo $DB_CREDS | jq -r '.password')
# Continue with application startup...
Outputs:
LaunchTemplateId:
Description: ID of the secure launch template
Value: !Ref SecureLaunchTemplate
LaunchTemplateName:
Description: Name of the secure launch template
Value: !GetAtt SecureLaunchTemplate.LaunchTemplateName
Deployment:
aws cloudformation create-stack \
--stack-name secure-launch-template \
--template-body file://template.yaml \
--parameters \
ParameterKey=AmiId,ParameterValue=ami-0123456789abcdef0 \
ParameterKey=SecretArn,ParameterValue=arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/database-credentials-AbCdEf \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
Terraform Configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "ami_id" {
description = "AMI ID for the EC2 instances"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.medium"
}
variable "secret_arn" {
description = "ARN of the secret in Secrets Manager"
type = string
}
# IAM Role for EC2 instances to access Secrets Manager
resource "aws_iam_role" "ec2_secrets_role" {
name = "EC2SecretsAccessRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "secrets_manager_access" {
name = "SecretsManagerReadAccess"
role = aws_iam_role.ec2_secrets_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = var.secret_arn
}
]
})
}
resource "aws_iam_instance_profile" "ec2_secrets_profile" {
name = "EC2SecretsAccessProfile"
role = aws_iam_role.ec2_secrets_role.name
}
# Secure Launch Template
resource "aws_launch_template" "secure_lt" {
name = "my-app-launch-template-secure"
image_id = var.ami_id
instance_type = var.instance_type
iam_instance_profile {
arn = aws_iam_instance_profile.ec2_secrets_profile.arn
}
user_data = base64encode(<<-USERDATA
#!/bin/bash
# Fetch secrets at runtime - no hardcoded credentials
DB_CREDS=$(aws secretsmanager get-secret-value \
--secret-id ${var.secret_arn} \
--query SecretString --output text \
--region us-east-1)
export DB_USERNAME=$(echo $DB_CREDS | jq -r '.username')
export DB_PASSWORD=$(echo $DB_CREDS | jq -r '.password')
# Continue with application startup...
USERDATA
)
update_default_version = true
}
output "launch_template_id" {
description = "ID of the secure launch template"
value = aws_launch_template.secure_lt.id
}
output "launch_template_name" {
description = "Name of the secure launch template"
value = aws_launch_template.secure_lt.name
}
Deployment:
terraform init
terraform plan -var="ami_id=ami-0123456789abcdef0" \
-var="secret_arn=arn:aws:secretsmanager:us-east-1:123456789012:secret:myapp/database-credentials-AbCdEf"
terraform apply
Verification
After remediation, verify the fix:
-
Re-run the Prowler check
prowler aws --check ec2_launch_template_no_secrets -r us-east-1 -
Manually verify in AWS Console
- Go to EC2 > Launch Templates
- Select your launch template
- Click the Versions tab and confirm old versions with secrets are deleted
- View the current version's User Data and confirm no secrets are visible
-
Test that instances can still access secrets
- Launch a test instance from the new template
- SSH into the instance and verify the application can retrieve credentials from Secrets Manager
Additional Resources
- AWS Secrets Manager User Guide
- AWS Systems Manager Parameter Store
- EC2 Launch Templates User Guide
- EC2 Instance Metadata Service
- Prowler Check Documentation
Notes
- Delete ALL versions with secrets - Unlike launch configurations, launch templates support versioning. You must delete every version that contained secrets, not just update to a new version
- Rotate compromised secrets immediately - Any secrets found in User Data should be considered compromised and replaced
- Use least privilege for IAM roles - Only grant access to the specific secrets the application needs
- Consider using EC2 Instance Connect or SSM Session Manager - Avoid storing SSH keys in User Data
- Enable secret rotation - AWS Secrets Manager supports automatic rotation for many secret types
- Review Auto Scaling groups - If Auto Scaling groups use the affected launch template, they will automatically use the new default version