Friday, 27 March 2026

AWS Credentials Setup — Best Practices Guide

 

Overview

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.


Architecture: What We're Creating

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)

Step 1: Create the IAM Policy

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.

1.1 — Go to IAM in the AWS Console

  1. Log in to AWS Console with your root account or an existing admin user
  2. Navigate to IAM → Policies → Create policy
  3. Click JSON tab and paste the policy below

1.2 — The Policy (Task C: Bare-Metal)

{
  "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"
        }
      }
    }
  ]
}
  1. Click Next
  2. Name it: HealthPulseBaremetalPolicy
  3. Description: Terraform permissions for HealthPulse bare-metal EC2 deployment (VPC, Subnet, EC2, EIP, SG)
  4. Click Create policy

1.3 — What Each Section Does

Policy SectionWhy It's Needed
EC2Describeec2: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.
EC2MutateCreate/destroy EC2 instances, import SSH key pair, tag resources. Region-locked.
VPCNetworkingCreate/modify/delete VPC, subnet, internet gateway, route table. Includes ModifyVpcAttribute (for dns_hostnames) and ModifySubnetAttribute (for map_public_ip). Region-locked.
SecurityGroupsCreate/modify security groups for HTTP/SSH firewall rules. Region-locked.
ElasticIPAllocate/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 targets eu-west-1, the API will deny it.


Step 2: Create the IAM Group

Groups make it easy to manage permissions for multiple students.

  1. Go to IAM → User groups → Create group
  2. Group name: healthpulse-devops
  3. Attach the policy: HealthPulseBaremetalPolicy
  4. 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.


Step 3: Create the IAM User

3.1 — Create User in Console

  1. Go to IAM → Users → Create user
  2. User name: healthpulse-terraform (or per-student: healthpulse-student-01)
  3. 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
  4. Click Next
  5. Select Add user to group → choose healthpulse-devops
  6. Click Next → Create user

3.2 — Create Access Keys

  1. Click on the newly created user → Security credentials tab
  2. Scroll to Access keys → Create access key
  3. Use case: Select Command Line Interface (CLI)
  4. Check the acknowledgment box
  5. Click Next → Create access key
  6. 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.


Step 4: Configure AWS CLI (OPTIONAL)

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 configure

It 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

4.1 — Verify It Works

# Check your identity
aws sts get-caller-identity

Expected 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 it

Step 5: Use Named Profiles (Best Practice)(OPTIONAL)

Instead 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 healthpulse

Enter 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 PowerShell

In 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 ...

Step 6: Run Terraform (Simplified)

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_id or subnet_id variables! 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"


Security Best Practices Summary

PracticeWhy
Never use root credentialsRoot has unlimited power — one leak and your account is gone
Programmatic access onlyNo console password = no one can click around and break things
Region-locked policiesPrevents accidental resource creation in wrong regions (and cost surprises)
Named profilesPrevents "wrong account" mistakes when you have multiple AWS accounts
IAM groupsChange permissions once, applies to all students
Per-student usersAudit trail, individual revocation
Rotate keysIf the program runs > 90 days, rotate access keys
Delete after programRemove all users, keys, and policies when the capstone is done
Never commit credentialsThis is why Task B exists (pre-commit hooks with detect-secrets)

Cost Control

To prevent surprise bills:

Set a Budget Alert

# In AWS Console: Billing → Budgets → Create budget
# Set monthly budget: $50 (or whatever your limit is)
# Alert at: 50%, 80%, 100%
# Notify: your email

What This Capstone Costs (Approximate)

ResourceTaskMonthly Cost
t2.micro EC2G (bare-metal)$0 (free tier) or ~$8.50
Elastic IP (attached)G$0
Elastic IP (detached)G~$3.60/mo
VPC + IGWG$0


Total for Task C only: Free tier eligible or under $15/monthReminder: Always run terraform destroy when done for the day if cost is a concern.


Quick Reference Card

# ─── 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)"

TASK C: Bare-Metal Deployment (Nginx on EC2) — Step-by-Step Guide

Overview In this task, you will deploy the HealthPulse Portal the  traditional way  — static files served directly by Nginx on an EC2 instan...