Add tooling to manage AWS IAM access keys#6872
Conversation
99360b7 to
11f2dc7
Compare
Implements the list subcommand of the IAM key management tool described in SAAS-19726. Outputs a JSON list of a user's access keys, including last-used date, service, and region. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These set the AWS IAM access key status to Inactive or Active respectively and print the updated key info as JSON. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates a new access key for the given IAM user and prints the credentials as CSV in the same format as the AWS console download, making the output directly consumable by derive_ses_smtp_password.py and the rest of the SES rotation tooling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the given access key and prints the removed key's info as JSON. Without --force, refuses to remove a key that is still Active or that has been used within the last 24 hours, so callers must deactivate first and let any in-flight usage drain before deletion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11f2dc7 to
c759854
Compare
millerdev
left a comment
There was a problem hiding this comment.
Couple suggestions, but I think nothing blocking.
| last_used = iam.get_access_key_last_used( | ||
| AccessKeyId=key['AccessKeyId'] | ||
| )['AccessKeyLastUsed'] |
There was a problem hiding this comment.
Could this hit rate limits? Is there a way to do a bulk query for last used times for a set of keys?
There was a problem hiding this comment.
I think there's already a built-in limit of two keys per IAM user.
Yeah okay here's where it says that: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html.
You can have a maximum of two access keys per user.
So no I don't see that being an issue—probably not worth trying to optimize further
| if args.subcommand == 'list': | ||
| print_json(list_keys(iam, args.iam_user)) | ||
| elif args.subcommand == 'create': | ||
| print_keyfile(create_key(iam, args.iam_user)) | ||
| elif args.subcommand == 'deactivate': | ||
| print_json(set_key_active(iam, args.iam_user, args.key_id, active=False)) | ||
| elif args.subcommand == 'reactivate': | ||
| print_json(set_key_active(iam, args.iam_user, args.key_id, active=True)) | ||
| elif args.subcommand == 'remove': | ||
| print_json(remove_key(iam, args.iam_user, args.key_id, force=args.force)) |
There was a problem hiding this comment.
Is it possible to run without a subcommand? Would it make sense to add an else clause to handle the case of no matching subcommand?
There was a problem hiding this comment.
Here's the behavior without a subcommand, tested locally:
(ccc) ➜ commcare-cloud git:(dmr/manage-iam-keys) ./scripts/aws/manage-iam-keys.py commcare-staging:ses ses-staging
usage: manage-iam-keys.py [-h] aws_profile iam_user {list,create,deactivate,reactivate,remove} ...
manage-iam-keys.py: error: the following arguments are required: subcommand
(ccc) ➜ commcare-cloud git:(dmr/manage-iam-keys) echo $?
2
So the parser will notice if the subcommand is missing, print an error, and exit with a non-zero status code. I think due to the required=True in the 'subcommand' arg (assigned to the subparser variable) above.
There was a problem hiding this comment.
I suppose I could add something like
else:
assert False, "Since `subcommand` is required, this code should not be reachable"
to make that extra-explicit, but on the flip side it's kind of redundant with the required=True on subcommand above, so it's a bit of a wash—in general I do like to see if/elif have an else like you're saying when it's clearly being used as like a switch like this, but I'm not sure it adds much in this case.
SAAS-19726
scripts/aws/manage-iam-keys.pyfor inspecting and rotating IAM access keys, building blocks for SES SMTP credential rotationlist(JSON),create(CSV in the AWS console download format consumed byderive_ses_smtp_password.py),deactivate,reactivate,removeremoverefuses without--forceunless the key isInactiveand either never used or last used 24h+ agoNote for this to work you need an aws profile (e.g. in your
~/.aws/config) that has access to the IAM keys you're interested in—it'll need to have the correct account number and role.Environments Affected
None — this adds a new standalone script under
scripts/aws/and does not modify any environment files.Test plan
./scripts/aws/manage-iam-keys.py ses-iam ses-staging listoutputs a JSON array withAccessKeyId,Status,CreateDate,LastUsedDate,LastUsedService,LastUsedRegion./scripts/aws/manage-iam-keys.py ses-iam ses-swiss createreturns CSV thatderive_ses_smtp_password.pycan consume (extract field 2 as the secret)./scripts/aws/manage-iam-keys.py ses-iam ses-swiss listshows a key./scripts/aws/manage-iam-keys.py ses-iam ses-swiss remove <key-id> --forcesucceeds, even while it's active. (Recreate the key, this time with> /dev/nullsince we don't need to see the output, for the next tests.)./scripts/aws/manage-iam-keys.py ses-iam ses-swiss remove <key-id>refuses while the key isActive./scripts/aws/manage-iam-keys.py ses-iam ses-swiss deactivate <key-id>flips status toInactive;reactivate <key-id>flips it back toActive(the deactivate again for the next test)./scripts/aws/manage-iam-keys.py ses-iam ses-swiss remove <key-id>succeeds for anInactivekey that is never-used or last used 24h+ ago