AWS for DevOps — MFA & Best Practices
2.4 — MFA & Best Practices
Multi-Factor Authentication (MFA) adds a second layer of security beyond a password. IAM best practices form the foundation of a secure AWS environment.
What Is MFA?
MFA requires two pieces of evidence to authenticate:
- Something you know — password
- Something you have — a time-based one-time password (TOTP) from a device
Login Attempt
│
▼
Enter password ──► Correct? ──► Enter MFA code ──► Valid? ──► Access Granted
│ │ │ │
NO │ NO │
▼ │ ▼ │
DENIED │ DENIED │
│ │
Wrong password Wrong MFA code
MFA Device Types
| Type | Example | Best For |
|---|---|---|
| Virtual MFA | Google Authenticator, Authy | Most users |
| Hardware MFA | YubiKey, Gemalto token | Root account, high-security |
| SMS MFA | Text message code | Not recommended (SIM swap attacks) |
Enabling MFA for the Root Account (Console)
- Sign in as root → My Security Credentials
- Multi-factor authentication → Activate MFA
- Choose Virtual MFA device
- Scan the QR code with your authenticator app
- Enter two consecutive codes to verify
- Done — root account now requires MFA
Enabling MFA for an IAM User (CLI)
# Step 1 — Create a virtual MFA device
$ aws iam create-virtual-mfa-device \
--virtual-mfa-device-name alice-mfa \
--outfile /tmp/QRCode.png \
--bootstrap-method QRCodePNG
# Step 2 — Scan the QR code, then enable with two consecutive codes
$ aws iam enable-mfa-device \
--user-name alice \
--serial-number arn:aws:iam::123456789012:mfa/alice-mfa \
--authentication-code1 123456 \
--authentication-code2 789012
Requiring MFA for Sensitive Actions
You can write a policy that denies all actions unless MFA is present:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllExceptListedIfNoMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:ListMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
This says: “Deny everything (except MFA setup actions) if MFA is not present on the session.”
The Principle of Least Privilege
Grant only the permissions required to perform a task — nothing more.
✗ Bad: "Action": "*", "Resource": "*"
✓ Good: "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-bucket/*"
How to implement it:
- Start with zero permissions — new users/roles have no access
- Add permissions as needed — one service at a time
- Use Access Advisor — check which permissions are actually used
- Review quarterly — remove unused permissions
Checking Last Accessed Information
# Generate a report
$ aws iam generate-service-last-accessed-details \
--arn arn:aws:iam::123456789012:user/alice
{
"JobId": "abc123"
}
# Get the report
$ aws iam get-service-last-accessed-details --job-id abc123
{
"ServicesLastAccessed": [
{
"ServiceName": "Amazon S3",
"LastAuthenticated": "2026-03-25T10:00:00Z",
"TotalAuthenticatedEntities": 1
},
{
"ServiceName": "Amazon EC2",
"LastAuthenticated": null,
"TotalAuthenticatedEntities": 0
}
]
}
If Alice has EC2 permissions but has never used them → remove them.
IAM Best Practices Checklist
| # | Practice | Priority |
|---|---|---|
| 1 | Enable MFA on root account | Critical |
| 2 | Never use root for daily tasks | Critical |
| 3 | Delete root access keys | Critical |
| 4 | One IAM user per person | High |
| 5 | Use groups for permissions | High |
| 6 | Use roles for applications / services | High |
| 7 | Apply least privilege | High |
| 8 | Enable CloudTrail for audit | High |
| 9 | Rotate access keys every 90 days | Medium |
| 10 | Use IAM Access Analyzer | Medium |
| 11 | Set a strong password policy | Medium |
| 12 | Use permission boundaries for delegation | Medium |
Setting a Password Policy
$ aws iam update-account-password-policy \
--minimum-password-length 14 \
--require-symbols \
--require-numbers \
--require-uppercase-characters \
--require-lowercase-characters \
--max-password-age 90 \
--password-reuse-prevention 12 \
--allow-users-to-change-password
Access Keys Best Practices
# List access keys and their age
$ aws iam list-access-keys --user-name deploy-bot
{
"AccessKeyMetadata": [
{
"UserName": "deploy-bot",
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"Status": "Active",
"CreateDate": "2025-12-01T00:00:00Z"
}
]
}
# Create a new key, update your applications, then deactivate the old one
$ aws iam create-access-key --user-name deploy-bot
$ aws iam update-access-key \
--user-name deploy-bot \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive
# After confirming everything works, delete the old key
$ aws iam delete-access-key \
--user-name deploy-bot \
--access-key-id AKIAIOSFODNN7EXAMPLE
IAM Credential Report
Generate a CSV report of all users and their credential status:
$ aws iam generate-credential-report
$ aws iam get-credential-report --query 'Content' --output text | base64 --decode > report.csv
$ head -1 report.csv
user,arn,user_creation_time,password_enabled,password_last_used,mfa_active,...
This report is essential for audits — it shows who has MFA, when passwords were last rotated, and which access keys are active.
Quick Reference
| Task | Command |
|---|---|
| Enable MFA | aws iam enable-mfa-device --user-name NAME --serial-number ARN --authentication-code1 CODE --authentication-code2 CODE |
| List MFA devices | aws iam list-mfa-devices --user-name NAME |
| Set password policy | aws iam update-account-password-policy --minimum-password-length 14 ... |
| Generate credential report | aws iam generate-credential-report |
| Check last accessed | aws iam generate-service-last-accessed-details --arn ARN |
| Rotate access key | Create new → update apps → deactivate old → delete old |
Next → AWS Organizations