Skip to content

Add tooling to manage AWS IAM access keys#6872

Merged
dannyroberts merged 4 commits into
masterfrom
dmr/manage-iam-keys
May 11, 2026
Merged

Add tooling to manage AWS IAM access keys#6872
dannyroberts merged 4 commits into
masterfrom
dmr/manage-iam-keys

Conversation

@dannyroberts
Copy link
Copy Markdown
Member

@dannyroberts dannyroberts commented May 7, 2026

SAAS-19726

  • Adds scripts/aws/manage-iam-keys.py for inspecting and rotating IAM access keys, building blocks for SES SMTP credential rotation
  • Subcommands: list (JSON), create (CSV in the AWS console download format consumed by derive_ses_smtp_password.py), deactivate, reactivate, remove
  • remove refuses without --force unless the key is Inactive and either never used or last used 24h+ ago

Note 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 list outputs a JSON array with AccessKeyId, Status, CreateDate, LastUsedDate, LastUsedService, LastUsedRegion
  • ./scripts/aws/manage-iam-keys.py ses-iam ses-swiss create returns CSV that derive_ses_smtp_password.py can consume (extract field 2 as the secret)
  • ./scripts/aws/manage-iam-keys.py ses-iam ses-swiss list shows a key
  • ./scripts/aws/manage-iam-keys.py ses-iam ses-swiss remove <key-id> --force succeeds, even while it's active. (Recreate the key, this time with > /dev/null since 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 is Active
  • ./scripts/aws/manage-iam-keys.py ses-iam ses-swiss deactivate <key-id> flips status to Inactive; reactivate <key-id> flips it back to Active (the deactivate again for the next test)
  • ./scripts/aws/manage-iam-keys.py ses-iam ses-swiss remove <key-id> succeeds for an Inactive key that is never-used or last used 24h+ ago

@dannyroberts dannyroberts force-pushed the dmr/manage-iam-keys branch from 99360b7 to 11f2dc7 Compare May 7, 2026 22:07
@dannyroberts dannyroberts marked this pull request as draft May 7, 2026 22:08
dannyroberts and others added 4 commits May 8, 2026 09:47
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>
@dannyroberts dannyroberts force-pushed the dmr/manage-iam-keys branch from 11f2dc7 to c759854 Compare May 8, 2026 15:00
@dannyroberts dannyroberts marked this pull request as ready for review May 8, 2026 15:12
Copy link
Copy Markdown
Contributor

@millerdev millerdev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple suggestions, but I think nothing blocking.

Comment on lines +41 to +43
last_used = iam.get_access_key_last_used(
AccessKeyId=key['AccessKeyId']
)['AccessKeyLastUsed']
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this hit rate limits? Is there a way to do a bulk query for last used times for a set of keys?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +63 to +72
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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dannyroberts dannyroberts merged commit 61f021d into master May 11, 2026
8 checks passed
@dannyroberts dannyroberts deleted the dmr/manage-iam-keys branch May 11, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants