<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Agustin De Mozzi</title>
    <description>The latest articles on DEV Community by Agustin De Mozzi (@agudemozzi).</description>
    <link>https://dev.to/agudemozzi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4097765%2Febc0b81a-acf3-4584-8a9f-513a735effdf.jpg</url>
      <title>DEV Community: Agustin De Mozzi</title>
      <link>https://dev.to/agudemozzi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/agudemozzi"/>
    <language>en</language>
    <item>
      <title>How to Use AWS Security Agent as a Free Vulnerability Scanner for Your Project</title>
      <dc:creator>Agustin De Mozzi</dc:creator>
      <pubDate>Thu, 27 Aug 2026 15:29:53 +0000</pubDate>
      <link>https://dev.to/agudemozzi/how-to-use-aws-security-agent-as-a-free-vulnerability-scanner-for-your-project-2mod</link>
      <guid>https://dev.to/agudemozzi/how-to-use-aws-security-agent-as-a-free-vulnerability-scanner-for-your-project-2mod</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl9rol6kdw1dpkc10rkx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl9rol6kdw1dpkc10rkx.png" width="800" height="369"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;AWS Security Agent&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’re building a side project or an early-stage startup, security is probably not at the top of your priority list. You’re focused on shipping features, fixing bugs, and getting users. But vulnerabilities don’t care about your roadmap.&lt;/p&gt;

&lt;p&gt;The good news is that AWS Security Agent offers code review and custom security requirements at no cost. You get up to 1,000 PR code reviews per month and 20 custom security requirements per account — more than enough for any personal project or small team. The agent automatically scans every pull request against your security rules and posts findings directly as PR comments.&lt;/p&gt;

&lt;p&gt;In this tutorial, I’ll walk you through how to set it up and, more importantly, how to write custom security requirements tailored to your specific project. We’ll use a Node.js e-commerce application as an example.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You’ll Get
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Automated security scanning on every pull request (up to 1,000/month)&lt;/li&gt;
&lt;li&gt;Custom security rules that match your project’s specific needs (up to 20 per account)&lt;/li&gt;
&lt;li&gt;Findings posted directly as comments in your PRs&lt;/li&gt;
&lt;li&gt;Zero cost for code review capabilities (&lt;a href="https://aws.amazon.com/security-agent/faqs/" rel="noopener noreferrer"&gt;see FAQ&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An AWS account&lt;/strong&gt;  — If you don’t have one, &lt;a href="https://aws.amazon.com/free/" rel="noopener noreferrer"&gt;create a free AWS account here&lt;/a&gt;. You’ll need a valid credit card for verification, but you won’t be charged for the code review features we’ll use in this tutorial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub or Bitbucket Cloud account&lt;/strong&gt; with at least one repository you want to scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin access&lt;/strong&gt; to your GitHub organization (or user account ownership)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create an Agent Space
&lt;/h3&gt;

&lt;p&gt;An Agent Space is a dedicated workspace where AWS Security Agent organizes all security assessments for a specific application. Think of it as a container for your project’s security configuration, reviews, and findings.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to the AWS Console and navigate to &lt;strong&gt;AWS Security Agent&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;Agent Spaces&lt;/strong&gt;  page.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Agent Space&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enter a name that identifies your project (e.g., “ecommerce-space”).&lt;/li&gt;
&lt;li&gt;Optionally add a description like “Node.js e-commerce application — payment processing and user management”.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet1gc2sh2s0wvx0gafft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet1gc2sh2s0wvx0gafft.png" width="800" height="251"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Agent space creation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For more details, see the &lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/create-agent-space.html" rel="noopener noreferrer"&gt;official documentation on creating an Agent Space&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Connect Your Repository
&lt;/h3&gt;

&lt;p&gt;Once your Agent Space is ready, connect it to your code repository so the agent can scan your pull requests. In this example we’ll use GitHub, but AWS Security Agent also supports Bitbucket Cloud, GitLab, and GitHub Enterprise Server — see the &lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/admin-integrations.html" rel="noopener noreferrer"&gt;full integrations documentation&lt;/a&gt; for other providers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the AWS Security Agent console, navigate to &lt;strong&gt;Integrations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add integration&lt;/strong&gt; and select  &lt;strong&gt;GitHub&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Open AWS Security Agent in GitHub&lt;/strong&gt;  — you’ll be redirected to GitHub.&lt;/li&gt;
&lt;li&gt;Select the organization or user account where your repository lives.&lt;/li&gt;
&lt;li&gt;Choose which repositories AWS Security Agent can access (you can grant access to all repos or select specific ones).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Install and authorize&lt;/strong&gt; in GitHub.&lt;/li&gt;
&lt;li&gt;Back in the AWS Console, enter a &lt;strong&gt;Registration name&lt;/strong&gt; (e.g., “ecommerce_repo”), select the account type, and click  &lt;strong&gt;Connect&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After connecting, navigate to your Agent Space, enable code review for the connected repositories, and you’re set. Every new PR will be automatically scanned.&lt;/p&gt;

&lt;p&gt;For the full step-by-step guide, see &lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/connect-github.html" rel="noopener noreferrer"&gt;Connect AWS Security Agent to GitHub repositories&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Understand the Managed Security Packs
&lt;/h3&gt;

&lt;p&gt;Before writing custom requirements, it’s worth knowing that AWS Security Agent already comes with managed security requirement packs based on industry standards and best practices. These are enabled with a single click and cover common vulnerabilities out of the box.&lt;/p&gt;

&lt;p&gt;You should enable the managed packs that are relevant to your project as a baseline. They’re maintained by AWS and updated automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4u79v34qmmnzi6es91p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4u79v34qmmnzi6es91p.png" width="799" height="284"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Managed Security Requirements Packs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The custom requirements we’ll create next are meant to complement these packs with rules that are specific to your business logic — things that a generic security scanner would never catch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create a Custom Security Requirement
&lt;/h3&gt;

&lt;p&gt;Custom security requirements let you teach AWS Security Agent what matters specifically for your project — the business logic that generic scanners can’t know about. You have 20 slots, so use them wisely.&lt;/p&gt;

&lt;p&gt;Before jumping into the console, let’s think about &lt;em&gt;what&lt;/em&gt; to write and &lt;em&gt;how&lt;/em&gt; to write it well.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to Think About Custom Requirements
&lt;/h4&gt;

&lt;p&gt;The key question is: what are the security rules that are specific to MY project that a generic scanner wouldn’t know about?&lt;/p&gt;

&lt;p&gt;For our Node.js e-commerce example, we need to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What sensitive data do we handle? (user info, payment data, order history)&lt;/li&gt;
&lt;li&gt;What are the critical business flows? (checkout, authentication, inventory)&lt;/li&gt;
&lt;li&gt;What resources belong to specific users? (orders, carts, addresses, invoices)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best custom requirements target business logic that a generic scanner would never catch. They also need to provide &lt;strong&gt;ongoing value&lt;/strong&gt;  — not just a one-time check, but something that gets re-evaluated every time new code is introduced.&lt;/p&gt;

&lt;h4&gt;
  
  
  How to Write a Good Requirement
&lt;/h4&gt;

&lt;p&gt;Here’s a framework you can follow. For the official guidelines, see &lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/security-requirements.html#_best_practices_for_defining_security_requirements" rel="noopener noreferrer"&gt;Best practices for defining security requirements&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Focus on recurring risks&lt;/strong&gt;  — The best custom requirements catch issues that can be reintroduced with every new feature. Ownership validation, input sanitization at specific boundaries, or business rule enforcement are great candidates because they apply to every new endpoint, not just existing ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cover your business logic&lt;/strong&gt;  — Generic scanners know about SQL injection and XSS. They don’t know that in YOUR app, orders have owners, prices should never come from the frontend, or that specific workflows require specific validations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be specific in compliance criteria&lt;/strong&gt;  — The more precise you are about what “compliant” and “non-compliant” look like, the better the agent will perform. Include concrete examples, field names, and patterns from your actual codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep applicability focused&lt;/strong&gt;  — Clearly state when the requirement should NOT apply to avoid false positives. If a requirement only applies to route handlers that accept resource IDs, say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let managed packs handle the generic stuff&lt;/strong&gt;  — Don’t waste your 20 custom requirement slots on things like “validate JWT tokens” or “don’t hardcode secrets.” The managed packs already cover those. Use your custom requirements for what only YOU know about your application.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Example: Enforce Resource Ownership Validation on All User-Scoped Endpoints
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; In an e-commerce app, resources like orders, carts, and addresses belong to specific users. Every time a new endpoint is created that accesses one of these resources by ID, there’s a risk of introducing an IDOR (Insecure Direct Object Reference) vulnerability — where any authenticated user can access another user’s data simply by guessing or enumerating IDs.&lt;/p&gt;

&lt;p&gt;This is not a one-time fix. Every new endpoint that touches a user-owned resource needs this check. It’s the kind of thing that slips through when a developer is in a rush to ship a feature — and that’s exactly why it makes a great custom requirement. The agent will re-evaluate it on every PR that adds or modifies route handlers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fss7w6xl6ylfxpvn0i6hf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fss7w6xl6ylfxpvn0i6hf.png" width="800" height="461"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Custom Security Requirements Fields&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading It Into the Console
&lt;/h3&gt;

&lt;p&gt;Now that we know what we want to enforce, let’s create it in AWS Security Agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the AWS Security Agent console, go to &lt;strong&gt;Security requirements&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Custom security requirements packs&lt;/strong&gt;  tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create security requirements pack&lt;/strong&gt; , give it a name (e.g., “ecommerce-packs”) and an optional description, then click &lt;strong&gt;Create security requirement pack&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Open the pack you just created. In the &lt;strong&gt;Pack security requirements&lt;/strong&gt; section, click &lt;strong&gt;Add requirement manually&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Fill in the fields with the values from the table above: name, description, applicability, compliance criteria, and optionally remediation guidance.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3uz5b2fbeqi6vdzfbj5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3uz5b2fbeqi6vdzfbj5m.png" width="800" height="359"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Custom Security Requirement&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once created, you’ll see it enabled on the security pack:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotz42rvgfvjk12pw2i83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fotz42rvgfvjk12pw2i83.png" width="800" height="194"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security Requirement Pack&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Seeing It in Action
&lt;/h3&gt;

&lt;p&gt;To test this setup, I created a simple Node.js e-commerce API with endpoints for orders, cart, and user profiles. All the existing endpoints correctly validate resource ownership using an authorizeOwnership middleware. Then I opened a PR that adds a new invoice endpoint — but this time, "forgot" to add the ownership check.&lt;/p&gt;

&lt;p&gt;Here’s what happened: the Security Agent reviewed the PR and flagged the exact vulnerability within minutes.&lt;/p&gt;

&lt;p&gt;The finding clearly explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What’s wrong:&lt;/strong&gt; the invoice endpoint loads an order by ID without verifying it belongs to the authenticated user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The impact:&lt;/strong&gt; any logged-in user can access another customer’s order details, including PII like shipping addresses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to fix it:&lt;/strong&gt; apply the authorizeOwnership middleware or scope the query with the user's ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1og6hhjghcai8zealbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv1og6hhjghcai8zealbg.png" width="800" height="799"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security Agent PR Review&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4mckp9wc7974bfpmgnx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz4mckp9wc7974bfpmgnx.png" width="800" height="868"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security Agent PR Review&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the custom requirement doing its job. A generic scanner might flag this as a general authorization issue, but because we defined the specific pattern our app uses (the authorizeOwnership middleware, the user field on resources), the finding is precise and actionable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: What the Managed Packs Found
&lt;/h3&gt;

&lt;p&gt;Here’s the thing — the custom requirement wasn’t the only thing that caught issues. The managed security packs (which require zero configuration) independently found 6 additional vulnerabilities in the same PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT verification without an algorithm allowlist (HIGH)&lt;/li&gt;
&lt;li&gt;A non-atomic checkout flow vulnerable to race conditions (HIGH)&lt;/li&gt;
&lt;li&gt;Missing input validation on cart endpoints (MEDIUM)&lt;/li&gt;
&lt;li&gt;NoSQL query injection via unvalidated ObjectIds (MEDIUM)&lt;/li&gt;
&lt;li&gt;Unreachable order cancellation logic due to a state machine contradiction (MEDIUM)&lt;/li&gt;
&lt;li&gt;Cart item deduplication bypass via case-sensitive ID comparison (MEDIUM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag007bxi9dzj0ypwbgrk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag007bxi9dzj0ypwbgrk.png" width="800" height="877"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Security Agent PR Review Sumamry&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s 7 total findings from a single PR review — 1 from your custom requirement and 6 from the managed packs — all without spending a cent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;With this setup, you now have an automated security review pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scans every pull request against both AWS-managed security standards and your custom rules&lt;/li&gt;
&lt;li&gt;Catches project-specific vulnerabilities that generic tools would miss&lt;/li&gt;
&lt;li&gt;Costs nothing for code review features (you only pay if you use penetration testing)&lt;/li&gt;
&lt;li&gt;Requires no maintenance — the managed packs are updated by AWS, and your custom requirements stay active until you change them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a solo developer or a small team, this is a significant improvement over having no security review at all. The 1,000 PR reviews per month and 20 custom requirements are more than enough to cover most early-stage projects.&lt;/p&gt;

&lt;p&gt;The most valuable part is the custom requirements. Take 30 minutes to think about what’s specific to your project — the data you handle, the services you connect to, the business rules that matter — and translate those into clear requirements. That’s 30 minutes that will save you from shipping vulnerabilities to production.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Useful links:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/security-agent/getting-started/" rel="noopener noreferrer"&gt;AWS Security Agent — Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/quotas.html" rel="noopener noreferrer"&gt;Service Quotas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/security-requirements.html" rel="noopener noreferrer"&gt;Managing Security Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/security-requirement-managed-packs.html" rel="noopener noreferrer"&gt;AWS Managed Security Requirement Packs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/securityagent/latest/userguide/admin-integrations.html" rel="noopener noreferrer"&gt;Connect Integrations (GitHub, Bitbucket, GitLab)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/security-agent/faqs/" rel="noopener noreferrer"&gt;AWS Security Agent FAQs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>amazonwebservices</category>
      <category>devsecops</category>
      <category>vulnerability</category>
    </item>
    <item>
      <title>How to Analyze AWS WAF Count Rules and Generate Automated Reports</title>
      <dc:creator>Agustin De Mozzi</dc:creator>
      <pubDate>Fri, 26 Jun 2026 18:02:40 +0000</pubDate>
      <link>https://dev.to/agudemozzi/how-to-analyze-aws-waf-count-rules-and-generate-automated-reports-307n</link>
      <guid>https://dev.to/agudemozzi/how-to-analyze-aws-waf-count-rules-and-generate-automated-reports-307n</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmhzlh1cmenbz3bt7ezy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmhzlh1cmenbz3bt7ezy.png" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;If you manage AWS WAF at scale — across multiple accounts, multiple WebACLs, and dozens of protected resources — you know the pain. AWS WAF logs are JSON blobs sitting in S3. The console gives you basic metrics, but when you need to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“What happens if we switch these rules from Count to Block?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Are we blocking legitimate traffic?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“What are the top attack patterns hitting our applications?”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you’re left manually querying Athena, exporting CSVs, and building spreadsheets. For every single WebACL. Every time.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;WAF Report Builder&lt;/strong&gt; to solve this. It’s a Python CLI tool that takes your WAF logs from S3, runs the analysis through Athena, and generates a self-contained HTML report you can share with your team, your clients, or your CISO — no AWS console access required.&lt;/p&gt;

&lt;p&gt;The repo is available at: &lt;a href="https://github.com/agudemozzi/aws-waf-report-builder" rel="noopener noreferrer"&gt;github.com/agudemozzi/aws-waf-report-builder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re managing AWS WAF at scale and tired of manual log analysis, give it a try. PRs and feedback welcome.&lt;/p&gt;

&lt;h3&gt;
  
  
  How you can generate reports for AWS WAF Count Rules with this tool?
&lt;/h3&gt;

&lt;p&gt;The tool operates in two modes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Count Mode (Pre-enforcement)
&lt;/h3&gt;

&lt;p&gt;This is the mode you use &lt;em&gt;before&lt;/em&gt; activating rules. AWS WAF allows you to set managed rules to Count mode with an override action of Block. This means the rule evaluates traffic and logs what &lt;em&gt;would&lt;/em&gt; have been blocked, without actually blocking anything.&lt;/p&gt;

&lt;p&gt;The report shows you exactly what traffic those rules are flagging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top source IPs&lt;/li&gt;
&lt;li&gt;Top targeted URIs&lt;/li&gt;
&lt;li&gt;Geographic distribution (with an interactive map)&lt;/li&gt;
&lt;li&gt;Which rules are triggering the most, broken down by protected resource&lt;/li&gt;
&lt;li&gt;Detailed drill-down: for each rule, what URIs and User-Agents are being flagged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you the confidence to flip the switch — or identifies rules that might generate false positives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Block Mode (Active Enforcement)
&lt;/h3&gt;

&lt;p&gt;Once rules are active, you need visibility into what’s actually being blocked. Are you stopping real attacks? Are you accidentally blocking legitimate users?&lt;/p&gt;

&lt;p&gt;Same report structure, but filtered to requests that were effectively terminated by a WAF rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Report
&lt;/h3&gt;

&lt;p&gt;The output is a single .html file that opens in any browser. No server needed, no dependencies for the viewer:&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fng9amgukuzi2ofa3x4zp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fng9amgukuzi2ofa3x4zp.png" width="700" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each report includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Summary section&lt;/strong&gt; explaining what the report covers, the analysis period, and the account/WebACL context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top IPs&lt;/strong&gt;  — immediately shows if there’s a single source hammering your application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geographic map&lt;/strong&gt;  — choropleth visualization of request origins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top URIs&lt;/strong&gt;  — what paths are being targeted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top rules per resource&lt;/strong&gt;  — separate tables for each CloudFront distribution, ALB or API Gateway, so you can see which resource is getting the most rule activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule drill-down&lt;/strong&gt;  — expandable accordions showing URI + User-Agent combinations for each rule, with full horizontal scrolling for long values. Rules involving query strings automatically include the Args column.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgypzsrbrp4wc61x3vo0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgypzsrbrp4wc61x3vo0.png" width="700" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ajurjrddqhh25gkasl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ajurjrddqhh25gkasl1.png" width="700" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih2oglqs7r00urpao3jf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fih2oglqs7r00urpao3jf.png" width="700" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not Just Use the AWS Console?
&lt;/h3&gt;

&lt;p&gt;A few reasons this is better for consulting and multi-account environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shareability&lt;/strong&gt; : The HTML file can be emailed, attached to a ticket, or embedded in a presentation. No AWS access needed for the recipient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-account&lt;/strong&gt; : When you manage 20+ accounts with Firewall Manager, you need reports per WebACL. This tool processes one S3 path at a time, or multiple in batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-enforcement confidence&lt;/strong&gt; : The Count mode analysis is something the console doesn’t surface well. You get a clear picture of impact before changing rule actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail&lt;/strong&gt; : Generated reports serve as point-in-time snapshots. Run them monthly and you have a security posture history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizable&lt;/strong&gt; : It’s Python + SQL + HTML. Need an extra metric? Add a query and a template section.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What’s Next
&lt;/h3&gt;

&lt;p&gt;Some ideas I’m considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timeline analysis&lt;/strong&gt; : Adding a time-series view to spot attack spikes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSV input mode&lt;/strong&gt; : For environments where you can’t run Athena directly but can export results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparative reports&lt;/strong&gt; : Diff between two periods to see if rule tuning had an effect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://medium.com/@agudemozzi" rel="noopener noreferrer"&gt;&lt;em&gt;Agustín De Mozzi&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudsecurity</category>
      <category>cybersecurity</category>
      <category>waf</category>
    </item>
    <item>
      <title>Centralizing AWS WAF Logs with Kinesis Firehose and S3</title>
      <dc:creator>Agustin De Mozzi</dc:creator>
      <pubDate>Sun, 14 Sep 2025 05:05:35 +0000</pubDate>
      <link>https://dev.to/agudemozzi/centralizing-aws-waf-logs-with-kinesis-firehose-and-s3-1f66</link>
      <guid>https://dev.to/agudemozzi/centralizing-aws-waf-logs-with-kinesis-firehose-and-s3-1f66</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftav73p8axfs25vlobw43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftav73p8axfs25vlobw43.png" width="797" height="525"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes to cloud security, &lt;strong&gt;logs are very important&lt;/strong&gt;. Having them centralized and well-structured is key for auditing, detecting incidents, and generating meaningful reports.&lt;/p&gt;

&lt;p&gt;In this article, we’ll walk through how to &lt;strong&gt;configure AWS WAF logging to a centralized S3 bucket using Amazon Kinesis Data Firehose&lt;/strong&gt; , leveraging &lt;strong&gt;dynamic partitioning&lt;/strong&gt; to keep logs organized by account, Web ACL, and date.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Firehose Instead of Direct S3 Logging?
&lt;/h3&gt;

&lt;p&gt;At first glance, sending logs directly from WAF to S3 seems like the simplest solution. However, costs and flexibility tell a different story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct S3 logging from WAF&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AmazonCloudWatch USE1-S3-Egress-Bytes costs &lt;strong&gt;$0.25 per GB&lt;/strong&gt; (first 10TB).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Via Firehose&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Kinesis Firehose Dynamic Partitioning → &lt;strong&gt;$0.02 per GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Kinesis Firehose PutRecordBatch → &lt;strong&gt;$0.029 per GB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: $0.049 per GB → ~80% cheaper.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, Firehose offers extra benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process logs in-transit with Lambda.&lt;/li&gt;
&lt;li&gt;Dynamically partition logs in S3.&lt;/li&gt;
&lt;li&gt;Custom compress data before storing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;This solution works whether you manage Web ACLs through &lt;strong&gt;Firewall Manager&lt;/strong&gt; or deploy them individually across multiple AWS accounts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37jbobsgmkjqnwb2sz9y.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37jbobsgmkjqnwb2sz9y.jpeg" width="657" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step-by-Step Setup
&lt;/h3&gt;

&lt;h3&gt;
  
  
  1. Create the Lambda Function
&lt;/h3&gt;

&lt;p&gt;The following Lambda extracts account ID and WebACL name from the WAF log and sets partition keys. You have to create this function in the same account where Firewall Manager is configured, or in the same account where the Web ACL is created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;webacl_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webaclId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UnknownAccount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;webacl_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UnknownWebACL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;arn_parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webacl_arn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arn_parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arn_parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Account ID
&lt;/span&gt;            &lt;span class="n"&gt;resource_parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webacl_arn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;webacl_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource_parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# WebACL name
&lt;/span&gt;        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Ok&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;partitionKeys&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accountId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webaclName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;webacl_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;records&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, logs will be stored in S3 like:&lt;br&gt;&lt;br&gt;
 s3://aws-waf-logs-your-bucket///year=2025/month=09/day=09/&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Allow Firehose to Invoke the Lambda
&lt;/h3&gt;

&lt;p&gt;Update the Lambda resource policy so that &lt;strong&gt;firehose.amazonaws.com&lt;/strong&gt; can invoke it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Inovke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kinesis.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:lambda:us-east-1:&amp;lt;AccountId&amp;gt;:function:&amp;lt;your-lambda-name&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"AWS:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:firehose:us-east-1:&amp;lt;AccountId&amp;gt;:deliverystream/&amp;lt;your-firehose&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GetConfig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kinesis.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:lambda:us-east-1:&amp;lt;AccountId&amp;gt;:function:&amp;lt;your-lambda-name&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"AWS:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:firehose:us-east-1:&amp;lt;AccountId&amp;gt;:deliverystream/&amp;lt;your-firehose&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the ”AWS:SourceArn” refers to a resource that does not exist yet, but it will be created in the next steps&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create the IAM Role for Firehose
&lt;/h3&gt;

&lt;p&gt;The role needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trust policy&lt;/strong&gt; for Firehose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; for the S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt; to invoke the Lambda function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trust policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firehose.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucketMultipartUploads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::aws-waf-logs-your-destination-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::aws-waf-logs-your-destination-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"lambda:GetFunctionConfiguration"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:lambda:us-east-1:&amp;lt;AccountID&amp;gt;:function:&amp;lt;your-lambda-name&amp;gt;:*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"logs:PutLogEvents"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:logs:us-east-1:&amp;lt;AccountID&amp;gt;:log-group:/aws/kinesisfirehose/waflogs:log-stream:*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Create the Destination S3 Bucket in the logs account
&lt;/h3&gt;

&lt;p&gt;The bucket must start with the prefix aws-waf-logs- , and have the following cross-account bucket policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCrossAccountFirehosePut"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;AccountID&amp;gt;:role/&amp;lt;Role created previously&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetBucketLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucketMultipartUploads"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObjectAcl"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::aws-waf-logs-your-destination-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::aws-waf-logs-your-destination-bucket/*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;AccountID&amp;gt;:role/&amp;lt;Role created previously&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::aws-waf-logs-your-destination-bucket/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"s3:x-amz-acl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bucket-owner-full-control"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Create the Firehose Delivery Stream
&lt;/h3&gt;

&lt;p&gt;This step can only be done via &lt;strong&gt;CLI or SDK&lt;/strong&gt; , as the console doesn’t allow choosing a bucket from another account. &lt;strong&gt;The delivery stream name must start with the prefix “aws-waf-logs-”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Example cli command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws firehose create-delivery-stream &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delivery-stream-name&lt;/span&gt; aws-waf-logs-&amp;lt;your-name&amp;gt;&lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--delivery-stream-type&lt;/span&gt; DirectPut &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--extended-s3-destination-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "RoleARN": "arn:aws:iam::&amp;lt;AccountId&amp;gt;:role/&amp;lt;Role created previously&amp;gt;",
    "BucketARN": "arn:aws:s3:::aws-waf-logs-your-destination-bucket",
    "Prefix": "!{partitionKeyFromLambda:accountId}/!{partitionKeyFromLambda:webaclName}/!{partitionKeyFromLambda:year}/!{partitionKeyFromLambda:month}/!{partitionKeyFromLambda:day}/",
    "ErrorOutputPrefix": "processing-failed/",
    "BufferingHints": {
      "SizeInMBs": 128,
      "IntervalInSeconds": 300
    },
    "CompressionFormat": "GZIP",
    "ProcessingConfiguration": {
      "Enabled": true,
      "Processors": [
        {
          "Type": "Lambda",
          "Parameters": [
            {
              "ParameterName": "LambdaArn",
              "ParameterValue": "arn:aws:lambda:us-east-1:&amp;lt;AccountId&amp;gt;:function:&amp;lt;your-lambda-name&amp;gt;:$LATEST"
            },
            {
              "ParameterName": "NumberOfRetries",
              "ParameterValue": "3"
            },
            {
              "ParameterName": "RoleArn",
              "ParameterValue": "arn:aws:iam::&amp;lt;AccountId&amp;gt;:role/&amp;lt;Role created previously&amp;gt;"
            },
            {
              "ParameterName": "BufferSizeInMBs", //Change this base on your need
              "ParameterValue": "1"
            },
            {
              "ParameterName": "BufferIntervalInSeconds",
              "ParameterValue": "300"
            }
          ]
        }
      ]
    },
    "DynamicPartitioningConfiguration": {
      "RetryOptions": {
        "DurationInSeconds": 300
      },
      "Enabled": true
    },
    "CustomTimeZone": "UTC"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Configure WAF or Firewall Manager
&lt;/h3&gt;

&lt;p&gt;Finally, update your &lt;strong&gt;Web ACLs&lt;/strong&gt; (or Firewall Manager policies) to use the delivery stream as the log destination.&lt;/p&gt;

&lt;p&gt;In a Web ACL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzc39azfdtflksr2l2e6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzc39azfdtflksr2l2e6.png" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;setting this up you’ll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save up to &lt;strong&gt;80% in logging costs&lt;/strong&gt; compared to direct S3 delivery.&lt;/li&gt;
&lt;li&gt;Keep logs &lt;strong&gt;centralized and neatly organized&lt;/strong&gt; by account and WebACL.&lt;/li&gt;
&lt;li&gt;Simplify analysis with &lt;strong&gt;Athena, OpenSearch, or SIEMs&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is scalable and works seamlessly across &lt;strong&gt;multi-account environments&lt;/strong&gt; managed with Firewall Manager, as well as standalone Web ACLs.&lt;/p&gt;

</description>
      <category>s3</category>
      <category>aws</category>
      <category>kinesisfirehose</category>
      <category>firewall</category>
    </item>
    <item>
      <title>How We Archived Over 14TB of S3 Logs and Cut Storage Costs by 95%</title>
      <dc:creator>Agustin De Mozzi</dc:creator>
      <pubDate>Wed, 30 Jul 2025 14:16:57 +0000</pubDate>
      <link>https://dev.to/agudemozzi/how-we-archived-over-14tb-of-s3-logs-and-cut-storage-costs-by-95-5d9i</link>
      <guid>https://dev.to/agudemozzi/how-we-archived-over-14tb-of-s3-logs-and-cut-storage-costs-by-95-5d9i</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftxvir2est347wpadmlpb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftxvir2est347wpadmlpb.png" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;During an internal audit aimed at optimizing our cloud infrastructure costs, we discovered that we had over &lt;strong&gt;14TB of AWS WAF logs&lt;/strong&gt; sitting in an S3 bucket — with &lt;strong&gt;no lifecycle policy&lt;/strong&gt; configured.&lt;/p&gt;

&lt;p&gt;These logs had accumulated over several years and were stored in the &lt;strong&gt;S3 Standard&lt;/strong&gt; storage class, which generated a significant monthly cost — especially considering these logs were &lt;strong&gt;rarely accessed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To make things worse, &lt;strong&gt;new logs were being added daily&lt;/strong&gt; , and our S3 costs were growing unchecked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;We were facing three main constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We couldn’t delete the logs&lt;/strong&gt; due to compliance requirements.&lt;/li&gt;
&lt;li&gt;The files were &lt;strong&gt;very small&lt;/strong&gt; (under 40 MB), making them &lt;strong&gt;too expensive to transition&lt;/strong&gt; to a colder storage class like Glacier or Deep Archive using standard tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloading logs locally to compress and re-upload&lt;/strong&gt; wasn’t an option: AWS S3 data egress costs exceed &lt;strong&gt;$1000 per TB&lt;/strong&gt; , making this strategy unfeasible at scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of that, we were spending nearly &lt;strong&gt;$350/month&lt;/strong&gt; just on S3 storage — an unnecessary cost for infrequently accessed data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;After evaluating multiple options, we implemented a &lt;strong&gt;simple but effective strategy&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We launched an &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/LaunchingAndUsingInstances.html" rel="noopener noreferrer"&gt;&lt;strong&gt;EC2 instance&lt;/strong&gt;&lt;/a&gt; inside our VPC, with an &lt;strong&gt;EBS volume sized for the largest log month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;We &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/create-interface-endpoint.html#create-interface-endpoint-aws" rel="noopener noreferrer"&gt;&lt;strong&gt;attached an S3 VPC endpoint&lt;/strong&gt;&lt;/a&gt;, ensuring all traffic remained within the AWS network to &lt;strong&gt;avoid data transfer charges&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;We wrote a &lt;strong&gt;lightweight Bash script&lt;/strong&gt;  to:&lt;/li&gt;
&lt;li&gt;List and download all logs from a specific month for a given Web ACL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle them&lt;/strong&gt; (using tar, since logs were already gzipped) into larger archive files.&lt;/li&gt;
&lt;li&gt;Upload those archives back to the same bucket — but this time, directly using the &lt;strong&gt;S3 Glacier Deep Archive&lt;/strong&gt; storage class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;BUCKET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"aws-waf-logs"&lt;/span&gt;
&lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1xxxxxxx-dxxx-4xxx-8xxx-axxxxxxxxxx"&lt;/span&gt; &lt;span class="c"&gt;#Only if you use firewall manager to manage WAF Logs&lt;/span&gt;
&lt;span class="nv"&gt;ORIGEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="nv"&gt;YEAR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2023"&lt;/span&gt;
&lt;span class="nv"&gt;TMP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./waf-archive"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# List account_ids&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$UUID&lt;/span&gt;&lt;span class="s2"&gt;/AWSLogs/"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s#/##'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; ACCOUNT_ID&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Account ID: &lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

  &lt;span class="c"&gt;# List WebACLs&lt;/span&gt;
  aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$UUID&lt;/span&gt;&lt;span class="s2"&gt;/AWSLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;/WAFLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ORIGEN&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s#/##'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; WEB_ACL&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"WebACL: &lt;/span&gt;&lt;span class="nv"&gt;$WEB_ACL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="c"&gt;# Listar months of the yeat&lt;/span&gt;
    aws s3 &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$UUID&lt;/span&gt;&lt;span class="s2"&gt;/AWSLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;/WAFLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ORIGEN&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$WEB_ACL&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$YEAR&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s#/##'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; MONTH&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Mes: &lt;/span&gt;&lt;span class="nv"&gt;$MONTH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="nv"&gt;S3_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UUID&lt;/span&gt;&lt;span class="s2"&gt;/AWSLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;/WAFLogs/&lt;/span&gt;&lt;span class="nv"&gt;$ORIGEN&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$WEB_ACL&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$YEAR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$MONTH&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
      &lt;span class="nv"&gt;TAR_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;YEAR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MONTH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar"&lt;/span&gt;
      &lt;span class="nv"&gt;TAR_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$TAR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="c"&gt;#Here you can choose another bucket to archive the logs, or create another folder in the same bucket&lt;/span&gt;
      &lt;span class="nv"&gt;ARCHIVE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"archived_logs/&lt;/span&gt;&lt;span class="nv"&gt;$ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$WEB_ACL&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$TAR_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Downloading from: s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$S3_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/files"&lt;/span&gt;
      aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$S3_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/files"&lt;/span&gt;

      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Packaging: &lt;/span&gt;&lt;span class="nv"&gt;$TAR_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-cf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TAR_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/files"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

      &lt;span class="c"&gt;# Change $BUCKET if you use another bucket&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Uploading to: s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$ARCHIVE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TAR_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="nv"&gt;$BUCKET&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$ARCHIVE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--storage-class&lt;/span&gt; DEEP_ARCHIVE

      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Removing temporal files"&lt;/span&gt;
      &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/files"&lt;/span&gt;
      &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TAR_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;done
  done
done

&lt;/span&gt;&lt;span class="nb"&gt;rmdir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Archived complete for &lt;/span&gt;&lt;span class="nv"&gt;$YEAR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This strategy allowed us to &lt;strong&gt;retain all logs&lt;/strong&gt; , fully meeting our &lt;strong&gt;compliance requirements&lt;/strong&gt; , while implementing a &lt;strong&gt;financially sustainable retention policy&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;We went from paying nearly &lt;strong&gt;$350/month&lt;/strong&gt; in S3 Standard to &lt;strong&gt;under $15/month&lt;/strong&gt; using S3 Glacier Deep Archive — a &lt;strong&gt;95.6% reduction&lt;/strong&gt; in storage costs.&lt;/p&gt;

&lt;p&gt;Key advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;entire process ran inside AWS&lt;/strong&gt; , so there were &lt;strong&gt;no egress charges&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly log processing was automated and scalable&lt;/strong&gt; across multiple Web ACLs.&lt;/li&gt;
&lt;li&gt;The total &lt;strong&gt;EC2 cost&lt;/strong&gt; for processing was around &lt;strong&gt;$10&lt;/strong&gt; , compared to over &lt;strong&gt;$600&lt;/strong&gt; if we had used S3’s native transition feature — even under optimistic assumptions (e.g., every object being ~75MB).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Lessons Learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don’t overlook lifecycle policies&lt;/strong&gt;  — buckets storing large volumes of data over time can lead to massive and unnecessary costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundling small files into larger archives&lt;/strong&gt; greatly reduces storage costs, especially when paired with colder S3 storage classes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep data movement within the AWS network&lt;/strong&gt; whenever possible — this avoids transfer charges and speeds up processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recommendations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configure lifecycle policies from day one&lt;/strong&gt;  — it’s easy to ignore logs until they become a financial burden.&lt;/li&gt;
&lt;li&gt;If you need to &lt;strong&gt;retain data long-term with little or no access&lt;/strong&gt; , &lt;strong&gt;S3 Glacier Deep Archive&lt;/strong&gt; is an excellent option (just account for longer retrieval times).&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>s3</category>
      <category>cloud</category>
      <category>cloudsecurity</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
