Replace AWS SSH with Session Manager
Use Terraform and Session Manager to administer EC2 without inbound SSH.
Introduction
30 Second Summary
Businesses need administrators to reach cloud servers without leaving a public doorway open or passing login keys between people. They also need a clear record of who connected, so security teams can control access and support audits.
In this project, you will use Terraform to replace public SSH on Amazon EC2 with browser-based administration through AWS Systems Manager Session Manager. You will prove the change with a safe plan review, a live browser shell, and five cropped screenshots.
What You'll Build
You will open a working shell to your locked-down EC2 instance in the browser while its security group allows no inbound traffic and no key pair exists.
By the end of this project, you'll have:
- A risk review where you can point to TCP port 22 and 0.0.0.0/0 in a Terraform plan without deploying the unsafe rule.
- A locked-down browser shell where you can administer the EC2 instance while its security group shows no inbound rules.
- A five-image evidence pack that tells the full security story from risky plan to secure access, with account details and unrelated desktop content removed.
- Secret Mission: Require IMDSv2 tokens on the instance and capture the hardening result while Session Manager access keeps working.
Are there any prerequisites?
You need a Windows host with a working Ubuntu Desktop virtual machine in Oracle VirtualBox.
You also need an AWS account with an approved non-root identity that can create the required networking, compute, and IAM resources.
Before We Start
This checkpoint locks in the security problem before any hands-on work begins. You will demonstrate that public SSH increases attack surface by administering an Amazon Linux 2023 EC2 instance through Session Manager without inbound security group rules.
Prepare the Ubuntu IaC Workstation
The Ubuntu guest currently has no infrastructure tooling. It cannot inspect your AWS identity or produce a Terraform plan.
This step turns Ubuntu into an Infrastructure as Code workstation. Terraform will build plans while the AWS CLI confirms which non-root identity is active.
In this step, get ready to:
- Install Terraform 1.16.6 from HashiCorp's signed Ubuntu repository.
- Install AWS CLI 2.38.0 and authenticate the approved non-root identity.
- Capture cropped evidence of the installed versions and verified identity.
Install and verify Terraform
Terraform converts infrastructure definitions into reviewable plans. The signed HashiCorp repository gives Ubuntu a trusted source for the required release.
- Inside the Ubuntu virtual machine, click Show Applications.
- Type Terminal into the search field.
- Select Ubuntu Terminal.
- Check whether Terraform is already available by running this command:
terraform version
What does this check do?
This command asks Terraform to print its installed release. Choose the tab that matches your Ubuntu Terminal output.
✔️ I see version 1.16.6
Terraform 1.16.6 is already available. Continue to the shared verification below.
ⓧ I see an older version
Ubuntu is finding an earlier Terraform release. The signed HashiCorp repository provides the required release.
Ubuntu may ask for your password. The final command may also ask you to approve the package installation.
- Install the signing utilities and current Terraform release by running these commands:
- Approve the Terraform package installation if Ubuntu requests confirmation.
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
What do these commands do?
- The first command installs the utilities Ubuntu needs to verify the repository signature.
- The second command stores HashiCorp's signing key in Ubuntu's keyring.
- The third command adds the signed HashiCorp repository for your Ubuntu release.
- The final command refreshes available packages before installing Terraform.
ⓧ Command not found
Terraform is absent from Ubuntu. You will install it from HashiCorp's signed repository.
Ubuntu may ask for your password. The final command may also ask you to approve the package installation.
- Install the signing utilities and Terraform by running these commands:
- Approve the Terraform package installation if Ubuntu requests confirmation.
sudo apt-get update && sudo apt-get install -y gnupg software-properties-common
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
What do these commands do?
- The first command installs the utilities Ubuntu needs to verify the repository signature.
- The second command stores HashiCorp's signing key in Ubuntu's keyring.
- The third command adds the signed HashiCorp repository for your Ubuntu release.
- The final command refreshes available packages before installing Terraform.
- Confirm the installed Terraform release by running this command:
terraform version
What should this confirm?
The version check confirms which Terraform executable Ubuntu Terminal can reach. This catches an older installation before it affects the project.
You should see Terraform 1.16.6 in the output. Your first infrastructure tool is ready.
Terraform version still wrong?
Close Ubuntu Terminal before opening a fresh terminal window. This refreshes the command path after installation.
If the release still differs, confirm that every repository command completed without a network or package-signing failure.
Help me diagnose the Terraform version shown in Ubuntu Terminal.
Install and authenticate the AWS CLI
Terraform handles infrastructure definitions. The AWS CLI provides a separate identity check before any infrastructure work begins.
- Check whether the AWS CLI is already available by running this command:
aws --version
What does this check do?
This command prints the installed AWS CLI release. Choose the tab that matches your Ubuntu Terminal output.
✔️ I see version 2.38.0
AWS CLI 2.38.0 is already available. Continue to the shared verification below.
ⓧ I see an older version
Ubuntu is finding an earlier AWS CLI release. The official Linux installer updates the CLI for the current user.
- Install the current AWS CLI release by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this command do?
The command downloads the official AWS CLI Linux installation script over HTTPS. It passes that script to the shell for the current-user installation.
ⓧ Command not found
The AWS CLI is absent from Ubuntu. The official Linux installation script supports recent 64-bit Ubuntu systems.
- Install the AWS CLI for the current user by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash
What does this command do?
The command downloads the official AWS CLI Linux installation script over HTTPS. It passes that script to the shell for the current-user installation.
- Confirm the installed AWS CLI release by running this command:
aws --version
What should this confirm?
The version output confirms that Ubuntu Terminal can reach the installed AWS CLI. It also identifies the release currently in use.
You should see AWS CLI 2.38.0 in the output. Ubuntu can now communicate with AWS services.
AWS CLI version still missing?
Close Ubuntu Terminal before opening a fresh terminal window. The new window reloads the current user's command path.
If the command is still unavailable, review the installer output for a download or installation failure.
Help me troubleshoot the AWS CLI installation.
Credentials deserve a careful pause. The AWS CLI keeps this authentication outside your Terraform files.
- Keep the approved non-root access key ID ready for the interactive setup.
- Keep the matching secret access key ready for the interactive setup.
- Start the AWS CLI configuration by running this command:
- Enter the approved access key ID when the CLI requests it.
- Enter the approved secret access key when the CLI requests it.
- Enter us-east-1 for the default region.
- Press Enter to keep the default output format.
aws configure
What does this command do?
The interactive configuration associates the AWS CLI with the approved identity. Terraform files remain free of access keys and secret access keys.
Before you verify the caller, do you expect AWS to report the approved non-root identity?
- Verify the active AWS identity by running this command:
aws sts get-caller-identity
What does this identity check show?
- The UserId field identifies the authenticated principal.
- The Account field identifies the AWS account.
- The Arn field shows the active user or role.
You should see the ARN of the approved non-root identity. That is the authentication check complete.
Identity verification did not succeed?
Repeat the AWS CLI configuration if an access key was entered incorrectly. Confirm that the credentials belong to the approved non-root identity.
Ask the AWS account administrator to confirm that the approved identity remains active if authentication still fails.
Help me diagnose my AWS identity verification result.
Capture the toolchain evidence
Your first evidence image proves that the Ubuntu workstation has the required tools. It also proves that AWS requests use the approved non-root identity.
Before you run the final checks, do you expect all three commands to confirm the required workstation state?
- Display the two tool versions and active identity by running these commands:
terraform version
aws --version
aws sts get-caller-identity
What do these checks prove?
- The first command proves that Ubuntu is using Terraform 1.16.6.
- The second command proves that Ubuntu is using AWS CLI 2.38.0.
- The third command proves that the CLI can authenticate as the approved non-root identity.
You should see Terraform 1.16.6 and AWS CLI 2.38.0. You should also see the approved identity's ARN.
One of the final checks failed?
A missing version points back to the relevant installation check above. An identity failure points back to the AWS CLI configuration.
Resolve the failed check before capturing evidence. The screenshot must prove all three results.
Help me identify which workstation check failed.
- On the Windows host, press the Windows key to open search.
- Type File Explorer into Windows search.
- Press Enter to open File Explorer.
- Select Desktop in the left sidebar.
- Click New in the File Explorer toolbar.
- Select Folder.
- Enter IaC-Security-Evidence as the folder name.
- Press Enter to create the folder.
What belongs in this folder?
This folder will hold the project's cropped security evidence. The first image records the verified Ubuntu toolchain before any Terraform project files or AWS resources exist.
The identity output includes an AWS account number. Your crop removes that sensitive detail before the image is saved.
- Return to the Ubuntu Terminal results displayed inside Oracle VirtualBox.
- Press Windows logo key + Shift + S on the Windows host.
- Drag the image snip over the version and identity results.
- Select the capture notification to open Microsoft Snipping Tool.
- Click Image crop.
- Crop out the AWS account number.
- Crop out unrelated desktop content.
Your crop should retain Terraform 1.16.6, AWS CLI 2.38.0, and enough of the identity result to prove it is non-root.
- Click Save as in Microsoft Snipping Tool.
- Select the IaC-Security-Evidence folder on your Windows Desktop.
- Enter 01-ubuntu-toolchain.png as the filename.
- Click Save.
Before you check the folder, do you expect the saved PNG to show all three results without revealing the AWS account number?
- Return to the IaC-Security-Evidence folder in File Explorer.
You should see 01-ubuntu-toolchain.png. Opening it should show the two required versions and approved non-root identity verification with unrelated desktop content removed.
Your Ubuntu workstation can now plan infrastructure under a verified non-root AWS identity. Next, you will use Terraform to expose the public SSH risk without deploying it.
Expose the SSH Risk
Your Ubuntu workstation can now inspect AWS through the approved non-root identity. You are ready to test whether a proposed network design supports the project's security goal.
A Terraform plan previews infrastructure changes before they reach the cloud. In this step, the preview makes unrestricted SSH visible without creating the unsafe resources.
In this step, get ready to:
- Create a Terraform project pinned to the required versions.
- Model a VPC with a public SSH security group rule.
- Produce an unapplied plan that exposes unrestricted SSH.
Create the pinned Terraform project
Version constraints keep the project tied to the toolchain you verified earlier. The provider block also fixes the AWS region to us-east-1.
- Create the project directory on your Desktop by running these commands:
cd ~/Desktop
mkdir iac-security-session-manager
cd iac-security-session-manager
What do these commands do?
- The first line moves the terminal to your Desktop.
- The second line creates the iac-security-session-manager directory.
- The final line moves the terminal into that directory so every project file lands in the same place.
- Create versions.tf with Nano by running this command:
nano versions.tf
What does nano do?
The command opens versions.tf in the terminal text editor. Nano creates the file when you save it.
- Paste this configuration into Nano:
terraform {
required_version = "= 1.16.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.68.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "iac-security-session-manager"
ManagedBy = "Terraform"
}
}
}
What does this configuration do?
- The required_version constraint keeps the project on Terraform 1.16.6.
- The provider requirement pins hashicorp/aws to 6.68.0.
- The provider block sends proposed resources to us-east-1.
- The default tags identify resources managed by this project.
- Press Ctrl+O to write the file.
- Press Enter to confirm the filename.
- Press Ctrl+X to return to Ubuntu Terminal.
- Initialize the project by running:
terraform init
What does initialization do?
Initialization reads versions.tf and downloads the pinned AWS provider. It prepares this directory for formatting checks, validation, and planning.
You will see a success message confirming that the project has been initialized. The provider is now ready to interpret AWS resources.
Provider download failed?
Check that the Ubuntu virtual machine still has internet access. Confirm that the provider source and version in versions.tf match the configuration above.
If initialization still fails, ask for help diagnosing the Terraform provider download.
Model the unrestricted SSH rule
The temporary configuration represents the network access Terraform would create. Its ingress rule accepts TCP traffic for port 22 from every IPv4 address.
- Create risk.tf with Nano by running this command:
nano risk.tf
What happens to the new file?
Nano opens a blank buffer for risk.tf. Saving that buffer adds the temporary risk configuration to the project directory.
- Paste this temporary configuration into Nano:
resource "aws_vpc" "risk" {
cidr_block = "10.20.0.0/16"
}
resource "aws_security_group" "risk" {
name = "iac-security-risk"
description = "Temporary insecure SSH demonstration"
vpc_id = aws_vpc.risk.id
}
resource "aws_vpc_security_group_ingress_rule" "ssh_anywhere" {
security_group_id = aws_security_group.risk.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
What does this configuration model?
- The aws_vpc.risk resource provides the temporary network boundary.
- The aws_security_group.risk resource provides the firewall container for the proposed rule.
- The aws_vpc_security_group_ingress_rule.ssh_anywhere resource permits TCP traffic on port 22.
- The 0.0.0.0/0 CIDR covers every IPv4 address.
- Press Ctrl+O to write the file.
- Press Enter to confirm the filename.
- Press Ctrl+X to return to Ubuntu Terminal.
- Format the Terraform files by running:
terraform fmt
What does formatting check?
Formatting applies Terraform's standard layout to the configuration files. Consistent spacing makes the planned settings easier to inspect.
- Validate the complete configuration by running:
terraform validate
What does validation prove?
Validation checks the configuration structure against the installed provider. A successful result confirms that Terraform can interpret both files.
You will see a success message confirming that the configuration is valid. The temporary design is ready for a plan.
Configuration did not validate?
Check the braces and quotation marks in both files. Confirm that risk.tf contains all three resource blocks.
If the validation result still points to a syntax problem, ask for help checking the Terraform configuration.
✔️ Awesome, I've got everything!
Your two Terraform files are formatted and valid. Keep the configuration unapplied while you inspect its plan.
ⓧ I'd like to double check the full code
Compare both files with these complete references. Every version, resource name, region, port, and CIDR must match.
terraform {
required_version = "= 1.16.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.68.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "iac-security-session-manager"
ManagedBy = "Terraform"
}
}
}
What should match in versions.tf?
This file pins Terraform to 1.16.6 and the AWS provider to 6.68.0. It also sets the region and project tags.
resource "aws_vpc" "risk" {
cidr_block = "10.20.0.0/16"
}
resource "aws_security_group" "risk" {
name = "iac-security-risk"
description = "Temporary insecure SSH demonstration"
vpc_id = aws_vpc.risk.id
}
resource "aws_vpc_security_group_ingress_rule" "ssh_anywhere" {
security_group_id = aws_security_group.risk.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
What should match in risk.tf?
This file contains one VPC, one security group, and one ingress rule. The ingress rule uses TCP port 22 with the 0.0.0.0/0 source.
Reveal the risk in the plan
Planning evaluates the proposed resources without creating them. This is the safety boundary that lets you inspect the dangerous design before AWS receives any change.
Before you run the plan, which network path do you expect Terraform to propose?
- Generate the infrastructure preview by running:
terraform plan
What does the plan do?
Terraform reads the two configuration files and calculates the proposed AWS changes. It displays those changes for review without deploying the resources.
You will see aws_vpc_security_group_ingress_rule.ssh_anywhere in the plan. Its fields show TCP port 22 with cidr_ipv4 = "0.0.0.0/0".
This is the intended shortfall. The proposed rule exposes SSH to every IPv4 address.
- Keep the configuration unapplied by stopping after the plan.
Plan did not complete?
Confirm that the terminal is still inside the iac-security-session-manager directory. Check that your approved AWS identity remains authenticated.
If the output reports a configuration or permissions problem, ask for help diagnosing the failed Terraform plan.
Cropping security evidence is a little fiddly because the image must prove the rule while hiding unrelated details.
- Press Windows logo key + Shift + S on the Windows host.
- Drag over the terminal area containing the ingress rule name, port, and CIDR.
- Open the capture in Microsoft Snipping Tool.
- Select Image crop.
- Crop out unrelated desktop content and any account details.
- Save the cropped image as 02-insecure-ssh-plan.png inside the Windows IaC-Security-Evidence folder.
You now have concrete proof of the public SSH risk without deploying it. Next, you will replace that design with Session Manager access and no inbound rules.
Replace SSH with Session Manager
The earlier Terraform plan exposed how unrestricted SSH would create a public path into the instance. You stopped before that unsafe design reached AWS.
The replacement uses AWS Systems Manager Session Manager for administration through outbound HTTPS. An IAM instance profile gives the server permission to register as a managed node.
In this step, get ready to:
- Remove the temporary configuration that proposed unrestricted SSH.
- Define a network with no inbound rules.
- Plan a managed EC2 instance with outbound Session Manager access.
Replace the risky network design
The temporary risk.tf file demonstrated the problem without creating it. Removing that file clears the unsafe proposal from the local configuration.
- Return to the Terraform project directory from the previous step in Ubuntu's file manager.
- Select risk.tf.
- Delete risk.tf.
- Switch back to the Ubuntu Terminal from earlier.
- Confirm that the remaining Terraform configuration is valid by running:
terraform validate
What does this checkpoint prove?
Terraform now reads versions.tf without the temporary risk resources. Successful validation confirms that the unrestricted SSH proposal has left the configuration.
Does validation still mention the risky resources?
Check that you deleted risk.tf from the same Terraform project directory that contains versions.tf.
Help me locate the Terraform file that still defines the risky resources.
Why Session Manager?
A restricted SSH rule would still leave an inbound administration path to manage. Session Manager removes inbound administration from the security group.
The managed instance initiates its connection over TCP port 443. This makes the management path outbound-only.
The new VPC provides DNS support plus an internet route for the managed instance. Its dedicated security group declares no ingress rules.
- Create main.tf in the current Terraform project by running:
nano main.tf
What does this command do?
Nano opens a buffer named main.tf. Saving the new buffer creates the file inside the current Terraform project directory.
- Add the VPC plus public subnet to main.tf by pasting:
resource "aws_vpc" "main" {
cidr_block = "10.20.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "iac-security-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.20.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "iac-security-public-subnet"
}
}
What does this code do?
- The VPC reserves 10.20.0.0/16 for the lab network.
- DNS support lets the instance resolve the Systems Manager service endpoints.
- The subnet uses 10.20.1.0/24 inside aws_vpc.main.
- The public IP setting prepares the instance for outbound internet connectivity.
- Save main.tf in Nano.
- Return to Ubuntu Terminal.
- Format the first network resources by running:
terraform fmt
What should you observe?
Terraform formats main.tf into its standard layout. A return to the terminal prompt confirms that the formatter could read the file.
- Reopen main.tf by running:
nano main.tf
Where should you continue?
Nano reopens the saved file. Move to the bottom of main.tf before adding the outbound route.
- Add the internet gateway plus public route below the subnet by pasting:
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "iac-security-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "iac-security-public-route-table"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
How does the route work?
- The internet gateway attaches an internet path to aws_vpc.main.
- The route sends destinations outside the VPC to that gateway.
- The association applies the route table to aws_subnet.public.
- Save main.tf in Nano.
- Return to Ubuntu Terminal.
- Check the network references by running:
terraform validate
What does validation check?
Terraform checks that the subnet plus routing resources refer to declared resources. Successful validation proves that the outbound network path is internally consistent.
Does validation report a network reference error?
Compare each reference with the declared resource labels. A reference such as aws_vpc.main.id must match the VPC declaration exactly.
Help me troubleshoot the Terraform network references.
- Reopen main.tf by running:
nano main.tf
What are you adding next?
The network now has an outbound route. The next resources define a security group with no inbound rules plus one outbound HTTPS rule.
- Add the zero-ingress security group plus HTTPS egress rule by pasting:
resource "aws_security_group" "secure_admin" {
name_prefix = "iac-security-admin-"
description = "No inbound access; outbound HTTPS for Systems Manager"
vpc_id = aws_vpc.main.id
tags = {
Name = "iac-security-admin"
}
}
resource "aws_vpc_security_group_egress_rule" "https_out" {
security_group_id = aws_security_group.secure_admin.id
description = "Allow outbound HTTPS for Systems Manager"
cidr_ipv4 = "0.0.0.0/0"
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
Where is the security boundary?
- The security group declares no ingress block.
- The project contains no separate ingress-rule resource.
- The egress rule allows TCP port 443 from the instance.
- The destination covers the Systems Manager endpoints reached through the internet path.
- Save main.tf in Nano.
- Return to Ubuntu Terminal.
- Validate the zero-ingress network by running:
terraform validate
What does this checkpoint prove?
Terraform can resolve the security group plus its standalone egress rule. The valid configuration now describes outbound HTTPS without an inbound SSH path.
Give the instance Session Manager access
Network connectivity covers one part of the management path. The Amazon EC2 instance also needs permission to register with Systems Manager.
A trust policy lets EC2 assume an IAM role. An instance profile delivers that role to the server.
- Reopen main.tf by running:
nano main.tf
Where does the identity layer go?
Move to the bottom of main.tf. The identity resources belong below the completed network resources.
- Add the EC2 trust policy below the HTTPS egress rule by pasting:
data "aws_iam_policy_document" "ec2_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
What does the trust policy establish?
- The statement permits the role-assumption action.
- The service principal limits that trust to EC2.
- Terraform exports the policy document as JSON for the IAM role.
- Add the role plus its policy attachment plus instance profile below the trust policy by pasting:
resource "aws_iam_role" "ssm" {
name_prefix = "iac-security-ssm-"
assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm" {
name_prefix = "iac-security-ssm-"
role = aws_iam_role.ssm.name
}
How do the IAM resources connect?
- The IAM role uses the generated EC2 trust policy.
- The policy attachment grants AmazonSSMManagedInstanceCore to the role.
- The instance profile packages the role for attachment to EC2.
- Save main.tf in Nano.
- Return to Ubuntu Terminal.
- Validate the identity layer by running:
terraform validate
What does this checkpoint prove?
Terraform confirms that the trust document feeds the IAM role. It also resolves the policy attachment plus instance profile references.
Does validation report an IAM error?
Check that each IAM resource uses the label ssm. A different label breaks the links between the role plus its attachment plus profile.
Help me troubleshoot the Session Manager IAM resources.
The final resource combines the outbound network with the instance profile. It uses Amazon Linux 2023 plus encrypted root storage.
- Reopen main.tf by running:
nano main.tf
What remains in main.tf?
The network plus identity layers are complete. The EC2 resource ties those layers together without defining an EC2 key pair.
- Add the managed EC2 instance at the bottom of main.tf by pasting:
resource "aws_instance" "secure_admin" {
ami = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.secure_admin.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
root_block_device {
encrypted = true
volume_type = "gp3"
volume_size = 8
delete_on_termination = true
}
tags = {
Name = "iac-security-session-manager"
}
depends_on = [aws_iam_role_policy_attachment.ssm]
}
What does the instance configuration enforce?
- The instance uses the Amazon Linux 2023 image reference.
- The instance type is t3.micro.
- The instance receives the zero-ingress security group.
- The instance receives the Session Manager instance profile.
- The encrypted root volume uses 8 GiB of gp3 storage.
Notice What Is Missing
The instance contains no key_name argument. Its attached security group also has no inbound rule.
- Save main.tf in Nano.
- Return to Ubuntu Terminal.
- Validate the complete resource graph by running:
terraform validate
What does this verification cover?
Terraform checks the instance references against the subnet plus security group plus instance profile. Successful validation confirms that main.tf is internally consistent.
Does the instance fail validation?
Check the profile reference against aws_iam_instance_profile.ssm.name. Check the security group reference against aws_security_group.secure_admin.id.
Help me troubleshoot the managed EC2 instance resource.
Plan and capture the secure design
Outputs give the deployment step stable values to retrieve. They also summarize the security posture in plain language.
- Create outputs.tf in the current Terraform project by running:
nano outputs.tf
What does this command do?
Nano opens a buffer named outputs.tf. Terraform reads this file together with versions.tf plus main.tf.
- Add the three output definitions to outputs.tf by pasting:
output "instance_id" {
description = "EC2 instance managed through Session Manager"
value = aws_instance.secure_admin.id
}
output "security_group_id" {
description = "Security group with no inbound rules"
value = aws_security_group.secure_admin.id
}
output "security_posture" {
description = "Administrative access design"
value = "No inbound rules; Session Manager over outbound HTTPS"
}
What will these outputs provide?
- The instance_id output identifies the managed EC2 instance.
- The security_group_id output identifies the zero-ingress security group.
- The security_posture output records the outbound-only administration design.
- Save outputs.tf in Nano.
- Return to Ubuntu Terminal.
- Validate the outputs against the resource graph by running:
terraform validate
What does this checkpoint prove?
Terraform confirms that every output refers to a declared resource. The combined configuration is now ready for its secure plan.
✔️ Awesome, I've got everything!
Your three Terraform files are saved. Make sure Nano is closed before generating the final plan.
ⓧ I'd like to double check the full code
Compare each file with these cumulative versions. Keep every identifier plus value aligned before continuing.
terraform {
required_version = "= 1.16.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 6.68.0"
}
}
}
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
Project = "iac-security-session-manager"
ManagedBy = "Terraform"
}
}
}
What should versions.tf contain?
This file pins Terraform to 1.16.6 plus the AWS provider to 6.68.0. It keeps the provider region set to us-east-1.
resource "aws_vpc" "main" {
cidr_block = "10.20.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "iac-security-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.20.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "iac-security-public-subnet"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "iac-security-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "iac-security-public-route-table"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "secure_admin" {
name_prefix = "iac-security-admin-"
description = "No inbound access; outbound HTTPS for Systems Manager"
vpc_id = aws_vpc.main.id
tags = {
Name = "iac-security-admin"
}
}
resource "aws_vpc_security_group_egress_rule" "https_out" {
security_group_id = aws_security_group.secure_admin.id
description = "Allow outbound HTTPS for Systems Manager"
cidr_ipv4 = "0.0.0.0/0"
from_port = 443
to_port = 443
ip_protocol = "tcp"
}
data "aws_iam_policy_document" "ec2_assume_role" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "ssm" {
name_prefix = "iac-security-ssm-"
assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.ssm.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssm" {
name_prefix = "iac-security-ssm-"
role = aws_iam_role.ssm.name
}
resource "aws_instance" "secure_admin" {
ami = "resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.secure_admin.id]
iam_instance_profile = aws_iam_instance_profile.ssm.name
root_block_device {
encrypted = true
volume_type = "gp3"
volume_size = 8
delete_on_termination = true
}
tags = {
Name = "iac-security-session-manager"
}
depends_on = [aws_iam_role_policy_attachment.ssm]
}
What should main.tf contain?
This file defines the complete network plus identity plus instance design. It includes no SSH ingress resource or EC2 key-pair argument.
output "instance_id" {
description = "EC2 instance managed through Session Manager"
value = aws_instance.secure_admin.id
}
output "security_group_id" {
description = "Security group with no inbound rules"
value = aws_security_group.secure_admin.id
}
output "security_posture" {
description = "Administrative access design"
value = "No inbound rules; Session Manager over outbound HTTPS"
}
What should outputs.tf contain?
This file exposes the instance ID plus security group ID after deployment. It also records the intended administrative access design.
- Format the complete configuration by running:
terraform fmt
What does formatting confirm?
Terraform standardizes the layout of every configuration file. A normal return to the prompt confirms that the formatter read the completed files.
- Validate the completed configuration by running:
terraform validate
What does final validation confirm?
Terraform checks the complete dependency graph before contacting AWS for a plan. Successful validation means the configuration is structurally ready.
Before you generate the plan, do you expect Terraform to propose any inbound rule or SSH resource?
- Generate the secure plan by running:
terraform plan
What should the secure plan contain?
- The plan includes aws_vpc_security_group_egress_rule.https_out with TCP port 443.
- The plan includes the EC2 instance plus its IAM role plus policy attachment plus instance profile.
- The plan contains no aws_vpc_security_group_ingress_rule.ssh_anywhere resource.
- The plan contains no inbound SSH rule.
The contrast is now visible. The earlier plan proposed public port 22. This plan proposes outbound HTTPS plus IAM-controlled administration.
- Review the displayed plan for aws_vpc_security_group_egress_rule.https_out.
- Confirm that the displayed plan contains no ssh_anywhere resource.
- Stop before running terraform apply.
Does the risky SSH resource still appear?
A remaining ssh_anywhere resource means Terraform is still reading a file that declares the temporary rule. Check the current project directory for another copy of risk.tf.
Help me find why the risky resource remains in the plan.
This plan proves that the secure replacement is ready before deployment. The evidence should preserve the HTTPS-only design without exposing account details.
- Switch to the Windows host while the secure plan remains visible in the Ubuntu virtual machine.
- Press Windows logo key + Shift + S to open the capture overlay.
- Capture the plan section showing the HTTPS egress rule plus the Session Manager resources.
- Open the capture in Microsoft Snipping Tool.
- Choose Image crop.
- Remove unrelated desktop content from the image.
- Remove any visible AWS account details from the image.
- Save the cropped image as 03-secure-session-manager-plan.png inside IaC-Security-Evidence.
Strong work. Your third screenshot now records the corrected zero-ingress design before any infrastructure is created.
Your secure design is validated without creating a public SSH path. Next, you will deploy it and prove that Session Manager opens a browser shell on the locked-down instance.
Deploy and Access the Instance
Your secure Terraform plan now describes a zero-ingress administration path. AWS still has no infrastructure from this project because the plan has not been applied.
This step creates the Amazon EC2 instance. AWS Systems Manager Session Manager then opens an administrative shell through outbound HTTPS.
The decisive proof is a working shell on an instance with no inbound security group rules. That result shows the earlier public SSH rule is unnecessary.
In this step, get ready to:
- Apply the reviewed Terraform configuration.
- Confirm that the instance registers as a Systems Manager managed node.
- Open a browser shell and capture the secure access evidence.
Apply the secure configuration
Applying the configuration turns the reviewed plan into live infrastructure. Terraform also records each deployed resource in local state.
This action creates AWS resources that can incur usage charges. You will remove them in the cleanup section.
- Switch back to the Ubuntu Terminal from earlier.
- Deploy the reviewed configuration by running this command:
terraform apply
What does the apply create?
- The network resources give the instance an outbound route to AWS service endpoints.
- The security group keeps all inbound traffic closed.
- The IAM instance profile gives SSM Agent its managed-node permissions.
- The EC2 resource launches Amazon Linux 2023 with encrypted gp3 storage.
- Terraform writes the deployed resource identities to local state.
- Compare the refreshed plan with the secure plan from the previous step.
- Confirm that the refreshed plan contains no aws_vpc_security_group_ingress_rule.ssh_anywhere resource.
- Enter yes when Terraform requests confirmation.
Terraform may take a few minutes to create the network resources and the instance. The changing status lines show the deployment progressing.
You'll see a completion summary once every resource has been created.
That is the high-stakes part complete: your zero-ingress infrastructure is now live in AWS.
Did the apply stop early?
An authorization failure means the approved identity lacks permission for one of the planned AWS actions. Copy the failing action from the terminal for your AWS administrator.
Keep using the approved non-root identity. Avoid switching to the root account to bypass a missing permission.
Ask for help with the exact failure: Help me diagnose why this Terraform apply stopped.
Before you inspect the outputs, which result do you expect to carry the no-inbound security claim?
- Read the deployed resource outputs by running this command:
terraform output
What do these outputs prove?
This command reads the values stored in Terraform state. It gives you the deployed instance identifier and security group identifier.
The posture output summarizes the administration design. It confirms that access uses outbound HTTPS with no inbound rules.
You'll see values for instance_id, security_group_id, and security_posture.
Managed-node registration can take a few minutes after the apply finishes. That delay is the SSM Agent establishing its outbound service connection.
Before you check, do you expect the new instance to appear as a Systems Manager managed node?
- Check the Systems Manager managed-node inventory by running this command:
aws ssm describe-instance-information
What does this check prove?
This command asks Systems Manager for its registered managed nodes. A record for your instance proves that the agent reached the service through outbound HTTPS.
The connection started from the instance. No inbound firewall access was required.
You'll see a managed-node record for the new instance. The result includes platform information and the installed SSM Agent version.
Is the managed-node list empty?
- Wait a few more minutes for the instance to finish registering.
- Confirm that versions.tf still configures us-east-1.
- Run the managed-node check again after the registration delay.
Get help with the registration path: Help me troubleshoot why my EC2 instance is missing from Systems Manager.
Open a browser shell with Session Manager
The managed-node record proves that the instance can reach Systems Manager. A browser session now tests whether that path supports practical administration.
You are using the approved non-root identity for this connection. No new credential is required.
- Press the Windows key on the Windows host to open search.
- Type the name of your web browser and press Enter.
- Sign in to the AWS Management Console with the approved non-root identity.
- Use the console service search to open Amazon EC2.
- Choose Instances from the EC2 navigation.
- Select the instance named iac-security-session-manager.
The instance is now selected for a connection attempt. Its running state confirms that the deployment reached EC2.
Before you connect, do you expect the shell to need port 22 or an EC2 key pair?
- Choose Connect.
- Choose the Session Manager tab.
- Choose Connect again.
You'll see a browser-based shell that is ready for commands. The connection works without an EC2 key pair or an inbound security group rule.
You have proved the full security path: the instance remains manageable while its inbound network surface stays closed.
Can't open the Session Manager shell?
- Return to the Ubuntu Terminal from earlier.
- Wait for the managed-node registration to finish.
- Run the managed-node check again before retrying the browser connection.
Get help without opening SSH: Help me restore Session Manager access to this instance.
Capture the working Session Manager shell
A shell screenshot turns the successful connection into reusable evidence. Microsoft Snipping Tool lets you keep the proof while excluding sensitive account details.
- Keep the working Session Manager browser tab in the foreground.
- Press Windows logo key + Shift + S to start a screen capture.
- Drag the capture rectangle around the instance context and working shell.
- Open the new capture preview in Microsoft Snipping Tool.
- Choose Image crop.
- Trim out the AWS account menu, unrelated tabs, and unused page chrome.
- Use the Snipping Tool save control to store the image as 04-session-manager-shell.png inside the existing IaC-Security-Evidence folder.
Before you review the saved image, do you think the crop keeps enough context to prove secure access?
- Return to the existing IaC-Security-Evidence folder in Windows File Explorer.
- Open 04-session-manager-shell.png.
The image should show the deployed instance context and a working Session Manager shell. The crop should exclude account details and unrelated applications.
That screenshot is your clearest proof that administration still works after public SSH has been removed.
Does the evidence feel incomplete?
If the crop removed the instance context, repeat the capture with a slightly wider rectangle. Keep the working shell visible.
If account details remain visible, crop the top-right console area before saving the file again.
Ask for a safe evidence review: Help me check whether my Session Manager screenshot proves secure access.
Your locked-down instance is live and accessible through Session Manager. Next, you'll complete the evidence pack by confirming the security group has no inbound rules.
Build the Security Evidence Pack
Your browser-based AWS Systems Manager Session Manager shell proves that the Amazon EC2 instance remains manageable without inbound SSH. However, the evidence pack still needs direct proof that the instance accepts no inbound connections.
Four cropped captures already document the work. This step adds the zero-ingress image before turning all five files into a concise security story.
In this step, get ready to:
- Capture the security group view that shows no inbound rules.
- Review all five screenshots for clarity.
- Confirm that the finished evidence pack tells the complete security story.
Capture the zero-ingress proof
A security group controls which network traffic can reach an instance. The final screenshot needs enough context to connect the empty inbound rules view to iac-security-admin.
- Switch back to the Amazon EC2 Instances page from earlier.
- Select the iac-security-session-manager instance.
- In the instance details, select the linked security group named iac-security-admin.
- Switch to the security group's inbound rules view.
You'll see that no inbound rules exist. This confirms that the instance does not accept incoming network connections.
What Does This Prove?
The empty inbound rules view proves that administrative access does not depend on an open network port. The earlier Session Manager shell proves that the instance remains manageable through its outbound connection.
Cropping this proof can feel fiddly because a blank rules area needs surrounding context. Microsoft Snipping Tool lets you keep the resource name visible while removing unrelated page content.
- Press Windows logo key + Shift + S to open the image snip overlay.
- Drag a rectangle around the security group context plus the empty inbound rules area.
- Open the capture in Microsoft Snipping Tool.
- Choose Image crop.
- Crop out the AWS account menu.
- Crop out unrelated browser tabs.
- Preserve the security group name plus the empty inbound rules area.
- Save the cropped image as 05-no-inbound-rules.png inside IaC-Security-Evidence.
That closes the final evidence gap. Your fifth image now proves the zero-ingress network posture directly.
Review every crop
Each PNG needs enough context to support its claim. A tight review keeps the proof readable without exposing irrelevant details.
- Open 01-ubuntu-toolchain.png in the Microsoft Snipping Tool window from earlier.
- Choose Image crop.
- Trim unrelated borders.
- Trim irrelevant menus.
- Preserve enough labels to show what the image proves.
- Save the changes under the same filename.
- Repeat this crop cycle for 02-insecure-ssh-plan.png through 05-no-inbound-rules.png.
Keep the Proof Visible
A tight crop should retain the resource name or command output that makes its claim verifiable. An isolated shell prompt or blank table loses the context that gives the image meaning.
This safety check is worth slowing down for because evidence files are easy to share beyond their original audience. A careful pass keeps sensitive details outside the finished pack.
- Check every image for visible credentials.
- Check every image for access keys.
- Check every image for AWS account numbers.
- Check every image for unrelated applications.
- Re-crop any image that fails one of these checks.
- Save each corrected image under its original filename.
Good work. Every image now focuses on one security claim without exposing unrelated information.
Verify the five-image story
Before you check the folder, what sequence do you expect the five numbered files to reveal?
- Return to the IaC-Security-Evidence folder on Windows.
You should see exactly five PNG files in numbered order. Together, they trace the project from toolchain verification to zero-ingress administration.
What Should Each File Prove?
- 01-ubuntu-toolchain.png shows Terraform 1.16.6. It also shows AWS CLI 2.38.0. The approved non-root identity remains clear while the AWS account number stays hidden.
- 02-insecure-ssh-plan.png shows aws_vpc_security_group_ingress_rule.ssh_anywhere. It captures TCP port 22 with cidr_ipv4 = "0.0.0.0/0". The unsafe configuration was never applied.
- 03-secure-session-manager-plan.png shows the HTTPS-only egress design. No ssh_anywhere resource or inbound SSH rule appears.
- 04-session-manager-shell.png shows the working Session Manager shell. The crop retains the deployed instance context.
- 05-no-inbound-rules.png shows that iac-security-admin has no inbound rules.
- Open each image in filename order.
- Confirm every crop is readable at normal zoom.
- Confirm no crop exposes credentials.
- Confirm no crop exposes access keys.
- Confirm no crop exposes an AWS account number.
- Confirm no crop includes unrelated applications.
Strong finish. Your five-file pack now proves that you identified public SSH risk before replacing it with manageable zero-ingress infrastructure.
Secret mission
Require IMDSv2
Your zero-ingress instance already removes public SSH. This mission adds another security control by requiring IMDSv2 tokens for instance metadata access while preserving Session Manager administration.
Clean Up Your Resources
Clean Up Your Resources
The live AWS lab can continue generating charges after you finish. Choose whether to keep it running, pause the instance, or destroy the Terraform-managed deployment.
Cost warning
In US East (N. Virginia), a running Linux t3.micro instance costs $0.0104 per hour. Its in-use public IPv4 address costs another $0.005 per hour.
The encrypted 8 GiB gp3 EBS volume adds prorated storage charges. T3 Unlimited can add CPU credit charges when sustained usage exceeds the baseline.
Free Tier credits may cover eligible usage. Eligibility depends on your account.
- Choose Pause or Delete unless you still need the live demonstration.
Resources you used:
- The Terraform-managed network stack with the iac-security-vpc VPC, its subnet, its internet gateway, its route table, and its route-table association.
- The iac-security-admin security group with no inbound rules and one outbound TCP 443 rule.
- The Amazon EC2 instance named iac-security-session-manager with an automatically assigned public IPv4 address.
- The encrypted 8 GiB gp3 root volume configured for deletion with the instance.
- The IAM role, the AmazonSSMManagedInstanceCore policy attachment, and the IAM instance profile.
Your Ubuntu Terraform configuration stays local. The IaC-Security-Evidence folder also stays on Windows with all six cropped PNG files.
Keep everything running
No action is needed. Choose this if you still want to demonstrate the working browser shell and the live zero-ingress instance.
- Leave the Terraform-managed AWS deployment running.
- Keep the Terraform configuration and local state in their existing Ubuntu project directory.
- Retain the IaC-Security-Evidence folder on Windows with all six PNG files.
Pause - I'll come back to this later
Stop the EC2 instance to pause compute billing. The EBS volume remains billable while the instance is stopped.
- Return to the Amazon EC2 console from earlier.
- Select Instances from the navigation.
- Select the row for iac-security-session-manager.
- Use the instance-state control to stop the selected instance.
- Confirm the stop when AWS asks.
- Wait until the instance state shows that it has stopped.
The instance no longer incurs compute charges. Its EBS storage remains in place for your return.
When you return:
- Use the instance-state control to start iac-security-session-manager.
- Check the public IPv4 address after startup because an automatically assigned address can change.
- Wait until Session Manager lists the instance again.
- Reopen the browser shell to confirm that administrative access works.
You will see the browser-based shell working again without an EC2 key pair or an inbound security-group rule.
Delete - I don't want to use this again
Deleting the AWS stack is a big step, but your Terraform files and six screenshots stay on disk. This option removes the live resources that can generate charges.
- In the Ubuntu Terminal from earlier, return to the Terraform project directory that contains main.tf and the local state.
- Destroy the deployed AWS stack by running:
terraform destroy
What does this command do?
Terraform uses the local state to identify this lab's tracked AWS resources. It removes those resources after you approve the destruction plan.
- Review the destruction plan to confirm that it covers the project resources.
- Approve the destruction when Terraform asks for confirmation.
- Wait until Terraform finishes removing the tracked resources.
Terraform reports that the resources were destroyed. You have closed the billable AWS lab while preserving the files that prove your work.
Did the Destruction Stop?
- Confirm that Ubuntu Terminal is in the same project directory that contains main.tf and the local Terraform state.
- Confirm that the approved non-root AWS credentials are still active.
- Help me diagnose the failed Terraform cleanup.
- Keep the Terraform files and local state in their existing Ubuntu project directory.
- Keep the IaC-Security-Evidence folder on Windows with all six PNG files.
Nice Work!
Nice Work!
Outstanding work. Your secure Amazon EC2 instance stays manageable through AWS Systems Manager Session Manager without public SSH.
Here's what you learned:
- Used a Terraform planning workflow to expose TCP port 22 from 0.0.0.0/0 before deployment. The unsafe rule never reached AWS.
- Built outbound-only administration with no inbound security group rules. An IAM instance profile carries the Systems Manager permissions used by the instance over TCP port 443.
- Produced a six-image evidence pack that traces the security story from unrestricted SSH to a zero-ingress deployment. Each crop removes account details and unrelated interface elements.
- Completed the Secret Mission by requiring IMDSv2 tokens through http_tokens = "required". Session Manager access still works after the hardening change.
Ready to quiz yourself?