{"id":80112,"date":"2026-06-16T14:41:17","date_gmt":"2026-06-16T09:11:17","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=80112"},"modified":"2026-06-19T12:06:09","modified_gmt":"2026-06-19T06:36:09","slug":"how-we-automated-iam-compliance-enforcement-across-multiple-aws-projects","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/how-we-automated-iam-compliance-enforcement-across-multiple-aws-projects\/","title":{"rendered":"How we automated IAM compliance enforcement across multiple AWS projects"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Managing IAM (Identity and Access Management) compliance manually is one of those tasks that sounds simple but quietly consumes hours every week. Someone has to read the daily report, identify non-compliant users, send individual emails, track who responded, follow up again, and eventually rotate keys manually for users who never got around to it.<\/p>\n<p>In our MSP team at ToTheNew, we decided to fix this once and for all. We built an end-to-end IAM compliance automation that detects violations, notifies users, auto-generates new credentials, and enforces policies \u2014 all without any manual intervention. This blog walks through what we built, how it works, and how you can implement it in your own AWS project.<\/p>\n<ul>\n<li>What IAM compliance issues we were dealing with<\/li>\n<li>The 5-stage escalation system we designed<\/li>\n<li>How emails, credentials, and enforcement are handled<\/li>\n<li>How to deploy it in any AWS project<\/li>\n<\/ul>\n<h2>The Problem<\/h2>\n<p>Every AWS project we manage runs an IAM compliance script that generates a daily report. The report tells us who hasn&#8217;t rotated their access keys, who hasn&#8217;t changed their password in months, who doesn&#8217;t have MFA enabled, and who hasn&#8217;t logged in for over 75 days. Detection was never the issue.<\/p>\n<p>The issue was everything that came after. There was no automated follow-up, no enforcement, and no guarantee that a non-compliant access key would ever get rotated. Keys could stay active for months if no one followed up aggressively enough. Our team was spending <strong>1\u20133 hours every week<\/strong> on repetitive manual work with inconsistent results.<\/p>\n<p>We needed a system that could handle the entire lifecycle automatically \u2014 from the first notification all the way to disabling old keys.<\/p>\n<h2>The Solution: 5-Stage Escalation Automation<\/h2>\n<p>We designed a Python-based automation that runs daily via a Jenkins pipeline or cron job. It scans all IAM users, compares them against compliance thresholds, and escalates through five stages over 14 days before automatically disabling non-compliant access keys.<\/p>\n<h3>Compliance Checks<\/h3>\n<ul>\n<li><strong>Access key 1 rotation<\/strong> \u2014 flagged if not rotated in 75+ days<\/li>\n<li><strong>Access key 2 stale<\/strong> \u2014 flagged if not used in 15+ days<\/li>\n<li><strong>Password age<\/strong> \u2014 flagged if not changed in 75+ days<\/li>\n<li><strong>MFA not enabled<\/strong> \u2014 flagged if console access is active without MFA<\/li>\n<li><strong>Inactive user<\/strong> \u2014 flagged if no console login in 75+ days<\/li>\n<li><strong>Inline policies<\/strong> \u2014 reported in weekly summary only<\/li>\n<\/ul>\n<h3>The 5-Stage Escalation Flow<\/h3>\n<div id=\"attachment_80109\" style=\"width: 690px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-80109\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-80109\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Escalation_Flow1.png\" alt=\"5-Stage Escalation Flow\" width=\"680\" height=\"240\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Escalation_Flow1.png 680w, \/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Escalation_Flow1-300x106.png 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Escalation_Flow1-624x220.png 624w\" sizes=\"(max-width: 680px) 100vw, 680px\" \/><p id=\"caption-attachment-80109\" class=\"wp-caption-text\">5-Stage Escalation Flow<\/p><\/div>\n<ul>\n<li><strong>Day 0 \u2014 Notification:<\/strong> User receives a consolidated email listing all their violations with specific remediation steps. MSP team is CC&#8217;d.<\/li>\n<li><strong>Day 3 \u2014 Reminder:<\/strong> A follow-up nudge is sent in the same email thread if the issue is unresolved.<\/li>\n<li><strong>Day 7 \u2014 New Credentials:<\/strong> New access keys are created and a password reset is triggered. Credentials are sent as a CSV attachment in a separate private email (no CC). Old keys remain active at this stage.<\/li>\n<li><strong>Day 10 \u2014 Warning:<\/strong> A warning is sent that the old key will be disabled in 4 days.<\/li>\n<li><strong>Day 11\u201313 \u2014 Countdown:<\/strong> Daily countdown reminders are sent.<\/li>\n<li><strong>Day 14 \u2014 Enforcement:<\/strong> Old access key is automatically disabled. It is never deleted \u2014 it can be re-enabled manually if needed.<\/li>\n<\/ul>\n<p>Each user&#8217;s escalation state is stored in an S3 JSON file (encrypted) so the automation remembers exactly where it left off between daily runs. If a user resolves their violation at any point, the state is cleared automatically.<\/p>\n<h2>One Email Per User. One Thread.<\/h2>\n<p>A core design goal was to avoid inbox clutter. Instead of sending separate emails for each violation type, the system sends <strong>one consolidated email per user<\/strong> listing all their issues with a per-violation stage badge. All follow-ups are threaded in the same Gmail conversation using SES threading headers (<code>In-Reply-To<\/code> and <code>References<\/code>).<\/p>\n<p>The email uses a navy banner header with color-coded stage badges (green for notification, amber for warning, red for enforcement) so the user can immediately understand the urgency at a glance.<\/p>\n<h3>Email Routing Rules<\/h3>\n<div id=\"attachment_80110\" style=\"width: 690px\" class=\"wp-caption alignnone\"><img aria-describedby=\"caption-attachment-80110\" decoding=\"async\" loading=\"lazy\" class=\"size-full wp-image-80110\" src=\"https:\/\/www.tothenew.com\/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Email_Routing.png\" alt=\"Email Routing\" width=\"680\" height=\"320\" srcset=\"\/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Email_Routing.png 680w, \/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Email_Routing-300x141.png 300w, \/blog\/wp-ttn-blog\/uploads\/2026\/06\/IAM_Email_Routing-624x294.png 624w\" sizes=\"(max-width: 680px) 100vw, 680px\" \/><p id=\"caption-attachment-80110\" class=\"wp-caption-text\">Email Routing Rules<\/p><\/div>\n<p>Credentials are <strong>never written in the email body<\/strong>. They are always sent as a CSV attachment in a separate private email with no CC. Every generated credential is also backed up to S3 under <code>iam-compliance\/{project}\/credentials\/<\/code> with AES-256 encryption as a fallback.<\/p>\n<h2>Built to Be Reused Across Projects<\/h2>\n<p>We made sure the automation is completely project-agnostic. Every hardcoded project reference was removed from the codebase. The entire project identity flows from a single line in <code>config.yaml<\/code>:<\/p>\n<p><code>project_name: \"ETV\"<\/code><\/p>\n<p>This one setting drives:<\/p>\n<ul>\n<li>Email subjects \u2192 <code>[ETV] IAM Compliance: john.doe@company.com<\/code><\/li>\n<li>S3 state path \u2192 <code>iam-compliance\/etv\/state.json<\/code><\/li>\n<li>Credential backups \u2192 <code>iam-compliance\/etv\/credentials\/access-keys\/<\/code><\/li>\n<\/ul>\n<p>We provide two deployment options:<\/p>\n<ul>\n<li><strong>Jenkins version<\/strong> \u2014 Scripted pipeline with built-in cron, DRY_RUN parameter, and log archiving<\/li>\n<li><strong>Cron version<\/strong> \u2014 Standalone <code>run.sh<\/code> wrapper for projects without Jenkins<\/li>\n<\/ul>\n<p>Deploying to a new project takes approximately 30 minutes including S3 bucket setup, IAM policy creation, SES verification, and user email tagging.<\/p>\n<h2>Key Technical Decisions<\/h2>\n<ul>\n<li><strong>Old keys are disabled, never deleted<\/strong> \u2014 manual re-enable is possible if a user urgently needs their old key<\/li>\n<li><strong>State persists across runs<\/strong> \u2014 S3 JSON tracks each user&#8217;s escalation stage independently<\/li>\n<li><strong>Weekday-only emails<\/strong> \u2014 notifications are sent Monday to Friday only; state advances every day including weekends so the 14-day timeline is always accurate<\/li>\n<li><strong>Enforcement survives key rotation<\/strong> \u2014 once Stage 3 creates a new key, the old key violation technically resolves. We track the old key ID separately so Stage 4 and Stage 5 still fire correctly<\/li>\n<li><strong>Password reset forces change on first login<\/strong> \u2014 <code>PasswordResetRequired: true<\/code> is set so users must immediately change their auto-generated password<\/li>\n<\/ul>\n<h2>Results<\/h2>\n<ul>\n<li>Deployed on <strong>2 live projects<\/strong> \u2014 AzamTV and SpoTV<\/li>\n<li><strong>Zero manual follow-ups<\/strong> needed per week<\/li>\n<li><strong>14-day maximum<\/strong> for any non-compliant key to be enforced<\/li>\n<li><strong>1\u20133 hours per week<\/strong> of manual effort eliminated<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>IAM compliance is a critical part of any AWS security posture, but enforcing it manually doesn&#8217;t scale. By building a 5-stage escalation automation with consolidated emails, auto-credential generation, threaded follow-ups, and guaranteed enforcement, our team went from spending hours every week on compliance chasing to spending zero.<\/p>\n<p>The system is fully open for adoption across any AWS project in the competency. All you need is an AWS account with SES configured, an S3 bucket, and about 30 minutes. Full documentation, setup guide, and deployment packages are available \u2014 reach out to the MSP team and we&#8217;ll get you set up.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Managing IAM (Identity and Access Management) compliance manually is one of those tasks that sounds simple but quietly consumes hours every week. Someone has to read the daily report, identify non-compliant users, send individual emails, track who responded, follow up again, and eventually rotate keys manually for users who never got around to it. [&hellip;]<\/p>\n","protected":false},"author":2266,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":2},"categories":[5877],"tags":[248,1916,8571,1499,8572],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80112"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2266"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=80112"}],"version-history":[{"count":2,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80112\/revisions"}],"predecessor-version":[{"id":80165,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80112\/revisions\/80165"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=80112"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=80112"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=80112"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}