IAM Access Key Hygiene Script
This section describes how IAM Access Key & User Hygiene script is used and managed within the Modernisation Platform.
IAM Access Key & User Hygiene (Extended Matrix)
This documentation describes the IAM hygiene automation consisting of:
- A scheduled GitHub Actions workflow that runs across multiple AWS accounts
- A reusable workflow that assumes a role into each account and executes a script
- A bash script that audits and enforces IAM user and access key hygiene
- An IAM role deployed to each member account to permit the required actions
High-level architecture
- A controller workflow builds a matrix of AWS accounts
- Accounts are split into multiple blocks to limit concurrency
- Each account invokes a reusable workflow
- The reusable workflow:
- Assumes an IAM role in the target account via OIDC
- Executes the IAM hygiene script
- The script:
- Discovers IAM users and access keys
- Classifies inactivity
- Optionally enforces lifecycle actions
- Optionally sends GOV.UK Notify emails
1. Workflow: IAM Access Key & User Hygiene – Extended Matrix
File: .github/workflows/iam-access-key-hygiene-extended-matrix.yml
Purpose
Runs the IAM hygiene script across all eligible member AWS accounts using a matrix strategy.
Triggers
- Scheduled:
cron: "30 9 1-7 * 1"- Runs at 09:30 UTC
- Only on days 1–7 of the month
- Only when the day is Monday
- Effectively: the first Monday of the month (when it falls within days 1–7)
- Manual:
workflow_dispatch
AWS region
- Default region:
eu-west-2
Permissions
id-token: write– required for GitHub OIDC role assumptioncontents: read– required for repository checkout
Concurrency
- Concurrency group: workflow name
- In-progress runs are not cancelled
Account matrix construction
The workflow:
- Decrypts the
ENVIRONMENT_MANAGEMENTsecret - Reads
account_idsfrom the decrypted JSON - Builds a sorted list of account names
- Applies hard exclusions
Excluded accounts
The following accounts are excluded by name or prefix:
core-*modernisation-*bichard7-*analytical-platform-*moj-network-operations-centre*testing-test
Workload splitting
To reduce execution time and avoid API throttling:
- The account list is split into 4 blocks
- Each block runs as a separate job:
iam-hygiene-1iam-hygiene-2iam-hygiene-3iam-hygiene-4
- Each job:
- Uses a matrix of accounts
- Has
max-parallel: 5 - Uses
fail-fast: false
Script execution
Each matrix job invokes the reusable workflow and passes the script command:
bash ./scripts/iam-monitoring/inactive-keys-and-users/check_iam_users_and_keys.sh --dry-run
Important notes
- The current configuration runs in dry-run mode
- No IAM resources are modified
- No GOV.UK Notify emails are sent
- Output files are still generated for review
To enable live enforcement, remove --dry-run.
2. Reusable workflow: Run Script Per Account
File: .github/workflows/reusable-run-per-account-script.yml
Purpose
Provides a reusable wrapper that:
- Checks out the repository
- Decrypts secrets
- Resolves the AWS account ID for a workspace
- Assumes an IAM role via OIDC
- Installs dependencies
- Executes a provided script
Inputs
Required:
JOB_NAME– label shown in GitHub ActionsAWS_REGION– AWS regionenvironment_management– encrypted environment management JSONTF_WORKSPACE– account name used to resolve account IDASSUME_ROLE_NAME– IAM role name to assumeSCRIPT_COMMAND– shell command to execute
Optional:
GOV_UK_NOTIFY_API_KEYTEMPLATE_IDEXPECTED_TEMPLATE_VERSIONKEY_NOTIFY_DAYSKEY_DISABLE_DAYSKEY_DELETE_DAYSUSER_NOTIFY_DAYSUSER_DISABLE_DAYSUSER_DELETE_DAYS
Secrets:
PASSPHRASE– required to decrypt secrets
AWS authentication
- Uses
aws-actions/configure-aws-credentials - Authenticates via GitHub OIDC
Assumes the role:
arn:aws:iam::
:role/
Execution behaviour
Before running the script, the workflow prints the caller identity:
aws sts get-caller-identity
It then executes the provided script command verbatim.
3. IAM Hygiene Script
File:
scripts/iam-monitoring/inactive-keys-and-users/check_iam_users_and_keys.sh
Purpose
Audits IAM users and access keys in the current AWS account and classifies them based on inactivity.
Depending on configuration, it can:
- Notify users
- Disable access keys
- Delete access keys
- Disable console login
- Delete IAM users
- Send GOV.UK Notify emails
Dry-run mode
Dry-run is enabled when:
- The first argument is
--dry-runordry-run, OR
In dry-run mode:
- No IAM write operations are performed
- No GOV.UK Notify emails are sent
- Output files are still generated
Inactivity thresholds
Defaults (in days):
Access keys
- Notify:
KEY_NOTIFY_DAYS=30 - Disable:
KEY_DISABLE_DAYS=60 - Delete:
KEY_DELETE_DAYS=90
Users
- Notify:
USER_NOTIFY_DAYS=30 - Disable:
USER_DISABLE_DAYS=60 - Delete:
USER_DELETE_DAYS=90
Thresholds can be overridden via environment variables.
Inactivity calculation
Access keys
- If the key has been used:
- Inactivity = days since last use
- If the key has never been used:
- Inactivity = days since creation
Users
Priority order:
- Console password last used
- Most recently used access key
- User creation date
Output files
The script generates the following files:
iam_hygiene.json– full structured outputkeys_notify.listkeys_disable.listkeys_delete.listusers_notify.listusers_disable.listusers_delete.list
All list files are deduplicated.
GOV.UK Notify integration
Notify is only used when:
- Not in dry-run mode
GOV_UK_NOTIFY_API_KEYis setTEMPLATE_IDis set
Recipient resolution
- The script reads an IAM user tag
- Default tag key:
infrastructure-support - Tag value must be an email address
If no email tag is present, no email is sent.
4. IAM Role: github-actions-iam-hygiene
Purpose
Allows GitHub Actions to perform IAM hygiene actions in member accounts via OIDC.
Deployment
- Deployed only to:
membermember-unrestrictedaccounts
- Created using the Modernisation Platform OIDC role module
Permissions
Discovery (read-only)
iam:ListUsersiam:ListUserTagsiam:ListAccessKeysiam:GetAccessKeyLastUsediam:ListAttachedUserPoliciesiam:ListUserPoliciesiam:ListGroupsForUser
Key lifecycle
iam:UpdateAccessKeyiam:DeleteAccessKey
Console access
iam:DeleteLoginProfile
User deletion cleanup
iam:DetachUserPolicyiam:DeleteUserPolicyiam:RemoveUserFromGroupiam:DeleteUser
All permissions are currently scoped to *.
Operational guidance
- Always run in dry-run mode first
- Review
iam_hygiene.jsonbefore enabling enforcement - Ensure Notify templates and IAM user tags are correctly configured
- User deletion is destructive and should be enabled with caution