Tools: Week 9: Setup AWS IAM and EC2 for Beginners

Tools: Week 9: Setup AWS IAM and EC2 for Beginners

What This Post Covers

Part 1: Why Not Root Credentials

Part 1.5: First — Create an AWS Account

Part 2: Create a Scoped IAM User

Step 1 — Create the User

Step 2 — Attach Permissions

Step 3 — Add MFA for Console Login

Part 3: Method 1 — IAM Access Keys (Static Credentials)

Install AWS CLI v2 on Debian 12/13

Create the Access Key

Configure the CLI

Verify Identity — What is STS?

Part 4: Create the IAM Role for EC2

Part 5: Security Group — Zero Open Ports

Part 6: Launch EC2 Instance — The User Data Bootstrap

Launch Configuration

User Data Script (paste in Advanced details → User data)

Part 7: Install Session Manager Plugin Locally

Part 8: Fish Shell Lab Functions

Set the Instance ID (run once after launch)

Create the Functions

**NOTE: Once you are done using an EC2 instance for tutorial exercises such as these you should execute lab-terminate in your local CLI to destory the EC2 instance. This will minimize Billing!

Part 9: Verify the Access Key Method Works

Part 10: Why Switch to IAM Identity Center?

Part 11: Method 2 — IAM Identity Center Full Setup

Enable IAM Identity Center

Configure MFA Settings (Critical)

Create a Permission Set

Create an Identity Center User

Register YubiKey for Identity Center User

Assign User to AWS Account

Configure AWS CLI for SSO

Verify Identity Center Works

Part 12: Create the Launch Template

Part 13: Update Fish Functions for SSO Profile

Part 14: Fix the iam:PassRole Error

Part 15: lab-create — Launch Fresh Instances On Demand

Part 16: Complete Daily Workflow

What Curriculum Weeks This Covers

Common Issues and Fixes

Issue 1 — SSO Token Expired

Issue 2 — lab-status Returns "None"

Issue 3 — lab-connect Fails After lab-create

Issue 4 — lab-terminate Skips Confirmation and Immediately Terminates

One More Thing — Quick Poll

Next in This Series AWS, GCP, and Azure are the three dominant cloud providers in the industry. As a Security Engineer you will encounter at least one of them in virtually every role — whether you are securing infrastructure, reviewing architecture, responding to incidents, or building detection rules. You do not need to master all three. But you do need to go deep on at least one. This post is about AWS — the most widely deployed of the three and the one most commonly required in Security Engineering job postings. Learning AWS deeply means understanding not just how to launch a virtual machine, but how identity works, how access is controlled, how services communicate securely, and how to build infrastructure that is defensible by design. This post is written for a complete beginner to AWS. By the end you will know how to do the following entirely from your local computer using the CLI — no browser required after initial setup: Along the way the post covers IAM (Identity and Access Management), IAM Identity Center, SSM Session Manager, security groups, IAM roles, Launch Templates, and fish shell automation — all grounded in real decisions made during a Week 9 Suricata IDS lab setup. Everything documented here is a real setup that was built step by step, including the failures. If something broke, it is noted and the fix is included. If you find this useful, I'd really appreciate a ⭐ on my open source secure coding exercise repo — it helps a lot: Support this project — Star it on GitHub! ⭐ Also — quick question: why do you read security engineering blog posts? One click helps me write better content: 👉 Take the poll (takes 10 seconds) This is the exact setup I built during Week 9 of my Security Engineering curriculum while preparing to run a Suricata IDS lab on AWS EC2. What started as "just spin up an instance" turned into a proper secure access architecture covering: Curriculum intersections: This covers Week 15 content (EC2, IAM basics) and Week 49 content (IAM Identity Center) — completed early as infrastructure prerequisites for the IDS lab. Every AWS account has a root user with unlimited permissions that cannot be restricted by any IAM policy. AWS explicitly states you should never use root for everyday tasks. Root should only ever be used for: ⚠️ Never run aws configure with root credentials. Never create root access keys. Root should only authenticate via the console with MFA. On this account root is protected with two YubiKeys registered as FIDO2 devices. That is the last line of defense. Before anything else you need an AWS account. Go to aws.amazon.com and sign up — it takes about 5 minutes and requires a credit card for identity verification. AWS has a generous free tier that covers most personal lab usage including the t2.micro instance type used throughout this post. The account you create becomes your root account. Read Part 1 above before using it for anything. Create an IAM user with only the permissions needed for lab work. This limits blast radius if credentials are ever compromised. Do NOT attach AdministratorAccess. The two policies above give enough permissions to manage EC2 and SSM without touching IAM or billing. AWS supports two MFA methods for IAM users. Choose one: Option A — Authenticator App (TOTP)

The most common method. Install Google Authenticator, Authy, or 1Password on your phone. AWS shows a QR code — scan it, enter two consecutive 6-digit codes to confirm, done. No hardware required. Option B — Hardware Security Key (FIDO2/YubiKey)More secure than TOTP. Requires a physical YubiKey or similar FIDO2 device (~$50). Immune to phishing because the key cryptographically binds to the origin domain — a fake AWS login page cannot steal it. This post uses Option B (YubiKey). TOTP is acceptable for a personal lab and requires no extra hardware. YubiKey is the stronger choice and becomes important in Part 11 where it enables hardware-backed MFA directly on CLI calls — something TOTP cannot do. Note the console sign-in URL shown on the user creation confirmation page: This is different from the root login page. Bookmark it. The simpler method. Permanent access key stored locally. Never expires unless manually deleted — which is both convenient and dangerous. ⚠️ AWS shows the Secret Access Key exactly once. If you close the page without saving it you must delete the key and create a new one. Always download the .csv before clicking Done. STS = Security Token Service. The get-caller-identity command asks AWS "who am I authenticated as right now?" It is the fastest way to verify credentials are working correctly. ✅ If the ARN shows user/your_username_here (not root) — the access key is working. The EC2 instance needs its own identity to communicate with AWS Systems Manager. This is separate from your user identity. Key distinction — two separate identities: The role is the instance's identity, not yours. This is why iam:PassRole matters (covered in Part 14). This is the key to the whole architecture. SSM Session Manager works over outbound HTTPS (port 443) to reach AWS endpoints. No inbound ports needed. No port 22. No bastion host. Your VPN IP is completely irrelevant. Note: EC2 Instance Connect will NOT work with no inbound rules. This is intentional. SSM replaces SSH entirely. This is where Debian 13 causes a surprise. Several approaches fail before finding the correct solution. The correct solution: User Data User Data runs as root on first boot before you ever connect. It installs the SSM Agent automatically. Wait 3-5 minutes after launch before trying to connect. The User Data script needs time to complete on first boot. One-time install on your local machine (not the EC2 instance): All lab operations wrapped in fish functions using the universal variable INSTANCE_ID. One set -U command updates everything simultaneously. Fish loads functions automatically from ~/.config/fish/functions/. Each function lives in its own file named after the function. For each snippet below, copy the contents and save it to the filename shown in bold — for example, copy the lab-status function into a file called lab-status.fish inside that directory. lab-login.fish (SSO only — covered in Part 11) ⚠️ Fish syntax bug: Use --prompt-str not --prompt for the read command. Using --prompt causes a syntax error that silently skips the confirmation and immediately terminates the instance. Ask me how I know. ✅ ssm-user means SSM Session Manager is working end-to-end. No SSH keys, no open ports, no IP whitelisting. Your VPN connection is irrelevant. The Access Key method works — but it has fundamental security weaknesses. IAM Identity Center addresses all of them. The FIDO2 on CLI insight: The reason YubiKey works with Identity Center but not a plain access key is that aws sso login opens a browser — and the browser is where WebAuthn/FIDO2 works. Every CLI session starts with a YubiKey tap. This maps directly to what SAED — a Security Engineer at Google with 6+ years of experience — wrote in a LinkedIn post listing 40 concepts junior Security Engineers should focus on in 2026. Under the Identity, Access and Secrets category he includes specifically: "Least privilege, just in time and time bound access patterns." (source) IAM Identity Center is the practical implementation of that concept. Before setting up Identity Center, delete the static access key. The two methods should not coexist long-term. Safer sequence: Deactivate the key first (reversible) → set up Identity Center → confirm lab-connect works → then permanently delete the key. Deleting before SSO is confirmed risks losing all CLI access. IAM Identity Center can only be enabled in a single AWS Region. Match it to your EC2 region. Authenticator apps (TOTP) is intentionally left unchecked. TOTP is phishing-vulnerable. Enforcing FIDO2-only means every user must have a hardware security key. No weaker fallback permitted. A permission set is the bridge between an Identity Center user and an AWS account. It answers the question: once this user authenticates, what are they actually allowed to do? Without a permission set you can authenticate successfully via IAM Identity Center and still have zero ability to do anything in AWS — authentication and authorization are separate steps. The permission set defines the authorization layer: which AWS actions the user can perform, which resources they can touch, and how long their session lasts before requiring re-authentication. This separation is itself a security principle. It means you can grant a user access to an account without giving them unlimited power inside it — and you can revoke or change their permissions without touching their login credentials. What PowerUserAccess allows and denies: Why this assignment matters: The Identity Center user (your_username_here) is the authentication layer. The AWS account (your_account_name_here) is where all resources live. The permission set (lab-power-user) defines what actions are allowed inside that account. You can assign one user to multiple AWS accounts with different permission sets — that is the real power of Identity Center in multi-account organizations. Notice assumed-role in the ARN. These are temporary credentials that expire in 8 hours. The access key showed user/your_username_here — a permanent identity. This is the difference. ✅ IAM Identity Center confirmed working. A Launch Template saves all instance configuration. lab-create uses it to launch an identical fresh instance with one command. Add --profile lab-sso to all functions that make AWS CLI calls. Overwrite the files created in Part 8: When you first run lab-create you will hit this: Why this happens: PowerUserAccess blocks all iam:* actions including iam:PassRole. Without it, your_username_here cannot assign suricata-lab-ssm-role to the new EC2 instance. Why this matters for security: Without restricting iam:PassRole, a user with EC2 launch permissions could attach an AdministratorAccess role to an instance and then connect to it — escalating from PowerUser to full Administrator through the instance. The restriction closes this path. The fix: Add a narrow inline policy to lab-power-user that grants iam:PassRole only for this specific role, only to EC2: Two restrictions make this safe: What this does step by step: This went well beyond the Week 9 IDS lab it was meant to support: Weeks 49-50 are normally scheduled for January 2027. Completed early as infrastructure prerequisites for the IDS lab. This is IAM Identity Center working exactly as designed. The 8-hour session expired. Fix: Browser opens → tap YubiKey → new 8-hour session issued. Then continue normally: This is the entire cost of IAM Identity Center over static access keys — one YubiKey tap per 8-hour session. A fair tradeoff for credentials that auto-expire and can never be leaked permanently. The previous instance was terminated. INSTANCE_ID is pointing to a dead instance ID. Fix: lab-create launches a fresh instance and automatically runs set -U INSTANCE_ID with the new ID. All other functions update immediately. No manual intervention needed. If lab-connect fails immediately after lab-create completes: Install the Session Manager plugin locally (see Part 7). One-time setup. If the plugin is installed but you still get "instance not reachable": The sleep 180 in lab-create handles most cases but on a slow boot it may need another minute. You used --prompt instead of --prompt-str in the function definition. Fish requires --prompt-str. Rewrite the function: These exercises are part of a larger project: an open source repository of LeetCode-style secure coding exercises designed to: If this post was useful, the best thing you can do is star the repo: Support this project — Star it on GitHub! ⭐ Stars help the repo appear in GitHub search and signal to AI companies that this dataset is worth using. Why do you read security engineering blog posts? Your answer helps me write better content: Week 9 Part 2 — Now that the lab is running, the actual Suricata IDS setup: installing Suricata on Debian 13, writing 5 custom detection rules (SQL injection focus), PCAP analysis, and the AWS Network Firewall connection — every Suricata rule in this lab is directly portable to AWS Network Firewall because AWS uses Suricata-compatible IPS rules natively since November 2020. Series: Security Engineering Interview Prep | Week 9 | March 2026

GitHub: fosres/SecEng-Exercises Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

IAM Console → Users → Create user → Username: your_username_here → Check: Provide user access to AWS Management Console → Select: I want to create an IAM user → Custom password: (set a strong password) → Uncheck: Users must create new password at next sign-in → Next IAM Console → Users → Create user → Username: your_username_here → Check: Provide user access to AWS Management Console → Select: I want to create an IAM user → Custom password: (set a strong password) → Uncheck: Users must create new password at next sign-in → Next IAM Console → Users → Create user → Username: your_username_here → Check: Provide user access to AWS Management Console → Select: I want to create an IAM user → Custom password: (set a strong password) → Uncheck: Users must create new password at next sign-in → Next → Attach policies directly → Search and check: AmazonEC2FullAccess → Search and check: AmazonSSMFullAccess → Next → Create user → Attach policies directly → Search and check: AmazonEC2FullAccess → Search and check: AmazonSSMFullAccess → Next → Create user → Attach policies directly → Search and check: AmazonEC2FullAccess → Search and check: AmazonSSMFullAccess → Next → Create user IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Authenticator app → Scan the QR code with your authenticator app → Enter two consecutive 6-digit codes to confirm IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Authenticator app → Scan the QR code with your authenticator app → Enter two consecutive 6-digit codes to confirm IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Authenticator app → Scan the QR code with your authenticator app → Enter two consecutive 6-digit codes to confirm IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Passkey or security key → Insert YubiKey and tap when prompted IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Passkey or security key → Insert YubiKey and tap when prompted IAM → Users → your_username_here → Security credentials tab → Multi-factor authentication (MFA) → Assign MFA device → MFA device type: Passkey or security key → Insert YubiKey and tap when prompted https://<YOUR_ACCOUNT_ID>.signin.aws.amazon.com/console https://<YOUR_ACCOUNT_ID>.signin.aws.amazon.com/console https://<YOUR_ACCOUNT_ID>.signin.aws.amazon.com/console # Install dependencies sudo apt update && sudo apt install curl unzip -y # Download from AWS directly # Note: never use apt install awscli — the Debian repo does not include v2 curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install # Verify aws --version # aws-cli/2.34.9 Python/3.13.11 Linux/6.1.0-43-amd64 exe/x86_64.debian.12 # Install dependencies sudo apt update && sudo apt install curl unzip -y # Download from AWS directly # Note: never use apt install awscli — the Debian repo does not include v2 curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install # Verify aws --version # aws-cli/2.34.9 Python/3.13.11 Linux/6.1.0-43-amd64 exe/x86_64.debian.12 # Install dependencies sudo apt update && sudo apt install curl unzip -y # Download from AWS directly # Note: never use apt install awscli — the Debian repo does not include v2 curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install # Verify aws --version # aws-cli/2.34.9 Python/3.13.11 Linux/6.1.0-43-amd64 exe/x86_64.debian.12 IAM → Users → your_username_here → Security credentials tab → Access keys → Create access key → Use case: Command Line Interface (CLI) → Check the amber warning confirmation box → Next → Create access key → DOWNLOAD .csv FILE IMMEDIATELY IAM → Users → your_username_here → Security credentials tab → Access keys → Create access key → Use case: Command Line Interface (CLI) → Check the amber warning confirmation box → Next → Create access key → DOWNLOAD .csv FILE IMMEDIATELY IAM → Users → your_username_here → Security credentials tab → Access keys → Create access key → Use case: Command Line Interface (CLI) → Check the amber warning confirmation box → Next → Create access key → DOWNLOAD .csv FILE IMMEDIATELY aws configure # AWS Access Key ID: AKIA... (from the .csv) # AWS Secret Access Key: ... (from the .csv) # Default region name: us-east-1 # Default output format: json aws configure # AWS Access Key ID: AKIA... (from the .csv) # AWS Secret Access Key: ... (from the .csv) # Default region name: us-east-1 # Default output format: json aws configure # AWS Access Key ID: AKIA... (from the .csv) # AWS Secret Access Key: ... (from the .csv) # Default region name: us-east-1 # Default output format: json aws sts get-caller-identity aws sts get-caller-identity aws sts get-caller-identity { "UserId": "AIDA xxxxxxxxxxxxxxxxxxxx", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:iam::YOUR_ACCOUNT_ID:user/your_username_here" } { "UserId": "AIDA xxxxxxxxxxxxxxxxxxxx", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:iam::YOUR_ACCOUNT_ID:user/your_username_here" } { "UserId": "AIDA xxxxxxxxxxxxxxxxxxxx", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:iam::YOUR_ACCOUNT_ID:user/your_username_here" } IAM Console → Roles → Create role → Trusted entity: AWS service → Use case: EC2 → Next → Search: AmazonSSMManagedInstanceCore → Check the box → Next → Role name: suricata-lab-ssm-role → Create role IAM Console → Roles → Create role → Trusted entity: AWS service → Use case: EC2 → Next → Search: AmazonSSMManagedInstanceCore → Check the box → Next → Role name: suricata-lab-ssm-role → Create role IAM Console → Roles → Create role → Trusted entity: AWS service → Use case: EC2 → Next → Search: AmazonSSMManagedInstanceCore → Check the box → Next → Role name: suricata-lab-ssm-role → Create role YOU (your_username_here) → authenticated via IAM credentials can start SSM sessions, launch EC2 EC2 INSTANCE → authenticated via suricata-lab-ssm-role can communicate with SSM can receive your session connection YOU (your_username_here) → authenticated via IAM credentials can start SSM sessions, launch EC2 EC2 INSTANCE → authenticated via suricata-lab-ssm-role can communicate with SSM can receive your session connection YOU (your_username_here) → authenticated via IAM credentials can start SSM sessions, launch EC2 EC2 INSTANCE → authenticated via suricata-lab-ssm-role can communicate with SSM can receive your session connection EC2 → Security Groups → Create security group → Name: suricata-lab-sg → Inbound rules: DELETE any default rules → NONE → Outbound rules: keep default (all traffic) → Create security group EC2 → Security Groups → Create security group → Name: suricata-lab-sg → Inbound rules: DELETE any default rules → NONE → Outbound rules: keep default (all traffic) → Create security group EC2 → Security Groups → Create security group → Name: suricata-lab-sg → Inbound rules: DELETE any default rules → NONE → Outbound rules: keep default (all traffic) → Create security group EC2 → Launch Instance Name: suricata-ids-lab AMI: Debian 13 (Bookworm) → Browse AMIs → AWS Marketplace AMIs → Search: Debian 13 → Publisher: Debian (official only — verify this) Instance type: t2.micro (free tier eligible) Key pair: Proceed without a key pair Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role EC2 → Launch Instance Name: suricata-ids-lab AMI: Debian 13 (Bookworm) → Browse AMIs → AWS Marketplace AMIs → Search: Debian 13 → Publisher: Debian (official only — verify this) Instance type: t2.micro (free tier eligible) Key pair: Proceed without a key pair Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role EC2 → Launch Instance Name: suricata-ids-lab AMI: Debian 13 (Bookworm) → Browse AMIs → AWS Marketplace AMIs → Search: Debian 13 → Publisher: Debian (official only — verify this) Instance type: t2.micro (free tier eligible) Key pair: Proceed without a key pair Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ -o "/tmp/session-manager-plugin.deb" sudo dpkg -i /tmp/session-manager-plugin.deb # Verify session-manager-plugin --version curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ -o "/tmp/session-manager-plugin.deb" sudo dpkg -i /tmp/session-manager-plugin.deb # Verify session-manager-plugin --version curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" \ -o "/tmp/session-manager-plugin.deb" sudo dpkg -i /tmp/session-manager-plugin.deb # Verify session-manager-plugin --version # set -U creates a universal variable — persists across all sessions permanently # No source command, no file editing, no restart needed set -U INSTANCE_ID "i-your_instance_id_here" # set -U creates a universal variable — persists across all sessions permanently # No source command, no file editing, no restart needed set -U INSTANCE_ID "i-your_instance_id_here" # set -U creates a universal variable — persists across all sessions permanently # No source command, no file editing, no restart needed set -U INSTANCE_ID "i-your_instance_id_here" mkdir -p ~/.config/fish/functions mkdir -p ~/.config/fish/functions mkdir -p ~/.config/fish/functions function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end function lab-login aws sso login --profile lab-sso end function lab-login aws sso login --profile lab-sso end function lab-login aws sso login --profile lab-sso end function lab-terminate echo "WARNING: This will permanently delete the instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end function lab-terminate echo "WARNING: This will permanently delete the instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end function lab-terminate echo "WARNING: This will permanently delete the instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end lab-status # → running lab-connect # → Starting session with SessionId: your_username_here-.... whoami # → ssm-user lab-status # → running lab-connect # → Starting session with SessionId: your_username_here-.... whoami # → ssm-user lab-status # → running lab-connect # → Starting session with SessionId: your_username_here-.... whoami # → ssm-user AWS Console → search "IAM Identity Center" → Enable IAM Identity Center → Enable with AWS Organizations → Continue → wait 1-2 minutes Save from the Settings summary: Access portal URL: https://d-xxxxxxxxxx.awsapps.com/start Primary Region: us-east-1 AWS Console → search "IAM Identity Center" → Enable IAM Identity Center → Enable with AWS Organizations → Continue → wait 1-2 minutes Save from the Settings summary: Access portal URL: https://d-xxxxxxxxxx.awsapps.com/start Primary Region: us-east-1 AWS Console → search "IAM Identity Center" → Enable IAM Identity Center → Enable with AWS Organizations → Continue → wait 1-2 minutes Save from the Settings summary: Access portal URL: https://d-xxxxxxxxxx.awsapps.com/start Primary Region: us-east-1 IAM Identity Center → Settings → Configure MFA Prompt users for MFA: ● Every time they sign in (always-on) MFA types: ☑ Security keys and built-in authenticators (FIDO2) ☐ Authenticator apps ← leave UNCHECKED If no MFA device: ● Require them to register at sign in → Save IAM Identity Center → Settings → Configure MFA Prompt users for MFA: ● Every time they sign in (always-on) MFA types: ☑ Security keys and built-in authenticators (FIDO2) ☐ Authenticator apps ← leave UNCHECKED If no MFA device: ● Require them to register at sign in → Save IAM Identity Center → Settings → Configure MFA Prompt users for MFA: ● Every time they sign in (always-on) MFA types: ☑ Security keys and built-in authenticators (FIDO2) ☐ Authenticator apps ← leave UNCHECKED If no MFA device: ● Require them to register at sign in → Save IAM Identity Center → Permission sets → Create permission set → Predefined permission set → Policy: PowerUserAccess → Permission set name: lab-power-user → Session duration: 8 hours → Create IAM Identity Center → Permission sets → Create permission set → Predefined permission set → Policy: PowerUserAccess → Permission set name: lab-power-user → Session duration: 8 hours → Create IAM Identity Center → Permission sets → Create permission set → Predefined permission set → Policy: PowerUserAccess → Permission set name: lab-power-user → Session duration: 8 hours → Create Allows: ALL AWS actions EXCEPT iam:*, organizations:*, account:* Explicitly allows (narrow exceptions): iam:CreateServiceLinkedRole iam:DeleteServiceLinkedRole iam:ListRoles organizations:DescribeOrganization Does NOT include: iam:PassRole ← this matters (see Part 14) Allows: ALL AWS actions EXCEPT iam:*, organizations:*, account:* Explicitly allows (narrow exceptions): iam:CreateServiceLinkedRole iam:DeleteServiceLinkedRole iam:ListRoles organizations:DescribeOrganization Does NOT include: iam:PassRole ← this matters (see Part 14) Allows: ALL AWS actions EXCEPT iam:*, organizations:*, account:* Explicitly allows (narrow exceptions): iam:CreateServiceLinkedRole iam:DeleteServiceLinkedRole iam:ListRoles organizations:DescribeOrganization Does NOT include: iam:PassRole ← this matters (see Part 14) IAM Identity Center → Users → Add user → Username: your_username_here → Password: Send an email to this user → Email address: [email protected] → First name / Last name: fill in → Next → Add user Check email → click activation link → set password IAM Identity Center → Users → Add user → Username: your_username_here → Password: Send an email to this user → Email address: [email protected] → First name / Last name: fill in → Next → Add user Check email → click activation link → set password IAM Identity Center → Users → Add user → Username: your_username_here → Password: Send an email to this user → Email address: [email protected] → First name / Last name: fill in → Next → Add user Check email → click activation link → set password IAM Identity Center → Users → your_username_here → MFA devices tab → Register device → Security key (FIDO2) → Insert YubiKey and tap when prompted → Register IAM Identity Center → Users → your_username_here → MFA devices tab → Register device → Security key (FIDO2) → Insert YubiKey and tap when prompted → Register IAM Identity Center → Users → your_username_here → MFA devices tab → Register device → Security key (FIDO2) → Insert YubiKey and tap when prompted → Register IAM Identity Center → Users → your_username_here → AWS accounts tab → Assign accounts → Select: your account (your_account_name_here / YOUR_ACCOUNT_ID) → Next → Select permission set: lab-power-user → Assign IAM Identity Center → Users → your_username_here → AWS accounts tab → Assign accounts → Select: your account (your_account_name_here / YOUR_ACCOUNT_ID) → Next → Select permission set: lab-power-user → Assign IAM Identity Center → Users → your_username_here → AWS accounts tab → Assign accounts → Select: your account (your_account_name_here / YOUR_ACCOUNT_ID) → Next → Select permission set: lab-power-user → Assign aws configure sso # Prompts: # SSO session name: lab-sso # SSO start URL: https://d-xxxxxxxxxx.awsapps.com/start # SSO region: us-east-1 # SSO registration scopes: sso:account:access (press Enter) # Browser opens automatically # Log in → tap YubiKey # Browser: "Your credentials have been shared successfully" # Back in terminal: # CLI default client Region: us-east-1 # CLI default output format: json # CLI profile name: lab-sso aws configure sso # Prompts: # SSO session name: lab-sso # SSO start URL: https://d-xxxxxxxxxx.awsapps.com/start # SSO region: us-east-1 # SSO registration scopes: sso:account:access (press Enter) # Browser opens automatically # Log in → tap YubiKey # Browser: "Your credentials have been shared successfully" # Back in terminal: # CLI default client Region: us-east-1 # CLI default output format: json # CLI profile name: lab-sso aws configure sso # Prompts: # SSO session name: lab-sso # SSO start URL: https://d-xxxxxxxxxx.awsapps.com/start # SSO region: us-east-1 # SSO registration scopes: sso:account:access (press Enter) # Browser opens automatically # Log in → tap YubiKey # Browser: "Your credentials have been shared successfully" # Back in terminal: # CLI default client Region: us-east-1 # CLI default output format: json # CLI profile name: lab-sso aws sts get-caller-identity --profile lab-sso aws sts get-caller-identity --profile lab-sso aws sts get-caller-identity --profile lab-sso { "UserId": "AROA xxxxxxxxxxxxxxxxxxxx:your_username_here", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:sts::YOUR_ACCOUNT_ID:assumed-role/AWSReservedSSO_lab-power-user_xxxxxxxxxxxxxxxxx/your_username_here" } { "UserId": "AROA xxxxxxxxxxxxxxxxxxxx:your_username_here", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:sts::YOUR_ACCOUNT_ID:assumed-role/AWSReservedSSO_lab-power-user_xxxxxxxxxxxxxxxxx/your_username_here" } { "UserId": "AROA xxxxxxxxxxxxxxxxxxxx:your_username_here", "Account": "YOUR_ACCOUNT_ID", "Arn": "arn:aws:sts::YOUR_ACCOUNT_ID:assumed-role/AWSReservedSSO_lab-power-user_xxxxxxxxxxxxxxxxx/your_username_here" } EC2 → Launch Templates → Create launch template Launch template name: suricata-lab-template Description: Debian 13 + SSM Agent via User Data AMI: Debian 13 (official Debian publisher) Instance type: t2.micro Key pair: Don't include in launch template Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role User data: #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent → Create launch template EC2 → Launch Templates → Create launch template Launch template name: suricata-lab-template Description: Debian 13 + SSM Agent via User Data AMI: Debian 13 (official Debian publisher) Instance type: t2.micro Key pair: Don't include in launch template Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role User data: #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent → Create launch template EC2 → Launch Templates → Create launch template Launch template name: suricata-lab-template Description: Debian 13 + SSM Agent via User Data AMI: Debian 13 (official Debian publisher) Instance type: t2.micro Key pair: Don't include in launch template Security group: Select existing → suricata-lab-sg Advanced details: IAM instance profile: suricata-lab-ssm-role User data: #!/bin/bash apt-get update -y wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb dpkg -i amazon-ssm-agent.deb systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent → Create launch template # Rewrite lab-connect echo 'function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end' > ~/.config/fish/functions/lab-connect.fish # Rewrite lab-status echo 'function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end' > ~/.config/fish/functions/lab-status.fish # Rewrite lab-start echo 'function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end' > ~/.config/fish/functions/lab-start.fish # Rewrite lab-stop echo 'function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end' > ~/.config/fish/functions/lab-stop.fish # Rewrite lab-terminate echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish # Rewrite lab-connect echo 'function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end' > ~/.config/fish/functions/lab-connect.fish # Rewrite lab-status echo 'function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end' > ~/.config/fish/functions/lab-status.fish # Rewrite lab-start echo 'function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end' > ~/.config/fish/functions/lab-start.fish # Rewrite lab-stop echo 'function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end' > ~/.config/fish/functions/lab-stop.fish # Rewrite lab-terminate echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish # Rewrite lab-connect echo 'function lab-connect aws ssm start-session --target $INSTANCE_ID --profile lab-sso end' > ~/.config/fish/functions/lab-connect.fish # Rewrite lab-status echo 'function lab-status aws ec2 describe-instances --instance-ids $INSTANCE_ID \ --profile lab-sso \ --query "Reservations[0].Instances[0].State.Name" \ --output text end' > ~/.config/fish/functions/lab-status.fish # Rewrite lab-start echo 'function lab-start aws ec2 start-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Starting... wait 60 seconds" end' > ~/.config/fish/functions/lab-start.fish # Rewrite lab-stop echo 'function lab-stop aws ec2 stop-instances --instance-ids $INSTANCE_ID --profile lab-sso echo "Stopping..." end' > ~/.config/fish/functions/lab-stop.fish # Rewrite lab-terminate echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish aws: [ERROR]: An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform: iam:PassRole on resource: ...suricata-lab-ssm-role... aws: [ERROR]: An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform: iam:PassRole on resource: ...suricata-lab-ssm-role... aws: [ERROR]: An error occurred (UnauthorizedOperation) when calling the RunInstances operation: You are not authorized to perform: iam:PassRole on resource: ...suricata-lab-ssm-role... IAM Identity Center → Permission sets → lab-power-user → Inline policy → Add inline policy IAM Identity Center → Permission sets → lab-power-user → Inline policy → Add inline policy IAM Identity Center → Permission sets → lab-power-user → Inline policy → Add inline policy { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/suricata-lab-ssm-role", "Condition": { "StringEquals": { "iam:PassedToService": "ec2.amazonaws.com" } } } ] } { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/suricata-lab-ssm-role", "Condition": { "StringEquals": { "iam:PassedToService": "ec2.amazonaws.com" } } } ] } { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "iam:PassRole", "Resource": "arn:aws:iam::YOUR_ACCOUNT_ID:role/suricata-lab-ssm-role", "Condition": { "StringEquals": { "iam:PassedToService": "ec2.amazonaws.com" } } } ] } → Save changes (Reprovisioning happens automatically — wait ~30 seconds) → Save changes (Reprovisioning happens automatically — wait ~30 seconds) → Save changes (Reprovisioning happens automatically — wait ~30 seconds) function lab-create echo "Launching new suricata-ids-lab instance..." set new_id (aws ec2 run-instances \ --launch-template LaunchTemplateName=suricata-lab-template \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=suricata-ids-lab}]" \ --profile lab-sso \ --query "Instances[0].InstanceId" \ --output text) if test -z "$new_id" echo "ERROR: Failed to launch instance." return 1 end echo "Instance launched: $new_id" echo "Updating INSTANCE_ID..." set -U INSTANCE_ID $new_id echo "Waiting for instance to reach running state..." aws ec2 wait instance-running \ --instance-ids $new_id \ --profile lab-sso echo "Instance is running." echo "Waiting 3 minutes for SSM Agent to initialize..." sleep 180 echo "Ready. Run lab-connect to start your session." end function lab-create echo "Launching new suricata-ids-lab instance..." set new_id (aws ec2 run-instances \ --launch-template LaunchTemplateName=suricata-lab-template \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=suricata-ids-lab}]" \ --profile lab-sso \ --query "Instances[0].InstanceId" \ --output text) if test -z "$new_id" echo "ERROR: Failed to launch instance." return 1 end echo "Instance launched: $new_id" echo "Updating INSTANCE_ID..." set -U INSTANCE_ID $new_id echo "Waiting for instance to reach running state..." aws ec2 wait instance-running \ --instance-ids $new_id \ --profile lab-sso echo "Instance is running." echo "Waiting 3 minutes for SSM Agent to initialize..." sleep 180 echo "Ready. Run lab-connect to start your session." end function lab-create echo "Launching new suricata-ids-lab instance..." set new_id (aws ec2 run-instances \ --launch-template LaunchTemplateName=suricata-lab-template \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=suricata-ids-lab}]" \ --profile lab-sso \ --query "Instances[0].InstanceId" \ --output text) if test -z "$new_id" echo "ERROR: Failed to launch instance." return 1 end echo "Instance launched: $new_id" echo "Updating INSTANCE_ID..." set -U INSTANCE_ID $new_id echo "Waiting for instance to reach running state..." aws ec2 wait instance-running \ --instance-ids $new_id \ --profile lab-sso echo "Instance is running." echo "Waiting 3 minutes for SSM Agent to initialize..." sleep 180 echo "Ready. Run lab-connect to start your session." end # Start of session lab-login # browser opens → tap YubiKey → 8hr credentials issued # Launch fresh instance lab-create # launches, waits for running, waits 3min for SSM Agent # Do work lab-connect # shell as ssm-user — no SSH, no ports, no keys # End of session lab-terminate # permanently deletes instance → zero ongoing cost # Start of session lab-login # browser opens → tap YubiKey → 8hr credentials issued # Launch fresh instance lab-create # launches, waits for running, waits 3min for SSM Agent # Do work lab-connect # shell as ssm-user — no SSH, no ports, no keys # End of session lab-terminate # permanently deletes instance → zero ongoing cost # Start of session lab-login # browser opens → tap YubiKey → 8hr credentials issued # Launch fresh instance lab-create # launches, waits for running, waits 3min for SSM Agent # Do work lab-connect # shell as ssm-user — no SSH, no ports, no keys # End of session lab-terminate # permanently deletes instance → zero ongoing cost aws: [ERROR]: Error when retrieving token from sso: Token has expired and refresh failed aws: [ERROR]: Error when retrieving token from sso: Token has expired and refresh failed aws: [ERROR]: Error when retrieving token from sso: Token has expired and refresh failed lab-status lab-connect lab-status lab-connect lab-status lab-connect lab-status # → None lab-status # → None lab-status # → None SessionManagerPlugin is not found SessionManagerPlugin is not found SessionManagerPlugin is not found # The SSM Agent may still be initializing # Wait 2-3 more minutes and try again lab-connect # The SSM Agent may still be initializing # Wait 2-3 more minutes and try again lab-connect # The SSM Agent may still be initializing # Wait 2-3 more minutes and try again lab-connect echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish echo 'function lab-terminate echo "WARNING: Permanently deletes instance and all data on it." echo "Instance: $INSTANCE_ID" read --prompt-str "Type YES to confirm: " confirm if test "$confirm" = "YES" aws ec2 terminate-instances \ --instance-ids $INSTANCE_ID --profile lab-sso echo "Instance terminated. Use lab-create to launch a fresh one." else echo "Cancelled." end end' > ~/.config/fish/functions/lab-terminate.fish - Login to AWS securely using either static credentials or a hardware security key (YubiKey) - Create an EC2 instance (a virtual machine in the cloud) with one command - Connect to that instance without SSH, without open ports, and without a key pair file - Stop the instance to pause billing when not in use - Terminate the instance permanently to pay zero ongoing cost between sessions - Why root credentials are dangerous (and what to use instead) - Two CLI auth methods: IAM Access Keys (simple) vs IAM Identity Center (secure) - EC2 instance with zero open inbound ports — no SSH, no bastion host - SSM Session Manager for terminal access through AWS - Fish shell lab functions: lab-login, lab-create, lab-connect, lab-terminate - A Launch Template for repeatable, cost-free instance lifecycle - The iam:PassRole error you will hit with PowerUserAccess — and the exact fix - Billing and payment settings - Account closure - IAM recovery if all other admin access is lost - EC2 Instance Connect → Debian 13 AMI has no ec2-instance-connect package - CloudShell aws ssm send-command → requires SSM Agent to already be installed - Temporarily opening port 22 → EC2 Instance Connect still fails (no package) - Resource — only suricata-lab-ssm-role, not any other role in the account - iam:PassedToService — only to EC2, not Lambda, ECS, or anything else - Calls aws ec2 run-instances using the Launch Template — no need to specify AMI, security group, IAM profile, or User Data manually - Captures the new instance ID - Automatically runs set -U INSTANCE_ID — all other functions point to the new instance immediately - aws ec2 wait instance-running — blocks until AWS confirms the instance is running - sleep 180 — waits for the User Data SSM Agent install to complete - Prints "Ready" - No running EC2 instance — no compute charges - No EBS volume — no storage charges - No static credentials on disk — credentials expired - Every lab-create starts completely fresh from the Launch Template - Train developers to write secure code - Prepare security engineers for technical interviews