Part 2 builds on Part 1 by implementing a production-grade security architecture. This project provisions:
- Private EC2 instance with NO public IP address
- VPC Endpoints for secure AWS service communication
- AWS Systems Manager (SSM) for browser-based shell access
- IAM roles for least-privilege access
- Zero exposed ports - no SSH keys required
| Feature | Part 1 (Public) | Part 2 (Private) |
|---|---|---|
| Subnet Type | Public | Private |
| Public IP | Yes | No |
| Internet Access | Via Internet Gateway | Via VPC Endpoints only |
| Access Method | SSH (port 22) | SSM Session Manager |
| Security Risk | Exposed to internet | Completely isolated |
| SSH Keys | Required | Not needed |
| Cost | ~$0 (free tier) | ~$22/month (VPC endpoints) |
AWS Console (Session Manager)
|
v
SSM Service
|
v
VPC Endpoints (Interface)
(ssm, ssmmessages, ec2messages)
|
v
VPC (10.0.0.0/16)
|
v
Private Subnet (10.0.1.0/24)
[No Internet Gateway]
[No NAT Gateway]
|
v
Security Group (HTTPS outbound only)
|
v
EC2 Instance (t2.micro)
[No Public IP]
[IAM Role with SSM permissions]
associate_public_ip_address = falseThe instance is completely unreachable from the internet.
map_public_ip_on_launch = falseEven if you wanted a public IP, the subnet won't allow it.
# EC2 Security Group has ZERO ingress rules
# Only egress allowed
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}No inbound connections possible - not even from within AWS.
Instead of routing through the internet, the instance communicates directly with AWS services via private connections.
Same as Part 1, except:
- ❌ No SSH key pair needed!
- ✅ AWS CLI configured
- ✅ Terraform installed
- ✅ AWS account with appropriate permissions
terraform-ec2-assignment/
├── main.tf # Infrastructure resources
├── variables.tf # Input variables
├── data.tf # Data sources
├── outputs.tf # Output values
└── README.md # This file
terraform initterraform planYou should see approximately 13 resources:
- 1 VPC
- 1 Private Subnet
- 1 Route Table + Association
- 2 Security Groups (EC2 + VPC Endpoints)
- 1 EC2 Instance
- 1 IAM Role
- 1 IAM Role Policy Attachment
- 1 IAM Instance Profile
- 3 VPC Endpoints (SSM, SSMMessages, EC2Messages)
terraform applyType yes when prompted.
Wait 3-5 minutes after apply completes for:
- EC2 instance to fully boot
- SSM agent to start and register
- VPC endpoints to become available
Check if your instance is registered with Systems Manager:
aws ssm describe-instance-information --region <your-region>Look for your instance ID in the output with PingStatus: "Online".
Option A: AWS Console (Recommended for first time)
- Go to AWS Console → EC2 → Instances
- Select your instance
- Click Connect button
- Choose Session Manager tab
- Click Connect
Option B: AWS CLI
aws ssm start-session --target <instance-id> --region <your-region>Option C: Using Terraform Output
# The output provides the exact command
terraform output ssm_connect_commandOnce in the session:
# Check you're on the instance
whoami
# Output: ssm-user
# Check instance metadata
curl http://169.254.169.254/latest/meta-data/instance-id
# Verify no public IP
curl http://169.254.169.254/latest/meta-data/public-ipv4
# Output: (should fail or return nothing)
# Check the status file from user data
cat /tmp/status.txtterraform destroyType yes when prompted.
Each endpoint serves a specific purpose:
1. SSM Endpoint (com.amazonaws.region.ssm)
- Main Systems Manager API
- Instance registration
- Retrieving commands and configurations
2. SSM Messages Endpoint (com.amazonaws.region.ssmmessages)
- Session Manager data channel
- Transmits your keystrokes and command output
- Real-time terminal session communication
3. EC2 Messages Endpoint (com.amazonaws.region.ec2messages)
- Instance messaging service
- Allows SSM agent to receive commands
- Enables Run Command functionality
If you removed even ONE endpoint, SSM would fail because:
- Instance couldn't register with SSM service
- Sessions couldn't transmit data
- Commands couldn't be delivered
vpc_endpoint_type = "Interface"Interface Endpoints:
- Create ENI (Elastic Network Interface) in your subnet
- Get private IP addresses
- Cost: ~$0.01/hour per endpoint
- Work with most AWS services
Gateway Endpoints (not used here):
- Free
- Only for S3 and DynamoDB
- Use route table entries instead of ENIs
private_dns_enabled = trueThis allows the instance to use standard AWS service URLs like:
ssm.us-east-1.amazonaws.comssmmessages.us-east-1.amazonaws.com
Without this, you'd need to use endpoint-specific URLs.
1. IAM Role
resource "aws_iam_role" "ec2_ssm_role" {
assume_role_policy = jsonencode({
# Allows EC2 service to assume this role
})
}2. Policy Attachment
resource "aws_iam_role_policy_attachment" "ssm_attach" {
role = aws_iam_role.ec2_ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}3. Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
name = "ec2-ssm-profile"
role = aws_iam_role.ec2_ssm_role.name
}- Role = Set of permissions (what you can do)
- Policy = The actual permissions document
- Instance Profile = Container that attaches the role to EC2
The AmazonSSMManagedInstanceCore policy allows:
- Register with Systems Manager
- Send logs to CloudWatch (if configured)
- Receive and execute commands
- Upload inventory data
The instance CANNOT:
- Access S3 (unless you add that policy)
- Modify other EC2 instances
- Create AWS resources
- Access databases
Part 1 (Public Instance):
- ✅ SSH brute force attacks possible
- ✅ Port scanning possible
- ✅ DDoS possible
- ✅ Exploit of SSH vulnerabilities
- ❌ Need to manage SSH keys
- ❌ SSH access logged only on instance
Part 2 (Private Instance):
- ❌ No exposed ports
- ❌ Unreachable from internet
- ❌ No SSH keys to steal
- ✅ All access logged in CloudTrail
- ✅ IAM-based access control
- ✅ Centralized session recording
Attacker capabilities:
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}The attacker COULD:
- Make outbound connections to anywhere
- Exfiltrate data to external servers
- Download malicious tools (if they could reach them)
Option 1: Restrict Egress to AWS Services Only
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block] # VPC only
}Option 2: Add VPC Flow Logs Monitor all network traffic for anomalies.
Option 3: Use AWS PrivateLink Connect to third-party SaaS without internet access.
| Resource | Cost |
|---|---|
| EC2 t2.micro | $0 (free tier) or ~$8.50 |
| EBS 40GB gp3 | $0 (free tier) or ~$3.20 |
| VPC Endpoint (SSM) | ~$7.30 |
| VPC Endpoint (SSMMessages) | ~$7.30 |
| VPC Endpoint (EC2Messages) | ~$7.30 |
| Data Processing | ~$0.01/GB |
| Total | ~$22-34/month |
1. Share Endpoints Across Subnets If you have multiple private subnets, attach all to the same endpoints.
2. Use for Production Only For dev/test, consider Part 1's public approach if cost is a concern.
3. NAT Gateway Alternative
- If you need internet access: NAT Gateway = ~$32/month + data
- If SSM only: VPC Endpoints = ~$22/month (current approach is cheaper)
4. Use AWS Systems Manager Endpoint for Multiple Services The same endpoints work for:
- Session Manager
- Run Command
- Patch Manager
- State Manager
Pros: Free, simple, can download packages Cons: Exposed to internet, requires SSH management Cost: $0
Pros: Most secure, no exposed ports, managed access Cons: More expensive, can't download from internet Cost: ~$22/month
Pros: Can download packages, still private Cons: Most expensive, more complex Cost: ~$32/month + $0.045/GB
- Private subnet with VPC endpoints for AWS services
- NAT Gateway for internet access (apt/yum updates)
- Bastion host in public subnet as backup access Cost: ~$54/month
Check 1: Wait 3-5 minutes The SSM agent needs time to register.
Check 2: Verify IAM role is attached
aws ec2 describe-instances --instance-ids <id> --query 'Reservations[0].Instances[0].IamInstanceProfile'Check 3: Verify VPC endpoints are available
aws ec2 describe-vpc-endpoints --region <region>All three should show State: "available".
Check 4: Security group on endpoints Must allow HTTPS (443) from the VPC CIDR block.
Check 5: Check SSM agent status (if you can access via another method)
sudo systemctl status amazon-ssm-agentSolution: You need IAM permissions. Add this policy to your IAM user:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:TerminateSession"
],
"Resource": "*"
}]
}Solution: Check your region supports VPC endpoints for SSM:
aws ec2 describe-vpc-endpoint-services --region <region> | grep ssmSolution: Remember to destroy when not in use:
terraform destroyVPC endpoints charge by the hour, even when not in use.
# Should return empty or fail
terraform output instance_public_ipaws ssm start-session --target $(terraform output -raw instance_id)# Should fail
ping google.com
# Should fail
curl https://google.com# Should work (metadata service)
curl http://169.254.169.254/latest/meta-data/instance-id# From within session
aws sts get-caller-identity
# Should show the EC2 role, not your user✅ Production databases - No public access needed ✅ Batch processing - Jobs that don't need internet ✅ Internal APIs - Backend services ✅ Compliance workloads - HIPAA, PCI-DSS requirements ✅ High-security applications - Financial, government
❌ Web servers - Need to serve internet traffic (use ALB instead) ❌ Dev environments - Cost may not be justified ❌ Applications needing package updates - Need NAT Gateway ❌ Learning/testing - Part 1 is simpler and free
| Feature | SSH | SSM Session Manager |
|---|---|---|
| Port Required | 22 | None |
| Public IP | Required | Not required |
| Key Management | Manual | Not needed |
| Access Control | Key-based | IAM-based |
| Audit Logging | Instance logs only | CloudTrail |
| Session Recording | Manual setup | Built-in (if enabled) |
| Bastion Host | Often needed | Not needed |
| MFA Support | Manual | Native via IAM |
| Browser Access | No | Yes |
Record all session activity to S3:
resource "aws_ssm_document" "session_manager_prefs" {
name = "SSM-SessionManagerRunShell"
document_type = "Session"
document_format = "JSON"
content = jsonencode({
schemaVersion = "1.0"
inputs = {
s3BucketName = aws_s3_bucket.session_logs.id
cloudWatchLogGroupName = aws_cloudwatch_log_group.session_logs.name
}
})
}Access private RDS databases from your laptop:
aws ssm start-session \
--target <instance-id> \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"portNumber":["3306"],"localPortNumber":["3306"]}'Execute commands on multiple instances:
aws ssm send-command \
--instance-ids "i-1234567890abcdef0" \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["echo Hello World"]'-
Why does removing even ONE VPC endpoint break SSM? Each endpoint handles a different part of the communication pipeline.
-
What would happen if you set
private_dns_enabled = false? The instance couldn't resolve standard AWS service URLs. -
Could you add a second EC2 instance using the same VPC endpoints? Yes! Endpoints can be shared across all instances in the subnet.
-
How would you allow the instance to download packages from the internet? Add a NAT Gateway or use VPC endpoints for specific package repos.
-
What's the difference between the IAM role and the instance profile? The role defines permissions; the profile attaches the role to EC2.
-
Why allow all outbound traffic in the EC2 security group? It needs to reach the VPC endpoints on port 443, but egress rules could be tightened.
-
If you wanted to access a private RDS database from this instance, what would you add? Security group rule on RDS allowing traffic from the EC2 security group.
-
How does SSM work without an exposed port? The instance initiates outbound connections to AWS services; you don't connect TO the instance.
- Defense in depth: Multiple layers (private subnet + no ingress + IAM)
- Least privilege: Instance has minimal permissions needed
- Zero trust: No assumptions about network security
- Free/cheap isn't always appropriate for production
- ~$22/month is reasonable for production security
- Know when each approach makes sense
- VPC Endpoints enable private AWS API access
- IAM roles eliminate credential management
- Systems Manager provides modern instance access
After completing this assignment, explore:
- VPC Flow Logs - Monitor all network traffic
- AWS Config - Track configuration changes
- CloudWatch Alarms - Alert on unusual activity
- Auto Scaling Groups - Automatically scale instances
- Application Load Balancer - Distribute traffic
- AWS Secrets Manager - Manage database passwords
- Parameter Store - Store configuration
- AWS Systems Manager Documentation
- VPC Endpoints Pricing
- IAM Roles for EC2
- Terraform AWS Provider
- VPC Endpoint Services
- Private subnet with no public IPs
- VPC endpoints for SSM, SSMMessages, EC2Messages
- IAM role with AmazonSSMManagedInstanceCore policy
- Security groups (EC2 + VPC endpoints)
- EC2 instance with IAM instance profile attached
- Successfully connect via Session Manager
- Verify no public IP assigned
- Document cost implications
- Explain security improvements over Part 1
Author: Jerrelle Johnson
Date: February 2026
Assignment: Terraform EC2 Provisioning - Part 2 (Secure Private Access)
Architecture: Private EC2 with SSM via VPC Endpoints