CloudFormation stack outputs do not contain secrets
CloudFormation stack Outputs are analyzed for hardcoded secrets-passwords, API keys, tokens-using pattern-based detection across output values. A finding indicates potential secret strings present within Outputs of the template or stack.
Risk
Secrets in Outputs are readable to anyone with stack metadata access, enabling credential theft, unauthorized API calls, and lateral movement. Exposure via consoles, exports, or CI logs undermines confidentiality and can lead to privilege escalation and data exfiltration.
Run this check with Prowler
Run in Prowler Cloudprowler aws --checks cloudformation_stack_outputs_find_secretsRecommendation
Remove secrets from Outputs. Store credentials in Secrets Manager or Parameter Store and reference them via dynamic references; set NoEcho for sensitive parameters. Apply least privilege to view stack metadata, avoid exporting sensitive values, and add automated IaC secret scanning for defense in depth.
Remediation
CLI
aws cloudformation update-stack --stack-name <STACK_NAME> --template-body file://<TEMPLATE_WITHOUT_SENSITIVE_OUTPUTS>.yamlTerraform
# Critical: the embedded CloudFormation template removes secret outputs
resource "aws_cloudformation_stack" "<example_resource_name>" {
name = "<example_resource_name>"
template_body = <<-YAML
AWSTemplateFormatVersion: '2010-09-09'
# Critical: delete Outputs that expose secrets; keep only non-sensitive values
Outputs:
SafeInfo:
Value: "non-sensitive" # Avoids exposing secrets in stack outputs
YAML
}NativeIaC
AWSTemplateFormatVersion: '2010-09-09'
Outputs:
# Critical: remove outputs that expose secrets (passwords/tokens/keys)
# Keeping only non-sensitive values in Outputs remediates the finding
SafeInfo:
Value: "non-sensitive"Other
Check Code
from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.lib.utils.utils import (
SecretsScanError,
annotate_verified_secrets,
detect_secrets_scan_batch,
)
from prowler.providers.aws.services.cloudformation.cloudformation_client import (
cloudformation_client,
)
class cloudformation_stack_outputs_find_secrets(Check):
"""Check if a CloudFormation Stack has secrets in their Outputs"""
def execute(self):
"""Execute the cloudformation_stack_outputs_find_secrets check"""
findings = []
secrets_ignore_patterns = cloudformation_client.audit_config.get(
"secrets_ignore_patterns", []
)
validate = cloudformation_client.audit_config.get("secrets_validate", False)
stacks = list(cloudformation_client.stacks)
# Collect one payload per stack (its Outputs) and scan them all in
# batched Kingfisher invocations instead of one subprocess per stack.
def payloads():
for index, stack in enumerate(stacks):
if stack.outputs:
yield index, "".join(f"{output}\n" for output in stack.outputs)
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, stack in enumerate(stacks):
report = Check_Report_AWS(metadata=self.metadata(), resource=stack)
report.status = "PASS"
report.status_extended = (
f"No secrets found in CloudFormation Stack {stack.name} Outputs."
)
if stack.outputs:
if scan_error:
report.status = "MANUAL"
report.status_extended = (
f"Could not scan CloudFormation Stack {stack.name} Outputs "
f"for secrets: {scan_error}; manual review is required."
)
findings.append(report)
continue
detect_secrets_output = batch_results.get(index)
if detect_secrets_output:
secrets_string = ", ".join(
[
f"{secret['type']} in Output {int(secret['line_number'])}"
for secret in detect_secrets_output
]
)
report.status = "FAIL"
report.status_extended = f"Potential secret found in CloudFormation Stack {stack.name} Outputs -> {secrets_string}."
annotate_verified_secrets(report, detect_secrets_output)
else:
report.status = "PASS"
report.status_extended = (
f"CloudFormation Stack {stack.name} has no Outputs."
)
findings.append(report)
return findings