Secure AWS Network with CloudFormation

Build a zero-compute AWS network baseline with CloudFormation.

Introduction

30 Second Summary

Security problems often begin before a web app has a single user. An open network path or copied credential can turn a future launch into avoidable risk.

In this project, you will deploy a zero-compute AWS security baseline on a strict $0 path with AWS CloudFormation. You will replace an intentionally broad HTTP rule with a controlled application boundary.

What You'll Build

You will inspect a live AWS CloudFormation stack where the application boundary accepts HTTP only from a separate front-door security group.

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

  • A repeatable Infrastructure as Code template that deploys a VPC across two Availability Zones. You can inspect a public route while the private route table has no default internet route.
  • An HTTP trust boundary you can verify in the deployed security group rules. The application boundary trusts only the separate front-door security group.
  • A credential-free starting point for future EC2 workloads. Its zero-permission IAM role models future machine identity. Its bootstrap.sh script creates a visible HTML file.
  • Secret Mission: Prove the baseline is reproducible by deleting the complete stack and recreating the same architecture from the final template.

Are there any prerequisites?

You need an AWS account with permission to use AWS CloudShell and create the resources in this project. AWS CloudShell provides the authenticated command-line workspace without requiring local access keys.

Before We Start

Before anything reaches AWS, this checkpoint locks in your plan for a two-Availability-Zone VPC baseline with public and private subnet layers, security boundaries, and future workload identity. Its strict zero-cost boundary excludes running compute, NAT gateways, load balancers, VPC endpoints, Elastic IP addresses, and public IPv4 addresses.

Open Your Pre-Authenticated AWS Workspace

Nothing has been deployed yet. Before the baseline becomes code, you need a command-line workspace tied to the AWS account and Region that will own it.

AWS CloudShell opens inside the AWS Management Console. It provides a managed Linux shell in your browser.

AWS CLI v2 is already available in that shell. CloudShell uses your console credentials automatically, which avoids local access keys.

In this step, get ready to:
  • Open AWS CloudShell in your selected project Region.
  • Verify AWS CLI v2 in the managed Linux shell.
  • Confirm the authenticated AWS account ID and ARN.
Open AWS CloudShell in your project Region

The Region selected in the console anchors this project. Opening CloudShell there keeps your command-line workspace aligned with the location of your later resources.

  • Sign in to the AWS Management Console with the AWS account for this project.
  • Use the Region menu in the top navigation bar to select the Region for this project.
  • Enter CloudShell in the console search bar.
  • Select CloudShell from the search results.
  • Wait until the CloudShell terminal displays a shell prompt.

You'll see a terminal inside the console with a shell prompt ready for commands. This confirms that your managed Linux environment is open.

Why Use CloudShell?

CloudShell is pre-authenticated from your console session. It includes Bash and AWS CLI version 2, so separate credential configuration is unnecessary.

CloudShell has no additional charge. The zero-cost boundary still depends on creating only the resources listed in this project.

Verify the CLI and authenticated identity

The shell prompt shows that the environment is ready. The next two checks verify the CLI version and the AWS identity behind every command.

Before you check, which major AWS CLI version do you expect CloudShell to report?

  • Check the installed AWS CLI version by running this command:
aws --version

What Does This Command Confirm?

The command prints information about the installed AWS CLI. Look for output that identifies version 2.

  • Confirm that the output identifies AWS CLI version 2.

Your managed CLI is ready for the template work ahead.

AWS CLI Version Missing?

  • Wait until the CloudShell prompt has finished loading before trying the check again.
  • Refresh the CloudShell browser tab if the terminal does not accept commands.
  • Confirm that your AWS account has permission to use CloudShell if the environment does not open.

Help me troubleshoot the AWS CLI version check in CloudShell.

CloudShell automatically makes your console credentials available to the CLI. The identity check returns the calling identity's UserId, Account, and Arn values.

Before you run the identity check, do you expect the account and ARN to match the AWS account you signed into?

  • Display the identity used by CloudShell by running this command:
aws sts get-caller-identity

What Does This Identity Check Show?

  • The UserId value identifies the caller.
  • The Account value is the AWS account ID behind the session.
  • The Arn value names the AWS identity used by CloudShell.

You'll see identity details containing UserId, Account, and Arn. These values prove which AWS identity runs your later deployment commands.

  • Compare the Account value with the AWS account you intend to use.
  • Confirm that the Arn value names the identity you expect CloudShell to use.

Identity Values Look Unfamiliar?

  • Check that the console session belongs to the AWS account intended for this project.
  • Restart CloudShell from the correct console session if the ARN names an unexpected identity.

Help me check why CloudShell is using an unexpected AWS identity.

That's your authenticated workspace confirmed. AWS CLI version 2 is available without local access keys.

Your pre-authenticated AWS workspace is ready. Next up, you'll deploy the first network baseline. Then you'll inspect its deliberately broad application boundary.

Deploy the Baseline and Expose the Weak Boundary

Your AWS CloudShell workspace is authenticated. You can now turn the planned architecture into deployed AWS resources.

This first AWS CloudFormation deployment creates a working two-zone VPC baseline. Its application security group deliberately accepts HTTP from 0.0.0.0/0, which gives you a real policy defect to inspect.

In this step, get ready to:
  • Define the public and private network layers in secure-baseline.yaml.
  • Add the front-door and application security boundaries.
  • Deploy the secure-web-baseline stack and inspect its broad application ingress rule.
Create the two-layer network template

The public layer provides a route toward an Internet Gateway. The private layer has no default internet route.

Every subnet disables automatic public IPv4 assignment. This keeps the deployment inside the zero-compute cost boundary.

  • Use the file editor in AWS CloudShell to create secure-baseline.yaml in your CloudShell home folder.
  • Paste the template header and VPC resource into secure-baseline.yaml.
AWSTemplateFormatVersion: "2010-09-09"
Description: Zero-compute secure AWS network and identity baseline

Resources:
  SecureVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.20.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: secure-web-baseline-vpc
        - Key: Project
          Value: secure-web-baseline

What does this section define?

  • The template format line identifies the CloudFormation template version.
  • SecureVpc reserves 10.20.0.0/16 for the baseline network.
  • DNS support prepares the VPC for future workloads without launching compute.
  • Paste the internet gateway resources immediately below SecureVpc.
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: secure-web-baseline-igw
        - Key: Project
          Value: secure-web-baseline

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref SecureVpc

How does the gateway connect?

InternetGateway creates the gateway. InternetGatewayAttachment connects it to SecureVpc.

The attachment provides a target for the public route. A subnet only uses that path after its route table association is defined.

  • Save secure-baseline.yaml.
  • Validate the current template by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does validation prove?

CloudFormation checks whether the file is valid JSON or YAML. A successful response confirms that the current template can be parsed.

This command does not assess security or cost. Those decisions still require your inspection.

You should receive template details instead of a validation error. The VPC and gateway definitions now form a valid template.

Seeing a validation error?

  • Check that Resources: starts at the far-left edge of the file.
  • Check that each resource name is indented by two spaces beneath Resources:.
  • Confirm that the filename is secure-baseline.yaml.

Help me diagnose the current CloudFormation validation error.

  • Paste the first public subnet immediately below InternetGatewayAttachment.
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.1.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-a
        - Key: Project
          Value: secure-web-baseline

How is the first public subnet placed?

  • The subnet receives the 10.20.1.0/24 address range.
  • Fn::GetAZs returns the Availability Zones available to your account in the selected Region.
  • Fn::Select chooses the first available zone with index 0.
  • MapPublicIpOnLaunch prevents automatic public IPv4 assignment.
  • Paste the second public subnet immediately below PublicSubnetA.
  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.2.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-b
        - Key: Project
          Value: secure-web-baseline

Why use a second public subnet?

PublicSubnetB uses 10.20.2.0/24 in the second available Availability Zone. The public layer now spans two physical locations.

  • Save secure-baseline.yaml.
  • Validate both public subnet definitions by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does this subnet check cover?

The validator now parses both public subnet definitions. It also checks that their references appear in the template.

You should receive template details again. Both public subnets are represented by valid YAML.

Does validation fail on an Availability Zone line?

  • Check that Fn::Select is nested beneath AvailabilityZone.
  • Check that PublicSubnetA uses index 0.
  • Check that PublicSubnetB uses index 1.

Help me fix the subnet Availability Zone syntax.

  • Paste the first private subnet immediately below PublicSubnetB.
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.11.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-a
        - Key: Project
          Value: secure-web-baseline

Where does the first private subnet live?

PrivateSubnetA uses 10.20.11.0/24 in the first available Availability Zone. It shares the zone with PublicSubnetA while keeping a separate address range.

  • Paste the second private subnet immediately below PrivateSubnetA.
  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.12.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-b
        - Key: Project
          Value: secure-web-baseline

How does the private layer span zones?

PrivateSubnetB uses 10.20.12.0/24 in the second available Availability Zone. The private layer now mirrors the two-zone public layout.

  • Save secure-baseline.yaml.
  • Validate the four-subnet layout by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What has this check covered?

The validator now parses four subnet definitions across the first two available Availability Zones. Every subnet references SecureVpc.

You should receive template details instead of an error. The complete subnet layer is valid.

Seeing a subnet reference error?

  • Confirm that every subnet uses VpcId: !Ref SecureVpc.
  • Confirm that each subnet has a distinct CIDR block.
  • Check that all four MapPublicIpOnLaunch values are false.

Help me diagnose the four-subnet section.

  • Paste the public route table and default route immediately below PrivateSubnetB.
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-public-routes
        - Key: Project
          Value: secure-web-baseline

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

What makes this route public?

PublicDefaultRoute sends destinations matching 0.0.0.0/0 toward InternetGateway. DependsOn waits for the gateway attachment before CloudFormation creates the route.

A subnet receives this route only after it is associated with PublicRouteTable.

  • Paste the public subnet associations immediately below PublicDefaultRoute.
  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetB

Why are the associations required?

Each association connects one public subnet to PublicRouteTable. Both public subnets now inherit its route through InternetGateway.

  • Save secure-baseline.yaml.
  • Validate the public routing layer by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does this routing check cover?

CloudFormation now parses the public route table with its default route. It also parses both public subnet associations.

You should receive template details again. The public routing layer is valid.

Does the public route fail validation?

  • Check that PublicDefaultRoute references PublicRouteTable.
  • Check that GatewayId references InternetGateway.
  • Confirm that each association references its matching public subnet.

Help me trace the public routing references.

  • Paste the private route table and associations immediately below PublicSubnetBRouteTableAssociation.
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-private-routes
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetA

  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetB

What keeps the private route layer isolated?

PrivateRouteTable contains no 0.0.0.0/0 route. Its associations give both private subnets that restricted route set.

The template creates no NAT gateway or VPC endpoint. Future private workloads would need an explicit path before reaching external services.

  • Save secure-baseline.yaml.
  • Validate the complete routing layout by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does this validation cover?

The template now contains separate public and private route layers. A successful response confirms that all four associations reference defined resources.

You should receive template details. The two-layer network structure is valid.

Does the private route layer fail validation?

  • Confirm that PrivateRouteTable references SecureVpc.
  • Confirm that the private associations reference PrivateSubnetA and PrivateSubnetB.
  • Check that PrivateRouteTable has no default route resource.

Help me diagnose the private routing resources.

Add the security boundaries and outputs

A security group controls traffic for resources attached to it. This baseline creates one group for a future front door and another for a future application tier.

Both groups begin with public TCP port 80 ingress from 0.0.0.0/0. That application rule is structurally valid while expressing an overly broad policy.

  • Paste the front-door security group immediately below PrivateSubnetBRouteTableAssociation.
  FrontDoorSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internet-facing HTTP boundary for a future managed entry point
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Public HTTP for a future front door
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.20.0.0/16
          Description: HTTP toward the application network
      Tags:
        - Key: Name
          Value: secure-web-front-door-sg
        - Key: Project
          Value: secure-web-baseline

What does the front-door group allow?

  • Its ingress rule accepts TCP port 80 from every IPv4 source.
  • Its egress rule permits TCP port 80 toward the VPC address range.
  • The group creates a boundary without attaching it to running compute.
  • Paste the intentionally broad application security group immediately below FrontDoorSecurityGroup.
  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: HTTP only from the front-door security group
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: HTTP only from the front-door security group
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS egress if a future private route is added
      Tags:
        - Key: Name
          Value: secure-web-application-sg
        - Key: Project
          Value: secure-web-baseline

Where is the weak boundary?

ApplicationSecurityGroup also accepts TCP port 80 from 0.0.0.0/0. Any future reachable application resource using this group could accept HTTP from every IPv4 source.

No server is exposed in this project because the stack creates no instance or network interface. The rule still expresses a policy that is too broad for the application tier.

  • Save secure-baseline.yaml.
  • Validate both security group definitions by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

Why does the broad rule validate?

The public application ingress uses supported CloudFormation syntax. Template validity does not guarantee least privilege.

You should receive template details instead of an error. The weak boundary is now part of a valid template.

Seeing a security group validation error?

  • Check that each group has GroupDescription and VpcId properties.
  • Confirm that every traffic rule includes IpProtocol.
  • Check that FromPort and ToPort are aligned beneath each rule.

Help me fix the security group structure.

CloudFormation outputs give stable names to values that AWS assigns during deployment. You can inspect the deployed resources without searching through separate service pages.

  • Paste the outputs at the far-left edge below the final resource.
Outputs:
  VpcId:
    Description: ID of the secure baseline VPC
    Value: !Ref SecureVpc
  PublicRouteTableId:
    Description: Route table with the internet gateway route
    Value: !Ref PublicRouteTable
  PrivateRouteTableId:
    Description: Route table with no default internet route
    Value: !Ref PrivateRouteTable
  FrontDoorSecurityGroupId:
    Description: Security group ID for the future managed entry point
    Value: !Ref FrontDoorSecurityGroup
  ApplicationSecurityGroupId:
    Description: Security group ID for the future private application tier
    Value: !Ref ApplicationSecurityGroup

Why define stack outputs?

The outputs expose the IDs assigned to important stack resources. Their names remain stable if AWS assigns new physical IDs during a later rebuild.

  • Save secure-baseline.yaml.
  • Validate the complete initial template by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What has the final validation confirmed?

CloudFormation can parse the complete network template with its security groups and outputs. No resources are deployed by this check.

You should receive template details without a validation error. The file is ready for deployment.

Does validation fail after adding outputs?

  • Check that Outputs: starts at the far-left edge.
  • Confirm that each output key is indented by two spaces.
  • Confirm that every Value references a resource defined under Resources.

Help me diagnose the outputs section.

✔️ Awesome, I've got everything!

Your complete initial template is valid. Double check that secure-baseline.yaml is saved before deployment.

ⓧ I'd like to double check the full code

Compare your saved secure-baseline.yaml with this initial version. Both security groups use public TCP port 80 ingress at this stage.

AWSTemplateFormatVersion: "2010-09-09"
Description: Zero-compute secure AWS network and identity baseline

Resources:
  SecureVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.20.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: secure-web-baseline-vpc
        - Key: Project
          Value: secure-web-baseline

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: secure-web-baseline-igw
        - Key: Project
          Value: secure-web-baseline

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref SecureVpc

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.1.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-a
        - Key: Project
          Value: secure-web-baseline

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.2.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-b
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.11.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-a
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.12.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-b
        - Key: Project
          Value: secure-web-baseline

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-public-routes
        - Key: Project
          Value: secure-web-baseline

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetB

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-private-routes
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetA

  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetB

  FrontDoorSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internet-facing HTTP boundary for a future managed entry point
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Public HTTP for a future front door
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.20.0.0/16
          Description: HTTP toward the application network
      Tags:
        - Key: Name
          Value: secure-web-front-door-sg
        - Key: Project
          Value: secure-web-baseline

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: HTTP only from the front-door security group
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: HTTP only from the front-door security group
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS egress if a future private route is added
      Tags:
        - Key: Name
          Value: secure-web-application-sg
        - Key: Project
          Value: secure-web-baseline

Outputs:
  VpcId:
    Description: ID of the secure baseline VPC
    Value: !Ref SecureVpc
  PublicRouteTableId:
    Description: Route table with the internet gateway route
    Value: !Ref PublicRouteTable
  PrivateRouteTableId:
    Description: Route table with no default internet route
    Value: !Ref PrivateRouteTable
  FrontDoorSecurityGroupId:
    Description: Security group ID for the future managed entry point
    Value: !Ref FrontDoorSecurityGroup
  ApplicationSecurityGroupId:
    Description: Security group ID for the future private application tier
    Value: !Ref ApplicationSecurityGroup
Deploy and inspect the weak boundary

The complete template now describes a deployable network baseline. CloudFormation creates its resources in dependency order under one stack.

This deployment can take a few minutes while AWS creates the network resources. It creates no EC2 instance, load balancer, NAT gateway, VPC endpoint, Elastic IP address, or public IPv4 address.

  • Create the secure-web-baseline stack by running:
aws cloudformation deploy --template-file secure-baseline.yaml --stack-name secure-web-baseline

What does deployment create?

The deploy command submits secure-baseline.yaml under the stack name secure-web-baseline. CloudFormation creates the VPC resources described by the template.

The stack contains network controls only. The broad application rule has no running server behind it.

That is the first deployment complete. Your two-zone network baseline now exists in the selected Region.

Did the stack deployment fail?

  • Read the failure reason printed in the CloudShell terminal.
  • Check that the selected Region exposes at least two Availability Zones.
  • Confirm that your authenticated identity can create CloudFormation and EC2 networking resources.

Help me diagnose the deployment failure.

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

  • Inspect the stack status and outputs by running:
aws cloudformation describe-stacks --stack-name secure-web-baseline

What should the stack response contain?

The response includes StackStatus with the five outputs from your template. Each output connects a stable architecture name to an AWS-assigned resource ID.

You should see CREATE_COMPLETE as the stack status. You should also see VpcId, PublicRouteTableId, PrivateRouteTableId, FrontDoorSecurityGroupId, and ApplicationSecurityGroupId.

  • Find the ApplicationSecurityGroupId output.
  • Copy its OutputValue into your-application-security-group-id.

Before you inspect the rule, do you expect its source list to contain one trusted group or every IPv4 address?

  • Inspect the application security group with your copied ID by running:
aws ec2 describe-security-groups --group-ids [[APPLICATION_SECURITY_GROUP_ID="your-application-security-group-id"]]

What does the response reveal?

IpPermissions describes inbound traffic rules. IpRanges lists the CIDR sources attached to those rules.

The HTTP permission uses TCP port 80. Its IpRanges entry contains CidrIp set to 0.0.0.0/0.

You found the intended shortfall. The stack works, but its future application boundary currently trusts every IPv4 source.

Cannot inspect the application security group?

  • Confirm that you copied the OutputValue paired with ApplicationSecurityGroupId.
  • Remove any spaces or quotation marks surrounding the copied security group ID.
  • Confirm that CloudShell is still using the Region where you deployed the stack.

Help me inspect the application security group.

Your live two-zone baseline now has an observable policy weakness. Next, you will replace the application's internet-wide source with the front-door security group.

Decouple the Application Boundary

Your first deployment proved that AWS CloudFormation can build the network baseline from one template. It also exposed an application security group that accepts HTTP from every IPv4 address through 0.0.0.0/0.

Any future application resource attached to that group could accept direct HTTP traffic when a route makes it reachable. This update makes the front-door group the only permitted HTTP source.

In this step, get ready to:
  • Replace the broad application CIDR with a front-door security group reference.
  • Redeploy the existing CloudFormation stack with the updated rule.
  • Verify that the application ingress contains the front-door security group ID.
Replace the public CIDR source

A security group reference ties the ingress decision to another group. Future resources associated with the front-door group can reach TCP port 80.

Other sources are excluded by the application rule. The network boundary now follows the workload layer that traffic came from.

  • In your open CloudShell workspace, switch back to secure-baseline.yaml.
  • Locate the ApplicationSecurityGroup resource.
  • Find this ingress source line:
          CidrIp: 0.0.0.0/0

Why is this source too broad?

The 0.0.0.0/0 CIDR represents every IPv4 address. A reachable resource associated with this group could accept HTTP directly from any source.

No server is exposed in this project because the stack contains no running compute. The rule still models a weak boundary for a future application tier.

  • Replace the CIDR line with this security group reference:
          SourceSecurityGroupId: !Ref FrontDoorSecurityGroup

What does the reference change?

The SourceSecurityGroupId property uses the front-door security group as the ingress source. Future resources associated with that group can send HTTP traffic to resources protected by the application group.

The template resolves !Ref FrontDoorSecurityGroup to the deployed front-door group ID. You can keep the template reusable without hard-coding a physical ID.

  • Save secure-baseline.yaml.

✔️ Awesome, I've got everything!

Your saved template now uses the front-door group as the application ingress source.

ⓧ I'd like to double check the full code

Compare your complete secure-baseline.yaml file with this version before redeploying the stack.

AWSTemplateFormatVersion: "2010-09-09"
Description: Zero-compute secure AWS network and identity baseline

Resources:
  SecureVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.20.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: secure-web-baseline-vpc
        - Key: Project
          Value: secure-web-baseline

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: secure-web-baseline-igw
        - Key: Project
          Value: secure-web-baseline

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref SecureVpc

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.1.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-a
        - Key: Project
          Value: secure-web-baseline

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.2.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-b
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.11.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-a
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.12.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-b
        - Key: Project
          Value: secure-web-baseline

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-public-routes
        - Key: Project
          Value: secure-web-baseline

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetB

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-private-routes
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetA

  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetB

  FrontDoorSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internet-facing HTTP boundary for a future managed entry point
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Public HTTP for a future front door
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.20.0.0/16
          Description: HTTP toward the application network
      Tags:
        - Key: Name
          Value: secure-web-front-door-sg
        - Key: Project
          Value: secure-web-baseline

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: HTTP only from the front-door security group
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref FrontDoorSecurityGroup
          Description: HTTP only from the front-door security group
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS egress if a future private route is added
      Tags:
        - Key: Name
          Value: secure-web-application-sg
        - Key: Project
          Value: secure-web-baseline

Outputs:
  VpcId:
    Description: ID of the secure baseline VPC
    Value: !Ref SecureVpc
  PublicRouteTableId:
    Description: Route table with the internet gateway route
    Value: !Ref PublicRouteTable
  PrivateRouteTableId:
    Description: Route table with no default internet route
    Value: !Ref PrivateRouteTable
  FrontDoorSecurityGroupId:
    Description: Security group ID for the future managed entry point
    Value: !Ref FrontDoorSecurityGroup
  ApplicationSecurityGroupId:
    Description: Security group ID for the future private application tier
    Value: !Ref ApplicationSecurityGroup

What should you compare?

The ApplicationSecurityGroup ingress uses SourceSecurityGroupId with !Ref FrontDoorSecurityGroup.

The network resources and front-door rules remain unchanged. The outputs still include both security group IDs for verification.

Redeploy and verify the boundary

Reusing the secure-web-baseline stack name lets CloudFormation update the existing resources from your changed template. The VPC layout keeps its current definition.

The stack outputs provide stable names for the two security group IDs. You can compare those outputs with the updated ingress rule after the deployment.

  • Update the existing stack by running this command:
aws cloudformation deploy --template-file secure-baseline.yaml --stack-name secure-web-baseline

What does this deployment do?

CloudFormation compares the saved template with the existing secure-web-baseline stack. It applies the changed application ingress source while retaining the rest of the declared architecture.

You should see the deployment finish successfully for secure-web-baseline.

Did the stack update fail?

Confirm that secure-baseline.yaml is saved. Check that the new line sits inside the SecurityGroupIngress rule for ApplicationSecurityGroup.

Check the YAML indentation around SourceSecurityGroupId. It must align with IpProtocol and the port properties in the same rule.

Use the deployment output to guide your next check. Help me troubleshoot my CloudFormation stack update.

  • Retrieve the current stack outputs by running this command:
aws cloudformation describe-stacks --stack-name secure-web-baseline

What does this inspection show?

The response includes the stack outputs under their logical names. Those names stay stable even when AWS assigns physical resource IDs.

You need the application group ID for the next command. The front-door group ID gives you the expected source for comparison.

  • Record the ApplicationSecurityGroupId output as your application security group ID.
  • Record the FrontDoorSecurityGroupId output as your front-door security group ID.
  • Replace your-application-security-group-id in the command below with your application security group ID.

Before you inspect the updated rule, which collection do you expect to hold the ingress source now?

  • Inspect the application security group by running this command:
aws ec2 describe-security-groups --group-ids your-application-security-group-id

What does this result prove?

The IpRanges collection shows CIDR-based IPv4 sources. The UserIdGroupPairs collection shows security-group-based sources.

Moving the source between those collections proves that the application rule now depends on the front-door group. The TCP protocol and port 80 remain unchanged.

You should see an empty IpRanges list. You should see a UserIdGroupPairs entry containing your front-door security group ID.

You have closed the direct HTTP path. Future application resources protected by this group now trust only the front-door group.

Still seeing the public CIDR?

Return to secure-baseline.yaml and confirm that ApplicationSecurityGroup contains SourceSecurityGroupId. Make sure the public CidrIp line remains only in FrontDoorSecurityGroup.

Redeploy the same secure-web-baseline stack after saving the file. Use the application group ID from the latest stack outputs when you inspect the rule again.

Use your current command output to pinpoint the mismatch. Help me verify why the application security group still has broad ingress.

Your network boundary now separates the public entry layer from the private application layer. Next, you will add a zero-permission workload identity and a credential-free bootstrap artifact.

Add a Credential-Free Compute Identity and Bootstrap Artifact

Your application boundary now trusts only the front-door security group. The network path is controlled, but a future machine still needs an identity that does not depend on stored access keys.

This step extends your AWS CloudFormation template with an IAM role that trusts Amazon EC2. The role starts with no permissions.

An instance profile prepares that role for attachment to future compute. A Bash bootstrap script proves the startup artifact can work without persistent AWS credentials or AWS API calls.

In this step, get ready to:
  • Model a future EC2 workload identity with a zero-permission role inside an instance profile.
  • Build a bootstrap artifact that writes an HTML file locally without embedded credentials.
  • Verify the updated stack contains IAM resources without running compute.
Add the workload identity

A trust policy controls which service may assume a role. Because no permissions policy is attached, trusted compute still receives no permission to call AWS APIs.

  • Return to secure-baseline.yaml in the AWS CloudShell editor.
  • Find the end of ApplicationSecurityGroup immediately above Outputs:
  • Add the ComputeRole resource by copying this block above Outputs:
  ComputeRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Future EC2 workload identity with no permissions by default
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Tags:
        - Key: Project
          Value: secure-web-baseline

What does this role allow?

  • The AssumeRolePolicyDocument identifies which service may use the role.
  • The ec2.amazonaws.com principal limits that trust to EC2.
  • The absence of a permissions policy leaves ComputeRole with no permission to call AWS APIs.
  • Save secure-baseline.yaml.
  • Check the role addition by running this validation command:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does this check prove?

The command checks whether CloudFormation can read the template as JSON or YAML. You should receive template details without a validation failure.

This check does not assess the role's security or cost. It confirms the document remains structurally valid.

Template validation failing?

Check that ComputeRole aligns with the other resource names under Resources:. Make sure Outputs: still begins at the far left.

YAML indentation controls which properties belong together. Compare each nested line with the snippet above.

Help me diagnose the template validation result.

The role establishes trust, while an instance profile provides the container that future EC2 compute can receive. The profile contains only ComputeRole.

  • Find the end of the ComputeRole block.
  • Add the ComputeInstanceProfile resource directly below it:
  ComputeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref ComputeRole

How does the profile connect to the role?

The Roles list references ComputeRole by its logical name. CloudFormation resolves that reference when it creates the profile.

This baseline creates no EC2 instance. The profile remains ready for a future workload.

  • Save secure-baseline.yaml.
  • Confirm the profile keeps the template valid by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What should you see?

CloudFormation should return template details without reporting a validation failure. That result confirms the role reference can be parsed.

Profile reference not validating?

Check that the role name is spelled ComputeRole in both resource blocks. Keep Roles as a list with one indented item.

Help me fix the instance profile reference.

Stack outputs provide stable names for values that AWS generates during deployment. Adding role and profile outputs lets you retrieve their ARNs without depending on physical resource names.

  • Find the ApplicationSecurityGroupId entry at the bottom of Outputs:
  • Add the two identity outputs directly below that entry:
  ComputeRoleArn:
    Description: ARN of the zero-permission EC2 workload role
    Value: !GetAtt ComputeRole.Arn
  ComputeInstanceProfileArn:
    Description: ARN of the EC2 instance profile
    Value: !GetAtt ComputeInstanceProfile.Arn

Why expose these outputs?

The ComputeRoleArn output exposes the role's ARN. The ComputeInstanceProfileArn output exposes the profile's ARN.

These output names remain consistent when CloudFormation recreates the resources with new physical identifiers.

  • Save secure-baseline.yaml.
  • Validate the template with its new outputs by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does this result confirm?

A successful response confirms CloudFormation can parse the final role, profile, and output structure. The stack has not been updated yet.

Output validation failing?

Keep ComputeRoleArn and ComputeInstanceProfileArn aligned with the existing output names. Check that each Value line is nested beneath its output.

Help me check the new CloudFormation outputs.

Create and run the bootstrap artifact

A bootstrap script prepares a machine when it starts. This script writes a local HTML artifact without storing an access key, secret, session token, or AWS API call.

  • Use the same CloudShell editing method from secure-baseline.yaml to create bootstrap.sh beside it.
  • Copy this script into bootstrap.sh exactly as shown:
#!/bin/bash
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Secure AWS Baseline</title></head><body><h1>Secure AWS Baseline</h1><p>Bootstrapped without embedded cloud credentials.</p></body></html>' > /tmp/secure-baseline-index.html
echo 'Bootstrap complete: /tmp/secure-baseline-index.html'

What does this script do?

  • The first line selects Bash as the script interpreter.
  • The first echo command writes the complete HTML document to /tmp/secure-baseline-index.html.
  • The final echo command prints the artifact path as a visible success signal.
  • Save bootstrap.sh.

Before you run the script, do you expect it to need any AWS credential or network request?

  • Run the bootstrap artifact locally by using this command:
bash bootstrap.sh

What should you see?

The terminal prints Bootstrap complete: /tmp/secure-baseline-index.html. The script has created the local HTML artifact without calling an AWS API.

That result demonstrates credential-free bootstrap behavior before any future compute exists.

Bootstrap message missing?

Confirm that bootstrap.sh is saved in the same CloudShell location where you ran the command. Check that both echo lines match the script above.

Help me debug the bootstrap script.

✔️ Awesome, I've got everything!

Great. Your bootstrap artifact runs locally without embedded AWS credentials.

ⓧ I'd like to double check the full code

#!/bin/bash
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Secure AWS Baseline</title></head><body><h1>Secure AWS Baseline</h1><p>Bootstrapped without embedded cloud credentials.</p></body></html>' > /tmp/secure-baseline-index.html
echo 'Bootstrap complete: /tmp/secure-baseline-index.html'

How to compare this file

Confirm that your saved bootstrap.sh matches all three lines. Pay close attention to the output path and the quotes surrounding the HTML.

Deploy and inspect the final stack

The local files are ready, but the deployed stack still lacks the workload identity. The final update validates the complete template before CloudFormation creates the two IAM resources.

  • Validate the final template by running:
aws cloudformation validate-template --template-body file://secure-baseline.yaml

What does final validation cover?

The command checks whether CloudFormation can parse the complete YAML template. A successful response means the file is ready for the deployment attempt.

Final template not validating?

Use the full-code comparison below to check the placement of ComputeRole, ComputeInstanceProfile, and both new outputs. Keep each resource under Resources:.

Help me resolve the final template validation failure.

The next command acknowledges that the template creates IAM resources. The role has no permissions policy, so this update establishes workload identity without granting AWS API access.

The terminal may stay busy while CloudFormation updates the stack. That pause is expected.

  • Deploy the IAM additions to the existing stack by running:
aws cloudformation deploy --template-file secure-baseline.yaml --stack-name secure-web-baseline --capabilities CAPABILITY_IAM

Why acknowledge CAPABILITY_IAM?

The --capabilities CAPABILITY_IAM option explicitly acknowledges that this update creates IAM resources. CloudFormation updates the existing secure-web-baseline stack.

When the command completes, the stack returns to CREATE_COMPLETE with the role and instance profile added.

Stack update not completing?

Check the deployment output for the first resource that failed. Confirm that your AWS identity can create IAM roles and instance profiles.

Make sure the command includes --capabilities CAPABILITY_IAM. CloudFormation requires that acknowledgement for this template.

Help me diagnose the CloudFormation update.

Before you inspect the stack, which resource types do you expect to see without any running EC2 instance?

  • Inspect every resource in the updated stack by running:
aws cloudformation list-stack-resources --stack-name secure-web-baseline

What should the stack contain?

You should see AWS::IAM::Role and AWS::IAM::InstanceProfile in the resource list. You should not see an EC2 instance.

That resource list confirms the baseline now includes workload identity without launching compute.

IAM resources missing?

Confirm the deployment command completed before running the inspection command. Check that you used the same secure-web-baseline stack name.

If the list shows the older resource set, review the deployment output for a failed or skipped update.

Help me find the missing IAM resources.

You have now added a future compute identity without persistent credentials. The stack still contains no running compute or other excluded infrastructure.

✔️ Awesome, I've got everything!

Your final template is saved. Your stack contains the decoupled network boundary, zero-permission role, and instance profile.

ⓧ I'd like to double check the full code

AWSTemplateFormatVersion: "2010-09-09"
Description: Zero-compute secure AWS network and identity baseline

Resources:
  SecureVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.20.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: secure-web-baseline-vpc
        - Key: Project
          Value: secure-web-baseline

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: secure-web-baseline-igw
        - Key: Project
          Value: secure-web-baseline

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref SecureVpc

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.1.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-a
        - Key: Project
          Value: secure-web-baseline

  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.2.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-public-b
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.11.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-a
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref SecureVpc
      CidrBlock: 10.20.12.0/24
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: secure-web-private-b
        - Key: Project
          Value: secure-web-baseline

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-public-routes
        - Key: Project
          Value: secure-web-baseline

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetA

  PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnetB

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref SecureVpc
      Tags:
        - Key: Name
          Value: secure-web-private-routes
        - Key: Project
          Value: secure-web-baseline

  PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetA

  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnetB

  FrontDoorSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internet-facing HTTP boundary for a future managed entry point
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
          Description: Public HTTP for a future front door
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 10.20.0.0/16
          Description: HTTP toward the application network
      Tags:
        - Key: Name
          Value: secure-web-front-door-sg
        - Key: Project
          Value: secure-web-baseline

  ApplicationSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: HTTP only from the front-door security group
      VpcId: !Ref SecureVpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref FrontDoorSecurityGroup
          Description: HTTP only from the front-door security group
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
          Description: HTTPS egress if a future private route is added
      Tags:
        - Key: Name
          Value: secure-web-application-sg
        - Key: Project
          Value: secure-web-baseline

  ComputeRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Future EC2 workload identity with no permissions by default
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Tags:
        - Key: Project
          Value: secure-web-baseline

  ComputeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref ComputeRole

Outputs:
  VpcId:
    Description: ID of the secure baseline VPC
    Value: !Ref SecureVpc
  PublicRouteTableId:
    Description: Route table with the internet gateway route
    Value: !Ref PublicRouteTable
  PrivateRouteTableId:
    Description: Route table with no default internet route
    Value: !Ref PrivateRouteTable
  FrontDoorSecurityGroupId:
    Description: Security group ID for the future managed entry point
    Value: !Ref FrontDoorSecurityGroup
  ApplicationSecurityGroupId:
    Description: Security group ID for the future private application tier
    Value: !Ref ApplicationSecurityGroup
  ComputeRoleArn:
    Description: ARN of the zero-permission EC2 workload role
    Value: !GetAtt ComputeRole.Arn
  ComputeInstanceProfileArn:
    Description: ARN of the EC2 instance profile
    Value: !GetAtt ComputeInstanceProfile.Arn

How to compare the final template

Confirm that the network and security resources remain unchanged. Check that ComputeRole and ComputeInstanceProfile appear immediately before Outputs:.

The final outputs should end with ComputeRoleArn and ComputeInstanceProfileArn.

Secret mission

Prove the Baseline Is Reproducible

Your stack exists today. A trustworthy Infrastructure as Code template can rebuild the same architecture after its managed resources are gone. Fresh stack outputs will prove that your template can do exactly that.

Clean Up Your Resources

Clean Up Your Resources

The current AWS CloudFormation baseline has no additional usage charge under this project's strict zero-cost configuration. Use the cleanup options below to choose how much of the deployed architecture you want to retain.

Protect the Zero-Cost Boundary

This project creates no EC2 instance, load balancer, NAT gateway, VPC endpoint, Elastic IP address, or public IPv4 address. Keep this boundary intact if you need the strict $0 path.

Resources you used:

  • The secure-web-baseline AWS CloudFormation stack containing one VPC, four subnets, two route tables, one public default route, one internet gateway with its attachment, four route associations, two security groups, one IAM role, and one instance profile.
  • The secure-baseline.yaml template retained in AWS CloudShell.
  • The executable bootstrap.sh script retained in AWS CloudShell.
  • The generated /tmp/secure-baseline-index.html bootstrap artifact.

Keep everything running

No action needed. This option keeps the deployed baseline available for more experiments.

  • Leave the secure-web-baseline stack deployed.
  • Keep secure-baseline.yaml as the reusable architecture source.
  • Keep bootstrap.sh as the credential-free bootstrap artifact.
  • Retain /tmp/secure-baseline-index.html for local inspection during the current CloudShell environment.

Pause - I'll come back to this later

Deleting the stack can feel final. Your retained template can recreate the architecture later.

  • Delete the secure-web-baseline stack and wait for CloudFormation to finish by running these commands:
aws cloudformation delete-stack --stack-name secure-web-baseline
aws cloudformation wait stack-delete-complete --stack-name secure-web-baseline

What Do These Commands Do?

  • The first command starts deleting the secure-web-baseline stack.
  • The second command waits until CloudFormation confirms that deletion is complete.
  • Waiting prevents an in-progress deletion from being mistaken for a completed cleanup.

When the shell prompt returns, the deployed network and identity resources are gone. The secure-baseline.yaml and bootstrap.sh files remain available for a later rebuild.

Stack Deletion Not Finishing?

  • Confirm that AWS CloudShell is still using the selected Region.
  • Check that your authenticated AWS identity can delete every resource owned by the stack.
  • Help me diagnose why my CloudFormation stack deletion is not finishing.

Delete - I don't want to use this again

Remove the stack plus all three project files to return to a clean slate. The commands target only the resources created in this project.

  • Delete the secure-web-baseline stack and wait for CloudFormation to finish by running these commands:
aws cloudformation delete-stack --stack-name secure-web-baseline
aws cloudformation wait stack-delete-complete --stack-name secure-web-baseline

What Do These Commands Do?

  • The first command starts deleting the stack plus every network and identity resource it owns.
  • The second command waits for CloudFormation to finish the teardown.
  • The waiter gives you a clear completion point before you remove the reusable files.

When the shell prompt returns, CloudFormation has removed the recreated stack. Its VPC, subnets, routing resources, security groups, IAM role, and instance profile are gone.

Stack Still Present?

  • Confirm that AWS CloudShell is still using the Region where you recreated the stack.
  • Check that your authenticated AWS identity has permission to delete every stack resource.
  • Help me troubleshoot why my recreated CloudFormation stack still exists.
  • Remove the retained template, bootstrap script, and generated HTML artifact by running this command:
rm -f secure-baseline.yaml bootstrap.sh /tmp/secure-baseline-index.html

What Does This Command Do?

The command removes the two project files from the current CloudShell directory. It also removes the generated HTML artifact from /tmp.

Removing these artifacts prevents an old template or script from being mistaken for a current deployment source.

The CloudFormation stack and all three project files are now removed. Your AWS account no longer contains the baseline resources from this project.

Still See a Project File?

  • Confirm that the current AWS CloudShell directory is the one that contained secure-baseline.yaml and bootstrap.sh.
  • Run the removal command again from that directory.
  • Help me remove the remaining CloudShell project file safely.

Nice Work!

Nice Work!

You did it! Your AWS CloudFormation template now builds a zero-compute network baseline with workload identity. You also proved that the template can recreate the complete architecture after deletion.

What you learned:

  • Built a two-Availability-Zone network baseline inside a VPC. Its public route table supports a future managed entry point. Its private route table has no default internet route.
  • Replaced open application ingress with a security group reference. The application boundary now accepts TCP port 80 only from the front-door security group.
  • Added a zero-permission EC2 workload identity through an IAM role plus an instance profile. Created a credential-free bootstrap artifact that writes /tmp/secure-baseline-index.html without calling AWS APIs.
  • Completed the optional Secret Mission by deleting and recreating secure-web-baseline. Fresh physical IDs now back the same logical architecture. This proves that repeatable Infrastructure as Code is the source of truth.

Ready to quiz yourself?