TL;DR: AWS WAF is the web firewall teams put in front of an app to block bad requests. Turn logging on and it writes a record for the traffic it inspects, and by default that record holds the headers the browser sent, with nothing blanked out. A session cookie is the header that proves a browser is already signed in. When one gets sent, it lands in the log with everything else, and whoever can read that log may be able to reuse it. AWS requires those logs to be named with an aws-waf-logs- prefix, which makes them trivial to find. Reading them takes two read-only permissions in CloudWatch, three in S3. RedactedFields, the setting that blanks fields on the way into those logs, does not cover the API that serves the same requests straight from the WAF. Also inside: a month of guessing AWS root passwords at 150-plus companies, why an AWS external access review never finishes, and a proxy key that only ever exists in memory.

We look for credentials in secret stores and in git history. This week they were sitting in a firewall log, in an email address, in an AWS sharing object with no document behind it, and in the memory of a running Python process.

Attack of the Week: AWS WAF logs every request header

Mayo was on an engagement reading CloudWatch logs for anything reusable. The WAFs were configured to capture all headers. As she puts it, "which was pretty convenient!"

What the default captures. Ask AWS WAF to log and you get the HTTP method, the URI path, the query string, the HTTP version, the client IP, the country, a timestamp, and httpRequest.headers[], meaning every header name and value the client sent. Mayo's summary of the default: "Surprisingly a lot, and none of it is automatically redacted." AWS's own CAPTCHA example in the WAF logging docs shows session cookie data in the log output. Full request bodies stay out. Mayo adds that this helps less than it sounds: "just because a POST request uses body content doesn't mean the service will fully validate it." And the Cookie header was never in the body anyway.

Finding them. AWS requires the names: a CloudWatch log group or an S3 bucket holding WAF logs "must start with aws-waf-logs-." Mayo calls it "the required prefix." One grep over that string finds them. The permissions are ordinary read actions, which Mayo notes makes them "more likely to be part of an overly permissive principal." For CloudWatch, two:

{ "Effect": "Allow",
  "Action": ["logs:DescribeLogGroups", "logs:FilterLogEvents"],
  "Resource": "*" }

For S3, three: s3:ListAllMyBuckets, s3:ListBucket, s3:GetObject.

Request sampling, the third read path. You can also pull requests from the WAF itself, through wafv2:GetSampledRequests. Sampling returns up to 500 entries drawn from a 5,000-request sample, so the coverage is thin.

RedactedFields is the setting on your logging configuration that blanks chosen fields before they are written. AWS's own documentation says it "has no impact on request sampling." So a team that redacted the Cookie header, checked its CloudWatch logs, and saw clean output still hands over full unredacted headers to anyone who can call GetSampledRequests. Four read actions get you there: wafv2:ListWebACLs, wafv2:GetWebACL, wafv2:GetLoggingConfiguration, wafv2:GetSampledRequests.

Mayo released waf-fu alongside the post: a TUI that pulls WAF log data into a local SQLite cache and replays a chosen session as curl, as an injected browser session in Chrome or Firefox, or as a HAR file for Burp.

Two more gaps in RedactedFields. It takes four match types, UriPath, QueryString, SingleHeader and Method. There is no cookie option, so you use SingleHeader with the name cookie. And per AWS's API reference, SingleHeader redaction does not apply to a rule using the Headers aggregate match type, so a rule inspecting all headers logs them in the clear even with that redaction configured.

DataProtectionConfig is the setting that covers all three of those paths. It arrived in February 2025 and redacts at the web ACL level, so CloudWatch, S3 and request sampling are all included. Two quirks survive. aws-waf-token is explicitly exempt, and SINGLE_HEADER protection has the same blind spot with rules inspecting all Headers.

Mayo found the redaction advice in one place, the WAFv2 Best Practices documentation, which does not live on the main docs.aws.amazon.com site.

Ship This Week.

  • List every log group and bucket starting with aws-waf-logs-. The prefix is required, so that finds every CloudWatch and S3 destination. It is still not the whole picture: Mayo scoped his post to three read paths and says so, noting Amazon Security Lake consumes an independent WAF stream and that "the three destinations for logs we talked about here aren't all that's supported." Read your logging configurations too.

  • Read one log entry. If a Cookie or Authorization header comes back in full, every principal who can read that log group is holding live material, and whether a replay lands depends on how long the session stays valid and what else the app checks.

  • Move to DataProtectionConfig rather than RedactedFields, and confirm it by calling GetSampledRequests yourself and reading the output.

  • Check who holds logs:FilterLogEvents and wafv2:GetSampledRequests on *. Mayo's point is that read-only actions are "more likely to be part of an overly permissive principal," because reviews wave them through.

Rule of the Week: a failed root login proves someone has the email

From July 24 to August 23, Datadog watched failed authentication attempts against AWS root users at more than 150 organizations. A median of two attempts each, up to eight. The requests came through proxies spanning many countries, and the targets share no industry or country pattern.

Generating that failed ConsoleLogin event requires the root email address, so the event itself is proof that somebody already had it. McCloskey: the attacker "either already had a list of root user email addresses, or brute forced through a list of account email addresses until finding a valid one." Most of us treat that address as contact data rather than an authentication factor, even though aws organizations list-accounts will hand that address to anyone who can call it. It rarely has an owner, and a failed root login is usually the first place it shows up as a security signal, which is the alert most of us never wrote.

So alert on every root ConsoleLogin, success or failure. Then match the two agent strings Datadog pinned to this campaign:

eventSource       = signin.amazonaws.com
eventName         = ConsoleLogin
userIdentity.type = Root
userAgent contains "Chrome/85.0.4183.83" and "Edg/85.0.564.41"
       or contains "rv:120.0) Gecko/20100101 Firefox/120.0"

The agent strings will change. The root-login alert will not, so build that one and treat the agent strings as a filter for this campaign.

Do this week. Search CloudTrail for userIdentity.type: Root with ConsoleLogin over the last 60 days. Datadog observed no successful authentication attempt and says plainly it cannot determine intent or motive, so a Failure is the expected hit, and a Success you cannot trace to a person is an incident. Then check the management account: service control policies and centralized root access both leave it out, and Datadog says so plainly.

Defender's Corner

Stop treating a clean external access report as an answer. Daniel Grzelak at Plerion, "I lost my mind doing an AWS external access review" [2026-08-27].

Grzelak's boss asked who outside the company can reach their AWS accounts. He figured two commands, maybe three. It took a couple of months.

AWS has seven separate mechanisms for handing access to an outsider, and they sort by "what object carries the permission, where does that object live, and who can read it back?" His seven, in his own headings: roles and trust policies, resource-based policies, RAM shares, account id lists, grants that live outside IAM, configuration that grants access, and credentials and copies.

Resource Access Manager, if you have not used it, "takes the permission off the resource and puts it in an object of its own, called a resource share." On most types that leaves nothing on the resource to read. On SSM parameters and CodeBuild projects it writes a policy onto the resource and the service shows it to you, so you have to know which is which before a clean policy read means anything.

Several of them mislead you depending on where you read:

  • An ECR registry policy is one per account per Region. Ask each repository and every one denies having a policy. "Loop over resources asking each one for its policy and you can sweep an entire account and find nothing."

  • A VPC Lattice auth policy reads back in full with state: Inactive while authType is NONE. Flip authType and the same document is live again, with no policy write.

  • An S3 ACL grant to AuthenticatedUsers, which means every AWS account, vanishes from get-bucket-acl once IgnorePublicAcls is on. It was stored the whole time. Clear the flag and it returns.

  • RAM defaults --allow-external-principals to true, and typing --no-allow-external-principals only means something if the account sits in an Organization. Outside one there is no organization for the share to stay inside, so there is nothing for false to express.

  • A second flag called external sits on each principal association. On a share RAM built out of your resource policy, it goes false whenever that policy carries aws:PrincipalOrgID or aws:PrincipalOrgPaths. RAM never checks whose organization that is, and never looks at the principal. So name any org on earth and a grant to a stranger reads as internal.

IAM Access Analyzer, AWS's own service for reporting external access, reads KMS grants, S3 bucket ACLs, and the RDS and EBS snapshot sharing attributes, so it reaches past policy documents and into several of the mechanisms above. Grzelak calls it "a genuine multi-mechanism tool with a short list." He counts fifteen resource types, then spends the next page on the exclusions: the supported-types page saying "It only analyzes resource-based policies," a Lambda gap on qualified ARNs, RDS cluster snapshots shared through RAM going unreported, and a first scan that had 13% of its findings at one minute and gave no signal it was still working.

Grzelak's conclusion: "You can't prove a negative about external access on AWS from inside your own account. Some permissions have no reader and some of them live in the other party's account."

One disclosure. The post is on Plerion's own blog and ends on the feature Plerion shipped to do this. The seven mechanisms hold either way, and the CLI transcripts are concrete enough to run in a sandbox account, though some depend on your Region and your Organizations setup.

Agent Bench: find the grants our review never queries

Every access review we run has an implicit list of API calls behind it. That list has probably never been checked against the seven mechanisms above, and Grzelak has now laid all seven out in one place.

We are looking for the gaps, not building another inventory. Point a coding agent at whatever runs our external-access review, the Config rules, the Steampipe or CloudQuery queries, the scheduled Lambda, and ask for three columns:

  1. Every AWS API call the review makes, at file:line.

  2. Which of Grzelak's seven mechanisms each call covers, and the resource types it enumerates.

  3. The mechanisms with no call against them at all.

Column three is the one to act on. Expect RAM shares, service-specific credentials, and the configuration objects (PrivateLink allowed-principals, Route 53 VPC association authorizations, Config aggregation authorizations) to come back empty. A review built around reading policies never asks for those.

What you walk away with: the questions our review has never asked, each traceable to the file that should have asked it.

Plan for the agent being confidently wrong. It will read an SDK call and assert coverage the call does not give, and Grzelak's post is a catalogue of calls that answer a question you did not ask. Verify the top three by hand before filing anything.

Try it this week: run column three, and only for RAM. Start with aws ram get-resource-shares --resource-owner SELF in every Region, then pull the principal and resource associations for each share and join them yourself. Reading the shared resources will not show you the same thing.

Also on the Radar

Attackers read the LiteLLM master key out of process memory. Yaara Shriki at Wiz, "Inside 90 days of attacks on AI infrastructure" [2026-08-27].

LiteLLM is a proxy teams put in front of the model providers. One of them can hold the real OpenAI, Anthropic, Azure and Gemini keys, so the apps behind it carry one. Wiz ran honeypots for 90 days and watched what attackers did after getting in. Generic credential-file hunting comes up short on a box like this, so they read the process instead: "rather than searching for credential files on disk, attackers queried the running process's Python module state directly to extract the master key from memory, since it doesn't exist in a file on the disk."

Five lines of Python: two against the litellm module, three against litellm.proxy.proxy_server. No file to scan, nothing in git. The provider keys are a different matter, and the same sessions went looking for them in /app/litellm_config.yaml, /etc/litellm/.env and ~/.litellm/config.yaml.

On boxes still running the default master key sk-1234, they asked the proxy to name its own backing model. Knowing the backend is how they choose between stealing the key, burning the inference quota, and moving on. And on the Langflow honeypot the miner sat in /app/data/.claude/ under the name unicorn. On a host running AI tooling, that directory looks like it belongs.

Four places a credential or a grant was sitting this week: a firewall log, an email address, an AWS share with no document behind it, and process memory. The WAF one takes ten minutes. List the log groups starting with aws-waf-logs- and read a single entry.

Stop Paying for 6 Tools. One AI Does It All.

Most e-commerce sellers juggle 6–8 tools and pay hundreds monthly to keep operations running. StoreClaw replaces the stack with one autonomous AI engine that monitors competitors, optimizes listings, automates marketing, and tracks profit 24/7. Connect your store and let AI handle the work — no prompts, no complex setup, no credit card required.