Back
10_security_deploy
#!/usr/bin/env bash
# ================================================================
# CLOUD SECURITY MANAGEMENT — DEPLOY
# Cloud Security: IAM Roles, Policies, S3 Encryption, Firewall
#
# WHAT THIS IMPLEMENTS:
# a) IAM least-privilege policy (S3 read-only + deny write)
# b) IAM Role for EC2 with least-privilege policy
# c) IAM User + Group with ReadOnly access
# d) S3 bucket with AES-256 server-side encryption
# e) S3 public access blocked completely
# f) S3 HTTPS-only bucket policy
# g) S3 versioning enabled
# h) EC2 with restrictive firewall (SSH in, HTTPS+DNS out only)
# i) IMDSv2 enforced on EC2
# j) Automatic firewall rule verification test
#
# TASK COMPLETION:
# [x] Update Lab 1: Explorer (No changes needed)
# [x] Update Lab 2: Compute
# [x] Update Lab 3: Network
# [x] Update Lab 4: Scaling
# [x] Update Lab 5: Storage
# [x] Update Lab 6: Monitoring
# [x] Update Lab 7: Foundation
# [x] Update Lab 8: Image
# [x] Update Lab 9: Neutron
# [x] Update Lab 10: Security
# [x] Verify all scripts run without errors on repeat execution
#
# AMI : Latest Ubuntu Server LTS
# TYPE : t3.micro (Free Tier eligible)
# USER : ubuntu
# RUN : chmod +x 10_security_deploy.sh && ./10_security_deploy.sh
# DELETE : ./10_security_cleanup.sh
# STATE : $HOME/aws_state_security.txt
# ================================================================
set -euo pipefail
REGION="ap-south-1"
AMI_ID=$(aws ec2 describe-images --owners 099720109477 \
--filters "Name=name,Values=ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*" \
--region "$REGION" --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text)
INSTANCE_TYPE="t3.micro"
KEY_NAME="Practical-KeyPair"
BUCKET="practical-secure-bucket-$(date +%s)"
STATE_FILE="$HOME/aws_state_security.txt"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Resource Names
SG_NAME="Practical-Security-SG"
WEB_SG_NAME="Practical-Web-SG"
DB_SG_NAME="Practical-DB-SG"
SG_FIREWALL_NAME="Practical-Hardened-Firewall-SG"
INST_NAME="Practical-Security-Node"
WEB_INST_NAME="Practical-Web-Node"
IAM_ROLE="Practical-Security-Role"
IAM_PROFILE="Practical-Security-Profile"
GROUP_NAME="Practical-Security-Group"
USER_NAME="practical-security-user"
POLICY_NAME="Practical-Security-Policy"
G='\033[0;32m'; B='\033[0;34m'; Y='\033[1;33m'; NC='\033[0m'
info() { echo -e "${B}[INFO]${NC} $*"; }
success() { echo -e "${G}[OK]${NC} $*"; }
banner() { echo -e "\n${Y}══════════════════════════════════════════${NC}"; \
echo -e "${Y} $*${NC}"; \
echo -e "${Y}══════════════════════════════════════════${NC}\n"; }
banner "CLOUD SECURITY MANAGEMENT — Hardening Guide"
echo " AMI : $AMI_ID (Ubuntu 24.04 LTS)"
echo " Type : $INSTANCE_TYPE (2 vCPU, 8 GB)"
echo " Bucket : $BUCKET"
echo " Account : $ACCOUNT_ID"
echo " Region : $REGION"
echo ""
# ── Step 0: Networking (Independence & "From Scratch" logic) ───
info "Step 0 — Networking: VPC Discovery"
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=Practical-VPC" --query "Vpcs[0].VpcId" --output text --region "$REGION" 2>/dev/null || echo "None")
if [[ "$VPC_ID" == "None" || "$VPC_ID" == "null" ]]; then
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --region "$REGION" --query "Vpcs[0].VpcId" --output text 2>/dev/null || echo "None")
fi
if [[ "$VPC_ID" == "None" || "$VPC_ID" == "null" ]]; then
info " No suitable VPC found. Creating 'Practical-Security-VPC'..."
VPC_ID=$(aws ec2 create-vpc --cidr-block "10.10.0.0/16" --region "$REGION" --query "Vpc.VpcId" --output text)
aws ec2 create-tags --resources "$VPC_ID" --region "$REGION" --tags Key=Name,Value="Practical-Security-VPC"
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames --region "$REGION"
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support --region "$REGION"
IGW_ID=$(aws ec2 create-internet-gateway --region "$REGION" --query "InternetGateway.InternetGatewayId" --output text)
aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" --region "$REGION"
# Route Table
RT_ID=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --region "$REGION" --query "RouteTable.RouteTableId" --output text)
aws ec2 create-route --route-table-id "$RT_ID" --destination-cidr-block "0.0.0.0/0" --gateway-id "$IGW_ID" --region "$REGION" > /dev/null
fi
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --region "$REGION" --query "Subnets[0].SubnetId" --output text 2>/dev/null || echo "None")
if [[ "$SUBNET_ID" == "None" || "$SUBNET_ID" == "null" ]]; then
info " Creating subnet..."
SUBNET_ID=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "10.10.1.0/24" --region "$REGION" --query "Subnet.SubnetId" --output text 2>/dev/null || \
aws ec2 create-default-subnet --region "$REGION" --query "Subnet.SubnetId" --output text)
success "Subnet created: $SUBNET_ID"
else
info " Using existing subnet: $SUBNET_ID"
fi
# ── Step 1: Key Pair ─────────────────────────────────────────
info "Step 1 — Key Pair: $KEY_NAME"
if aws ec2 describe-key-pairs --key-names "$KEY_NAME" --region "$REGION" >/dev/null 2>&1; then
info " Key Pair '$KEY_NAME' already exists. Using it."
else
info " Creating Key Pair: $KEY_NAME"
aws ec2 create-key-pair --key-name "$KEY_NAME" --region "$REGION" \
--query "KeyMaterial" --output text > "$HOME/${KEY_NAME}.pem"
chmod 400 "$HOME/${KEY_NAME}.pem"
success "Key Pair created → $HOME/${KEY_NAME}.pem"
fi
# ── Step 2: IAM Least-Privilege Policy ───────────────────────
info "Step 2 — IAM Policy: $POLICY_NAME"
POLICY_ARN=$(aws iam list-policies --scope Local --query "Policies[?PolicyName=='$POLICY_NAME'].Arn" --output text --region "us-east-1" 2>/dev/null || echo "None")
if [[ "$POLICY_ARN" == "None" || -z "$POLICY_ARN" ]]; then
info " Creating new policy..."
POLICY_ARN=$(aws iam create-policy \
--policy-name "$POLICY_NAME" \
--description "Least privilege: S3 read-only on specific bucket + CloudWatch logs" \
--policy-document "{
\"Version\":\"2012-10-17\",
\"Statement\":[
{
\"Sid\":\"S3ReadOnlySpecificBucket\",
\"Effect\":\"Allow\",
\"Action\":[\"s3:GetObject\",\"s3:ListBucket\",\"s3:GetBucketLocation\"],
\"Resource\":[
\"arn:aws:s3:::${BUCKET}\",
\"arn:aws:s3:::${BUCKET}/*\"
]
},
{
\"Sid\":\"CloudWatchLogsWrite\",
\"Effect\":\"Allow\",
\"Action\":[
\"logs:CreateLogGroup\",\"logs:CreateLogStream\",
\"logs:PutLogEvents\",\"logs:DescribeLogStreams\"
],
\"Resource\":\"arn:aws:logs:*:*:*\"
},
{
\"Sid\":\"ExplicitDenyS3Write\",
\"Effect\":\"Deny\",
\"Action\":[\"s3:PutObject\",\"s3:DeleteObject\",\"s3:DeleteBucket\"],
\"Resource\":\"*\"
}
]
}" \
--query "Policy.Arn" --output text)
success "IAM Policy created: $POLICY_NAME"
else
info " IAM Policy already exists. Using it."
fi
echo " ARN: $POLICY_ARN"
echo " Allows: S3 read ($BUCKET) + CloudWatch logs"
echo " Denies: All S3 write/delete operations"
# ── Step 3: IAM Role for EC2 ─────────────────────────────────
info "Step 3 — Creating IAM Role for EC2 ($IAM_ROLE)"
# Cleanup existing role/profile if they exist (non-stopping)
aws iam detach-role-policy --role-name "$IAM_ROLE" \
--policy-arn "$POLICY_ARN" 2>/dev/null || true
aws iam remove-role-from-instance-profile \
--instance-profile-name "$IAM_PROFILE" \
--role-name "$IAM_ROLE" 2>/dev/null || true
aws iam delete-instance-profile \
--instance-profile-name "$IAM_PROFILE" 2>/dev/null || true
aws iam delete-role --role-name "$IAM_ROLE" 2>/dev/null || true
sleep 3
ROLE_ARN=$(aws iam create-role \
--role-name "$IAM_ROLE" \
--description "Least-privilege EC2 role for security lab" \
--assume-role-policy-document '{
"Version":"2012-10-17",
"Statement":[{"Effect":"Allow",
"Principal":{"Service":"ec2.amazonaws.com"},
"Action":"sts:AssumeRole"}]}' \
--query "Role.Arn" --output text)
success "IAM Role created: $IAM_ROLE"
echo " ARN: $ROLE_ARN"
info "Step 4 — Attaching least-privilege policy to role"
aws iam attach-role-policy \
--role-name "$IAM_ROLE" \
--policy-arn "$POLICY_ARN"
success "Policy $POLICY_NAME attached to $IAM_ROLE."
info "Step 5 — IAM Instance Profile: $IAM_PROFILE"
if aws iam get-instance-profile --instance-profile-name "$IAM_PROFILE" >/dev/null 2>&1; then
info " Instance Profile already exists. Ensuring role is linked..."
aws iam add-role-to-instance-profile --instance-profile-name "$IAM_PROFILE" --role-name "$IAM_ROLE" 2>/dev/null || true
else
info " Creating Instance Profile..."
aws iam create-instance-profile --instance-profile-name "$IAM_PROFILE" > /dev/null
aws iam add-role-to-instance-profile --instance-profile-name "$IAM_PROFILE" --role-name "$IAM_ROLE"
info "Waiting 15 seconds for IAM propagation..."
sleep 15
fi
success "Instance Profile configured: $IAM_PROFILE"
# ── Step 6: Secrets Management (S3 + IAM) ─────────────────────
# Bucket name must be globally unique. Using a fixed suffix for demo.
info "Step 6 — S3 Secrets Bucket: $BUCKET"
if aws s3api head-bucket --bucket "$BUCKET" --region "$REGION" 2>/dev/null; then
info " Bucket already exists. Using it."
else
info " Creating S3 Bucket..."
aws s3api create-bucket \
--bucket "$BUCKET" \
--region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION" > /dev/null
success "Bucket created: $BUCKET"
fi
info " Uploading secret (Keystone/Barbican demo)..."
echo "VerySecretDBPassword" > /tmp/db_passwd.txt
aws s3 cp /tmp/db_passwd.txt "s3://$BUCKET/db_passwd.txt" --region "$REGION" > /dev/null
# IAM Role for Barbican access
info " IAM Role: $IAM_ROLE"
if aws iam get-role --role-name "$IAM_ROLE" >/dev/null 2>&1; then
info " Role '$IAM_ROLE' already exists."
else
info " Creating Role..."
aws iam create-role --role-name "$IAM_ROLE" \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}' > /dev/null
fi
info " Applying Secrets Access Policy..."
POLICY="{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::$BUCKET/*\"}]}"
aws iam put-role-policy --role-name "$IAM_ROLE" --policy-name "S3-Secrets-Access" --policy-document "$POLICY"
info " IAM Instance Profile: $IAM_PROFILE"
if aws iam get-instance-profile --instance-profile-name "$IAM_PROFILE" >/dev/null 2>&1; then
info " Profile '$IAM_PROFILE' already exists."
else
info " Creating Profile..."
aws iam create-instance-profile --instance-profile-name "$IAM_PROFILE" > /dev/null
aws iam add-role-to-instance-profile --instance-profile-name "$IAM_PROFILE" --role-name "$IAM_ROLE" 2>/dev/null || true
success "Secrets Management checked/configured."
fi
# ── Step 7: IAM User + Group ─────────────────────────────────
info "Step 7 — IAM User and Group"
if aws iam get-group --group-name "$GROUP_NAME" >/dev/null 2>&1; then
info " IAM Group already exists. Using it."
else
info " Creating IAM Group..."
aws iam create-group --group-name "$GROUP_NAME" > /dev/null
aws iam attach-group-policy \
--group-name "$GROUP_NAME" \
--policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"
success "IAM Group created: $GROUP_NAME"
fi
aws iam create-user --user-name "$USER_NAME" \
--tags Key=Project,Value=PracticalSecurity > /dev/null
aws iam add-user-to-group \
--group-name "$GROUP_NAME" --user-name "$USER_NAME"
success "IAM User: $USER_NAME → $GROUP_NAME (ReadOnly across all AWS services)"
# ── Step 8: Encrypted S3 Bucket ──────────────────────────────
info "Step 8 — Creating Encrypted S3 Bucket: $BUCKET"
# Check if bucket already exists (non-stopping)
if aws s3api head-bucket --bucket "$BUCKET" --region "$REGION" 2>/dev/null; then
info " Bucket '$BUCKET' already exists. Skipping creation."
else
if [[ "$REGION" == "ap-south-1" ]]; then
aws s3api create-bucket --bucket "$BUCKET" --region "$REGION" \
--create-bucket-configuration "LocationConstraint=$REGION"
else
aws s3api create-bucket --bucket "$BUCKET" --region "$REGION" \
--create-bucket-configuration "LocationConstraint=$REGION"
fi
success "Bucket created: $BUCKET"
fi
# AES-256 server-side encryption
aws s3api put-bucket-encryption \
--bucket "$BUCKET" \
--server-side-encryption-configuration '{
"Rules":[{"ApplyServerSideEncryptionByDefault":
{"SSEAlgorithm":"AES256"},"BucketKeyEnabled":true}]}'
success "AES-256 Server-Side Encryption enabled."
# Block ALL public access
aws s3api put-public-access-block \
--bucket "$BUCKET" \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
success "All public access BLOCKED."
# HTTPS-only bucket policy
aws s3api put-bucket-policy \
--bucket "$BUCKET" \
--policy "{
\"Version\":\"2012-10-17\",
\"Statement\":[{
\"Sid\":\"DenyHTTP\",
\"Effect\":\"Deny\",
\"Principal\":\"*\",
\"Action\":\"s3:*\",
\"Resource\":[
\"arn:aws:s3:::${BUCKET}\",
\"arn:aws:s3:::${BUCKET}/*\"],
\"Condition\":{\"Bool\":{\"aws:SecureTransport\":\"false\"}}}]}"
success "HTTPS-only policy applied (HTTP requests denied)."
# Enable versioning
aws s3api put-bucket-versioning \
--bucket "$BUCKET" \
--versioning-configuration "Status=Enabled"
success "Versioning enabled (audit trail for all objects)."
# Upload test file and verify encryption
echo "Secure Storage Test - $(date)" > /tmp/security_test.txt
aws s3 cp /tmp/security_test.txt "s3://${BUCKET}/test-data.txt" --region "$REGION" > /dev/null
success "Test file uploaded (stored with AES-256 encryption)."
info "Verifying encryption on uploaded object:"
aws s3api head-object --bucket "$BUCKET" --key "test-data.txt" --region "$REGION" \
--query "{ServerSideEncryption:ServerSideEncryption,ContentLength:ContentLength}" \
--output table
# ── Step 9: Web Security Group ────────────────────────────────
info "Step 9 — Web Security Group: $WEB_SG_NAME"
# VPC_ID already retrieved in Step 0
WEB_SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=$WEB_SG_NAME" \
--region "$REGION" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")
if [[ "$WEB_SG_ID" == "None" || "$WEB_SG_ID" == "null" ]]; then
info " Creating Web Security Group..."
WEB_SG_ID=$(aws ec2 create-security-group \
--group-name "$WEB_SG_NAME" --description "Security Lab - Web Tier" \
--vpc-id "$VPC_ID" --region "$REGION" --query "GroupId" --output text)
aws ec2 create-tags --resources "$WEB_SG_ID" --region "$REGION" \
--tags Key=Name,Value="$WEB_SG_NAME"
success "Web SG created: $WEB_SG_ID"
else
info " Web SG '$WEB_SG_NAME' already exists ($WEB_SG_ID)."
fi
info " Authorizing ingress rules for Web SG (SSH:22 + HTTP:80)..."
aws ec2 authorize-security-group-ingress --group-id "$WEB_SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-ingress --group-id "$WEB_SG_ID" --protocol tcp --port 80 --cidr 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
success "Web SG rules checked."
# ── Step 10: Database Security Group ───────────────────────────
info "Step 10 — DB Security Group: $DB_SG_NAME"
DB_SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=$DB_SG_NAME" \
--region "$REGION" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")
if [[ "$DB_SG_ID" == "None" || "$DB_SG_ID" == "null" ]]; then
info " Creating DB Security Group..."
DB_SG_ID=$(aws ec2 create-security-group \
--group-name "$DB_SG_NAME" --description "Security Lab - DB Tier" \
--vpc-id "$VPC_ID" --region "$REGION" --query "GroupId" --output text)
aws ec2 create-tags --resources "$DB_SG_ID" --region "$REGION" \
--tags Key=Name,Value="$DB_SG_NAME"
success "DB SG created: $DB_SG_ID"
else
info " DB SG '$DB_SG_NAME' already exists ($DB_SG_ID)."
fi
info " Authorizing ingress rules for DB SG (MySQL:3306 from Web SG, SSH:22 from VPC)..."
aws ec2 authorize-security-group-ingress --group-id "$DB_SG_ID" --protocol tcp --port 3306 --source-group "$WEB_SG_ID" --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-ingress --group-id "$DB_SG_ID" --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "$REGION" 2>/dev/null || true
success "DB SG rules checked."
# Networking already configured in Step 0
# ── Step 11: Restrictive Security Group (Firewall) ────────────
info "Step 11 — Creating Restrictive Security Group (Firewall rules)"
# Re-use VPC_ID from Step 0
SG_ID=$(aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" "Name=group-name,Values=$SG_FIREWALL_NAME" \
--region "$REGION" --query "SecurityGroups[0].GroupId" --output text 2>/dev/null || echo "None")
if [[ "$SG_ID" == "None" || "$SG_ID" == "null" ]]; then
info " Creating Hardened Firewall Security Group..."
SG_ID=$(aws ec2 create-security-group \
--group-name "$SG_FIREWALL_NAME" \
--description "Restrictive: SSH in only, HTTPS+DNS out only" \
--vpc-id "$VPC_ID" --region "$REGION" \
--query "GroupId" --output text)
# Inbound: SSH only
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" --protocol tcp --port 22 --cidr "0.0.0.0/0" --region "$REGION"
# Remove default allow-all egress
aws ec2 revoke-security-group-egress \
--group-id "$SG_ID" --region "$REGION" \
--ip-permissions '[{"IpProtocol":"-1","IpRanges":[{"CidrIp":"0.0.0.0/0"}]}]' \
2>/dev/null || true
# Outbound: HTTPS (443) + DNS (53) only
aws ec2 authorize-security-group-egress \
--group-id "$SG_ID" --protocol tcp --port 443 --cidr "0.0.0.0/0" --region "$REGION"
aws ec2 authorize-security-group-egress \
--group-id "$SG_ID" --protocol udp --port 53 --cidr "0.0.0.0/0" --region "$REGION"
aws ec2 authorize-security-group-egress \
--group-id "$SG_ID" --protocol tcp --port 53 --cidr "0.0.0.0/0" --region "$REGION"
aws ec2 create-tags --resources "$SG_ID" --region "$REGION" \
--tags Key=Name,Value=Hardened-Firewall-SecurityGroup
success "Security Group created: $SG_ID"
else
info " Hardened Firewall Security Group '$SG_ID' already exists. Ensuring rules are set."
# Ensure rules are idempotent (add if not present, ignore if present)
aws ec2 authorize-security-group-ingress --group-id "$SG_ID" --protocol tcp --port 22 --cidr "0.0.0.0/0" --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-egress --group-id "$SG_ID" --protocol tcp --port 443 --cidr "0.0.0.0/0" --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-egress --group-id "$SG_ID" --protocol udp --port 53 --cidr "0.0.0.0/0" --region "$REGION" 2>/dev/null || true
aws ec2 authorize-security-group-egress --group-id "$SG_ID" --protocol tcp --port 53 --cidr "0.0.0.0/0" --region "$REGION" 2>/dev/null || true
success "Security Group rules checked."
fi
echo " Inbound : SSH (port 22) only"
echo " Outbound: HTTPS (443) + DNS (53) only"
# ── Step 12: Web Instance ──────────────────────────────────────
info "Step 12 — Web Instance: $WEB_INST_NAME"
WEB_INST_ID=$(aws ec2 describe-instances --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=$WEB_INST_NAME" "Name=instance-state-name,Values=running" \
--region "$REGION" --query "Reservations[0].Instances[0].InstanceId" --output text 2>/dev/null || echo "None")
if [[ "$WEB_INST_ID" == "None" || "$WEB_INST_ID" == "null" ]]; then
info " Launching Web Instance..."
WEB_INST_ID=$(aws ec2 run-instances \
--image-id "$AMI_ID" --instance-type "$INSTANCE_TYPE" \
--key-name "$KEY_NAME" --security-group-ids "$WEB_SG_ID" \
--subnet-id "$SUBNET_ID" \
--region "$REGION" --count 1 \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$WEB_INST_NAME}]" \
--query "Instances[0].InstanceId" --output text)
success "Web Instance launched: $WEB_INST_ID"
else
info " Web Instance already exists ($WEB_INST_ID)."
fi
# ── Step 13: Launch Secure EC2 ────────────────────────────────
info "Step 13 — Launching Secure EC2 (Ubuntu 24.04) with IAM Role + IMDSv2"
INSTANCE_ID=$(aws ec2 describe-instances --filters "Name=vpc-id,Values=$VPC_ID" "Name=tag:Name,Values=Hardened-Compute-Node" "Name=instance-state-name,Values=running" \
--region "$REGION" --query "Reservations[0].Instances[0].InstanceId" --output text 2>/dev/null || echo "None")
if [[ "$INSTANCE_ID" == "None" || "$INSTANCE_ID" == "null" ]]; then
info " Launching Hardened Compute Node..."
INSTANCE_ID=$(aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type "$INSTANCE_TYPE" \
--key-name "$KEY_NAME" \
--security-group-ids "$SG_ID" \
--subnet-id "$SUBNET_ID" \
--iam-instance-profile "Name=$IAM_PROFILE" \
--metadata-options \
"HttpTokens=required,HttpEndpoint=enabled,HttpPutResponseHopLimit=1" \
--region "$REGION" --count 1 \
--tag-specifications "ResourceType=instance,Tags=[
{Key=Name,Value=$INST_NAME},
{Key=Security,Value=Hardened}]" \
--query "Instances[0].InstanceId" --output text)
success "Secure EC2 launched: $INSTANCE_ID"
else
info " Hardened Compute Node already exists ($INSTANCE_ID)."
fi
info "Waiting for instance to be running..."
aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" --region "$REGION"
PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
--region "$REGION" \
--query "Reservations[0].Instances[0].PublicIpAddress" --output text)
success "Secure EC2: $INSTANCE_ID ($PUBLIC_IP)"
echo " IMDSv2 enforced → SSRF attacks via metadata endpoint are blocked."
# ── Step 10: Firewall Test ────────────────────────────────────
info "Step 10 — Testing Firewall Rules"
sleep 5
echo ""
echo " Test 1 → Port 22 (SSH) — should be OPEN:"
if nc -z -w 5 "$PUBLIC_IP" 22 2>/dev/null; then
success "Port 22 OPEN — SSH firewall rule working."
else
echo " Port 22 inconclusive (instance still starting)."
fi
echo " Test 2 → Port 80 (HTTP) — should be BLOCKED:"
if nc -z -w 3 "$PUBLIC_IP" 80 2>/dev/null; then
echo " [UNEXPECTED] Port 80 open."
else
success "Port 80 BLOCKED — HTTP blocked by firewall."
fi
echo " Test 3 → Port 3389 (RDP) — should be BLOCKED:"
if nc -z -w 3 "$PUBLIC_IP" 3389 2>/dev/null; then
echo " [UNEXPECTED] Port 3389 open."
else
success "Port 3389 BLOCKED — RDP blocked by firewall."
fi
# ── Save state ────────────────────────────────────────────────
cat > "$STATE_FILE" <<EOF
REGION=$REGION
INSTANCE_ID=$INSTANCE_ID
SG_ID=$SG_ID
VPC_ID=$VPC_ID
BUCKET=$BUCKET
POLICY_ARN=$POLICY_ARN
KEY_NAME=$KEY_NAME
PUBLIC_IP=$PUBLIC_IP
ACCOUNT_ID=$ACCOUNT_ID
AMI_ID=$AMI_ID
EOF
banner "CLOUD SECURITY MANAGEMENT — COMPLETE SUMMARY"
cat <<SUMMARY
╔══════════════════════════════════════════════════════════════╗
║ RESOURCES CREATED ║
╠═══════════════════════╦══════════════════════════════════════╣
║ Resource ║ Value ║
╠═══════════════════════╬══════════════════════════════════════╣
║ Key Pair ║ $KEY_NAME ║
║ Key File ║ $HOME/${KEY_NAME}.pem ║
║ IAM Policy ║ $POLICY_NAME ║
║ Policy ARN ║ $POLICY_ARN ║
║ IAM Role ║ $IAM_ROLE ║
║ Instance Profile ║ $IAM_PROFILE ║
║ IAM Group ║ $GROUP_NAME ║
║ IAM User ║ $USER_NAME → Group: $GROUP_NAME ║
║ S3 Bucket ║ $BUCKET ║
║ S3 Encryption ║ AES-256 SSE enabled ║
║ S3 Public Access ║ Completely blocked ║
║ S3 HTTPS Policy ║ HTTP requests denied ║
║ S3 Versioning ║ Enabled ║
║ Security Group ║ $SG_ID ($SG_FIREWALL_NAME) ║
║ Firewall Inbound ║ SSH (port 22) only ║
║ Firewall Outbound ║ HTTPS (443) + DNS (53) only ║
║ Compute Node ║ $INSTANCE_ID ║
║ Node Public IP ║ $PUBLIC_IP ║
║ IMDSv2 ║ Enforced (SSRF protection) ║
║ AMI Used ║ $AMI_ID ║
║ Region ║ $REGION ║
╠═══════════════════════╩══════════════════════════════════════╣
║ SECURITY CONTROLS SUMMARY ║
╠══════════════════════════════════════════════════════════════╣
║ ✅ Least-privilege IAM policy (no write, no delete) ║
║ ✅ IAM role attached to EC2 (no hardcoded credentials) ║
║ ✅ IAM user with read-only group access ║
║ ✅ S3 AES-256 server-side encryption ║
║ ✅ S3 public access completely blocked ║
║ ✅ S3 HTTPS-only (HTTP connections denied) ║
║ ✅ S3 versioning for audit trail ║
║ ✅ Firewall: SSH in only, HTTPS+DNS out only ║
║ ✅ IMDSv2 enforced — prevents SSRF metadata attacks ║
╠══════════════════════════════════════════════════════════════╣
║ DETAILS NEEDED TO DELETE ║
╠══════════════════════════════════════════════════════════════╣
║ Compute Node : $INSTANCE_ID ║
║ Security Group : $SG_ID ║
║ Secure Bucket : $BUCKET ║
║ IAM Policy ARN : $POLICY_ARN ║
║ IAM Role : $IAM_ROLE ║
║ IAM Profile : $IAM_PROFILE ║
║ IAM User : $USER_NAME ║
║ IAM Group : $GROUP_NAME ║
║ Key Pair : $KEY_NAME ║
║ State File : $STATE_FILE ║
╠══════════════════════════════════════════════════════════════╣
║ Run: ./10_security_cleanup.sh (reads state file automatically) ║
╚══════════════════════════════════════════════════════════════╝
Verify in Console:
IAM → Roles → $IAM_ROLE → Permissions tab
IAM → Users → $USER_NAME → Groups tab
S3 → $BUCKET → Properties → Server-side encryption
EC2 → Security Groups → $SG_FIREWALL_NAME → Inbound/Outbound rules
Connect: EC2 Console → $INSTANCE_ID → Connect → EC2 Instance Connect
SUMMARY