Build a Financial Evidence Vault

Build a policy-gated S3 evidence vault with KMS encryption and IAM controls.

Introduction

30 Second Summary

Financial evidence often moves from the people who submit records to the people who review them. When access is too broad, either group can read, change, or delete information outside its job.

In this project, you will build a policy-gated financial evidence vault on AWS. The finished workflow produces an evidence report that proves its security controls work.

What You'll Build

Picture showing a reviewer an audit-ready report tracing a weak template's rejection through to the vault's encrypted upload evidence.

By the end of this project, you'll have:

  • A runnable policy gate where an insecure AWS CloudFormation template visibly fails before the hardened version passes every AWS CloudFormation Guard rule.
  • A private Amazon S3 vault where protected uploads use AWS KMS encryption while uploads missing the approved key are denied.
  • An IAM authorization matrix that shows the uploader allowed to write under incoming/ while the auditor is allowed to read. Both roles are denied permission to delete evidence.
  • Secret Mission: Add a KMS rotation guardrail that makes the policy gate fail whenever automatic key rotation is disabled.

Are there any prerequisites?

You need a personal AWS account with administrator access. A modern browser gives you access to AWS CloudShell, so no local terminal or CLI installation is required.

Before We Start

First, lock in the business outcome behind your financial evidence vault. This gives every security control a clear purpose before you start working in AWS.

Prepare Your AWS Security Workspace

Your vault's controls depend on the AWS identity that creates them. Confirming that identity keeps every later result tied to your intended administrator account.

A browser-based AWS CloudShell session removes the need for local Windows setup. It provides the same AWS CLI workflow used in automation pipelines.

In this step, you will verify AWS CLI version 2. You will install AWS CloudFormation Guard version 3.2.1 inside a dedicated project workspace.

In this step, get ready to:
  • Confirm the administrator identity used by AWS CloudShell.
  • Install AWS CloudFormation Guard version 3.2.1.
  • Create the ~/financial-evidence-vault workspace.
Confirm your AWS identity in CloudShell

CloudShell inherits the credentials from your console session. Its persistent home storage belongs to the selected Region.

  • Sign in to the AWS Management Console with your administrator account.

You will see the AWS Management Console home page for your account.

  • Use the Region selector in the top navigation bar to choose US East (N. Virginia) as your commercial AWS Region.

The Region selector now shows US East (N. Virginia).

  • Click the CloudShell icon in the top navigation bar to launch the browser shell.

You will see a terminal prompt inside the console. The shell is already authenticated with your current AWS credentials.

Before you run the identity check, predict which AWS account and principal the shell is using.

  • Confirm the CLI version plus the active identity by running these commands:
aws --version
aws sts get-caller-identity

What Do These Commands Confirm?

  • The aws --version command confirms that AWS CLI version 2 is available.
  • The aws sts get-caller-identity command returns the account identifier plus the principal ARN used by CloudShell.

You should see AWS CLI version information first. The identity response should show your intended account identifier plus an administrator principal ARN.

That first check is complete. Your terminal output now ties the project to the intended AWS identity.

Seeing the Wrong AWS Identity?

  • End the current AWS Management Console session if the account identifier is unexpected.
  • Start a new console session with the intended administrator account.
  • Help me verify why AWS CloudShell is using an unexpected account or principal ARN.
Install AWS CloudFormation Guard 3.2.1

Guard evaluates YAML infrastructure against policy rules before deployment. Pinning the release keeps the security gate consistent throughout the project.

  • Install the pinned Guard release by running this command:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh -s -- -v 3.2.1

What Does This Installer Do?

The official installer downloads Guard into ~/.guard/bin. The version option requests release 3.2.1.

The installer returns you to the CloudShell prompt when it finishes.

Having Trouble Installing Guard?

  • Rerun the installer after confirming that CloudShell can access the internet.
  • Keep the installation destination inside your CloudShell home directory if you encounter a permissions problem.
  • Help me troubleshoot the Guard installer in AWS CloudShell.
  • Add the Guard installation directory to the current shell path by running this command:
export PATH=~/.guard/bin:$PATH

What Does This PATH Change Do?

The PATH value tells the shell where to find executable commands. This update makes cfn-guard available in the current CloudShell session.

Before you check the installation, predict which Guard version the shell should report.

  • Check the installed Guard version by running this command:
cfn-guard --version

What Does This Check Prove?

The command reads the version of the Guard binary currently available through PATH. Its result confirms whether the pinned release is active.

✔️ I see the required version

You should see cfn-guard 3.2.1. That is the installation complete: your shell now has the policy engine required for the security gate.

The required version is active in this CloudShell session. You are ready to create the project workspace.

ⓧ I see an older version

An older result means the shell is resolving a different Guard release. Installing the pinned binary under ~/.guard/bin gives this project a consistent version.

  • Install the pinned release plus activate its path by running these commands:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh -s -- -v 3.2.1
export PATH=~/.guard/bin:$PATH
cfn-guard --version

What Do These Commands Change?

  • The installer places release 3.2.1 under your CloudShell home directory.
  • The path update gives that installation priority in the current shell.
  • The final command verifies the active Guard version.

You should now see cfn-guard 3.2.1.

Still Seeing the Older Version?

  • Confirm that the path command completed without an error.
  • Help me find why CloudShell still resolves an older Guard binary.

ⓧ Command not found

The shell cannot locate a Guard binary through its current path. Reinstalling the pinned release plus refreshing PATH restores the command.

  • Install the pinned binary plus expose it to the shell by running these commands:
curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh -s -- -v 3.2.1
export PATH=~/.guard/bin:$PATH
cfn-guard --version

How Does This Restore Guard?

  • The installer places Guard in ~/.guard/bin.
  • The path update makes that directory searchable in the current shell.
  • The version check confirms that the command is now available.

You should now see cfn-guard 3.2.1.

Guard Still Not Available?

  • Check the installer output for a download or permissions problem.
  • Help me restore the cfn-guard command in AWS CloudShell.
Create the project workspace

A dedicated directory keeps the policy rules plus infrastructure files together. CloudShell stores this workspace in the selected Region's persistent home storage.

  • Create the ~/financial-evidence-vault directory plus enter it by running this command:
mkdir -p ~/financial-evidence-vault && cd ~/financial-evidence-vault

What Does This Command Do?

The command creates ~/financial-evidence-vault if it does not exist. It then makes that directory the current CloudShell location.

The command should return without an error. Your current directory is now ~/financial-evidence-vault.

Before the final check, predict which identity details plus Guard version should appear.

  • Verify the authenticated identity plus the Guard installation by running these commands from the workspace:
aws sts get-caller-identity
cfn-guard --version

What Does the Final Check Prove?

  • The identity response proves that CloudShell is authenticated to your intended administrator account.
  • The version response proves that Guard 3.2.1 is available through the current shell path.

You will see your account identifier plus principal ARN. You will then see cfn-guard 3.2.1.

Your authenticated security workspace is ready. The CLI plus policy engine can now support repeatable checks.

Final Check Not Matching?

  • Return to the identity instructions if the account identifier is unexpected.
  • Rerun the path export command if the Guard command is unavailable.
  • Help me diagnose my final AWS CloudShell workspace check.

Your authenticated workspace is ready. Next, you will prove the security gate can reject a weak Amazon S3 template.

Make an Insecure Template Fail the Security Gate

Your AWS CloudShell workspace is authenticated and ready. The next risk is a security gate that appears healthy only because it has never faced a weak design.

Credible security controls detect a deliberately weak template before deployment. In this step, AWS CloudFormation Guard will evaluate a minimal Amazon S3 bucket against the controls your financial evidence vault needs.

In this step, get ready to:
  • Create a deliberately insecure AWS CloudFormation template.
  • Encode bucket presence, SSE-KMS with an S3 Bucket Key, versioning, and public-access requirements as Guard rules.
  • Run the policy gate and connect each failed rule to a missing business control.
Create the insecure starter template

The starter is a valid AWS CloudFormation template with one bucket resource. It deliberately omits the protections required for financial evidence.

  • Create template.yaml in your current CloudShell workspace by running this command:
edit template.yaml

What Does This Command Do?

The edit command opens template.yaml in CloudShell's visual editor. Saving the tab creates the file inside ~/financial-evidence-vault.

CloudShell can pause multiline pastes for a Safe Paste review. This gives you a chance to inspect the complete text before it reaches the editor.

  • Paste the deliberately insecure starter into the editor:
  • Choose Paste if the Safe Paste dialog appears.
AWSTemplateFormatVersion: '2010-09-09'
Description: Deliberately insecure starter for policy-as-code testing
Resources:
  EvidenceBucket:
    Type: AWS::S3::Bucket

What Does This Template Define?

  • The Resources section contains one resource named EvidenceBucket.
  • The AWS::S3::Bucket type tells CloudFormation to create an S3 bucket.
  • The template has no bucket properties. That empty configuration gives the policy gate a weak design to test.
  • Save template.yaml by pressing Ctrl+S.
  • Close the editor tab to return to the CloudShell terminal.

Your saved file contains the five-line EvidenceBucket template. The bucket has no encryption, versioning, or public-access configuration.

Is the Template Missing After You Save?

  • Check that the editor tab is named template.yaml.
  • Confirm that you pressed Ctrl+S before closing the editor.
  • Ask for help with creating template.yaml in AWS CloudShell.

✔️ Awesome, I've got everything!

  • Confirm that template.yaml is saved in CloudShell.

ⓧ I'd like to double check the full code

The complete template.yaml file should match this reference.

AWSTemplateFormatVersion: '2010-09-09'
Description: Deliberately insecure starter for policy-as-code testing
Resources:
  EvidenceBucket:
    Type: AWS::S3::Bucket
Build the first policy checks

Policy as code turns security requirements into executable rules. The first rules locate every S3 bucket before checking its encryption and recovery settings.

  • Create security.guard in your current CloudShell workspace by running this command:
edit security.guard

What Does This Command Do?

The edit command opens a visual editor tab for security.guard. This file becomes the security gate for your CloudFormation template.

  • Add the S3 selector and bucket-presence rule by pasting this code:
  • Choose Paste if the Safe Paste dialog appears.
let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]

rule S3_BUCKET_PRESENT {
  %s3_buckets !empty
  <<
    Violation: The template must contain an Amazon S3 evidence bucket.
    Fix: Add an AWS::S3::Bucket resource.
  >>
}

How Does the Selector Work?

  • The s3_buckets selector collects every resource whose type is AWS::S3::Bucket.
  • The S3_BUCKET_PRESENT rule requires that collection to contain at least one bucket.
  • The violation text connects an empty collection to the missing evidence-storage requirement.
  • Save security.guard by pressing Ctrl+S.
  • Switch back to the CloudShell terminal tab.
  • Confirm that the selector finds EvidenceBucket by running the gate command:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Does This First Check Prove?

The report shows no violation for S3_BUCKET_PRESENT. This proves that the selector found EvidenceBucket in the template.

Does the Presence Rule Report a Violation?

  • Confirm that template.yaml contains the EvidenceBucket resource.
  • Check that the resource type is exactly AWS::S3::Bucket.
  • Ask for help with debugging the Guard bucket selector.

Good, the selector has found the evidence bucket. The next rule tests whether that bucket protects evidence with the approved encryption design.

  • Switch back to the security.guard editor tab.
  • Add the encryption rule below S3_BUCKET_PRESENT by pasting this code:
rule S3_KMS_ENCRYPTION when %s3_buckets !empty {
  %s3_buckets.Properties.BucketEncryption exists
  %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm == 'aws:kms'
  %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].BucketKeyEnabled == true
  <<
    Violation: The evidence bucket must use SSE-KMS with an S3 Bucket Key.
    Fix: Configure BucketEncryption with SSEAlgorithm aws:kms and BucketKeyEnabled true.
  >>
}

What Does the Encryption Rule Require?

  • The rule requires a BucketEncryption configuration on every selected bucket.
  • The SSEAlgorithm assertion requires the literal value aws:kms.
  • The BucketKeyEnabled assertion requires an S3 Bucket Key.
  • Save security.guard by pressing Ctrl+S.
  • Switch back to the CloudShell terminal tab.

Before you run the gate, which encryption condition do you expect the empty bucket configuration to satisfy?

  • Test the template against the encryption rule by running the gate command:
cfn-guard validate --rules security.guard --data template.yaml -s all

The Encryption Failure Is Intentional

You should see S3_KMS_ENCRYPTION fail because EvidenceBucket has no BucketEncryption configuration. The gate has now caught its first missing business control.

Encryption protects confidentiality. Versioning protects recovery when evidence is overwritten or removed, so the next rule checks that separate control.

  • Switch back to the security.guard editor tab.
  • Add the versioning rule below S3_KMS_ENCRYPTION by pasting this code:
rule S3_VERSIONING_ENABLED when %s3_buckets !empty {
  %s3_buckets.Properties.VersioningConfiguration.Status == 'Enabled'
  <<
    Violation: Evidence versioning is not enabled.
    Fix: Set VersioningConfiguration.Status to Enabled.
  >>
}

Why Require Versioning?

The S3_VERSIONING_ENABLED rule requires VersioningConfiguration.Status to equal Enabled. This makes the recovery requirement testable before deployment.

  • Save security.guard by pressing Ctrl+S.
  • Switch back to the CloudShell terminal tab.
  • Test the expanded rule set by running the gate command:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Changed in the Report?

The detailed report now includes failures for S3_KMS_ENCRYPTION and S3_VERSIONING_ENABLED. Each failure identifies a different missing protection in the same bucket.

Is the Versioning Rule Missing?

  • Confirm that you pasted S3_VERSIONING_ENABLED after the encryption rule's closing brace.
  • Save security.guard before running the validation again.
  • Ask for help with a missing Guard rule result.
Complete and run the security gate

A private evidence vault needs every S3 Block Public Access setting enabled. The final rule checks all four properties independently so a single weak setting cannot hide inside the configuration.

  • Switch back to the security.guard editor tab.
  • Add the public-access rule below S3_VERSIONING_ENABLED by pasting this code:
rule S3_PUBLIC_ACCESS_BLOCKED when %s3_buckets !empty {
  %s3_buckets.Properties.PublicAccessBlockConfiguration exists
  %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicAcls == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicPolicy == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.IgnorePublicAcls == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.RestrictPublicBuckets == true
  <<
    Violation: All four S3 Block Public Access controls must be enabled.
    Fix: Set BlockPublicAcls, BlockPublicPolicy, IgnorePublicAcls, and RestrictPublicBuckets to true.
  >>
}

What Does the Public-Access Rule Cover?

  • The rule requires PublicAccessBlockConfiguration to exist.
  • The BlockPublicAcls and IgnorePublicAcls checks prevent ACL-based public access.
  • The BlockPublicPolicy and RestrictPublicBuckets checks protect the bucket from public policy exposure.
  • Save security.guard by pressing Ctrl+S.
  • Close the editor tab to return to the CloudShell terminal.

Before the final run, which rule should recognize the existing bucket? Which rules should expose missing controls?

  • Run the complete security gate against the insecure template:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Should I See?

  • The report finds the existing bucket without a bucket-presence violation.
  • The S3_KMS_ENCRYPTION rule reports FAIL for the missing SSE-KMS configuration and S3 Bucket Key.
  • The S3_VERSIONING_ENABLED rule reports FAIL for the missing versioning configuration.
  • The S3_PUBLIC_ACCESS_BLOCKED rule reports FAIL for the missing configuration and all four public-access settings.

Not Seeing Every Expected Failure?

  • Confirm that template.yaml contains only the deliberately insecure EvidenceBucket starter.
  • Compare security.guard with the complete reference below.
  • Ask for help with unexpected CloudFormation Guard results.

✔️ Awesome, I've got everything!

  • Confirm that security.guard is saved with all four rules.
  • Keep the failed validation report visible for your checkpoint.

ⓧ I'd like to double check the full code

The complete security.guard file should match this reference.

let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]

rule S3_BUCKET_PRESENT {
  %s3_buckets !empty
  <<
    Violation: The template must contain an Amazon S3 evidence bucket.
    Fix: Add an AWS::S3::Bucket resource.
  >>
}

rule S3_KMS_ENCRYPTION when %s3_buckets !empty {
  %s3_buckets.Properties.BucketEncryption exists
  %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm == 'aws:kms'
  %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].BucketKeyEnabled == true
  <<
    Violation: The evidence bucket must use SSE-KMS with an S3 Bucket Key.
    Fix: Configure BucketEncryption with SSEAlgorithm aws:kms and BucketKeyEnabled true.
  >>
}

rule S3_VERSIONING_ENABLED when %s3_buckets !empty {
  %s3_buckets.Properties.VersioningConfiguration.Status == 'Enabled'
  <<
    Violation: Evidence versioning is not enabled.
    Fix: Set VersioningConfiguration.Status to Enabled.
  >>
}

rule S3_PUBLIC_ACCESS_BLOCKED when %s3_buckets !empty {
  %s3_buckets.Properties.PublicAccessBlockConfiguration exists
  %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicAcls == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.BlockPublicPolicy == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.IgnorePublicAcls == true
  %s3_buckets.Properties.PublicAccessBlockConfiguration.RestrictPublicBuckets == true
  <<
    Violation: All four S3 Block Public Access controls must be enabled.
    Fix: Set BlockPublicAcls, BlockPublicPolicy, IgnorePublicAcls, and RestrictPublicBuckets to true.
  >>
}

Strong start. Your security gate now proves that the weak bucket is missing essential controls. Next, you will harden the template until every rule passes.

Harden the Vault Until Every Control Passes

Your AWS CloudFormation Guard gate has already exposed the weak design. Now you will turn each failed control into a repeatable AWS CloudFormation definition.

The vault needs AWS KMS encryption to protect stored evidence. Amazon S3 recovery controls preserve previous object versions.

IAM role boundaries enforce separation of duties. Bucket-policy denies protect transport security plus approved-key usage.

In this step, get ready to:
  • Build the encrypted storage baseline.
  • Define separation of duties across both business roles.
  • Validate the completed template with two independent checks.
Build the encrypted storage baseline

The first layer combines a customer-managed key with an SSE-KMS bucket configuration. Versioning supplies a recovery path for overwritten evidence.

  • In the CloudShell editor, replace all content in template.yaml with this key definition:
AWSTemplateFormatVersion: '2010-09-09'
Description: Policy-gated financial evidence vault with least-privilege IAM and SSE-KMS

Resources:
  EvidenceKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Encrypts financial compliance evidence stored in the vault
      EnableKeyRotation: true
      PendingWindowInDays: 7
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: EnableAccountPermissions
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
      Tags:
        - Key: Project
          Value: FinancialEvidenceVault
        - Key: DataClassification
          Value: Confidential

What Does This Key Definition Do?

  • The EvidenceKey resource creates the customer-managed key that protects financial evidence.
  • The EnableKeyRotation setting enables automatic key rotation.
  • The PendingWindowInDays setting establishes a seven-day waiting period when stack deletion schedules the key for deletion.
  • The key policy preserves account-level control through the account root principal.
  • Save template.yaml.

Before you run the gate, predict whether a key-only template can satisfy rules written for an evidence bucket.

  • Test the key-only template by running:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Does This Result Prove?

You will see S3_BUCKET_PRESENT fail because the template does not contain an S3 bucket. This checkpoint proves the gate continues to protect the storage requirement while you harden the template.

Is the Template Failing to Parse?

  • Check that Resources: begins at the left edge of template.yaml.
  • Check each nested YAML level for consistent spacing.
  • Ask for help with the key definition by selecting help me diagnose my CloudFormation key YAML.
  • Append the bucket resource directly below EvidenceKey by pasting:

  EvidenceBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: !GetAtt EvidenceKey.Arn
            BucketKeyEnabled: true
      VersioningConfiguration:
        Status: Enabled
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Project
          Value: FinancialEvidenceVault
        - Key: DataClassification
          Value: Confidential

How Does the Bucket Pass the Gate?

  • The encryption configuration names EvidenceKey.Arn as the approved customer-managed key.
  • The BucketKeyEnabled setting enables an S3 Bucket Key.
  • The versioning configuration preserves distinct object versions.
  • The public-access configuration enables all four controls checked by security.guard.
  • Save template.yaml.

Before you rerun the gate, predict how the four bucket-focused rules respond to these new properties.

  • Test the encrypted bucket baseline by running:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Does the Passing Gate Prove?

You will see every Guard rule report PASS. The template now satisfies the encryption, versioning, bucket-key, and public-access requirements encoded in security.guard.

Still Seeing a Guard Failure?

  • Compare each property name with the matching rule in security.guard.
  • Check that each required Boolean value uses lowercase true.
  • Ask for help with the failed rule by selecting help me compare my S3 template with my Guard rules.

That is the first major turnaround. Your storage baseline now passes the same gate that exposed the insecure starter.

Encode policy and role boundaries

Least privilege gives each role only the actions required for its business job. Identity-based policies define those role permissions.

A resource-based bucket policy adds explicit denies for unsafe requests. Those denies apply even when another policy allows the underlying S3 action.

  • Append the bucket policy below EvidenceBucket by pasting:

  EvidenceBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref EvidenceBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: RestrictToTLSRequestsOnly
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !GetAtt EvidenceBucket.Arn
              - !Sub '${EvidenceBucket.Arn}/*'
            Condition:
              Bool:
                'aws:SecureTransport': 'false'
                'aws:PrincipalIsAWSService': 'false'
          - Sid: DenyObjectsThatAreNotSSEKMSWithSpecificKey
            Effect: Deny
            Principal: '*'
            Action: 's3:PutObject'
            Resource: !Sub '${EvidenceBucket.Arn}/*'
            Condition:
              ArnNotEqualsIfExists:
                's3:x-amz-server-side-encryption-aws-kms-key-id': !GetAtt EvidenceKey.Arn

What Does the Bucket Policy Enforce?

  • The first statement denies requests when secure transport is absent.
  • The service-principal condition preserves supported AWS service requests.
  • The second statement denies uploads when the request omits the approved KMS key ARN.
  • The same statement denies uploads that name a different KMS key.
  • Save template.yaml.

Before you validate the template, predict whether the bucket policy changes the existing Guard result.

  • Check the expanded CloudFormation structure by running:
aws cloudformation validate-template --template-body file://template.yaml

What Does This Validation Check?

CloudFormation returns template metadata without a validation error. This complements Guard because CloudFormation checks the template structure while Guard evaluates your project-specific controls.

Is the Bucket Policy Invalid?

  • Check that both policy statements sit inside the same Statement list.
  • Check that each condition remains nested inside its matching statement.
  • Ask for help with the policy structure by selecting help me diagnose my S3 bucket policy YAML.
  • Paste this uploader role one blank line below the bucket policy:
  EvidenceUploaderRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Allows evidence submission without read or delete access
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: UploadEvidenceOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: UploadOnlyToIncomingPrefix
                Effect: Allow
                Action: 's3:PutObject'
                Resource: !Sub '${EvidenceBucket.Arn}/incoming/*'
              - Sid: EncryptWithEvidenceKey
                Effect: Allow
                Action:
                  - 'kms:Encrypt'
                  - 'kms:GenerateDataKey'
                  - 'kms:DescribeKey'
                Resource: !GetAtt EvidenceKey.Arn
      Tags:
        - Key: BusinessRole
          Value: EvidenceUploader

How Is the Uploader Restricted?

  • The S3 permission allows uploads only beneath the incoming/ prefix.
  • The role receives no S3 read permission.
  • The role receives no S3 delete permission.
  • The KMS permissions support encryption with EvidenceKey.
  • Save template.yaml.
  • Validate the uploader role by running:
aws cloudformation validate-template --template-body file://template.yaml

What Does the Uploader Check Confirm?

CloudFormation returns template metadata without a validation error. The uploader role now forms a valid part of the template.

Is the Uploader Role Causing an Error?

  • Check that Policies remains nested inside the uploader role's Properties.
  • Check that the incoming/* suffix remains inside the resource ARN.
  • Ask for help with the role by selecting help me diagnose my uploader role YAML.
  • Paste this auditor role foundation one blank line below EvidenceUploaderRole.
  ComplianceAuditorRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Allows evidence review without write or delete access
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'sts:AssumeRole'

What Does the Role Foundation Establish?

The role description records the auditor's business purpose. The trust policy allows the account to assume the role for authorized testing.

  • Save template.yaml.
  • Validate the auditor role foundation by running:
aws cloudformation validate-template --template-body file://template.yaml

What Does This Check Confirm?

CloudFormation accepts the role foundation without a validation error. You can now add its tightly scoped review permissions.

Is the Auditor Trust Policy Invalid?

  • Check that AssumeRolePolicyDocument remains under the auditor role's Properties.
  • Check the indentation beneath Principal.
  • Ask for help with the trust policy by selecting help me diagnose my auditor trust policy.
  • Append the auditor permissions directly below its AssumeRolePolicyDocument block:
      Policies:
        - PolicyName: ReadEvidenceOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: ListEvidenceVault
                Effect: Allow
                Action: 's3:ListBucket'
                Resource: !GetAtt EvidenceBucket.Arn
              - Sid: ReadEvidenceObjects
                Effect: Allow
                Action:
                  - 's3:GetObject'
                  - 's3:GetObjectVersion'
                Resource: !Sub '${EvidenceBucket.Arn}/*'
              - Sid: DecryptEvidence
                Effect: Allow
                Action:
                  - 'kms:Decrypt'
                  - 'kms:DescribeKey'
                Resource: !GetAtt EvidenceKey.Arn
      Tags:
        - Key: BusinessRole
          Value: ComplianceAuditor

How Is the Auditor Restricted?

  • The auditor can list the evidence bucket.
  • The auditor can read current object data.
  • The auditor can read previous object versions.
  • The policy includes no upload permission or delete permission.
  • Save template.yaml.
  • Validate the completed auditor role by running:
aws cloudformation validate-template --template-body file://template.yaml

What Does the Auditor Check Confirm?

CloudFormation returns template metadata without a validation error. Both business roles now have valid identity-policy definitions.

Are the Auditor Permissions Invalid?

  • Check that the bucket-level s3:ListBucket action uses EvidenceBucket.Arn.
  • Check that the object-level read actions use the ARN ending in /*.
  • Ask for help with the permissions by selecting help me diagnose my auditor policy YAML.
  • Append the stack outputs below ComplianceAuditorRole by pasting:

Outputs:
  BucketName:
    Description: Name of the financial evidence bucket
    Value: !Ref EvidenceBucket
  KmsKeyArn:
    Description: ARN of the customer-managed encryption key
    Value: !GetAtt EvidenceKey.Arn
  UploaderRoleArn:
    Description: ARN of the write-only evidence uploader role
    Value: !GetAtt EvidenceUploaderRole.Arn
  AuditorRoleArn:
    Description: ARN of the read-only compliance auditor role
    Value: !GetAtt ComplianceAuditorRole.Arn

Why Add Stack Outputs?

The outputs expose the bucket name plus three resource ARNs after deployment. Later commands can use those exact deployed values without reconstructing them.

  • Save template.yaml.
Validate the complete control set

The completed template must pass the policy gate plus CloudFormation's template validation. Reviewing the permission boundaries first connects each technical statement to its business purpose.

  • Trace EvidenceUploaderRole to confirm that its only S3 action is s3:PutObject beneath incoming/*.
  • Trace ComplianceAuditorRole to confirm that its S3 permissions cover listing plus reading.
  • Inspect EvidenceBucketPolicy to identify the secure-transport deny.
  • Inspect the same policy to identify the approved-key deny.

Before you run Guard, predict whether every original failure has a matching property in the completed bucket definition.

  • Run the final policy-as-code check:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Should Guard Report?

You will see PASS for every rule. The detailed summary links each passing assertion to the matching bucket property.

Does a Rule Still Fail?

  • Read the failed rule name in the detailed summary.
  • Compare that rule with the matching property in EvidenceBucket.
  • Ask for help with the remaining failure by selecting help me resolve my final Guard failure.

Before you run the second check, predict whether a policy-compliant template can still contain a CloudFormation structure error.

  • Run the final CloudFormation validation:
aws cloudformation validate-template --template-body file://template.yaml

What Should CloudFormation Return?

CloudFormation returns template metadata without a validation error. Together, the two checks prove that the template satisfies your policy rules plus CloudFormation's template validation.

Does CloudFormation Report a Validation Error?

  • Check the reported line area for incorrect YAML indentation.
  • Compare every resource reference with the logical IDs in the full-code checkpoint below.
  • Ask for help with the final validation by selecting help me diagnose my complete CloudFormation template.

You have closed the gap that the insecure starter exposed. The full vault definition now passes both preventative checks.

✔️ Awesome, I've got everything!

  • Save template.yaml.
  • Keep security.guard unchanged.

ⓧ I'd like to double check the full code

Compare your complete template.yaml with this reference. Your existing security.guard file remains unchanged.

AWSTemplateFormatVersion: '2010-09-09'
Description: Policy-gated financial evidence vault with least-privilege IAM and SSE-KMS

Resources:
  EvidenceKey:
    Type: AWS::KMS::Key
    Properties:
      Description: Encrypts financial compliance evidence stored in the vault
      EnableKeyRotation: true
      PendingWindowInDays: 7
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: EnableAccountPermissions
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
      Tags:
        - Key: Project
          Value: FinancialEvidenceVault
        - Key: DataClassification
          Value: Confidential

  EvidenceBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'aws:kms'
              KMSMasterKeyID: !GetAtt EvidenceKey.Arn
            BucketKeyEnabled: true
      VersioningConfiguration:
        Status: Enabled
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Project
          Value: FinancialEvidenceVault
        - Key: DataClassification
          Value: Confidential

  EvidenceBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref EvidenceBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: RestrictToTLSRequestsOnly
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !GetAtt EvidenceBucket.Arn
              - !Sub '${EvidenceBucket.Arn}/*'
            Condition:
              Bool:
                'aws:SecureTransport': 'false'
                'aws:PrincipalIsAWSService': 'false'
          - Sid: DenyObjectsThatAreNotSSEKMSWithSpecificKey
            Effect: Deny
            Principal: '*'
            Action: 's3:PutObject'
            Resource: !Sub '${EvidenceBucket.Arn}/*'
            Condition:
              ArnNotEqualsIfExists:
                's3:x-amz-server-side-encryption-aws-kms-key-id': !GetAtt EvidenceKey.Arn

  EvidenceUploaderRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Allows evidence submission without read or delete access
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: UploadEvidenceOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: UploadOnlyToIncomingPrefix
                Effect: Allow
                Action: 's3:PutObject'
                Resource: !Sub '${EvidenceBucket.Arn}/incoming/*'
              - Sid: EncryptWithEvidenceKey
                Effect: Allow
                Action:
                  - 'kms:Encrypt'
                  - 'kms:GenerateDataKey'
                  - 'kms:DescribeKey'
                Resource: !GetAtt EvidenceKey.Arn
      Tags:
        - Key: BusinessRole
          Value: EvidenceUploader

  ComplianceAuditorRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Allows evidence review without write or delete access
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: ReadEvidenceOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: ListEvidenceVault
                Effect: Allow
                Action: 's3:ListBucket'
                Resource: !GetAtt EvidenceBucket.Arn
              - Sid: ReadEvidenceObjects
                Effect: Allow
                Action:
                  - 's3:GetObject'
                  - 's3:GetObjectVersion'
                Resource: !Sub '${EvidenceBucket.Arn}/*'
              - Sid: DecryptEvidence
                Effect: Allow
                Action:
                  - 'kms:Decrypt'
                  - 'kms:DescribeKey'
                Resource: !GetAtt EvidenceKey.Arn
      Tags:
        - Key: BusinessRole
          Value: ComplianceAuditor

Outputs:
  BucketName:
    Description: Name of the financial evidence bucket
    Value: !Ref EvidenceBucket
  KmsKeyArn:
    Description: ARN of the customer-managed encryption key
    Value: !GetAtt EvidenceKey.Arn
  UploaderRoleArn:
    Description: ARN of the write-only evidence uploader role
    Value: !GetAtt EvidenceUploaderRole.Arn
  AuditorRoleArn:
    Description: ARN of the read-only compliance auditor role
    Value: !GetAtt ComplianceAuditorRole.Arn

How to Use This Reference

Compare resource names, indentation, policy actions, and output references exactly. The final file should contain five resources plus four outputs.

Your vault definition is secure enough to leave the planning stage. Next up, you will deploy these reviewed controls as a working AWS environment.

Deploy the Compliant Infrastructure

Your template now passes its policy checks and CloudFormation syntax validation. Those results prove the design is ready for deployment.

Static checks cannot prove that the approved resources work in your AWS account. In this step, you use AWS CloudFormation to create the encrypted Amazon S3 vault as one managed stack.

In this step, get ready to:
  • Deploy the validated CloudFormation template as a stack.
  • Confirm the stack reaches CREATE_COMPLETE.
  • Store the four stack outputs as CloudShell environment variables.
Deploy the validated stack

A CloudFormation stack turns the validated template into one managed deployment. Updating or deleting the stack later keeps its resources tied to the same blueprint.

This action starts a customer-managed AWS KMS key that costs $1 per month prorated hourly while active. The cleanup section schedules that key for deletion, so you stay in control of the charge.

  • Deploy template.yaml as the financial-evidence-vault stack by running this command:
aws cloudformation deploy --template-file template.yaml --stack-name financial-evidence-vault --capabilities CAPABILITY_IAM

What Does This Deployment Do?

  • The --template-file template.yaml argument selects the validated infrastructure definition.
  • The --stack-name financial-evidence-vault argument gives the managed deployment its name.
  • The --capabilities CAPABILITY_IAM argument acknowledges that the template creates IAM roles.
  • CloudFormation creates the five resources defined under Resources.
  • The deployment does not modify template.yaml or security.guard.

CloudFormation can take several minutes to create the key, bucket, policy, and roles. A quiet stretch is normal while the stack settles.

When the command finishes, you should see a message confirming a successful stack operation.

Did the Deployment Fail?

  • Confirm your CloudShell prompt is still inside ~/financial-evidence-vault so the command can read template.yaml.
  • Confirm you are deploying in the same AWS Region you selected for this project.
  • Check that your signed-in administrator identity can create CloudFormation, S3, KMS, and IAM resources.
  • Help me troubleshoot my CloudFormation deployment.
Inspect the stack status and outputs

Stack outputs expose the resource identifiers that later tests need. The inspection also confirms that AWS finished creating the reviewed infrastructure.

Before you check, which stack status do you expect after the successful deployment?

  • Inspect the deployed stack by running this command:
aws cloudformation describe-stacks --stack-name financial-evidence-vault

What Does This Command Show?

  • The stack description reports the current deployment status.
  • The Outputs section contains the generated bucket name.
  • The same section contains the KMS key ARN.
  • The final two outputs contain the uploader and auditor role ARNs.

You should see CREATE_COMPLETE with outputs named BucketName, KmsKeyArn, UploaderRoleArn, and AuditorRoleArn.

  • Record the BucketName output here: your BucketName output.
  • Record the KmsKeyArn output here: your KmsKeyArn output.
  • Record the UploaderRoleArn output here: your UploaderRoleArn output.
  • Record the AuditorRoleArn output here: your AuditorRoleArn output.
Export the stack outputs

Environment variables give later commands short names for the generated resource identifiers. These values remain available for the current CloudShell session.

  • Assign the four recorded outputs to the project environment variables by running:
export BUCKET_NAME="[[BUCKET_NAME="your BucketName output"]]"
export KMS_KEY_ARN="[[KMS_KEY_ARN="your KmsKeyArn output"]]"
export UPLOADER_ROLE_ARN="[[UPLOADER_ROLE_ARN="your UploaderRoleArn output"]]"
export AUDITOR_ROLE_ARN="[[AUDITOR_ROLE_ARN="your AuditorRoleArn output"]]"

How Do These Variables Help?

  • Each assignment connects a stack output to the matching project variable.
  • The export command makes each value available to later commands in this CloudShell session.
  • Quotation marks preserve each ARN as one complete value.

Before you print the variables, do you expect any line to be blank?

  • Print the four environment variables by running:
echo "$BUCKET_NAME"
echo "$KMS_KEY_ARN"
echo "$UPLOADER_ROLE_ARN"
echo "$AUDITOR_ROLE_ARN"

What Should You See?

You should see four non-empty lines. They contain the bucket name, the KMS key ARN, the uploader role ARN, and the auditor role ARN.

Is a Variable Blank?

  • Check that each export line contains the matching value from the stack outputs.
  • Check that every copied value remains inside quotation marks.
  • Re-run the affected export line before printing the variables again.
  • Help me fix my blank CloudShell variables.

That is the deployment complete. Your validated vault is live, and its four outputs are ready for security testing.

Next, you will challenge the deployed controls and preserve the results as audit evidence.

Prove Encryption Enforcement and Separation of Duties

Your AWS CloudFormation stack is live from the AWS CloudShell session you prepared. Its Amazon S3 bucket uses the AWS KMS key you approved.

A credible security project shows denied behavior before recording a protected success. You will also prove each role's business duty in the final evidence report.

In this step, get ready to:
  • Prove that the bucket policy rejects an upload without encryption headers.
  • Confirm that the approved upload uses the expected KMS key.
  • Demonstrate the uploader and auditor authorization matrix in an evidence report.
Test the protected upload path

The bucket uses a resource-based policy to enforce SSE-KMS uploads. A negative test gives you live evidence that the explicit deny applies.

  • Create evidence.txt in the ~/financial-evidence-vault workspace by running this command:
edit evidence.txt

What Does This Command Do?

The native edit command opens the CloudShell visual editor. The new file stays inside ~/financial-evidence-vault.

  • Paste this control-test content into evidence.txt:
Financial Evidence Vault control test
Classification: Confidential
Purpose: Demonstrate enforced SSE-KMS upload controls
Contains production data: No

What Does This Evidence Contain?

The file labels the test as confidential evidence. It also states that the sample contains no production data.

  • Press Cmd+S (macOS) or Ctrl+S (Windows) to save evidence.txt.
  • Close the editor tab.
  • Confirm that the file exists by running:
ls

What Should You See?

The output includes evidence.txt beside the existing project files.

Cannot See the Evidence File?

  • Return to the editor when evidence.txt is missing.
  • Save the file before closing its tab.
  • Check that your terminal is still working inside ~/financial-evidence-vault.
  • Help me find or save evidence.txt in AWS CloudShell.

✔️ Awesome, I've got everything!

Your control-test file is saved inside ~/financial-evidence-vault.

ⓧ I'd like to double check the full code

Compare your complete evidence.txt file with this reference.

Financial Evidence Vault control test
Classification: Confidential
Purpose: Demonstrate enforced SSE-KMS upload controls
Contains production data: No

Before you run the first upload, consider whether the bucket accepts an object that omits the required encryption headers.

  • Attempt the upload without encryption headers by running this command:
aws s3api put-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt --body evidence.txt

Why Is AccessDenied the Right Result?

You should see an error containing AccessDenied. The bucket policy blocked an upload that omitted the approved KMS key.

This intended failure proves that the live policy enforces the encryption requirement.

The approved request supplies the required encryption algorithm. It also names the full ARN stored in KMS_KEY_ARN.

  • Upload the object with the approved KMS key by running this command:
VERSION_ID=$(aws s3api put-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt --body evidence.txt --server-side-encryption aws:kms --ssekms-key-id "$KMS_KEY_ARN" --query VersionId --output text)

What Does This Command Do?

  • The --server-side-encryption aws:kms option requests the approved encryption method.
  • The --ssekms-key-id "$KMS_KEY_ARN" option supplies the customer-managed key ARN.
  • The AWS CLI extracts the returned VersionId value.
  • The shell stores that value in VERSION_ID for cleanup later.

Before you inspect the object, predict which encryption fields its metadata contains.

  • Inspect the protected object by running this command:
aws s3api head-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt

What Should You See?

  • The ServerSideEncryption field is set to aws:kms.
  • The SSEKMSKeyId field matches the approved key stored in KMS_KEY_ARN.
  • The response contains a VersionId.
  • The BucketKeyEnabled field is set to true.

That is the live enforcement proven. The bucket rejected the weak request before accepting the protected object.

Missing Encryption Metadata?

  • Confirm that the upload command used the value stored in KMS_KEY_ARN.
  • Wait before retrying when the response lacks a VersionId. S3 versioning changes can take up to 15 minutes to propagate.
  • Preserve every returned version identifier if you repeat the successful upload.
  • Help me troubleshoot missing SSE-KMS metadata.
Simulate each role's permissions

The two roles use identity-based policies to implement least privilege. The IAM Policy Simulator checks each requested S3 action against those policies.

What Does the Simulator Prove?

The simulator evaluates policy decisions without making live API calls. It returns allowed, explicitDeny, or implicitDeny for each action.

Role simulations exclude the S3 bucket policy. Your denied upload tested that resource policy separately.

Before you run the uploader simulation, predict which one of the three S3 actions its policy allows.

  • Simulate the uploader role by running this command:
aws iam simulate-principal-policy --policy-source-arn "$UPLOADER_ROLE_ARN" --action-names s3:PutObject s3:GetObject s3:DeleteObject --resource-arns "arn:aws:s3:::$BUCKET_NAME/incoming/q3-control-evidence.txt"

What Should the Uploader Decisions Show?

  • The decision for s3:PutObject is allowed.
  • The decision for s3:GetObject is implicitDeny.
  • The decision for s3:DeleteObject is implicitDeny.

The uploader can submit evidence without gaining read access. It also lacks delete access.

Before you run the auditor simulation, predict which one of the three S3 actions its policy allows.

  • Simulate the auditor role by running this command:
aws iam simulate-principal-policy --policy-source-arn "$AUDITOR_ROLE_ARN" --action-names s3:PutObject s3:GetObject s3:DeleteObject --resource-arns "arn:aws:s3:::$BUCKET_NAME/incoming/q3-control-evidence.txt"

What Should the Auditor Decisions Show?

  • The decision for s3:PutObject is implicitDeny.
  • The decision for s3:GetObject is allowed.
  • The decision for s3:DeleteObject is implicitDeny.

The complementary decisions prove separation of duties. Neither business role can delete evidence.

Seeing Unexpected Simulator Decisions?

  • Confirm that UPLOADER_ROLE_ARN contains the uploader role output.
  • Confirm that AUDITOR_ROLE_ARN contains the auditor role output.
  • Check that the resource ARN ends with incoming/q3-control-evidence.txt.
  • Help me troubleshoot the IAM simulator decisions.
Capture the compliance evidence

An audit report connects each business control to an observed result. The report preserves the static gate beside the runtime tests.

Before you rerun the policy gate, predict whether the unchanged secure template still passes.

  • Confirm the AWS CloudFormation Guard result by running this command:
cfn-guard validate --rules security.guard --data template.yaml -s all

What Does This Check Prove?

The detailed evaluation reports PASS for the S3 bucket presence rule. It also reports PASS for encryption, versioning, and public-access protection.

  • Create evidence-report.md in the current CloudShell workspace by running this command:
edit evidence-report.md

What Does This Command Do?

The CloudShell editor opens a Markdown file for the security evidence. This report maps each control to the result you observed.

  • Paste the business requirement and preventative-control results into evidence-report.md:
# Financial Evidence Vault Security Evidence

## Business requirement

Operations can submit evidence without reading or deleting it. Compliance auditors can review evidence without modifying or deleting it.

## Preventative controls

| Control | Evidence | Result |
|---|---|---|
| SSE-KMS required | CloudFormation Guard `S3_KMS_ENCRYPTION` | PASS |
| Versioning enabled | CloudFormation Guard `S3_VERSIONING_ENABLED` | PASS |
| Public access blocked | CloudFormation Guard `S3_PUBLIC_ACCESS_BLOCKED` | PASS |
| Approved KMS key enforced | Unencrypted `PutObject` test | `AccessDenied` |
| Encryption metadata | `HeadObject` response | `ServerSideEncryption: aws:kms`, approved `SSEKMSKeyId`, `VersionId`, and `BucketKeyEnabled: true` |

## IAM authorization matrix

| Principal | PutObject | GetObject | DeleteObject |
|---|---|---|---|

What Does This Part Record?

The first table connects each preventative control to its evidence. The authorization matrix header prepares the report for the role decisions.

  • Press Cmd+S (macOS) or Ctrl+S (Windows) to save evidence-report.md.
  • Check that the preventative-controls table contains five result rows.
  • Append the authorization decisions and deployment evidence below the table header:
| EvidenceUploaderRole | allowed | implicitDeny | implicitDeny |
| ComplianceAuditorRole | implicitDeny | allowed | implicitDeny |

## Deployment evidence

- Stack status: CREATE_COMPLETE
- Bucket name: Value stored in `BUCKET_NAME`
- KMS key ARN: Value stored in `KMS_KEY_ARN`
- Region: Selected CloudShell Region
- Test date: Recorded by learner

## Residual risk and next control

This project validates infrastructure before deployment and tests selected permissions. Continuous monitoring with AWS Config and AWS Security Hub CSPM is intentionally deferred to a follow-on project.

What Does This Part Record?

The completed matrix records the separation-of-duties results. The deployment section ties those results to the live stack.

The final section states the remaining monitoring gap. That boundary gives reviewers an honest view of the project's scope.

  • Press Cmd+S (macOS) or Ctrl+S (Windows) to save the completed evidence-report.md.
  • Check that the report ends with the residual-risk section.
  • Close the editor tab.

Report Tables Look Misaligned?

  • Confirm that every table row starts with a vertical bar.
  • Confirm that every table row ends with a vertical bar.
  • Check that each separator row has one divider for every column.
  • Help me fix the evidence report tables.

✔️ Awesome, I've got everything!

Your completed evidence-report.md records the policy gate, encryption test, deployment state, and authorization matrix.

ⓧ I'd like to double check the full code

Compare your complete evidence-report.md file with this reference.

# Financial Evidence Vault Security Evidence

## Business requirement

Operations can submit evidence without reading or deleting it. Compliance auditors can review evidence without modifying or deleting it.

## Preventative controls

| Control | Evidence | Result |
|---|---|---|
| SSE-KMS required | CloudFormation Guard `S3_KMS_ENCRYPTION` | PASS |
| Versioning enabled | CloudFormation Guard `S3_VERSIONING_ENABLED` | PASS |
| Public access blocked | CloudFormation Guard `S3_PUBLIC_ACCESS_BLOCKED` | PASS |
| Approved KMS key enforced | Unencrypted `PutObject` test | `AccessDenied` |
| Encryption metadata | `HeadObject` response | `ServerSideEncryption: aws:kms`, approved `SSEKMSKeyId`, `VersionId`, and `BucketKeyEnabled: true` |

## IAM authorization matrix

| Principal | PutObject | GetObject | DeleteObject |
|---|---|---|---|
| EvidenceUploaderRole | allowed | implicitDeny | implicitDeny |
| ComplianceAuditorRole | implicitDeny | allowed | implicitDeny |

## Deployment evidence

- Stack status: CREATE_COMPLETE
- Bucket name: Value stored in `BUCKET_NAME`
- KMS key ARN: Value stored in `KMS_KEY_ARN`
- Region: Selected CloudShell Region
- Test date: Recorded by learner

## Residual risk and next control

This project validates infrastructure before deployment and tests selected permissions. Continuous monitoring with AWS Config and AWS Security Hub CSPM is intentionally deferred to a follow-on project.

For the final check, predict whether the live object still reports every required encryption field.

  • Verify the live object metadata one more time by running this command:
aws s3api head-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt

What Should You See?

The response includes ServerSideEncryption set to aws:kms. The SSEKMSKeyId value matches the approved key.

You should also see a VersionId. The BucketKeyEnabled field is set to true.

Strong finish. Your vault blocks weak uploads, protects accepted evidence, separates business duties, and preserves the proof for review.

Secret mission

Add a KMS Rotation Guardrail

Extend your policy gate so it catches any template that disables automatic KMS key rotation. Prove the guardrail with an intentional failure followed by a passing validation of the secure template.

Clean Up Your Resources

Clean Up Your Resources

Your deployed vault can continue generating charges while its customer-managed AWS KMS key stays active. Amazon S3 storage and request charges can also apply to the uploaded object.

Decide whether to keep your resources running, pause cloud spending while preserving your files, or delete everything.

Cost warning

The customer-managed AWS KMS key costs $1 per month, prorated hourly, while active.

Amazon S3 storage and request charges can also apply. This project uses one tiny object.

AWS KMS does not charge key storage during the scheduled deletion period. Choose the pause or delete option to stop ongoing project charges.

Resources you used:

  • The AWS CloudFormation stack named financial-evidence-vault.
  • The versioned Amazon S3 evidence bucket.
  • The uploaded object version identified by VERSION_ID.
  • The customer-managed AWS KMS key configured for automatic rotation.
  • The AWS Identity and Access Management (IAM) role named EvidenceUploaderRole.
  • The IAM role named ComplianceAuditorRole.
  • The Amazon S3 bucket policy that enforces secure transport and the approved encryption key.
  • The AWS CloudShell workspace at ~/financial-evidence-vault.

Keep everything running

No action is needed. Choose this if you plan to continue testing the deployed vault.

  • Retain the financial-evidence-vault stack for future security tests.
  • Preserve evidence-report.md as your record of the project results.
  • Expect the customer-managed AWS KMS key to keep generating hourly prorated charges.
  • Expect applicable Amazon S3 storage and request charges to continue.

Pause - I'll come back to this later

Pause cloud spending by deleting the live stack. Your CloudShell workspace remains available for a later redeploy.

This Removes the Live Vault

The uploaded object version is permanently removed. The deployed stack is also permanently removed.

Your files remain in ~/financial-evidence-vault so you can rebuild the vault later.

  • Permanently delete the uploaded object version by running this command in AWS CloudShell:
aws s3api delete-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt --version-id "$VERSION_ID"

What Does This Command Do?

The command permanently deletes the exact object version stored in VERSION_ID. Emptying that version lets CloudFormation remove the versioned bucket.

  • Start deleting the financial-evidence-vault stack by running this command:
aws cloudformation delete-stack --stack-name financial-evidence-vault

What Happens to the Stack?

  • CloudFormation deletes the empty Amazon S3 bucket.
  • CloudFormation schedules EvidenceKey for deletion under the template's seven-day waiting period.
  • CloudFormation removes EvidenceUploaderRole.
  • CloudFormation removes ComplianceAuditorRole.
  • CloudFormation removes the bucket policy.

Expect stack deletion to take several minutes. CloudFormation is working through each resource during this wait.

  • Check whether the stack still exists by running this command:
aws cloudformation describe-stacks --stack-name financial-evidence-vault

How Do I Know Deletion Finished?

While deletion continues, the command returns the current stack record. Once deletion finishes, the command no longer returns a stack record.

  • Repeat the inspection after a short wait if the command still returns the stack.
  • Retain ~/financial-evidence-vault in CloudShell for a future redeploy.

Stack Deletion Not Finishing?

A versioned Amazon S3 bucket must be empty before CloudFormation can delete it. If you repeated the successful upload, run the object deletion command once for every saved VERSION_ID value.

Help me diagnose a CloudFormation stack deletion that is not completing.

Delete - I don't want to use this again

Remove the live AWS resources when you no longer need this project. Remove the CloudShell workspace after the stack deletion finishes.

Deletion Is Permanent

The live vault cannot be restored after stack deletion. The workspace files also disappear after the final cleanup command.

  • Permanently delete the uploaded object version by running this command in AWS CloudShell:
aws s3api delete-object --bucket "$BUCKET_NAME" --key incoming/q3-control-evidence.txt --version-id "$VERSION_ID"

What Does This Command Do?

The command permanently deletes the object version identified by VERSION_ID. The versioned bucket must be empty before CloudFormation can remove it.

  • Start deleting the financial-evidence-vault stack by running this command:
aws cloudformation delete-stack --stack-name financial-evidence-vault

What Does Stack Deletion Remove?

  • CloudFormation deletes the empty Amazon S3 bucket.
  • CloudFormation schedules the customer-managed AWS KMS key for deletion.
  • CloudFormation removes both IAM roles.
  • CloudFormation removes the bucket policy.

Expect stack deletion to take several minutes. The wait means CloudFormation is removing the resources created from template.yaml.

  • Check whether the stack still exists by running this command:
aws cloudformation describe-stacks --stack-name financial-evidence-vault

How Do I Confirm the Stack Is Gone?

The command returns the stack record while deletion continues. Deletion is complete when the command no longer returns that record.

  • Repeat the inspection after a short wait if the stack record is still returned.

Stack Deletion Not Finishing?

A repeated successful upload creates another object version. Run the object deletion command once for each saved VERSION_ID value before retrying stack deletion.

Help me find the object version blocking stack deletion.

Your cloud cleanup is complete once the stack lookup stops returning a record.

Final File Check

The next command permanently removes evidence-report.md and every other file in the workspace. Continue only when you no longer need those files.

  • Remove the CloudShell workspace by running this command:
rm -rf ~/financial-evidence-vault

What Does This Command Do?

The command permanently removes ~/financial-evidence-vault from CloudShell storage. This includes the templates, Guard rules, test evidence, and security report.

Cleanup complete. The live stack is removed from your account.

Nice Work!

Nice Work!

Excellent work! You built a policy-gated AWS financial evidence vault that proves its security controls with audit evidence.

You've learned how to:

  • Built a policy-as-code gate with AWS CloudFormation Guard that rejects insecure infrastructure before deployment.
  • Deployed an encrypted Amazon S3 evidence vault from a reusable AWS CloudFormation template. Protected its data with a customer-managed AWS KMS key.
  • Proved least privilege through separate uploader and auditor permissions. Used the IAM Policy Simulator to capture each role's authorization decisions. Recorded denied uploads plus encryption metadata in evidence-report.md.
  • Secret Mission: Extended the policy gate with KMS_ROTATION_ENABLED. Proved that it rejects templates with automatic KMS key rotation disabled.

Ready to quiz yourself?