Back to Checks
aws

CloudFormation stack outputs do not contain secrets

cloudformation_stack_outputs_find_secrets
Service: cloudformationSeverity: critical
OfficialBuilt-inVerified
Categories:secrets
Resource Type:AwsCloudFormationStack

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 Cloud
Run
prowler aws --checks cloudformation_stack_outputs_find_secrets

Recommendation

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>.yaml

Terraform

# 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

1In the AWS Console, go to CloudFormation > Stacks and select the stack
2Click Update > Replace current template
3Upload or paste the template with any secret-bearing Outputs removed (do not output passwords/tokens/keys)
4Click Next through the wizard and choose Submit to apply the change set
5Verify the stack Outputs tab no longer shows sensitive values

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