This project provisions a secure, production-ready AWS VPC using Terraform, demonstrating two secure access patterns for private EC2 instances:
- Bastion Host (classic SSH jump box)
- AWS SSM Session Manager (no inbound SSH required)
The project is modular, extensible, and now includes advanced monitoring, compliance, and alerting features.
┌──────────────────────────────┐
│ Internet │
└─────────────┬────────────────┘
│
┌─────────────┴─────────────┐
│ Public Subnet │
│ ┌──────────────────────┐ │
│ │ Bastion Host (1) │ │
│ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ NAT Gateway │ │
│ └──────────────────────┘ │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ Private Subnet │
│ ┌──────────────────────┐ │
│ │ Private Instance(s) │ │
│ └──────────────────────┘ │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ SSM Access │
└───────────────────────────┘
│
┌─────────────┴─────────────┐
│ CloudWatch Monitoring │
└─────────────┬─────────────┘
│
┌─────────────┴─────────────┐
│ AWS Config Compliance │
└───────────────────────────┘
- Bastion Host: Only public entry point for SSH, restricted by security group.
- NAT Gateway: Allows private instances to access the internet for updates, but not be accessed from the internet.
- Private Instances: No public IP, only accessible via the bastion or SSM.
- SSM Access: Enables secure, auditable access to private instances without opening SSH.
- CloudWatch: Monitors EC2 and sends notifications via SNS.
- AWS Config: Monitors compliance and configuration drift.
VPC-Hardening/
├── README.md
├── .gitignore
├── bastion-access
│ └── terraform
│ ├── main.tf
│ ├── outputs.tf│
│ └── variables.tf
├── modules
│ ├── aws_config
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── bastion
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── cloudwatch
│ │ ├── cloudwatch-agent-config.json
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── cloudwatch_alarms
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── nat_gateway
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── private_instance
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── security
│ │ ├── nacls.tf
│ │ ├── outputs.tf
│ │ ├── security_groups.tf
│ │ └── variables.tf
│ ├── ssm_role
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── vpc
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vpc_endpoints
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── ssm-access
└── terraform
├── main.tf
├── outputs.tf
└── variables.tf
- Terraform v1.0.0 or later
- AWS CLI configured
- An existing AWS EC2 key pair (for Bastion access)
- Your public IP address for SSH access (for Bastion access)
- For SSM: Attach the necessary IAM permissions to your user/role and install Session Manager Plugin
- (Recommended) Add a
versions.tfto pin provider versions for reproducibility
All variables are set in the respective variables.tf files. You can edit defaults or override with CLI/environment variables.
Example:
variable "region" {
default = "us-east-1"
}
variable "vpc_name" {
default = "hardened-vpc"
}
# ...etcChoose your access pattern:
cd bastion-access/terraform
terraform init
terraform plan
terraform applycd ssm-access/terraform
terraform init
terraform plan
terraform apply-
SSH to Bastion Host
ssh -i /path/to/Your-Key.pem ubuntu@<bastion-public-ip>
-
Copy Private Key to Bastion
scp /path/to/Your-Key.pem ubuntu@<bastion-public-ip>:~/ chmod 600 ~/Your-Key.pem
-
SSH from Bastion to Private Instance
ssh -i ~/Your-Key.pem ubuntu@<private-instance-ip>
Default user for Ubuntu AMIs is
ubuntu.
- Ensure your AWS CLI user/role has
AmazonSSMFullAccessor the minimum required SSM permissions. - Start a session:
Or use the AWS Console > Systems Manager > Session Manager.
aws ssm start-session --target <private-instance-id>
- CloudWatch: EC2 instance metrics, logs, and alarms. Alerts sent to your configured email via SNS.
- CloudWatch Alarms: Predefined alarms for CPU, memory, and other metrics.
- AWS Config: Tracks configuration changes and enforces compliance rules (e.g., S3 encryption, no public EC2 IPs, root MFA enabled).
- Bastion Host: Only allows SSH from your IP (edit in
main.tf). - Private Instance: Only allows SSH from the bastion security group.
- NAT Gateway: Private instances have outbound internet, but no inbound from the internet.
- Network ACLs: Restrictive, only necessary ports open.
- No hardcoded secrets: Key names only, never private keys.
- SSM Access: No inbound SSH needed, all access is logged and auditable.
- S3 Buckets: Block public access, versioning, and encryption enabled.
- IAM Roles: Least privilege for EC2 and SSM.
- AWS Config Rules: Enforce best practices and compliance.
To destroy all resources:
terraform destroy- bastion-access/terraform/: Bastion-based access deployment.
- ssm-access/terraform/: SSM-based access deployment (no SSH key required).
- modules/: Reusable infrastructure modules:
bastion/: Bastion host resourcesprivate_instance/: Private EC2 instancesnat_gateway/: NAT Gateway setupsecurity/: Security groups and NACLsssm_role/: SSM instance profile and permissionsvpc/: VPC and subnet definitionsvpc_endpoints/: VPC endpoints for SSM and other servicescloudwatch/: CloudWatch log group, SNS, and monitoringcloudwatch_alarms/: CloudWatch alarms for EC2aws_config/: AWS Config rules, recorder, and compliance
- Add more modules for additional AWS services as needed.
- Integrate with CI/CD for automated deployments.
- Use Terraform Cloud or Atlantis for team workflows.
- Pin provider versions in a
versions.tffor reproducibility. - Never commit secrets (private keys, credentials, etc).
- Review AWS Config and CloudWatch alarms to ensure they meet your compliance needs.
- Use separate state files for prod/staging/dev environments.
Questions?
Open an issue or check the module documentation for more details.