<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[I Built an AWS Compliance Auditor That Uses No Static Keys — and AWS Still Fought Me]]></title><description><![CDATA[Build story of AuditTrail SDK, an AWS compliance auditor using Terraform, boto3, OIDC, and Postman—with lessons from IAM edge cases, throttling, structured logging, and secure cloud automation.]]></description><link>https://mdixondevsecops2.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/567830c04d5f969862dc485f/9bd2c158-d4a0-46a1-baeb-4807b272ffd0.png</url><title>I Built an AWS Compliance Auditor That Uses No Static Keys — and AWS Still Fought Me</title><link>https://mdixondevsecops2.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 04:09:40 GMT</lastBuildDate><atom:link href="https://mdixondevsecops2.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I Built an AWS Compliance Auditor That Uses No Static Keys — and AWS Still Fought Me]]></title><description><![CDATA[DSB Capstone Project
Most cloud projects look clean in a diagram and messy in production.
That was the point of AuditTrail SDK, a DSB capstone project I built to automate a basic AWS compliance task t]]></description><link>https://mdixondevsecops2.hashnode.dev/i-built-an-aws-compliance-auditor-that-uses-no-static-keys-and-aws-still-fought-me</link><guid isPermaLink="true">https://mdixondevsecops2.hashnode.dev/i-built-an-aws-compliance-auditor-that-uses-no-static-keys-and-aws-still-fought-me</guid><dc:creator><![CDATA[Malik Dixon]]></dc:creator><pubDate>Wed, 01 Apr 2026 22:05:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/d2d0a431-3549-4dd4-9be4-96554c46fe5e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>DSB Capstone Project</strong></p>
<p>Most cloud projects look clean in a diagram and messy in production.</p>
<p>That was the point of <strong>AuditTrail SDK</strong>, a DSB capstone project I built to automate a basic AWS compliance task the right way: use the official SDK, authenticate with temporary credentials only, handle throttling, log every API call in structured form, provision the stack with Terraform, and validate the API with Postman. GitHub Actions OIDC lets AWS access happen without long-lived GitHub secrets, and Postman supports AWS Signature auth for signed requests.</p>
<h2>The project in one sentence</h2>
<p><strong>AuditTrail SDK</strong> is a Terraform-provisioned AWS compliance auditor that inventories compute and storage resources with <code>boto3</code>, stores findings, exposes audit endpoints, and creates enough logging and cloud-side visibility to let you audit the auditor. Boto3 supports configurable retry behavior, and CloudTrail provides control-plane visibility for AWS API activity.</p>
<h2>What it does</h2>
<p>The platform checks resources such as EC2, EBS, S3, and Lambda, then runs basic compliance checks, including required-tag validation, EBS encryption checks, and S3 public access checks. The results are stored and exposed via an API, allowing the system to be validated operationally rather than just by reading logs after the fact. For S3 public access checks, AWS documents that the bucket-level public access block configuration is retrieved via <code>GetPublicAccessBlock</code>, which requires the <code>s3:GetBucketPublicAccessBlock</code> permission.</p>
<h2>Why this project is stronger than a script demo</h2>
<p>A throwaway script can list resources. That proves almost nothing.</p>
<p>A real cloud security project has to answer harder questions:</p>
<ul>
<li><p>Can it authenticate without static keys?</p>
</li>
<li><p>Can it survive throttling?</p>
</li>
<li><p>Can it be reproduced with infrastructure as code?</p>
</li>
<li><p>Can it be tested like a service?</p>
</li>
<li><p>Can it explain what happened when something fails?</p>
</li>
</ul>
<p>This project was built to answer yes to all five. GitHub’s AWS OIDC flow exists specifically so that workflows can access AWS without storing long-lived credentials, making it the right default for CI/CD.</p>
<h2>Project Architecture</h2>
<p>Here’s the shape of the system:</p>
<ul>
<li><p>Terraform provisions the infrastructure</p>
</li>
<li><p>Lambda + boto3 runs the inventory and compliance checks</p>
</li>
<li><p>DynamoDB stores findings</p>
</li>
<li><p>S3 stores raw reports</p>
</li>
<li><p>API Gateway exposes <code>/audit/run</code> and <code>/audit/results</code></p>
</li>
<li><p>CloudWatch Logs stores structured JSON logs</p>
</li>
<li><p>CloudTrail records AWS-side activity</p>
</li>
<li><p>Postman validates the API behavior</p>
</li>
</ul>
<p>That architecture matters because it moves the project from “script” to “small internal platform.” CloudTrail’s <code>DescribeTrails</code> and related API coverage make it useful for verifying control-plane setup and visibility.</p>
<pre><code class="language-plaintext">GitHub Actions CI/CD
  |                         |
  | deploy.yml              | terraform.yml
  | (src/** changes)        | (terraform/** changes)
  v                         v
OIDC → GitHubActionsDeployRole (Terraform-managed)
  |                         |
  | lambda:UpdateFunctionCode  | terraform plan/apply
  v                         v
Lambda code deploy    HCP Terraform (remote state)
                            |
                            v
              Terraform-managed AWS environment
                - API Gateway (HTTP API)
                - Lambda audit function
                - DynamoDB findings table (with GSI for query)
                - S3 reports bucket
                - CloudWatch Logs
                - CloudTrail
                - EventBridge schedule (daily full audit)
                - EventBridge rule (real-time CloudTrail S3 events)
                - SNS topic for public bucket alerts
                - IAM roles (least-privilege)
                            |
                            v
              boto3 multi-region inventory + compliance checks
                            |
                            +--&gt; findings saved to DynamoDB
                            +--&gt; raw reports saved to S3
                            +--&gt; findings imported to Security Hub
                            +--&gt; SNS notifications for public buckets
                            +--&gt; structured JSON logs to CloudWatch
                            |
                            v
              CloudTrail S3 event --&gt; EventBridge --&gt; Lambda micro-audit
                            |
                            +--&gt; single-bucket compliance check
                            +--&gt; findings + SNS notification with actor context
                            |
                            v
              Postman collection / GET /audit/results
                - trigger audit
                - query findings by check_id
                - validate responses
</code></pre>
<h2>What I wanted to prove</h2>
<blockquote>
<p>Secure SDK usage is not just “call an API.”<br />It is authentication design, IAM precision, resilience under throttling, and evidence you can actually inspect afterward.</p>
</blockquote>
<h2>Temporary credentials only</h2>
<p>One hard rule shaped the whole project: <strong>no static AWS access keys</strong>.</p>
<p>For CI/CD, I used the GitHub Actions OIDC model. GitHub documents that OIDC allows workflows to access AWS without storing long-lived AWS credentials as GitHub secrets. That is the security baseline I wanted the project to enforce from day one.</p>
<p>For local execution and certain bootstrap tasks, the pattern is short-lived role assumption instead of hardcoded keys. That kept the authentication story consistent: trust relationships and temporary credentials, not secret sprawl.</p>
<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/54225411-baae-4536-989e-85f7b29e6951.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/cdfb9749-8d44-4616-9a6a-4146d1dbd6b7.png" alt="" style="display:block;margin:0 auto" />

<h2>Throttling is not an edge case</h2>
<p>When a script starts walking multiple AWS services, throttling is not hypothetical.</p>
<p>So the project uses two layers of protection:</p>
<ul>
<li><p>SDK-level retry behavior in <code>boto3</code>/<code>botocore.</code></p>
</li>
<li><p>Lightweight pacing and exponential backoff with jitter in the application logic</p>
</li>
</ul>
<p>That combination matters because retries alone are not always enough if your script is needlessly aggressive. It is better to reduce pressure up front than to slam into rate limits and “recover” from self-inflicted errors. AWS’s SDK guidance supports configurable retry behavior for exactly these kinds of transient failures and throttling scenarios.</p>
<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/9be8caa8-173f-4e77-b104-cd99ea7ff8ae.png" alt="" style="display:block;margin:0 auto" />

<h2>Audit the audit</h2>
<p>A big part of the assignment was not just running checks, but creating the means to <strong>audit the audit</strong>.</p>
<p>So I built two layers of visibility:</p>
<ul>
<li><p>Structured JSON logs from the application for every API call</p>
</li>
<li><p>CloudTrail for AWS-side history of account activity</p>
</li>
</ul>
<p>That gives you two useful perspectives. App logs tell you what the code thought it was doing. CloudTrail tells you what AWS actually recorded. For a security-oriented capstone, that is a much stronger story than “the script printed some output.”</p>
<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/ac73ef61-68ae-450c-ae5d-e00fc659d78e.png" alt="" style="display:block;margin:0 auto" />

<h2>Postman made the project operational</h2>
<p>Adding Postman turned the project into something demoable and repeatable.</p>
<p>Instead of only proving that the Lambda function exists, I could validate:</p>
<ul>
<li><p>health checks</p>
</li>
<li><p>audit triggers</p>
</li>
<li><p>findings retrieval</p>
</li>
<li><p>response structure</p>
</li>
<li><p>signed request behavior where needed</p>
</li>
</ul>
<p>Postman officially supports AWS Signature authentication, which makes it a clean fit for testing AWS-backed endpoints.</p>
<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/c431bdec-a684-4f95-ba6f-edeec8553244.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/06734023-3fd4-4528-9bf4-ef2d145106fe.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/567830c04d5f969862dc485f/58bdebd4-a40b-4fa9-9a0b-a9daec3f0387.png" alt="" style="display:block;margin:0 auto" />

<h2>The most valuable part: what broke</h2>
<p>The best lessons in this project did not come from the happy path. They came from the implementation failures.</p>
<p>Below are the biggest issues I hit and what they taught me.</p>
<h2>1) Security Hub AccessDeniedException</h2>
<p>I ran into a nasty issue where setting <code>AWS_ACCOUNT_ID</code> the SNS caused Security Hub logic to attempt <code>BatchImportFindings</code> instead of skipping that path. The result was an <code>AccessDeniedException</code>.</p>
<p>The fix was not to force permissions through. The fix was to wrap the call in a <code>try/except</code> function and return a graceful skipped status with the error message. That was the right move because AWS documents that <code>BatchImportFindings</code> have caller/account requirements, including account alignment in certain import paths. Optional integrations should fail safely rather than crash the entire audit flow.</p>
<h3>Lesson</h3>
<blockquote>
<p>Optional security integrations should degrade gracefully.<br />“Skip with evidence” beats “hard fail for everything.”</p>
</blockquote>
<h2>2) The S3 IAM action name was wrong</h2>
<p>The original IAM policy used:</p>
<pre><code class="language-json">"s3:GetPublicAccessBlock"
</code></pre>
<p>Well, that looked right. It was wrong.</p>
<p>For bucket-level retrieval, the correct permission is:</p>
<pre><code class="language-json">"s3:GetBucketPublicAccessBlock"
</code></pre>
<p>AWS’s S3 API reference is explicit: the GetPublicAccessBlock bucket operation requires s3:GetBucketPublicAccessBlock. Fixing that permission unblocked the S3 public-access checks immediately.</p>
<h2>3) DynamoDB GSI permissions were more specific than expected</h2>
<p>A query against a DynamoDB global secondary index failed even though the table permissions looked fine.</p>
<p>The issue was that querying a GSI requires permissions on the index ARN, not just the base table ARN. AWS documents IAM patterns that grant access to a table and all of its indexes, which is the pattern I needed. Once the policy included the index ARN path, the query worked.</p>
<p>That is one of those AWS details that punishes surface-level understanding. “Table access” does not automatically mean “index access” as people often assume.</p>
<h2>4) Some describe actions that cannot be tightly ARN-scoped</h2>
<p>This one is easy to miss if you are trying to be extremely strict with IAM.</p>
<p>During Terraform state refresh, these actions failed under tight scoping:</p>
<pre><code class="language-plaintext">cloudtrail:DescribeTrails
logs:DescribeLogGroups
</code></pre>
<p>The practical fix was to give them their own IAM statement with Resource: "*". That is not laziness; it is matching the way these read/list operations are authorized. AWS’s service authorization references and CloudWatch Logs documentation make clear that some describe-style actions do not behave like normal resource-scoped operations. CloudWatch Logs also notes limits on IAM control patterns for DescribeLogGroups.</p>
<h3>Lesson</h3>
<blockquote>
<p>Least privilege does not mean “force every action onto an ARN.” Sometimes the tightest correct policy is a narrow action set with Resource: "*".</p>
</blockquote>
<h2>5) OIDC IAM bootstrapping was real</h2>
<p>This was one of the most practical issues in the whole build.</p>
<p>I was using Terraform to manage the CI/CD role’s own permissions, but terraform plan needed permission to inspect the very OIDC and IAM resources the role depended on. Initially, the role lacked read permissions like:</p>
<ul>
<li><p>iam:GetOpenIDConnectProvider</p>
</li>
<li><p>iam:GetRolePolicy</p>
</li>
<li><p>iam:ListAttachedRolePolicies</p>
</li>
</ul>
<p>That caused plan/state-refresh failures.</p>
<p>The fix was to bootstrap once locally: apply the GitHubActionsDeployPolicy first, then push to CI. After that, the role had enough visibility to read and manage its own policy path, and later CI runs became self-managing. GitHub’s AWS OIDC documentation supports this overall trust model; the bootstrap nuance is the real-world layer you only feel when you actually implement it.</p>
<h3>Lesson</h3>
<blockquote>
<p>Self-managing IAM in Terraform is possible, but the first step often cannot come from a role that does not yet have permission to see itself.</p>
</blockquote>
<h2>What this project actually proves</h2>
<p>This project proves more than “I can write boto3.”</p>
<p>It shows:</p>
<ul>
<li><p>secure use of the official AWS SDK</p>
</li>
<li><p>temporary-credential authentication design</p>
</li>
<li><p>Terraform-based reproducibility</p>
</li>
<li><p>throttling-aware engineering</p>
</li>
<li><p>structured operational logging</p>
</li>
<li><p>API validation through Postman</p>
</li>
<li><p>real IAM debugging under CI/CD pressure</p>
</li>
</ul>
<p>That is why the implementation issues matter so much. They make the project more credible, not less.</p>
<h2>Final takeaway</h2>
<p>AuditTrail SDK began as a compliance automation capstone project. It became a much better lesson than that.</p>
<p>It exposed how AWS services behave when IAM is tight, how Terraform refresh interacts with identity resources, how subtle permission names can break security checks, and how optional integrations like Security Hub need graceful failure paths to avoid turning useful tooling into brittle tooling. AWS’s own docs back up the key mechanics here: OIDC for GitHub Actions avoids long-lived secrets; S3 public access checks depend on the right bucket-level permissions; DynamoDB index access requires index-aware policies; and describe-style APIs sometimes need wildcard resources.</p>
<p>That is the difference between building a project for screenshots and building one that survives contact with reality.</p>
]]></content:encoded></item></channel></rss>