Skip to main content

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:DescribeStacks permission 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 outputs
  • cloudformation:UpdateStack - Modify existing stacks
  • secretsmanager:CreateSecret - Create new secrets
  • secretsmanager:GetSecretValue - Retrieve secret values
  • ssm:PutParameter - Create Parameter Store entries (alternative)

AWS Console Method

Step 1: Identify the secrets in stack outputs

  1. Sign in to the AWS Management Console
  2. Navigate to CloudFormation > Stacks
  3. Select the flagged stack
  4. Click the Outputs tab
  5. Review the Prowler finding to identify which outputs contain secrets

Step 2: Store secrets in AWS Secrets Manager

  1. Open Secrets Manager in the AWS Console
  2. Click Store a new secret
  3. For Secret type, choose:
    • Credentials for Amazon RDS database (for database passwords)
    • Other type of secret (for API keys, tokens, etc.)
  4. Enter your secret key-value pairs (e.g., api_key = your-actual-api-key)
  5. Click Next
  6. For Secret name, enter a descriptive name (e.g., myapp/prod/api-credentials)
  7. Add a description and tags if desired
  8. Click Next, configure rotation if desired, then click Store
  9. Copy the Secret ARN for reference

Step 3: Update your CloudFormation template

  1. Open your CloudFormation template file
  2. Locate the Outputs section
  3. Remove any outputs that expose sensitive values
  4. If consumers need access to secrets, output the Secrets Manager ARN instead of the actual value
  5. Use NoEcho: true for any sensitive parameters

Step 4: Update the stack

  1. Go to CloudFormation > Stacks
  2. Select your stack and click Update
  3. Choose Replace current template and upload your modified template
  4. Review the changes and click Update stack
  5. 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:

  1. Navigate to CloudFormation > Stacks > your stack in the AWS Console
  2. Click the Outputs tab
  3. Verify no sensitive values are displayed (only ARNs or non-sensitive information)
  4. If you exported any outputs, verify the exports are also clean
  5. 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

Notes

  • Stack outputs are visible to all: Unlike Secrets Manager, CloudFormation outputs are visible in plain text to anyone with cloudformation:DescribeStacks permissions. 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 NoEcho attribute 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.