CloudFormation Stack Outputs Contain No Secrets
Overview
This check scans AWS CloudFormation stack outputs for hardcoded secrets such as passwords, API keys, tokens, and database credentials. Prowler uses pattern-based detection to identify potential secrets stored directly in the Outputs section of CloudFormation templates or deployed stacks.
Risk
Storing secrets in CloudFormation stack outputs creates serious security vulnerabilities:
- Credential exposure: Anyone with
cloudformation:DescribeStackspermission can see output values in plain text via the console, CLI, or API - Export visibility: Exported outputs are visible across your entire AWS account and can be referenced by other stacks
- Log leakage: Stack outputs may appear in CI/CD logs, deployment pipelines, or debugging output
- Lateral movement: Attackers who obtain credentials can access connected systems (databases, APIs, other AWS services)
- Compliance violations: Storing secrets in plain-text outputs violates PCI-DSS, HIPAA, SOC 2, and other security frameworks
Severity: Critical
Remediation Steps
Prerequisites
- AWS account access with permissions to modify CloudFormation stacks
- Knowledge of which stack outputs contain sensitive values
Required IAM permissions
You will need the following permissions:
cloudformation:DescribeStacks- View stack details and outputscloudformation:UpdateStack- Modify existing stackssecretsmanager:CreateSecret- Create new secretssecretsmanager:GetSecretValue- Retrieve secret valuesssm:PutParameter- Create Parameter Store entries (alternative)
AWS Console Method
Step 1: Identify the secrets in stack outputs
- Sign in to the AWS Management Console
- Navigate to CloudFormation > Stacks
- Select the flagged stack
- Click the Outputs tab
- Review the Prowler finding to identify which outputs contain secrets
Step 2: Store secrets in AWS Secrets Manager
- Open Secrets Manager in the AWS Console
- Click Store a new secret
- For Secret type, choose:
- Credentials for Amazon RDS database (for database passwords)
- Other type of secret (for API keys, tokens, etc.)
- Enter your secret key-value pairs (e.g.,
api_key=your-actual-api-key) - Click Next
- For Secret name, enter a descriptive name (e.g.,
myapp/prod/api-credentials) - Add a description and tags if desired
- Click Next, configure rotation if desired, then click Store
- Copy the Secret ARN for reference
Step 3: Update your CloudFormation template
- Open your CloudFormation template file
- Locate the
Outputssection - Remove any outputs that expose sensitive values
- If consumers need access to secrets, output the Secrets Manager ARN instead of the actual value
- Use
NoEcho: truefor any sensitive parameters
Step 4: Update the stack
- Go to CloudFormation > Stacks
- Select your stack and click Update
- Choose Replace current template and upload your modified template
- Review the changes and click Update stack
- Wait for the update to complete
AWS CLI (optional)
View current stack outputs:
aws cloudformation describe-stacks \
--stack-name <your-stack-name> \
--region us-east-1 \
--query 'Stacks[0].Outputs'
Create a secret in Secrets Manager:
aws secretsmanager create-secret \
--name myapp/prod/database-credentials \
--description "Database credentials for my application" \
--secret-string '{"username":"admin","password":"your-db-password"}' \
--region us-east-1
Update the stack with a modified template:
aws cloudformation update-stack \
--stack-name <your-stack-name> \
--template-body file://updated-template.yaml \
--parameters ParameterKey=ExistingParam,UsePreviousValue=true \
--capabilities CAPABILITY_IAM \
--region us-east-1
Check stack update status:
aws cloudformation describe-stacks \
--stack-name <your-stack-name> \
--region us-east-1 \
--query 'Stacks[0].StackStatus'
CloudFormation (optional)
Before (insecure - secrets in outputs):
# DO NOT DO THIS - secrets exposed in outputs
AWSTemplateFormatVersion: '2010-09-09'
Description: Insecure template with secrets in outputs
Parameters:
DatabasePassword:
Type: String
NoEcho: true
Resources:
MyDatabase:
Type: AWS::RDS::DBInstance
Properties:
# ... database configuration
Outputs:
# INSECURE: This exposes the password to anyone who can read stack outputs!
DatabasePassword:
Description: The database password
Value: !Ref DatabasePassword
Export:
Name: MyApp-DbPassword
After (secure - use Secrets Manager):
AWSTemplateFormatVersion: '2010-09-09'
Description: Secure template using Secrets Manager for sensitive data
Parameters:
DatabasePassword:
Type: String
NoEcho: true
Description: Database password (will be stored in Secrets Manager)
Resources:
DatabaseSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: myapp/prod/database-credentials
Description: Database credentials for my application
SecretString: !Sub |
{
"username": "admin",
"password": "${DatabasePassword}"
}
MyDatabase:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: my-database
Engine: mysql
MasterUsername: admin
MasterUserPassword: !Ref DatabasePassword
# ... other database configuration
Outputs:
# SECURE: Output the secret ARN, not the actual password
DatabaseSecretArn:
Description: ARN of the secret containing database credentials
Value: !Ref DatabaseSecret
Export:
Name: MyApp-DbSecretArn
DatabaseEndpoint:
Description: Database endpoint (non-sensitive)
Value: !GetAtt MyDatabase.Endpoint.Address
Using dynamic references (even more secure):
AWSTemplateFormatVersion: '2010-09-09'
Description: Template using dynamic references to Secrets Manager
Resources:
MyDatabase:
Type: AWS::RDS::DBInstance
Properties:
DBInstanceIdentifier: my-database
Engine: mysql
# Dynamic reference - password retrieved at deployment time
MasterUsername: '{{resolve:secretsmanager:myapp/prod/database-credentials:SecretString:username}}'
MasterUserPassword: '{{resolve:secretsmanager:myapp/prod/database-credentials:SecretString:password}}'
# ... other database configuration
Outputs:
# Only output non-sensitive information
DatabaseEndpoint:
Description: Database connection endpoint
Value: !GetAtt MyDatabase.Endpoint.Address
Deploy the secure template:
aws cloudformation deploy \
--template-file secure-template.yaml \
--stack-name my-secure-stack \
--parameter-overrides DatabasePassword=your-actual-password \
--capabilities CAPABILITY_IAM \
--region us-east-1
Terraform (optional)
Before (insecure - secrets in outputs):
# DO NOT DO THIS - secrets exposed in outputs
output "database_password" {
description = "Database password"
value = var.database_password
# Even with sensitive = true, the value is stored in state
sensitive = true
}
After (secure - use Secrets Manager):
provider "aws" {
region = "us-east-1"
}
# Store credentials in Secrets Manager
resource "aws_secretsmanager_secret" "database_credentials" {
name = "myapp/prod/database-credentials"
description = "Database credentials for my application"
}
resource "aws_secretsmanager_secret_version" "database_credentials" {
secret_id = aws_secretsmanager_secret.database_credentials.id
secret_string = jsonencode({
username = "admin"
password = var.database_password
})
}
# Database instance
resource "aws_db_instance" "main" {
identifier = "my-database"
engine = "mysql"
instance_class = "db.t3.micro"
username = "admin"
password = var.database_password
# ... other configuration
}
# SECURE: Output the secret ARN, not the actual password
output "database_secret_arn" {
description = "ARN of the secret containing database credentials"
value = aws_secretsmanager_secret.database_credentials.arn
}
# Non-sensitive outputs are fine
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
}
# Variables - marked as sensitive
variable "database_password" {
type = string
sensitive = true
description = "Database password to store in Secrets Manager"
}
Using data sources to reference existing secrets:
# Reference an existing secret (no password in Terraform at all)
data "aws_secretsmanager_secret_version" "database_credentials" {
secret_id = "myapp/prod/database-credentials"
}
locals {
db_credentials = jsondecode(data.aws_secretsmanager_secret_version.database_credentials.secret_string)
}
resource "aws_db_instance" "main" {
identifier = "my-database"
engine = "mysql"
instance_class = "db.t3.micro"
username = local.db_credentials["username"]
password = local.db_credentials["password"]
# ... other configuration
}
# Only output non-sensitive values
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
}
Verification
After completing the remediation:
- Navigate to CloudFormation > Stacks > your stack in the AWS Console
- Click the Outputs tab
- Verify no sensitive values are displayed (only ARNs or non-sensitive information)
- If you exported any outputs, verify the exports are also clean
- Re-run the Prowler check to confirm the issue is resolved
CLI verification commands
Verify stack outputs no longer contain secrets:
aws cloudformation describe-stacks \
--stack-name <your-stack-name> \
--region us-east-1 \
--query 'Stacks[0].Outputs'
Check exported outputs across your account:
aws cloudformation list-exports \
--region us-east-1
Verify the secret exists in Secrets Manager:
aws secretsmanager describe-secret \
--secret-id myapp/prod/database-credentials \
--region us-east-1
Re-run the Prowler check:
prowler aws --checks cloudformation_stack_outputs_find_secrets --region us-east-1
Additional Resources
- AWS Secrets Manager Best Practices
- CloudFormation Outputs Section Structure
- Using Dynamic References in CloudFormation
- NoEcho Parameter Attribute
- AWS Systems Manager Parameter Store
Notes
- Stack outputs are visible to all: Unlike Secrets Manager, CloudFormation outputs are visible in plain text to anyone with
cloudformation:DescribeStackspermissions. This includes the AWS Console, CLI, and API responses. - Exports are account-wide: When you export a stack output, it becomes visible and referenceable across your entire AWS account. Never export sensitive values.
- NoEcho only protects parameters: The
NoEchoattribute on parameters hides values in the console and API responses for parameters, but does NOT protect output values. Outputs are always visible. - Alternative: AWS Systems Manager Parameter Store: For simpler secrets or cost savings, you can use Parameter Store with SecureString parameters instead of Secrets Manager.
- Scan templates before deployment: Implement pre-deployment scanning in your CI/CD pipeline using tools like cfn-nag, checkov, or Prowler to catch secrets before they reach production.
- Redeploy to clear history: Even after updating a stack, old output values may be visible in CloudTrail logs or deployment history. Consider rotating any exposed credentials.
- Dynamic references: Use CloudFormation dynamic references (
{{resolve:secretsmanager:...}}) to retrieve secrets at deployment time without exposing them in templates or outputs.