1. IAM: Users, Roles & Least Privilege
The root account you signed up with can do literally anything in your AWS account, including deleting the account itself — which is exactly why you never use it day to day. Instead, AWS Identity and Access Management (IAM) lets you create scoped-down identities: users for people, roles for services and temporary access, and policies — JSON documents — that define exactly which actions on which resources are allowed or denied.
aws configure # enter your root/admin access key once, interactively
aws sts get-caller-identity # confirm which identity the CLI is using
aws iam create-group --group-name developers
aws iam create-user --user-name ada
aws iam add-user-to-group --user-name ada --group-name developers
A policy is built from Effect (Allow/Deny), Action
(the API calls it covers, like s3:GetObject), and Resource
(the ARN it applies to). Least privilege means granting only the actions and resources
a role actually needs — not "Action": "*" against "Resource": "*":
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadAppLogsBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::codeverse-app-logs-ada",
"arn:aws:s3:::codeverse-app-logs-ada/*"
]
}
]
}
aws iam create-policy \
--policy-name AppLogsReadOnly \
--policy-document file://app-logs-readonly.json
aws iam attach-group-policy \
--group-name developers \
--policy-arn arn:aws:iam::123456789012:policy/AppLogsReadOnly
Roles work like a policy without a permanent identity attached — an EC2 instance or a Lambda function assumes a role and receives short-lived credentials for exactly as long as it runs, which is safer than baking a long-lived access key into an instance.
Enable MFA on the root account, store its credentials somewhere offline, and never generate an access key for it. Every day-to-day action — including everything else in this lesson — should run as an IAM user or role with only the permissions that task needs.
2. EC2 Instances & Security Groups
EC2 (Elastic Compute Cloud) is a virtual machine you rent by the second. Launching one requires an AMI (the OS image), an instance type (the hardware size), a key pair (for SSH, same public/private key model as Week 1), and a security group — a stateful virtual firewall attached to the instance's network interface.
aws ec2 create-security-group \
--group-name web-sg \
--description "SSH from my IP only, HTTPS from anywhere" \
--vpc-id vpc-0123456789abcdef0
# Allow SSH only from your own IP -- never 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id sg-0123456789abcdef0 \
--protocol tcp --port 22 --cidr 203.0.113.10/32
# Allow HTTPS from anywhere, since this is a public web server
aws ec2 authorize-security-group-ingress \
--group-id sg-0123456789abcdef0 \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.micro \
--key-name my-key \
--security-group-ids sg-0123456789abcdef0 \
--subnet-id subnet-0123456789abcdef0 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-01},{Key=Environment,Value=dev},{Key=Owner,Value=ada}]'
"Stateful" means a security group automatically allows the response traffic for a connection you initiated — you don't need a separate outbound rule just to receive a reply to a request the instance made. Rules are additive and default-deny: nothing is reachable until you explicitly open it.
Opening port 22 (SSH) or a database port to 0.0.0.0/0 is how compromised EC2 instances become cryptomining hosts within minutes of launch — automated scanners find open ports constantly. Scope SSH to your own IP (or better, a bastion host or Session Manager) and never open a database port to the public internet at all.
3. S3 Buckets & Bucket Policies
S3 (Simple Storage Service) stores objects — files of any size, up to 5TB each — inside buckets, which need a globally unique name across all of AWS, not just your account. It's where you'll put build artifacts, log archives, static site assets, and later, Terraform state.
aws s3 mb s3://codeverse-app-logs-ada --region us-east-1
aws s3 cp app.log s3://codeverse-app-logs-ada/2026-08-03/app.log
aws s3 sync ./dist s3://codeverse-static-site-ada # upload a whole directory
aws s3 ls s3://codeverse-app-logs-ada --recursive
aws s3api put-bucket-versioning \
--bucket codeverse-app-logs-ada \
--versioning-configuration Status=Enabled
Every new bucket blocks public access by default, and you should leave it that way unless you're intentionally hosting something public, like static site assets behind CloudFront. A bucket policy is a resource-based policy — attached to the bucket rather than a user — that can further restrict access even beyond what IAM allows:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::codeverse-app-logs-ada",
"arn:aws:s3:::codeverse-app-logs-ada/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
}
]
}
aws s3api put-bucket-policy \
--bucket codeverse-app-logs-ada \
--policy file://bucket-policy.json
aws s3api put-public-access-block \
--bucket codeverse-app-logs-ada \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
A deleted bucket's name can be claimed by anyone else, and buckets are frequently found by scanning common name patterns. Never put a secret in a bucket name, and always run put-public-access-block explicitly rather than trusting the console default not to change.
4. VPC Fundamentals
A VPC (Virtual Private Cloud) is your own isolated slice of the AWS
network, defined by a CIDR block like 10.0.0.0/16. Inside it, you carve
out subnets — smaller CIDR ranges tied to a single Availability
Zone — and each subnet is public or private purely
based on whether its route table sends internet-bound traffic to an internet gateway.
aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=codeverse-vpc}]'
aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 \
--cidr-block 10.0.1.0/24 --availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]'
aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 \
--cidr-block 10.0.101.0/24 --availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-1a}]'
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=codeverse-igw}]'
aws ec2 attach-internet-gateway \
--vpc-id vpc-0123456789abcdef0 --internet-gateway-id igw-0123456789abcdef0
aws ec2 create-route-table --vpc-id vpc-0123456789abcdef0
aws ec2 create-route \
--route-table-id rtb-0123456789abcdef0 \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id igw-0123456789abcdef0
aws ec2 associate-route-table \
--subnet-id subnet-0123456789abcdef0 --route-table-id rtb-0123456789abcdef0
The private subnet has no route to an internet gateway, so nothing inside it is directly reachable from outside the VPC — that's exactly where a database or internal service belongs. If a private-subnet resource needs outbound internet access (to pull an OS update, say) without being inbound-reachable, that route instead points at a NAT gateway sitting in the public subnet.
Every AWS region starts with a default VPC where every subnet is public — convenient for a first EC2 instance, dangerous for anything holding real data. Once you get to Terraform in Week 9, you'll define this subnet layout as code instead of clicking through the console.
5. AWS CLI & Cost Control
You've been using the AWS CLI all week — it's the same tool your CI pipelines and,
soon, Terraform will use under the hood. aws configure writes credentials
to ~/.aws/credentials, and named profiles let you keep
multiple accounts or roles separate without overwriting each other:
aws configure --profile personal
export AWS_PROFILE=personal # every subsequent command uses this profile
aws sts get-caller-identity --profile personal
Tagging every resource you create (Environment, Owner,
Project) isn't optional busywork — it's how you'll filter a cost report
or figure out whose EC2 instance is still running six weeks after an experiment. Set
a billing alarm on day one so a misconfigured resource doesn't surprise you at the
end of the month:
{
"BudgetName": "monthly-guardrail",
"BudgetLimit": { "Amount": "20", "Unit": "USD" },
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}
aws budgets create-budget \
--account-id 123456789012 \
--budget file://budget.json
# Find anything still running before you close your laptop
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=running" \
--query "Reservations[].Instances[].[InstanceId,Tags]"
# Unattached Elastic IPs are billed hourly even when idle
aws ec2 describe-addresses --query "Addresses[?AssociationId==null]"
aws ec2 release-address --allocation-id eipalloc-0123456789abcdef0
An EC2 t2.micro or t3.micro is free for the first 12 months on a new account only — after that, or on an older account, it bills per second. Make cleaning up test resources at the end of every session (this week's exercise included) a habit before the bill teaches you the hard way.
6. Hands-on Exercise
Stand up a locked-down AWS environment by hand
Build the exact environment you'll reproduce in Terraform next week, entirely with the AWS CLI, so you understand what the code will actually create.
Requirements:
- Create an IAM user for yourself in a
developersgroup, attach a custom least-privilege policy (notAdministratorAccess), and confirm the identity withaws sts get-caller-identity. - Create a VPC with one public subnet and one private subnet, an internet gateway, and a route table that gives only the public subnet a route to
0.0.0.0/0. - Create a security group in that VPC allowing SSH only from your current public IP and HTTPS from anywhere, then launch a
t3.microEC2 instance into the public subnet using it. - Create an S3 bucket, enable versioning, block all public access explicitly, and attach a bucket policy that denies any non-HTTPS request.
- Tag every resource you created with
Environment=devandOwner=<your-name>, and create a $20 monthly budget alert. - Terminate the instance, delete the bucket, and remove the VPC's resources when you're done, then confirm nothing billable is left running.
Run curl -s ifconfig.me to get your current public IP for the security group's --cidr value — it's the same trick a curl -v trace from Week 2 relies on, just pointed at a service that echoes your address back.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why shouldn't you use the AWS root account, or an IAM user with AdministratorAccess, for everyday work?
Why shouldn't you use the AWS root account, or an IAM user with AdministratorAccess, for everyday work?
Both can perform any action on any resource, so a leaked credential, a typo'd CLI command, or a compromised laptop puts the entire account at risk instead of one narrow blast radius. Least-privilege IAM users and roles limit the damage a mistake or a stolen key can do to exactly the actions that identity actually needs.
Q2
Why is a security group rule opening port 22 to 0.0.0.0/0 dangerous, and what's the fix?
Why is a security group rule opening port 22 to 0.0.0.0/0 dangerous, and what's the fix?
It lets any host on the internet attempt to connect to SSH on that instance, and automated scanners find newly opened ports within minutes, immediately trying default credentials and known exploits. Scoping the rule's CIDR to your own IP address (or routing through a bastion host or AWS Session Manager) means only you can even attempt a connection.
Q3
Why run put-public-access-block on an S3 bucket explicitly, even though new buckets already block public access by default?
Why run put-public-access-block on an S3 bucket explicitly, even though new buckets already block public access by default?
Defaults can be changed later by a teammate, a script, or a misapplied bucket policy, and a bucket holding sensitive data with public access silently re-enabled is one of the most common real-world data leaks. Setting the block explicitly, and treating any change to it as something that should require review, makes the intent durable rather than relying on a setting nobody is watching.
Q4
Why put a database in a private subnet instead of a public one, if you could just add a restrictive security group to it either way?
Why put a database in a private subnet instead of a public one, if you could just add a restrictive security group to it either way?
A security group is one layer of defense that depends on every rule being correct forever; a private subnet's route table has no path to an internet gateway at all, so the resource isn't reachable from the internet even if a security group rule is misconfigured or accidentally widened later. Defense in depth means the network topology itself, not just the firewall rules on top of it, should reflect what's supposed to be reachable from outside.