You can now write any permission. The craft is writing as few as possible. Least privilege means an identity can do exactly what its job requires, and nothing else, and that this stays true as months pass.
Grant from the errors
Start a new user or role with nothing. Run the real workflow. Each AccessDenied names an action and a resource; add that line, run again. Three or four rounds produce a policy that fits, with no * you cannot explain. It feels slow the first time and is faster than every alternative, because permissions granted "to be safe" are never removed.
Read the credential report
aws iam generate-credential-report
aws iam get-credential-report --query Content --output text | base64 -dOne CSV, one line per user: whether a password exists, whether MFA is on, and for each access key whether it is active, when it was created and when and where it was last used. Three things to look for:
| Finding | Action |
|---|---|
| a human user without MFA | enable it, or deny everything sensitive until they do |
| a key not used in 90 days | delete it |
| a user with two active keys for months | rotation never finished; delete the old one |
aws iam get-access-key-last-used --access-key-id AKIA…
aws iam update-access-key --user-name legacy-bot --access-key-id AKIA… --status Inactive
aws iam delete-access-key --user-name legacy-bot --access-key-id AKIA…Inactive is the reversible step when you are not sure. Deletion is the goal.
Make MFA a condition
{
"Effect": "Deny",
"NotAction": ["iam:ChangePassword", "iam:GetUser", "iam:ListMFADevices", "iam:EnableMFADevice", "iam:CreateVirtualMFADevice", "sts:GetSessionToken"],
"Resource": "*",
"Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } }
}Attached to a group, this denies everything except setting up MFA until the user signs in with MFA. NotAction lists the exceptions. It is the one policy that belongs in every account's all-users group.
The admin user
admin exists for setup. People get their own users; programs get roles; admin and root go in a drawer with MFA on both. An account where everyone shares admin has one identity in its audit trail, which is the same as none.