Scaling AWS WAF Exceptions Across Your Org

Here's a problem that comes up frequently once you're running AWS WAF across more than a handful of accounts. You turn on the AWS Managed Rules everywhere, same baseline for every app, and one app starts tripping a single rule in the Core Rule Set on a request that's actually fine. You want to let that one app past that one rule, without opening a hole in the baseline for anyone else, and without the central security team hand-editing a web ACL per app forever.
The answer has two parts: a label pattern that makes the exception safe, and a decision about where the rules sit once a central tool is pushing them to every account. I'll walk through both, then give you the concrete setup at the end.
The label pattern
Labels are the building block. A label is just metadata a rule attaches to a request when it matches. Two things make the pattern work: a rule set to Count is non-terminating (it stamps its label and WAF keeps evaluating), and a rule can only match a label that an earlier-priority rule already added.
The AWS Security Services Best Practices guide documents a three-stage version of this:
Run the AWS Managed Rules in Count. Every rule that matches stamps its label, for example
awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body, nothing blocks yet.Add a narrow exception rule that matches only the app's traffic (one host, one URI path) and stamps an exception label (e.g.
exception:core-rule-set:CrossSiteScripting_Body). Also Count, so it doesn't allow anything, it just marks the request.End with a block stage: one rule per managed-rule label, each blocking a request that carries its label unless the matching exception label is also present.
The useful property here is isolation. Each block rule checks exactly one label pair, so an exception for the XSS rule does nothing to the SQL-injection rule. One application's carve-out doesn't quietly weaken other application's protection, and if a request trips two managed rules and only has an exception for one, the other still blocks it.
What the two rules actually look like
The exception rule and the block rule are where the pattern becomes real, so here they are in full. Both are stripped to the parts that matter (the VisibilityConfig block is required on every rule, but I've left it out for readability).
First, the app team's exception rule. It matches only this app's traffic, one host and one path, and stamps the exception label. The action is Count, so it decides nothing on its own, it only marks the request:
{
"Name": "except-app1-xss-body",
"Priority": 100,
"Statement": {
"AndStatement": {
"Statements": [
{
"ByteMatchStatement": {
"FieldToMatch": { "SingleHeader": { "Name": "host" } },
"SearchString": "app1.example.com",
"PositionalConstraint": "EXACTLY",
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }]
}
},
{
"ByteMatchStatement": {
"FieldToMatch": { "UriPath": {} },
"SearchString": "/custom-feature/",
"PositionalConstraint": "STARTS_WITH",
"TextTransformations": [{ "Priority": 0, "Type": "NONE" }]
}
}
]
}
},
"Action": { "Count": {} },
"RuleLabels": [
{ "Name": "exception:core-rule-set:CrossSiteScripting_Body" }
]
}
Then the central block rule. This is the "block if the managed-rule label is present, unless the matching exception label is also present" logic, which in WAF is an AndStatement of the managed-rule LabelMatchStatement and a NotStatement wrapping the exception LabelMatchStatement:
{
"Name": "block-xss-body-unless-excepted",
"Priority": 100,
"Statement": {
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body"
}
},
{
"NotStatement": {
"Statement": {
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "exception:core-rule-set:CrossSiteScripting_Body"
}
}
}
}
]
}
},
"Action": { "Block": {} }
}
That's the whole trick in two rules. The block rule names exactly one managed-rule label and exactly one exception label, so it can only ever be relaxed for the one rule it targets. A request that trips CrossSiteScripting_Body and carries exception:core-rule-set:CrossSiteScripting_Body passes this rule, but if the same request also trips SizeRestrictions_Body, the separate block rule for that label still blocks it, because this app never got an exception for it. You write one block rule per managed-rule label you care about, and because the exception label is just the managed-rule label with an exception: prefix, you can generate the whole block layer mechanically from the list of managed-rule labels.
Managing it at scale: who pushes the rules, and where they sit
The practical question is "I have dozens of accounts with various applications, how do I protect them all without creating a management nightmare?" You do this by pushing a standard config from a central policy, which in AWS means AWS Firewall Manager or, now, AWS Network Security Manager.
Once a central tool is distributing the rules, the question changes. It's no longer "what priority number goes on each rule," it's "where do the central rules sit relative to the rules each app team owns." Both tools are built around the same answer: some rules run first, some run last.
A Firewall Manager WAF policy doesn't hand an account a finished web ACL. It gives you rule groups that run first, then the account owner's own rules, then rule groups that run last. The groups FMS creates carry PREFMManaged- and POSTFMManaged- prefixes, which is the mechanism telling you where each one is slotted. Network Security Manager works the same way with different names: you mark each WAF rule First (pre-process) or Last (post-process), and templates assemble rules in priority order inside those positions.
Map that onto the three stages and it lines up directly:
Central count goes in the first position, because the labels have to exist before anything can match them.
The app team's exception rules go in the middle, because that's the part the app team should own without the central team touching it.
Central block goes in the last position, because it has to run after every exception rule.
So the label pattern is the logic, and the distribution tool (FMS or NSM) is the structure: first, middle, last. Together they let a central team enforce one baseline across hundreds of accounts while each app team keeps a narrow lane in the middle for the exceptions only they understand.
Which tool: Firewall Manager or Network Security Manager
You'll find two tools for this now, and the structure above holds in both. Network Security Manager went GA in September 2026, and it will eventually replace Firewall Manager. Today Network Security Manager distributes AWS WAF and AWS Shield Advanced from a single multi-account policy. The two run side by side, and where their scope overlaps, Network Security Manager takes precedence during remediation.
Network Security Manager only supports AWS WAF and Shield Advanced at the time of publishing. If you only need to centrally manage those services, and you do not already use Firewall Manager, you should start with Network Security Manager. If you are currently using Firewall Manager, you should continue using it until the migration path is released.
This is about to matter for DDoS too
AWS Shield Advanced is moving application-layer protection onto the AWSManagedRulesAntiDDoSRuleSet managed rule group, and that rule group emits labels (an event-detected label plus graduated suspicion levels). So the same machinery you built for managed-rule exceptions is what you'll use to act on DDoS signal. Auto-upgrade for eligible web ACLs begins October 25, 2026, and the legacy L7 automatic mitigation sunsets January 29, 2027.
How to implement
Central baseline: a rule group running the AWS Managed Rules in Count, so every match stamps its label and nothing blocks yet. Put it in the first (pre-process) position of the FMS or NSM policy.
Central block layer: a rule group with one label-match block rule per managed-rule label, each blocking on its label unless the matching
exception:label is present. Put it in the last (post-process) position.The middle, left to app teams: a narrow exception rule (one host, one URI path, one header) that stamps the matching
exception:label in Count. Name each exception label after the managed-rule label it exempts, so the block layer can be generated mechanically and an app team knows exactly which label to add.Keep every exception as tight as the app actually needs, because an exception label removes a protection for whatever traffic it matches.
Give app teams a standard playbook
The reason this pattern scales is that app teams can add their own exceptions without the central team touching anything. But "self-service" only works if everyone builds exceptions the same way, so write it down as a short standard operating procedure and hand it to every app team. It doesn't need to be long. It needs to make the safe path the obvious one. A workable SOP is about six steps:
Confirm it's a real false positive first. Pull the sampled requests or WAF logs for the managed-rule label that's firing, and confirm the blocked request is legitimate traffic, not an attack you're about to wave through. The label on the request tells you exactly which rule tripped.
Scope to the smallest match that covers it. Match on your host plus the specific URI path, and the HTTP method if it narrows things further. Never except a whole host. The exception removes that protection for every request the rule matches, so the match should be as tight as the feature that needs it.
Name the exception label after the managed-rule label. Take the managed-rule label (
awswaf:managed:aws:core-rule-set:CrossSiteScripting_Body) and build the exception label asexception:core-rule-set:CrossSiteScripting_Body. The naming is not cosmetic: it's how the central block layer knows which exception pairs with which rule, and it's how you keep the whole thing greppable.Set the action to Count. The exception rule marks the request, it doesn't allow it. Allow is terminating and would skip the rest of your own rules too.
Test before you rely on it. Deploy, replay the request that was failing, and confirm two things in the logs: the request now carries your exception label, and it's no longer blocked. Then send a request that trips a different managed rule and confirm it still blocks, so you know you scoped the exception to one rule and not the whole app.
Record it. One line somewhere durable: the label, the host and path it covers, who approved it, and why. Exceptions are standing holes in a managed rule, and the next person to look needs to know this one was deliberate.
If you give app teams this list and the one-rule template above, the central team's job shrinks to owning the first and last rule groups and reviewing the occasional exception, instead of being the bottleneck for every false positive across the org.
The next time someone needs one app past one managed rule without weakening the baseline, don't reach for a per-app web ACL. Reach for labels, and decide where the rules sit: count first so the labels exist, exceptions in the middle owned by the people who know the app, block last so nothing gets ahead of it.





