AWS CLI First #9: Zero-Trust Fleet Operations: AWS Systems Manager (SSM) Session Manager & Run Command
Eliminate Port 22 SSH forever. Learn how to manage EC2 instances with zero open inbound ports using AWS Systems Manager (SSM) Session Manager, enable encrypted CloudWatch session auditing, tunnel private ports, and run fleet-wide remote commands via CLI.
AWS with CLI: The Terminal-First Mastery Path
Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.
For decades, remote server administration relied on a single standard: Port 22 Secure Shell (SSH).
However, in modern enterprise cloud architecture, SSH is considered an operational and security anti-pattern:
- SSH Key Management Hell: Generating, distributing, rotating, and revoking
.pemkey pairs across dozens of developers is fragile and prone to credential leaks. - The Corporate Firewall Problem: Corporate office firewalls, campus networks, and zero-trust proxies block outbound Port 22 by default, preventing engineers from connecting to servers from the office.
- Massive Attack Surface: Opening Port 22 to
0.0.0.0/0(or even a shifting home IP) invites automated Internet bots to continuously brute-force your compute nodes. - Zero Audit Trail: Standard SSH sessions are encrypted black boxes—there is no centralized, tamper-proof log recording what commands an engineer ran on production servers.
In this guide, we will implement Zero-Trust Fleet Operations using AWS Systems Manager (SSM). We will eliminate SSH entirely, lock down Security Groups to 0 open inbound rules, stream encrypted keystroke audits to CloudWatch Logs, tunnel internal web ports securely, and execute fleet-wide remote commands across 100+ servers simultaneously—all from the command line.
💰 Estimated Lab Cost & Resource Architecture
| Component | Usage in Lab | AWS Free Tier | On-Demand Rate |
|---|---|---|---|
| Zero-Cost Public VPC + IGW | Base Network Foundation | 100% Free | $0.00 |
t4g.micro Graviton Compute | 1 Instance (~1 hour) | 750 hrs/month Free | $0.0084 / hr |
| AWS Systems Manager (SSM) | Session Manager & Run Command | 100% Free | $0.00 |
| Amazon CloudWatch Logs | Encrypted Session Auditing (~5 MB) | 5 GB/month Free | < $0.001 |
| Zero Open Inbound Ports | Security Group Inbound: NONE | 100% Free | $0.00 |
| Total Estimated Cost | 1–2 Hour Hands-On Session | $0.00 Total | ~$0.01 Total |
[!TIP] Systems Manager communication operates entirely over outbound HTTPS (Port 443). No NAT Gateway ($0.045/hr) is needed! Running the automated Phase 6 Teardown Script cleans up all resources and brings ongoing costs immediately back to $0.00.
Prerequisites: Base Node Setup (0 Inbound Firewall Rules)
Step 1: Install the AWS Session Manager CLI Plugin
To start interactive terminal sessions from your local workstation, install the official AWS Session Manager plugin:
- macOS (Homebrew):
brew install --cask session-manager-plugin
Run the plugin verification command:
session-manager-plugin
Expected Output:
The Session Manager plugin is installed successfully. Use the AWS CLI to start a session.
Step 2: Provision Base VPC, IAM Role & 0-Inbound Compute Node
Run the following fast-track script to spin up an IAM Instance Profile (AmazonSSMManagedInstanceCore) and an EC2 instance with zero open inbound security group ports:
cat <<'EOF' > setup-ssm-node.sh
#!/usr/bin/env bash
set -e
PROFILE="cloud-dev-admin"
REGION="ap-southeast-1"
echo "=== 1. Creating IAM Role & Instance Profile for SSM ==="
cat <<'TRUST' > ssm-trust-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
TRUST
aws iam create-role \
--role-name "SSMFleetRole" \
--assume-role-policy-document file://ssm-trust-policy.json \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
aws iam attach-role-policy \
--role-name "SSMFleetRole" \
--policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" \
--region "$REGION" \
--profile "$PROFILE"
aws iam create-instance-profile \
--instance-profile-name "SSMFleetProfile" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
aws iam add-role-to-instance-profile \
--instance-profile-name "SSMFleetProfile" \
--role-name "SSMFleetRole" \
--region "$REGION" \
--profile "$PROFILE" 2>/dev/null || true
rm -f ssm-trust-policy.json
echo "Waiting 10 seconds for IAM instance profile replication..."
sleep 10
echo "=== 2. Setting up Base Network ==="
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --query "Vpcs[0].VpcId" --output text --region "$REGION" --profile "$PROFILE" 2>/dev/null || true)
if [ -z "$VPC_ID" ] || [ "$VPC_ID" = "None" ]; then
VPC_ID=$(aws ec2 create-vpc --cidr-block "10.0.0.0/16" --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=Production-VPC}]' --region "$REGION" --profile "$PROFILE" --query "Vpc.VpcId" --output text)
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support '{"Value":true}' --region "$REGION" --profile "$PROFILE"
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames '{"Value":true}' --region "$REGION" --profile "$PROFILE"
fi
PUB_SUB_A=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Subnet-A" --query "Subnets[0].SubnetId" --output text --region "$REGION" --profile "$PROFILE" 2>/dev/null || true)
if [ -z "$PUB_SUB_A" ] || [ "$PUB_SUB_A" = "None" ]; then
PUB_SUB_A=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "10.0.1.0/24" --availability-zone "${REGION}a" --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-A}]' --region "$REGION" --profile "$PROFILE" --query "Subnet.SubnetId" --output text)
aws ec2 modify-subnet-attribute --subnet-id "$PUB_SUB_A" --map-public-ip-on-launch --region "$REGION" --profile "$PROFILE"
fi
IGW_ID=$(aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text --region "$REGION" --profile "$PROFILE" 2>/dev/null || true)
if [ -z "$IGW_ID" ] || [ "$IGW_ID" = "None" ]; then
IGW_ID=$(aws ec2 create-internet-gateway --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=Production-IGW}]' --region "$REGION" --profile "$PROFILE" --query "InternetGateway.InternetGatewayId" --output text)
aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" --region "$REGION" --profile "$PROFILE"
fi
PUB_RTB_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Route-Table" --query "RouteTables[0].RouteTableId" --output text --region "$REGION" --profile "$PROFILE" 2>/dev/null || true)
if [ -z "$PUB_RTB_ID" ] || [ "$PUB_RTB_ID" = "None" ]; then
PUB_RTB_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Public-Route-Table}]' --region "$REGION" --profile "$PROFILE" --query "RouteTable.RouteTableId" --output text)
aws ec2 create-route --route-table-id "$PUB_RTB_ID" --destination-cidr-block "0.0.0.0/0" --gateway-id "$IGW_ID" --region "$REGION" --profile "$PROFILE"
aws ec2 associate-route-table --route-table-id "$PUB_RTB_ID" --subnet-id "$PUB_SUB_A" --region "$REGION" --profile "$PROFILE"
fi
echo "=== 3. Creating ZERO-INBOUND Security Group ==="
# Notice: No inbound authorization! 0 ports opened!
ZERO_SG_ID=$(aws ec2 create-security-group \
--group-name "ZeroInbound-SSM-SG" \
--description "Security Group with 0 Open Inbound Ports for SSM" \
--vpc-id "$VPC_ID" \
--region "$REGION" \
--profile "$PROFILE" \
--query "GroupId" \
--output text 2>/dev/null || aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=ZeroInbound-SSM-SG" --region "$REGION" --profile "$PROFILE" --query "SecurityGroups[0].GroupId" --output text)
echo "=== 4. Launching SSM Managed Node ==="
cat <<'USERDATA' > ssm-userdata.sh
#!/usr/bin/env bash
dnf update -y
dnf install -y nginx
systemctl enable --now nginx
echo "<h1>🔒 Private Internal Server (SSM Managed)</h1>" > /usr/share/nginx/html/index.html
USERDATA
AMI_ID=$(aws ssm get-parameter --name "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64" --region "$REGION" --profile "$PROFILE" --query "Parameter.Value" --output text 2>/dev/null || aws ec2 describe-images --owners amazon --filters "Name=name,Values=al2023-ami-2023*-arm64" "Name=state,Values=available" "Name=architecture,Values=arm64" --region "$REGION" --profile "$PROFILE" --query "reverse(sort_by(Images, &CreationDate))[0].ImageId" --output text)
INSTANCE_ID=$(aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type "t4g.micro" \
--iam-instance-profile "Name=SSMFleetProfile" \
--security-group-ids "$ZERO_SG_ID" \
--subnet-id "$PUB_SUB_A" \
--user-data file://ssm-userdata.sh \
--metadata-options "HttpTokens=required" \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=Production-SSM-Node-01},{Key=Environment,Value=Production}]' \
--region "$REGION" \
--profile "$PROFILE" \
--query "Instances[0].InstanceId" \
--output text)
rm -f ssm-userdata.sh
echo "Waiting for instance $INSTANCE_ID to boot..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" --region "$REGION" --profile "$PROFILE"
echo "=== Base SSM Node Ready ==="
echo "INSTANCE_ID=$INSTANCE_ID"
echo "ZERO_SG_ID=$ZERO_SG_ID"
EOF
source setup-ssm-node.sh
Phase 1: Zero-Trust Interactive Terminal via SSM Session Manager
The Architecture: Inbound Listening vs Outbound Polling
Why does Systems Manager work with zero open inbound ports and zero SSH keys?
+-----------------------------------------------------------------------------------+
| Traditional SSH vs. AWS Session Manager |
+-----------------------------------------------------------------------------------+
1. TRADITIONAL SSH (Inbound Attack Surface):
[ Developer Laptop ] ===== Inbound Port 22 =====> [ EC2 Instance (sshd) ]
* Requires: Open Inbound Port 22 in Security Group.
* Requires: Public IPv4 Address on EC2.
* Requires: Private SSH Key (.pem) distributed to developer.
* Blocked by: Corporate firewalls & campus networks.
2. AWS SYSTEMS MANAGER (Zero-Trust Outbound HTTPS):
[ Developer Laptop ] [ EC2 Instance (ssm-agent) ]
| |
| (HTTPS 443) | (HTTPS 443)
v v
+------------------------------------------------------------------+
| AWS Systems Manager Control Plane |
+------------------------------------------------------------------+
* Requires: ZERO Inbound Ports (Port 22 is CLOSED).
* Requires: Outbound HTTPS (Port 443) to AWS SSM APIs.
* Authorization: Controlled by IAM Policies (e.g. `ssm:StartSession`).
* Works seamlessly behind corporate firewalls and corporate VPNs!
Step 1: Verify SSM Agent Registration
Before starting a session, ensure the instance’s SSM agent has successfully registered with the Systems Manager control plane. Poll until PingStatus is Online (usually takes 30–45 seconds after initial boot):
aws ssm describe-instance-information \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "InstanceInformationList[*].[InstanceId, PingStatus, PlatformName, AgentVersion]" \
--output table
Expected Output:
-------------------------------------------------------------------------
| DescribeInstanceInformation |
+----------------------+---------+----------------------+---------------+
| i-0123456789abcdef0 | Online | Amazon Linux 2023 | 3.3.1234.0 |
+----------------------+---------+----------------------+---------------+
Step 2: Start an Interactive Terminal Session
Start an interactive zero-trust shell session:
aws ssm start-session \
--target "$INSTANCE_ID" \
--region ap-southeast-1 \
--profile cloud-dev-admin
You will immediately drop into a live terminal prompt:
Starting session with SessionId: cloud-dev-admin-0123456789abcdef0
sh-5.2$
Step 3: Explore the Session as ssm-user
Inside the interactive SSM terminal session, run these commands individually:
1. Check the current logged-in user:
whoami
2. Elevate to root (the default ssm-user has passwordless sudo):
sudo -i
3. Check listening network ports:
ss -tulpn
(Notice that while the Linux OS runs sshd by default, the AWS Security Group blocks 100% of inbound Port 22 traffic at the cloud hypervisor. Because we use SSM, you can even completely disable SSH with systemctl disable --now sshd without losing terminal access!)
4. Exit the root shell and close the SSM session:
exit
(Run exit once more to return to your local workstation prompt).
🔍 How to Validate Zero-Inbound Security Group:
Verify on your local workstation that the instance’s Security Group has zero inbound rules:
aws ec2 describe-security-groups \
--group-ids "$ZERO_SG_ID" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "SecurityGroups[0].IpPermissions" \
--output json
Expected Output:
[]
(An empty array [] proves that 0 inbound rules exist in the firewall. If using --output table, AWS CLI returns an empty blank line).
Phase 2: Encrypted Keystroke Logging & Auditing with CloudWatch
The Architecture: SOC2 / ISO27001 Compliance Auditing
In enterprise environments, security teams must be able to answer: “Who logged into the server, and what exact commands did they run?”
With SSM Session Manager, you can stream every keystroke, command, and terminal output in real time into an encrypted Amazon CloudWatch Log Group.
+-----------------------------------------------------------------------------------+
| Real-Time Keystroke Audit Pipeline |
+-----------------------------------------------------------------------------------+
[ Developer ] ===> Runs: `sudo cat /etc/shadow`
|
v (Encrypted WebSockets)
[ SSM Control Plane ]
|
v (Real-time Stream)
[ CloudWatch Log Group: /aws/ssm/audit-logs ] ===> SOC2 / ISO27001 Compliance Audit
Step 1: Create a Dedicated CloudWatch Log Group
aws logs create-log-group \
--log-group-name "/aws/ssm/fleet-audit-logs" \
--region ap-southeast-1 \
--profile cloud-dev-admin
Step 2: Configure SSM Session Manager Logging Preferences & Permissions
Grant the EC2 instance role permission to write audit streams to CloudWatch Logs, and configure the regional SSM-SessionManagerRunShell preferences:
aws iam attach-role-policy \
--role-name "SSMFleetRole" \
--policy-arn "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" \
--region ap-southeast-1 \
--profile cloud-dev-admin
aws ssm create-document \
--name "SSM-SessionManagerRunShell" \
--document-type "Session" \
--content '{
"schemaVersion": "1.0",
"description": "Session Manager Preferences",
"sessionType": "Standard_Stream",
"inputs": {
"cloudWatchLogGroupName": "/aws/ssm/fleet-audit-logs",
"cloudWatchEncryptionEnabled": false,
"cloudWatchStreamingEnabled": true
}
}' \
--region ap-southeast-1 \
--profile cloud-dev-admin 2>/dev/null || aws ssm update-document \
--name "SSM-SessionManagerRunShell" \
--content '{
"schemaVersion": "1.0",
"description": "Session Manager Preferences",
"sessionType": "Standard_Stream",
"inputs": {
"cloudWatchLogGroupName": "/aws/ssm/fleet-audit-logs",
"cloudWatchEncryptionEnabled": false,
"cloudWatchStreamingEnabled": true
}
}' \
--document-version "$LATEST" \
--region ap-southeast-1 \
--profile cloud-dev-admin
🔍 How to Validate Audit Logs via CLI:
Start a quick 10-second session (aws ssm start-session --target "$INSTANCE_ID"), run a command like whoami, exit, and query the CloudWatch logs:
1. Fetch the latest active log stream:
LOG_STREAM=$(aws logs describe-log-streams \
--log-group-name "/aws/ssm/fleet-audit-logs" \
--order-by LastEventTime \
--descending \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "logStreams[0].logStreamName" \
--output text)
2. View the recorded session keystrokes and output:
aws logs get-log-events \
--log-group-name "/aws/ssm/fleet-audit-logs" \
--log-stream-name "$LOG_STREAM" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "events[*].message" \
--output text
Phase 3: Secure Port Forwarding (Tunneling Without VPNs or Bastions)
The Architecture: Tunneling Internal Ports over HTTPS
Imagine your database or internal admin panel is running on localhost:80 inside a private EC2 instance.
Without opening Port 80 to the Internet or connecting to an expensive VPN, SSM Port Forwarding tunnels traffic between a port on your local laptop (e.g. localhost:8080) and the private instance port (80) through an encrypted WebSocket!
+-----------------------------------------------------------------------------------+
| SSM Secure Port Forwarding Tunnel |
+-----------------------------------------------------------------------------------+
[ Local Workstation ] [ Private EC2 Instance ]
`curl localhost:8080` Internal Nginx (Port 80)
| ^
| (Local Port 8080) | (Remote Port 80)
v |
[ SSM Plugin ] <==== Encrypted HTTPS Tunnel ====> [ SSM Agent ]
Step 1: Start the Port Forwarding Session
Run the following command on your local workstation:
aws ssm start-session \
--target "$INSTANCE_ID" \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["80"],"localPortNumber":["8080"]}' \
--region ap-southeast-1 \
--profile cloud-dev-admin
Expected Output:
Starting session with SessionId: cloud-dev-admin-0987654321
Port 8080 opened for cpu target i-0123456789abcdef0.
Waiting for connections...
Step 2: Test Access via Local Browser or curl
Open a second terminal window on your local machine and run:
curl -i http://localhost:8080
Expected Output:
HTTP/1.1 200 OK
Server: nginx
Content-Type: text/html
<h1>🔒 Private Internal Server (SSM Managed)</h1>
You accessed a private internal web application on localhost:8080 with zero open firewall ports!
(Press Ctrl + C in the first terminal to close the port-forwarding tunnel).
Phase 4: Remote Fleet Automation with SSM Run Command
The Architecture: Synchronous & Asynchronous Fleet Automation
What if you need to check disk space, restart Nginx, or apply an emergency security patch across 50 production servers?
- The Old Way: Writing fragile bash
forloops to SSH into each server sequentially. - The SSM Way: Using SSM Run Command (
AWS-RunShellScript) to execute scripts concurrently across tagged server fleets with full status tracking, retry logic, and centralized stdout capture.
+-----------------------------------------------------------------------------------+
| SSM Run Command Architecture |
+-----------------------------------------------------------------------------------+
[ aws ssm send-command ]
|
+---------------------+---------------------+
| |
v v
[ Production Node 1 ] [ Production Node 2 ]
`systemctl restart nginx` `systemctl restart nginx`
| |
+---------------------+---------------------+
|
v
[ Centralized Output Table ]
Step 1: Send a Remote Shell Script across Tagged Fleet
Execute commands across all instances tagged with Environment=Production:
COMMAND_ID=$(aws ssm send-command \
--document-name "AWS-RunShellScript" \
--targets "Key=tag:Environment,Values=Production" \
--parameters 'commands=[
"echo \"=== System Uptime ===\"",
"uptime",
"echo \"=== Disk Utilization ===\"",
"df -h /",
"echo \"=== Nginx Service Status ===\"",
"systemctl is-active nginx"
]' \
--comment "Production Fleet Health Audit" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "Command.CommandId" \
--output text)
Step 2: Query Command Execution Status & Stdout
Wait 3 seconds and retrieve the aggregated stdout output directly from the terminal:
1. Check invocation status across the fleet:
aws ssm list-command-invocations \
--command-id "$COMMAND_ID" \
--details \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "CommandInvocations[*].[InstanceId, Status, DocumentName]" \
--output table
2. View the exact stdout output from the instance:
aws ssm get-command-invocation \
--command-id "$COMMAND_ID" \
--instance-id "$INSTANCE_ID" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "[Status, StandardOutputContent]" \
--output text
Expected Output:
Success
=== System Uptime ===
08:30:15 up 12 min, 0 users, load average: 0.00, 0.01, 0.04
=== Disk Utilization ===
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p1 8.0G 1.8G 6.3G 22% /
=== Nginx Service Status ===
active
Phase 5: Fleet Patch Management & Compliance Auditing
The Architecture: Automated CVE Scanning with AWS Patch Manager
Keeping Linux fleets patched against zero-day vulnerabilities is a core security requirement.
With AWS Systems Manager Patch Manager, you can execute automated patch baseline scans across your fleet using the pre-built AWS-RunPatchBaseline document:
PATCH_COMMAND_ID=$(aws ssm send-command \
--document-name "AWS-RunPatchBaseline" \
--targets "Key=tag:Environment,Values=Production" \
--parameters 'Operation=Scan' \
--comment "Fleet Vulnerability & CVE Scan" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "Command.CommandId" \
--output text)
Step 2: Check Scan Completion Status
The patch baseline scan evaluates all installed RPM packages against the security baseline, which takes 15–20 seconds. Verify that the scan status transitions to Success:
aws ssm list-command-invocations \
--command-id "$PATCH_COMMAND_ID" \
--details \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "CommandInvocations[*].[InstanceId, Status]" \
--output table
🔍 How to Validate Fleet Patch Compliance:
Once the status reports Success, inspect the compliance state and installed vs. missing security updates:
aws ssm describe-instance-patch-states \
--instance-ids "$INSTANCE_ID" \
--region ap-southeast-1 \
--profile cloud-dev-admin \
--query "InstancePatchStates[*].[InstanceId, Operation, InstalledCount, MissingCount, FailedCount]" \
--output table
Expected Output:
------------------------------------------------------------------
| DescribeInstancePatchStates |
+----------------------+-------+-----------+---------------+-----+
| i-0123456789abcdef0 | Scan | 482 | 0 | 0 |
+----------------------+-------+-----------+---------------+-----+
Phase 6: Automated Teardown & Resource Cleanup
Clean up all compute instances, IAM roles, and network resources in the strict AWS dependency order:
PROFILE="cloud-dev-admin"
REGION="ap-southeast-1"
echo "=== 1. Terminating EC2 Instance ==="
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" --region "$REGION" --profile "$PROFILE"
aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" --region "$REGION" --profile "$PROFILE"
echo "=== 2. Cleaning up IAM Roles & Instance Profile ==="
aws iam remove-role-from-instance-profile --instance-profile-name "SSMFleetProfile" --role-name "SSMFleetRole" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
aws iam delete-instance-profile --instance-profile-name "SSMFleetProfile" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
aws iam detach-role-policy --role-name "SSMFleetRole" --policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
aws iam delete-role --role-name "SSMFleetRole" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
echo "=== 3. Cleaning up Security Groups ==="
aws ec2 delete-security-group --group-id "$ZERO_SG_ID" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
echo "=== 4. Cleaning up Public VPC, Subnets & IGW ==="
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Production-VPC" --query "Vpcs[0].VpcId" --output text --region "$REGION" --profile "$PROFILE" 2>/dev/null || true)
if [ -n "$VPC_ID" ] && [ "$VPC_ID" != "None" ]; then
# Delete Subnets first
SUB_IDS=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query "Subnets[*].SubnetId" --output text --region "$REGION" --profile "$PROFILE")
for SUB in $SUB_IDS; do
echo "Deleting Subnet: $SUB..."
aws ec2 delete-subnet --subnet-id "$SUB" --region "$REGION" --profile "$PROFILE"
done
# Delete Custom Route Tables
RTB_IDS=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Public-Route-Table" --query "RouteTables[*].RouteTableId" --output text --region "$REGION" --profile "$PROFILE")
for RTB in $RTB_IDS; do
echo "Deleting Route Table: $RTB..."
aws ec2 delete-route-table --route-table-id "$RTB" --region "$REGION" --profile "$PROFILE"
done
# Detach & Delete IGW
IGW_ID=$(aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" --query "InternetGateways[0].InternetGatewayId" --output text --region "$REGION" --profile "$PROFILE")
if [ -n "$IGW_ID" ] && [ "$IGW_ID" != "None" ]; then
aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
fi
# Delete VPC
aws ec2 delete-vpc --vpc-id "$VPC_ID" --region "$REGION" --profile "$PROFILE"
fi
echo "=== 5. Cleaning up CloudWatch Logs & Local Files ==="
aws logs delete-log-group --log-group-name "/aws/ssm/fleet-audit-logs" --region "$REGION" --profile "$PROFILE" 2>/dev/null || true
rm -f setup-ssm-node.sh
echo "=== Part 9 Teardown Complete! Zero Resources Remaining. ==="
Quick Reference: Essential AWS Systems Manager CLI Commands
| Action | CLI Command |
|---|---|
| Start Interactive Session | aws ssm start-session --target <instance-id> |
| Start Port Forwarding Tunnel | aws ssm start-session --target <id> --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["80"],"localPortNumber":["8080"]}' |
| Send Remote Shell Script | aws ssm send-command --document-name "AWS-RunShellScript" --targets "Key=tag:<key>,Values=<val>" --parameters 'commands=["<cmd>"]' |
| Query Run Command Output | aws ssm get-command-invocation --command-id <id> --instance-id <inst> |
| Scan Fleet Patch Baseline | aws ssm send-command --document-name "AWS-RunPatchBaseline" --targets "<targets>" --parameters 'Operation=Scan' |
| Describe Instance Patch States | aws ssm describe-instance-patch-states --instance-ids <id> |
Summary & What’s Next
In this ninth installment of AWS with CLI, we eliminated SSH and established zero-trust fleet management:
- We locked down our compute firewall to 0 open inbound rules and connected via outbound HTTPS (Port 443).
- We streamed interactive sessions to CloudWatch Logs for immutable compliance auditing.
- We established secure port forwarding tunnels to access private web services without bastions or VPNs.
- We executed synchronous remote commands across tagged fleets using SSM Run Command.
- We automated fleet-wide vulnerability scans using SSM Patch Manager.
Now that our compute fleet can be operated securely with zero inbound access, how do we distribute incoming web traffic across multiple availability zones with high availability and SSL offloading?
In Part 10, we explore Layer 7 Traffic Distribution: Application Load Balancers, Target Groups & SSL Routing, building multi-AZ load balancers, health check automation, and path-based routing entirely from the terminal!