AWS 📖 20 min read

Secure Your AWS Account with IAM

By the end, the root user has MFA and no access keys, you sign in day to day through IAM Identity Center or an MFA-protected admin user, workloads use roles instead of stored keys, and CloudTrail plus a budget alert tell you when something unusual happens. It's for the owner of a personal or small-team AWS account that was set up with the root login and hasn't been locked down since.

What You Need

  • Hardware: Any computer with a web browser, plus a phone authenticator app, passkey or security key for MFA
  • Software: An AWS account and its root email/password. IAM, IAM Identity Center, CloudTrail event history and AWS Budgets alerts used here don't need a paid plan, but check the Free Tier rules for your account type (they changed for accounts created on or after July 15, 2025). AWS CLI v2 is optional.
  • Time: 30-60 minutes

Quick Security Checklist

Each item closes off one way an account typically gets abused. If you do nothing else from this article, do these.

  1. ☐ MFA enabled on the root user
  2. ☐ No root access keys (there shouldn't be any)
  3. ☐ A separate admin identity for daily use (IAM Identity Center user, or an IAM user)
  4. ☐ MFA enabled on that admin identity
  5. ☐ No access keys in code repositories
  6. ☐ Budget alert configured
  7. ☐ CloudTrail event history checked, and a trail created for long-term records
  8. ☐ Old/unused credentials removed

The steps below cover each item in order, with the console path and a way to confirm it worked. AWS's own list is Security best practices in IAM, which this guide follows.

What's IAM?

IAM stands for Identity and Access Management. Short version: it's the system that decides who gets to do what inside your AWS account. Every API call (launching an EC2 instance, reading a file from S3, invoking a Lambda function) is authorized by IAM first. If no policy allows it, the call fails.

Four things you need to know:

  • Users: identities with long-term credentials (a password and/or access keys).
  • Groups: bundles of users who need the same access.
  • Roles: identities that are assumed temporarily. AWS services use them (your Lambda function and your EC2 instance each get a role), and so do people who sign in through IAM Identity Center.
  • Policies: JSON documents that spell out what's allowed and what isn't.

Step 1: Stop Using Root

Your AWS account comes with a root user. It can do everything: close the account, change billing, delete every resource in every Region. No IAM policy in the account can restrict it. That's why the root password and its MFA device are the most valuable credentials you have.

Don't use root for anything day-to-day. Keep it for the small set of tasks that require it, such as changing account settings, some billing tasks and closing the account. AWS lists them under tasks that require root user credentials. For everything else, use the admin identity from Step 2.

Enable MFA on Root (Right Now)

Do this first. AWS now requires MFA on the root user of every account type: if it isn't set up, you have to register it within 35 days of your first console sign-in attempt. AWS recommends a passkey or security key where possible because they resist phishing; an authenticator app also works. These steps are for an authenticator app, as described in AWS's root user MFA guide:

  1. Sign in to the AWS Management Console as the root user
  2. On the right side of the navigation bar, choose your account name, then Security credentials
  3. In the Multi-Factor Authentication (MFA) section, choose Assign MFA device
  4. Enter a Device name, choose Authenticator app, then Next
  5. Choose Show QR code and scan it with your authenticator app (or use Show secret key and type it in)
  6. Enter two consecutive codes in MFA code 1 and MFA code 2
  7. Choose Add MFA

Now a stolen root password alone isn't enough to sign in. Store the secret key or QR code somewhere safe, or register a second MFA device (up to eight are allowed), so losing your phone doesn't lock you out. Before you rely on MFA, also check that the account's email address and phone number are current: they're how AWS verifies you if the MFA device is lost.

Check it worked: the device now appears in the MFA section of Security credentials. Sign out, sign back in as root, and confirm you're asked for a code or passkey. On the same page, the Access keys section should be empty. If a root access key exists, choose Actions → Deactivate, confirm nothing breaks, then delete it; root keys can't be limited by any policy.

Step 2: Set Up Your Daily Admin Sign-In

Once root is locked down, you need a separate identity for daily work. AWS's best practice is for people to sign in through federation with IAM Identity Center and get temporary credentials, rather than to have IAM users with long-term passwords and keys. For a personal account an IAM user is simpler and still common, so both options are below. Pick one.

Terminal window with the commands from the section Step 2: Set Up Your Daily Admin Sign-In
One-time SSO setup. The ARN from get-caller-identity should contain AWSReservedSSO_AdministratorAccess.

Don't lock yourself out: keep the root session open in one browser window while you do this, and test the new identity in a private window. Sign out of root only after the new admin sign-in works and has MFA.

Option A: IAM Identity Center (Recommended)

Identity Center gives you a sign-in portal (the AWS access portal), users with their own MFA, and "permission sets" that become short-lived role sessions in your account. Nothing you use day-to-day is a long-term access key. The flow, per the Identity Center setup guide:

  1. Open the IAM Identity Center console and enable it. For access to AWS accounts you need an organization instance; on a standalone account, enabling it this way also sets up AWS Organizations with your account as the management account.
  2. Create a user for yourself, with your own email address, and set up MFA for it.
  3. Create a permission set. The predefined AdministratorAccess permission set is the equivalent of an admin user; PowerUserAccess is a narrower choice for development work.
  4. Under AWS accounts, assign your user and the permission set to your account.
  5. Bookmark the AWS access portal URL shown in the Identity Center settings and sign in there from now on.

For the CLI, run aws configure sso once and then aws sso login when your session expires. The CLI caches temporary credentials; there is nothing long-lived in ~/.aws/credentials to leak (see Configuring IAM Identity Center authentication with the AWS CLI).

Bash
# One-time setup: prompts for the access portal URL, Region and a profile name
aws configure sso

# Verify: the Arn should contain assumed-role/AWSReservedSSO_AdministratorAccess_...
aws sts get-caller-identity --profile my-admin

Option B: An IAM User

Simpler, and fine for a single-person account as long as it has MFA and no unnecessary access keys. The console may suggest Identity Center at this point; choose to create an IAM user.

  1. Open the IAM Console
  2. Choose Users, then Create user
  3. Give it a name that says what it is, for example yourname-admin
  4. Give the user access to the AWS Management Console
  5. Pick a strong password (or let AWS generate one)
  6. Choose Next

Attach Permissions

  1. Select Attach policies directly (or add the user to an admin group, see Step 6)
  2. Search for AdministratorAccess and check the box
  3. Click through to Create user

AWS gives you a sign-in URL specific to IAM users. It looks something like https://123456789012.signin.aws.amazon.com/console. Bookmark that URL and use it from now on. The root login page is for the root-only tasks above. Open it in a private window and sign in as the new user to confirm it works; the account menu at the top right should show yourname-admin, not the root account name.

Enable MFA for the Admin User Too

Yes, this one too. The admin user has AdministratorAccess, which lets it do almost everything root can. MFA here is not optional.

  1. Open the user you just created
  2. Open the Security credentials tab
  3. Under MFA, choose Assign MFA device
  4. Same steps as root: device name, authenticator app (or passkey/security key), scan, two codes, done

Step 3: Understand Least Privilege

Least privilege means every user, role and service gets only the permissions it actually needs. Nothing extra. Nothing "just in case." It's what limits the damage when a credential leaks: keys scoped to one S3 bucket can't launch EC2 instances, so a leak costs you a bucket's contents rather than an account full of resources you didn't create.

Editor window showing Custom policy from this section
A custom policy that names one bucket. AmazonS3FullAccess would cover every bucket in the account.

Practically, this means: a developer working on Lambda doesn't get billing access. A CI/CD pipeline deploying to S3 doesn't get EC2 permissions. A monitoring tool reading CloudWatch logs gets read access, not write.

Start With a Managed Policy, Then Narrow It

AWS managed policies are a reasonable starting point. A policy like AmazonS3FullAccess limits an identity to one service: it can read, write and delete objects in every bucket, but it can't launch instances, invoke functions or touch databases.

For scripts, though, prefer a role (Step 5) over an IAM user with access keys. Only create a programmatic IAM user, with console access turned off, when the code runs somewhere that can't assume a role.

Custom Policies

The catch with AmazonS3FullAccess is that it covers every bucket in your account. If you've got a bucket with production data and a bucket with throwaway test files, that policy treats them the same. A custom policy can name the bucket:

  1. IAM → Policies → Create policy
  2. Switch to the JSON editor
Custom Policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ObjectAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::my-specific-bucket/*"
    },
    {
      "Sid": "ListTheBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-specific-bucket"
    }
  ]
}

That JSON limits the identity to my-specific-bucket and four actions. Note the two different resource ARNs: object actions apply to my-specific-bucket/*, while s3:ListBucket applies to the bucket itself. Getting that split wrong is a common reason for an unexpected AccessDenied. If these credentials leak, the damage stops at one bucket.

The JSON policy editor runs IAM Access Analyzer policy validation as you type and lists errors, security warnings and suggestions below the editor. Read them before saving.

Test Policies Before You Rely on Them

IAM errors usually don't say which part of a policy failed, and a single wrong character in an ARN (user-session-prod instead of user-sessions-prod) produces the same "AccessDenied" as a missing permission. Two habits help:

  • Copy ARNs from the console or CLI instead of typing them. Most resource pages show the ARN with a copy button.
  • Test with the IAM policy simulator. It evaluates policies against the actions and resources you choose without sending real requests. Principal mode tests the policies already attached to a user, group or role; Custom mode tests a policy you paste in before attaching it. For allows and explicit denies it shows which statement decided the result. Its results can differ from the live environment, so confirm with a real request afterwards (Testing IAM policies with the IAM policy simulator).

Step 4: Access Keys (Programmatic Access)

Access keys are long-term credentials for an IAM user. Code that has them can call AWS as that user until the keys are deactivated, which is why they're the credential that most often leaks. Two pieces:

Terminal window with the commands from the section Step 4: Access Keys (Programmatic Access)
Named profiles keep work and personal credentials apart; --profile picks one per command.
  • Access Key ID: the identifier, looks like AKIAIOSFODNN7EXAMPLE
  • Secret Access Key: the secret part. AWS shows it exactly once. If you lose it, you create a new key.

First ask whether you need them at all: on EC2, Lambda and other AWS compute, use a role (Step 5); on your own machine, Identity Center's aws sso login gives the CLI temporary credentials. If you do need a key:

  1. IAM, then Users, then click your user
  2. Open the Security credentials tab
  3. Access keys section, click Create access key
  4. Pick your use case (CLI, application, third-party service). The console suggests alternatives for each
  5. Copy both values somewhere safe immediately

Rules for Access Keys

  • Never commit keys to Git. Add .env to your .gitignore before the first commit. Public repositories are scanned continuously for credentials, so assume a pushed key is compromised even if you delete the commit.
  • Never hardcode keys in source files. Use environment variables, or better, AWS Secrets Manager.
  • Rotate and remove keys. AWS recommends updating access keys when needed (for example when someone leaves) and using "last used" information to find keys that can be removed.
  • Use roles instead of keys wherever you can. Roles don't have permanent credentials to leak. More on this in the next section.

If a key does leak: deactivate it in IAM immediately, create a replacement only if you still need one, and then check CloudTrail event history (Step 7) for what the key was used for.

Set Up AWS CLI Safely

If you do use access keys for the CLI, configure them properly:

Bash
aws configure

It prompts you for the access key, secret key, default region, and output format. The keys are stored in plain text in ~/.aws/credentials and the rest in ~/.aws/config. Treat that directory like a password vault: if someone copies those files, they have your keys. Run aws sts get-caller-identity afterwards; it should print your IAM user's ARN.

If you work across multiple AWS accounts (personal, work, client), use profiles:

Bash
aws configure --profile work
aws configure --profile personal

# Use a specific profile
aws s3 ls --profile work

Step 5: IAM Roles (The Right Way)

Roles are the default choice for anything running on AWS. A role has permissions but no permanent password or keys; whoever assumes it (an EC2 instance, a Lambda function, an Identity Center user) gets temporary credentials.

Terminal window with the commands from the section Step 5: IAM Roles (The Right Way)
Run on the instance: the ARN shows the assumed role, and listing the bucket works while writes fail.

Because the credentials expire on their own and AWS replaces them before they do, there's nothing long-lived to paste into a config file or push to GitHub.

Example: EC2 Instance Role

You've got an EC2 instance that needs to pull files from S3. Instead of creating access keys and baking them into the instance, you create a role:

  1. IAM, then Roles, then Create role
  2. Trusted entity: AWS service
  3. Use case: EC2
  4. Click Next
  5. Attach AmazonS3ReadOnlyAccess (or a custom policy limited to one bucket, as above)
  6. Name it ec2-s3-reader
  7. Create the role

Then attach it to your running instance:

  1. EC2 Console, select the instance
  2. Actions, Security, Modify IAM role
  3. Pick ec2-s3-reader
  4. Save

When you create a role for EC2 in the console, IAM also creates an instance profile with the same name; that's what the Modify IAM role list shows. The AWS CLI and SDKs on the instance fetch the role's temporary credentials from the instance metadata service, so there are no keys in config files or environment variables. To check, run this on the instance:

Bash (on the instance)
# Arn should look like arn:aws:sts::123456789012:assumed-role/ec2-s3-reader/i-0abc...
aws sts get-caller-identity

# Should list objects; a write such as "aws s3 cp" should fail with AccessDenied
aws s3 ls s3://my-specific-bucket/

If get-caller-identity says "Unable to locate credentials", the role isn't attached yet or the instance can't reach the metadata service (check that it isn't disabled under Actions → Instance settings → Modify instance metadata options). The AWS EC2 Deployment Guide uses exactly this to pull build artifacts from S3, and Your First Serverless Lambda Function shows the same idea as a Lambda execution role.

Step 6: Groups for Easy Management

If you use IAM users, managing permissions user by user gets painful once more than two or three people share an account. Groups fix this: define the permissions once on the group, then add or remove people. (In Identity Center the equivalent is assigning permission sets to Identity Center groups.)

  1. IAM, then User groups, then Create group
  2. Name it developers
  3. Attach the policies this group needs
  4. Add users

A typical split:

  • admins: full access, as few people as possible
  • developers: the services the team actually builds on, such as Lambda, API Gateway, DynamoDB, CloudWatch
  • data-team: S3, Glue, Athena, Redshift
  • billing: Cost Explorer and Budgets only

Someone joins, you add them to the right group. Someone leaves, you remove them and delete their user. No chasing individual policies across many users.

Step 7: Monitor and Audit

Permissions limit what a leaked credential can do; monitoring tells you that it's happening. Three free or low-cost tools cover most of it.

CloudTrail

CloudTrail records API calls in your account: who did what, when, and from which IP. It's on by default, and the Event history page shows the last 90 days of management events per Region at no charge.

  1. CloudTrail, then Event history. This shows recent API calls in the selected Region
  2. Filter by one attribute at a time, such as user name, resource type or event name

Event history is limited to 90 days, management events only and one Region per search. For a lasting record, create a trail that delivers logs to an S3 bucket (see Working with CloudTrail event history). After a suspected leak, this is where you find out what the credential was used for.

IAM Credential Report and Access Analyzer

Go to IAM, then Credential report, then Download report. You get a CSV with:

  • Every user and when they last signed in
  • Every access key and when it was last used
  • Who has MFA and who doesn't

Review it regularly and disable or delete users and keys that haven't been used for a long time. Unused credentials are attack surface that nobody is watching.

IAM Access Analyzer automates parts of this review. An external access analyzer reports resources such as S3 buckets, KMS keys and IAM roles that are shared outside your account; it only analyzes the Region it's created in. An unused access analyzer reports unused roles, access keys, passwords and permissions; it is charged per IAM role and user analyzed, so check the price before enabling it on a large account. Access Analyzer can also generate a least-privilege policy from a role's CloudTrail activity.

AWS Budgets

A budget alert is often the fastest signal that something is wrong, because abuse of a leaked credential usually shows up as spending.

  1. Billing and Cost Management, then Budgets, then Create budget
  2. Type: Cost budget
  3. Set a monthly amount close to what you normally spend
  4. Add email alerts at several thresholds, for example 50%, 80% and 100%, including a forecasted-cost alert

Billing data isn't real-time, so a budget alert won't fire the minute something starts, but it will usually fire long before the monthly invoice. The AWS Budgets documentation covers the options. If you do find unauthorized usage, contact AWS Support through the console and keep the CloudTrail records.

Troubleshooting AccessDenied

An "AccessDenied" (or "UnauthorizedOperation") error means the request was evaluated and not allowed. Work through it in this order:

  1. Check who you are. Run aws sts get-caller-identity. Many denials come from the wrong profile, an expired SSO session, or code running under a different role than you assumed.
  2. Read the whole error. Many services now name the action, the resource and the type of policy that denied it (identity-based, resource-based, permissions boundary, SCP).
  3. Decode the message if there is one. Some operations, notably EC2, return an encoded authorization failure message. With permission for sts:DecodeAuthorizationMessage, run aws sts decode-authorization-message --encoded-message <encoded-message>. The result says whether it was an explicit deny or a missing allow, plus the principal, action, resource and condition values (see the CLI reference).
  4. Compare ARNs character by character. Copy the ARN of the real resource and compare it with the policy. Check bucket vs object ARNs (bucket vs bucket/*), Region and account ID.
  5. Simulate. Put the principal, action and resource into the policy simulator to see which statement allows or denies it.
  6. Look for an explicit deny. A Deny anywhere (identity policy, bucket policy, permissions boundary, SCP) beats any number of allows.

AWS's Troubleshoot access denied error messages page lists the message formats each policy type produces.

Sign-in and Session Problems

  • The CLI says your SSO session or token has expired. Identity Center sessions are short-lived by design. Run aws sso login --profile my-admin and retry. If it keeps happening within the hour, check the session duration on the permission set.
  • Lost the root MFA device. On the MFA prompt, use the troubleshooting link to sign in with alternative factors: AWS verifies the account's email address, then calls the phone number on file. This is why the contact details need to be current before you rely on MFA (Recover an MFA protected identity).
  • An IAM user can't sign in on the root login page. The root page asks for the account email. IAM users sign in at the account-specific URL, or choose IAM user on the sign-in page and enter the 12-digit account ID or alias.
  • Identity Center user sees no accounts in the access portal. The user (or a group they belong to) hasn't been assigned to the account with a permission set yet. Check AWS accounts in the Identity Center console.

Clean Up Test Identities

Anything you created only to try the steps above is an unwatched credential. Remove it in this order:

  1. Access keys: deactivate the key first, wait long enough to be sure nothing depends on it (the Last used column helps), then delete it.
  2. Test roles: detach the role from any instance (Actions → Security → Modify IAM role → No IAM role), then delete it in IAM. Deleting a role in the console also removes its instance profile.
  3. Test users and custom policies: delete users you no longer need, then delete custom policies that show no attached entities.
Bash
# Find access keys and when they were last used
aws iam list-access-keys --user-name yourname-admin
aws iam get-access-key-last-used --access-key-id AKIAIOSFODNN7EXAMPLE

# Deactivate, then later delete
aws iam update-access-key --user-name yourname-admin \
  --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive
aws iam delete-access-key --user-name yourname-admin \
  --access-key-id AKIAIOSFODNN7EXAMPLE

Next Steps