Time: ~2 hours · Difficulty: Beginner · Stack: Docker · LocalStack · Terraform · AWS CLI · Checkov
LocalStack runs a mock of the AWS cloud as a single container on your laptop. Every aws command you already know works against it - you just point the CLI at http://localhost:4566 instead of the real AWS endpoints. Nothing you do here can ever generate a bill, which makes it the perfect first lab: create, mangle, and destroy S3 buckets, IAM roles, and Lambda functions as fast as you can type, with zero risk. This walkthrough takes you from an empty laptop to a deliberately-misconfigured "cloud" you can scan and fix.
Read this before you start. LocalStack emulates the AWS API surface, not AWS's security enforcement. The free Community edition happily lets you create IAM users and attach policies, but it does not actually enforce them - a Deny will not stop anything. That makes LocalStack unbeatable for learning the AWS API, CLI, SDKs, Terraform, and IaC scanning, and the wrong tool for testing whether an IAM policy actually blocks an action. For that, use a real free-tier account (see the break-it-then-catch-it walkthrough).
On this page
What you will build
- Docker + LocalStack running locally, with the
awslocalandtflocalwrappers. - A Terraform stack that deploys S3, IAM, and Lambda into the mock cloud.
- A deliberately vulnerable configuration you wrote on purpose.
- A Checkov and Trivy scan that catches those issues from the code alone, before any "deploy."
- Real muscle memory for the AWS CLI and Terraform, built without ever touching a live account.
Prerequisites
- Docker - Desktop or Engine. LocalStack runs as a container. Confirm with
docker --version. - Python 3.8+ and pip - for the LocalStack CLI and the wrapper tools.
- Terraform or OpenTofu - see the Terraform primer if it is new to you.
- About 4 GB of free RAM for the container.
Step-by-step
1. Install LocalStack and the CLI wrappers
# The LocalStack CLI plus the thin awslocal / tflocal wrappers python3 -m pip install --user localstack awscli-local terraform-local # confirm localstack --version awslocal --version
awslocal is a one-line wrapper that calls the real aws CLI with --endpoint-url=http://localhost:4566 injected, so you never have to type the endpoint. tflocal does the same for Terraform: it auto-points every AWS provider at LocalStack. Both call the tools you already have, so you can always fall back to aws --endpoint-url=http://localhost:4566 … directly.
2. Start LocalStack
localstack start -d # -d = detached (runs in the background) localstack status services # list which service mocks are up
You will see a table of services (s3, iam, lambda, sqs, …) marked available. LocalStack pulls its Docker image on first run, so give it a minute. Tail logs with localstack logs -f; stop it later with localstack stop.
3. Your first calls against the mock cloud
# dummy credentials are fine - LocalStack accepts anything export AWS_ACCESS_KEY_ID=test export AWS_SECRET_ACCESS_KEY=test export AWS_DEFAULT_REGION=us-east-1 # who am I? LocalStack returns the canned account 000000000000 awslocal sts get-caller-identity # create a bucket, upload an object, list it back awslocal s3 mb s3://lab-bucket echo "top secret" > secret.txt awslocal s3 cp secret.txt s3://lab-bucket/ awslocal s3 ls s3://lab-bucket/
Every command is identical to real AWS; only the endpoint differs. Getting 000000000000 back from get-caller-identity is your proof you are hitting LocalStack and not a real account - check it any time you are unsure.
4. Deploy infrastructure with Terraform
Create a working directory and a provider file. tflocal wires the endpoints for you:
mkdir -p ~/localstack-lab && cd ~/localstack-lab
main.tf:
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"
# Belt-and-suspenders: these let the config work even if you accidentally
# run plain `terraform` instead of `tflocal`.
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
}tflocal init tflocal plan
5. Build a deliberately-vulnerable stack
Now add resources that mirror the classic real-world misconfigurations. Append to main.tf:
# 1) A bucket that is public to the world - the classic data-leak setup
resource "aws_s3_bucket" "public_data" {
bucket = "customer-exports-lab"
}
resource "aws_s3_bucket_public_access_block" "public_data" {
bucket = aws_s3_bucket.public_data.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "public_data" {
bucket = aws_s3_bucket.public_data.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Sid = "PublicRead",
Effect = "Allow",
Principal = "*",
Action = "s3:GetObject",
Resource = "${aws_s3_bucket.public_data.arn}/*"
}]
})
}
# 2) An IAM user with an inline admin policy and a long-lived access key
resource "aws_iam_user" "app" {
name = "legacy-app-user"
}
resource "aws_iam_user_policy" "app_admin" {
name = "app-admin"
user = aws_iam_user.app.name
policy = jsonencode({
Version = "2012-10-17",
Statement = [{ Effect = "Allow", Action = "*", Resource = "*" }]
})
}
resource "aws_iam_access_key" "app" {
user = aws_iam_user.app.name
}tflocal apply -auto-approve
Everything you just wrote is a real finding you will meet in production: a bucket policy with Principal: "*", an access block that blocks nothing, a wildcard Action: "*" IAM policy, and a static access key that never rotates. LocalStack Community will happily create all of it (it just will not enforce the IAM half) - which is exactly what you want for practising the build-and-scan loop.
6. Poke at what you built
# Upload a fake "loot" file, then fetch it with NO credentials at all echo "aws_secret_access_key=AKIAFAKE..." > creds.txt awslocal s3 cp creds.txt s3://customer-exports-lab/ curl http://localhost:4566/customer-exports-lab/creds.txt # Enumerate IAM the way an attacker with the leaked key would awslocal iam list-users awslocal iam list-access-keys --user-name legacy-app-user awslocal iam list-user-policies --user-name legacy-app-user awslocal iam get-user-policy --user-name legacy-app-user --policy-name app-admin
That anonymous curl succeeding is the whole point: a public bucket policy means the object is on the internet. The iam list-* / get-user-policy sequence is the enumeration muscle memory you will use for real in the break-it-then-catch-it walkthrough.
7. Catch the issues with IaC scanning
This is where a local lab genuinely shines: static scanners find every one of these problems from the Terraform files alone - no deploy, no cloud, no cost.
# Checkov - policy-as-code scanner for Terraform python3 -m pip install --user checkov checkov -d . # Trivy in config mode does the same and also covers Dockerfiles and k8s # install: brew install trivy (or see aquasecurity.github.io/trivy) trivy config .
Read the output. Checkov flags CKV_AWS_* IDs for the public bucket, the wildcard IAM policy, the missing default encryption, and more. Map each finding back to the exact resource you wrote, fix them one at a time, and re-scan until the run is clean. That loop - write, scan, remediate, re-scan - is precisely what an AppSec / IaC engineer does on every pull request. See the CI/CD security page for wiring the same scan into a pipeline.
8. Tear it down
tflocal destroy -auto-approve # remove the stack localstack stop # stop the container; state is ephemeral
Community-edition state lives in memory, so stopping the container wipes everything. That is a feature: every session starts from a clean slate.
What LocalStack does and does not teach
- Great for: AWS CLI and SDK fluency, boto3 scripting, Terraform / OpenTofu, IaC scanning (Checkov, Trivy, tfsec), and event-driven plumbing (S3 → Lambda → SQS).
- Not for: IAM policy enforcement,
AssumeRole/ session-policy boundaries, GuardDuty or CloudTrail detection, and anything that depends on real network, quota, or billing behavior.
When you outgrow the mock - when you need to know whether a policy actually blocks something, or whether a detection fires - move to a real free-tier account with the guardrails from the home-lab guide.
Common mistakes
- Expecting IAM denies to be enforced. Community edition creates policies but does not enforce them. Use a real account to test enforcement.
- Forgetting
-d. Without it,localstack startholds your terminal in the foreground. - Running plain
terraformagainst real AWS by accident. Always sanity-checksts get-caller-identityreturns000000000000. - Treating LocalStack as a security-control test-bed. It is an API and IaC learning tool, not an enforcement simulator.
Where next
- Break it, then catch it - graduate to a real AWS account and attack it safely.
- Kubernetes security lab - the same $0 approach, applied to a local cluster.
- Prowler audit + remediation - turn a scan-and-fix session into a portfolio piece.
- Terraform primer and the home-lab hub.
