This guide walks you through creating a dedicated IAM user for the HealthPulse capstone project with the minimum permissions needed. This follows the Principle of Least Privilege — the user can only do what it needs, nothing more.
Why not use your root account? The root account has unlimited access. If those credentials leak (in a git commit, a CI log, a student's laptop), your entire AWS account is compromised. A scoped IAM user limits the blast radius.
AWS Account (root — NEVER use for Terraform)
│
├── IAM Group: "healthpulse-devops"
│ └── Attached Policies:
│ ├── HealthPulseBaremetalPolicy (EC2, VPC, EIP)
│ ├── HealthPulseContainerPolicy (ECS, ECR, ALB) ← for Task H
│ └── HealthPulseK8sPolicy (EKS) ← for Task I
│
└── IAM User: "healthpulse-terraform"
├── Member of: healthpulse-devops group
├── Access Key: AKIA... (for CLI/Terraform)
└── NO console password (programmatic access only)
This policy covers everything needed for Task G (bare-metal) and Task C (VPC infrastructure). It's scoped to only the resource types Terraform will create.
- Log in to AWS Console with your root account or an existing admin user
- Navigate to IAM → Policies → Create policy
- Click JSON tab and paste the policy below
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EC2Describe",
"Effect": "Allow",
"Action": [
"ec2:Describe*"
],
"Resource": "*"
},
{
"Sid": "EC2Mutate",
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances",
"ec2:ModifyInstanceAttribute",
"ec2:CreateKeyPair",
"ec2:DeleteKeyPair",
"ec2:ImportKeyPair",
"ec2:CreateTags",
"ec2:DeleteTags"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
},
{
"Sid": "VPCNetworking",
"Effect": "Allow",
"Action": [
"ec2:CreateVpc",
"ec2:DeleteVpc",
"ec2:ModifyVpcAttribute",
"ec2:CreateSubnet",
"ec2:DeleteSubnet",
"ec2:ModifySubnetAttribute",
"ec2:CreateInternetGateway",
"ec2:DeleteInternetGateway",
"ec2:AttachInternetGateway",
"ec2:DetachInternetGateway",
"ec2:CreateRouteTable",
"ec2:DeleteRouteTable",
"ec2:CreateRoute",
"ec2:DeleteRoute",
"ec2:ReplaceRoute",
"ec2:AssociateRouteTable",
"ec2:DisassociateRouteTable",
"ec2:CreateNatGateway",
"ec2:DeleteNatGateway"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
},
{
"Sid": "SecurityGroups",
"Effect": "Allow",
"Action": [
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RevokeSecurityGroupEgress",
"ec2:UpdateSecurityGroupRuleDescriptionsIngress",
"ec2:UpdateSecurityGroupRuleDescriptionsEgress"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
},
{
"Sid": "ElasticIP",
"Effect": "Allow",
"Action": [
"ec2:AllocateAddress",
"ec2:ReleaseAddress",
"ec2:AssociateAddress",
"ec2:DisassociateAddress"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "us-east-1"
}
}
}
]
}- Click Next
- Name it:
HealthPulseBaremetalPolicy - Description:
Terraform permissions for HealthPulse bare-metal EC2 deployment (VPC, Subnet, EC2, EIP, SG) - Click Create policy
| Policy Section | Why It's Needed |
|---|---|
| EC2Describe | ec2:Describe* — read-only wildcard. Terraform calls dozens of Describe* actions (DescribeInstances, DescribeVpcAttribute, DescribeInstanceAttribute, DescribeSubnets, etc.). Wildcard here is safe because Describe actions are read-only and cannot create, modify, or delete anything. |
| EC2Mutate | Create/destroy EC2 instances, import SSH key pair, tag resources. Region-locked. |
| VPCNetworking | Create/modify/delete VPC, subnet, internet gateway, route table. Includes ModifyVpcAttribute (for dns_hostnames) and ModifySubnetAttribute (for map_public_ip). Region-locked. |
| SecurityGroups | Create/modify security groups for HTTP/SSH firewall rules. Region-locked. |
| ElasticIP | Allocate/release a static public IP for the server. Region-locked. |
Region lock: Every mutating action is restricted to
us-east-1. If a student accidentally targetseu-west-1, the API will deny it.
Groups make it easy to manage permissions for multiple students.
- Go to IAM → User groups → Create group
- Group name:
healthpulse-devops - Attach the policy:
HealthPulseBaremetalPolicy - Click Create group
Why a group? If you have 20 students, you attach the policy to the group once. Each student's IAM user joins the group and automatically gets the permissions. If you need to change permissions later, you update the group policy — all students get the change instantly.
- Go to IAM → Users → Create user
- User name:
healthpulse-terraform(or per-student:healthpulse-student-01) - DO NOT check "Provide user access to the AWS Management Console" → This user is for CLI/Terraform only, not for clicking around in the console
- Click Next
- Select Add user to group → choose
healthpulse-devops - Click Next → Create user
- Click on the newly created user → Security credentials tab
- Scroll to Access keys → Create access key
- Use case: Select Command Line Interface (CLI)
- Check the acknowledgment box
- Click Next → Create access key
- CRITICAL: Copy both values now — the Secret Access Key is shown only once:
Access Key ID: AKIAIOSFODNN7EXAMPLE
Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Store these securely. Do NOT email them, Slack them, or commit them to git. Use a password manager or encrypted file.
On your local machine (or the student's machine):
# Install AWS CLI (if not already installed)
# Windows: Download from https://aws.amazon.com/cli/
# Mac: brew install awscli
# Linux: sudo apt install awscli
# Configure credentials
aws configureIt will prompt:
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json
# Check your identity
aws sts get-caller-identityExpected output:
{
"UserId": "AIDAIOSFODNN7EXAMPLE",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/healthpulse-terraform"
}# Test EC2 permissions (should list instances, even if empty)
aws ec2 describe-instances --region us-east-1 --query "Reservations" --output text
# Test that another region is DENIED
aws ec2 describe-instances --region eu-west-1
# → An error occurred (UnauthorizedOperation) ← This is CORRECT, policy blocks itInstead of setting credentials as the default, use a named profile. This prevents accidentally running Terraform against the wrong AWS account.
# Configure a named profile
aws configure --profile healthpulseEnter the same credentials. Now use it explicitly:
# Test with the profile
aws sts get-caller-identity --profile healthpulse
# Set for your terminal session
export AWS_PROFILE=healthpulse # Linux/Mac
set AWS_PROFILE=healthpulse # Windows CMD
$env:AWS_PROFILE = "healthpulse" # Windows PowerShellIn Terraform, reference it in the provider:
provider "aws" {
region = var.aws_region
profile = "healthpulse" # ← optional, uses named profile
}Or set the environment variable so Terraform picks it up automatically:
export AWS_PROFILE=healthpulse
terraform plan -var-file=dev.tfvars ...Now that the VPC/subnet are created by Terraform (no longer need to pass them manually):
cd terraform/baremetal
terraform init
# Plan — only need SSH key and optionally your IP for SSH restriction
terraform plan \
-var-file=dev.tfvars \
-var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)"
# Apply
terraform apply \
-var-file=dev.tfvars \
-var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)"Notice: No more
vpc_idorsubnet_idvariables! Terraform creates the entire network stack.
To restrict SSH to your IP (recommended):
terraform apply \
-var-file=dev.tfvars \
-var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)" \
-var="ssh_allowed_cidr=$(curl -s ifconfig.me)/32"| Practice | Why |
|---|---|
| Never use root credentials | Root has unlimited power — one leak and your account is gone |
| Programmatic access only | No console password = no one can click around and break things |
| Region-locked policies | Prevents accidental resource creation in wrong regions (and cost surprises) |
| Named profiles | Prevents "wrong account" mistakes when you have multiple AWS accounts |
| IAM groups | Change permissions once, applies to all students |
| Per-student users | Audit trail, individual revocation |
| Rotate keys | If the program runs > 90 days, rotate access keys |
| Delete after program | Remove all users, keys, and policies when the capstone is done |
| Never commit credentials | This is why Task B exists (pre-commit hooks with detect-secrets) |
To prevent surprise bills:
# In AWS Console: Billing → Budgets → Create budget
# Set monthly budget: $50 (or whatever your limit is)
# Alert at: 50%, 80%, 100%
# Notify: your email| Resource | Task | Monthly Cost |
|---|---|---|
| t2.micro EC2 | G (bare-metal) | $0 (free tier) or ~$8.50 |
| Elastic IP (attached) | G | $0 |
| Elastic IP (detached) | G | ~$3.60/mo |
| VPC + IGW | G | $0 |
Total for Task C only: Free tier eligible or under $15/month. Reminder: Always run
terraform destroywhen done for the day if cost is a concern.
# ─── FIRST TIME SETUP ───
aws configure --profile healthpulse
export AWS_PROFILE=healthpulse
# ─── VERIFY CREDENTIALS ───
aws sts get-caller-identity
# ─── GENERATE SSH KEY ───
ssh-keygen -t ed25519 -f ~/.ssh/healthpulse-key -N "" -C "healthpulse-capstone"
# ─── TERRAFORM (Task G) ───
cd terraform/baremetal
terraform init
terraform plan -var-file=dev.tfvars -var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)"
terraform apply -var-file=dev.tfvars -var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)"
# ─── CHECK OUTPUTS ───
terraform output
# ─── SSH INTO SERVER ───
ssh -i ~/.ssh/healthpulse-key ubuntu@$(terraform output -raw public_ip)
# ─── DESTROY WHEN DONE ───
terraform destroy -var-file=dev.tfvars -var="ssh_public_key=$(cat ~/.ssh/healthpulse-key.pub)"
No comments:
Post a Comment