Deleting unused access keys across AWS accounts with StackSets
How two StackSets deploy daily workers that delete keys unused for more than 180 days, with local IAM actions and central cross-account reporting.
Chris Baran
· 6 min read
On this page
With deletion enabled, the rule is straightforward: delete IAM access keys unused for more than 180 days. A key with no recorded use qualifies once it is more than 180 days old. Daily Lambda workers apply that policy within participating AWS accounts.
For an oil and gas company, our engineering contribution covered a two-stage StackSet rollout, account-local credential automation and central DynamoDB reporting. A separate authenticated, private API gives an operator interface read-only access to those records. The deployment and reporting cross account boundaries; the IAM deletion happens locally.
Apply the 180-day rule precisely
The policy has two configurable thresholds, defaulting to 30 days since creation and 180 days since last use. A key must pass the creation-age threshold before entering the deletion or rotation branch. Deletion then requires either a last-use age greater than 180 days or, for a key with no recorded use, a creation age greater than 180 days.
The decision is made per key, so another key belonging to the same user can receive a different outcome.
This original TypeScript simplifies the deployed classification. Its inputs are calculated day counts; null represents no recorded last-use date.
type Decision = "retain" | "delete" | "rotation-path";
function classify(
ageDays: number,
lastUseDays: number | null
): Decision {
if (ageDays <= 30) return "retain";
const idleDays = lastUseDays ?? ageDays;
return idleDays > 180 ? "delete" : "rotation-path";
}The comparison is strictly greater than the threshold. A calculated inactivity age of exactly 180 days does not qualify. An old key used recently goes to the rotation path, as do other older keys that miss the deletion test.
The snippet shows the classification decision only. Collection, IAM calls and reporting remain separate. The rotation/ticket integration is unfinished; classifying a key for rotation does not mean a replacement key has been issued or a ticket successfully created.
Use two StackSet rollout stages
The first stage bootstraps deployment permissions. A service-managed StackSet uses the AWS Organizations integration to install a target execution role in the selected account set.
The second stage deploys the application. A self-managed StackSet uses an administration role in the deployment account and the execution role in each target account. Through that relationship, CloudFormation provisions the daily schedule, Lambda worker and its runtime role.
The split also accommodates the application’s AWS SAM template. AWS does not support templates containing transforms in service-managed StackSets, so the plain IAM bootstrap and SAM application use different deployment paths. AWS service-managed StackSet considerations.
This YAML sketches the relationship using fictional labels. It is explanatory configuration, not a deployable CloudFormation template.
bootstrap:
permission_model: SERVICE_MANAGED
targets: selected_accounts
creates: target_deployment_role
application:
permission_model: SELF_MANAGED
administration_role: deployment_administration
execution_role: target_deployment_role
deploys:
- daily_schedule
- lambda_worker
- worker_runtime_roleThe bootstrap must establish the target role before application deployment can use it. Provisioning that role and deploying a working scheduled function are separate milestones, each with its own StackSet status. Keeping them distinct makes a failed rollout easier to locate: first check the deployment trust relationship, then the application stack and worker.
Follow the permission boundaries
The table separates deployment authority, account-local execution and central inspection using descriptive role labels.
| Flow | Path and purpose |
|---|---|
| StackSet deployment | The service-managed StackSet installs the target execution role. The self-managed StackSet follows CloudFormation → administration role → target execution role to provision the daily schedule, Lambda worker and runtime role. |
| Account-local runtime | Daily schedule → Lambda worker using its runtime role → local IAM inspection, deactivation and deletion. The worker also writes across accounts to central DynamoDB and assumes a read-only role for the Organizations account-name lookup. |
| Central inspection | DynamoDB key, action and report records → private, authenticated, read-only operator API → operator interface. |
For deployment, CloudFormation assumes the administration role, which can assume the target execution role. The target role’s trust policy accepts the administration role, and its permissions allow the application resources to be provisioned. This is the self-managed StackSet role relationship.
For runtime IAM operations, the Lambda worker uses its own account’s credentials. It enumerates users and keys through ListUsers and ListAccessKeys, then obtains usage information through GetAccessKeyLastUsed. Its runtime role also permits UpdateAccessKey and DeleteAccessKey. The worker does not assume a role in a remote account to delete that account’s keys.
For central reporting, the worker’s identity policy permits writes to the central DynamoDB tables. The tables’ resource policies admit writes using conditions on organisation membership and the calling principal’s role pattern. Both sides participate in authorising cross-account access. The worker addresses the central tables by their resource identifiers. AWS cross-account DynamoDB access.
Account-name lookup is another, separate path. A worker outside the organisation management account assumes an Organizations read-only role and uses DescribeAccount to obtain the account’s display name. That assumed session supports metadata lookup; IAM deletion continues under the local worker role.
The operator API reads the central records through a private, authenticated interface. It exposes read-only operations, while the scheduled workers perform credential changes. Access to inspect the service and permission to execute its IAM actions are separate concerns.
Deactivate and delete, with an explicit dry-run mode
When enforcement is enabled, an eligible active key is deactivated through UpdateAccessKey. The worker waits five seconds, then calls DeleteAccessKey. An eligible key already marked inactive goes directly to deletion.
The short interval sequences the calls. It is not a recovery grace period or an opportunity to observe an application through its normal operating cycle. Dependency review belongs before enabling this path. Deleting an access key also does not create or distribute a replacement for an application that still needs one.
The delivered dry-run mode prevents IAM mutation and records proposed actions for inspection. A proposal describes what the worker would do; a successful IAM API response provides evidence that an operation was executed. Operators should interpret the execution mode and outcome together when reviewing credential changes.
The daily schedule defines when evaluation runs. It does not promise deletion at the exact moment a key crosses a threshold; execution and a successful IAM response still have to follow.
Check executed changes and continuing coverage
The central store separates key observations, action records and run reports. Action records include the operation, target key, time and returned result or error. A report brings together the run’s observations and decisions.
Live action records contain successful deletion responses, and individual deletions have also been matched to CloudTrail events. For an operating review, correlate the target, operation, calling identity, time and request identifier where available, and inspect error details. The relevant fields are described in the CloudTrail event record reference.
Coverage requires a separate check. Review application StackSet status alongside the last completed worker report for each intended account. An up-to-date deployment and recent reporting answer different questions; a missing report needs investigation before that account can be treated as recently assessed.
Keep the baseline connected to application dependencies
A 180-day rule is a clear baseline that teams can explain and test. It does not prove that every quiet credential has no remaining dependency. An annual export or rarely invoked recovery job may legitimately sit idle beyond that interval.
Before enabling deletion, review those infrequent workflows with their service owners and decide how their access will be maintained or replaced. Dry-run provides candidates for that review; it does not resolve ownership or grant exceptions automatically.
Our AWS Workload Foundations Assessment can examine account targeting, deployment roles, worker permissions and central reporting before extending automation.
Read the business companion: making credential lifecycle management an operating process.
