If you have ever stared at an ASA configuration and wondered how a single firewall applies HTTP inspection here, a connection limit there, and TCP normalization somewhere else without a tangle of one-off commands, the answer is the Modular Policy Framework (MPF). MPF is the engine underneath nearly every advanced ASA feature, and once you see its three-layer shape you stop memorizing syntax and start reasoning about it. This article is part of our Cisco ASA series, and it builds the framework from scratch on a real ASAv 9.24 running in CML, with live counters climbing as a Linux client hits a published web server.
The reason MPF matters is that Cisco deliberately reused one structure for everything. Application inspection, quality of service, connection limits, TCP normalization, and policing all plug into the same three-layer model. Learn it once and every feature after that is just a different action bolted onto the same skeleton. That reuse is the literal meaning of the word "modular" in Modular Policy Framework.
The three-layer model
MPF answers three separate questions, and it keeps them in three separate objects. This separation is the whole design. You never mix "which traffic" with "what to do about it", because that is exactly the mess MPF was invented to avoid.
- class-map answers WHICH traffic. It is a classifier: match a port, an ACL, a DSCP value, or a protocol. It says nothing about what happens next.
- policy-map answers WHAT action. It references one or more class-maps and attaches an action to each: inspect this, police that, limit connections on the other.
- service-policy answers WHERE. It binds a policy-map to an interface (or globally), which is the moment the policy actually starts acting on packets.
Nothing happens until all three exist and the service-policy is applied. A class-map with no policy-map is inert. A policy-map with no service-policy is just a definition sitting in the config. The service-policy is the switch that turns the whole stack on.
Building MPF from scratch
Let us build the simplest useful policy: inspect HTTP on the DMZ interface, where a published nginx server lives. We work bottom up, matching the three questions in order. First, classify the traffic we care about.
class-map PLZ-HTTP-CLASS <-- 1. class-map: WHICH traffic
match port tcp eq wwwThat is the entire classifier. It matches TCP destined for port 80. It does not inspect anything, drop anything, or care which interface the traffic arrives on. It only names a set of packets so a policy-map can point at them.
Next, decide what to do with that class. We create a policy-map, reference the class inside it, and attach an action.
policy-map PLZ-DMZ-POLICY <-- 2. policy-map: WHAT action
class PLZ-HTTP-CLASS
inspect httpNow the policy says: for traffic matched by PLZ-HTTP-CLASS, run the HTTP application inspection engine. Still nothing is happening to live packets, because the policy is not attached anywhere. That is the final step.
service-policy PLZ-DMZ-POLICY interface dmz <-- 3. service-policy: WHERE (bind to an interface)The instant that command lands, the ASA starts classifying traffic on the DMZ interface, matching HTTP, and inspecting it. Three objects, three questions, one live policy.
Watching the counters climb
Theory is cheap. The proof is in the counters. With the policy applied, we drove five real HTTP requests from a Linux client against the published server and then read the per-interface service policy on the ASA. These are genuine packets from a genuine curl loop, not a lab simulation.
FW1# show service-policy interface dmz
Interface dmz:
Service-policy: PLZ-DMZ-POLICY
Class-map: PLZ-HTTP-CLASS
Inspect: http, packet 60, lock fail 0, drop 0, reset-drop 0, 5-min-pkt-rate 0 pkts/secpacket 60 is the whole story in two words. Sixty packets were classified by PLZ-HTTP-CLASS and handed to the HTTP inspection engine, which is exactly what you would expect from a handful of real HTTP transactions once you count the TCP handshakes, requests, responses, and teardowns. drop 0 and reset-drop 0 confirm the engine inspected everything and dropped nothing, because plain HTTP to a normal URI is perfectly conformant. Run more requests and refresh, and the number climbs. That live counter is how you confirm a class-map is actually catching the traffic you think it is.
If packet stays at zero while you know traffic is flowing, your class-map is not matching. That single number is the fastest MPF troubleshooting signal on the box: it tells you whether the "WHICH" layer is doing its job before you ever look at the action.
How a class-map can match
Our example matched a single TCP port, but that is only one of the ways a class-map classifies traffic. In practice you will reach for several match types depending on how precisely you need to carve up flows, and knowing the menu keeps you from writing an ACL when a one-line match would do.
- match port - a single port or a range, as in
match port tcp eq www. Fast and readable for a well-known service. - match access-list - point at an extended ACL when you need to classify by source, destination, and port together. This is the most flexible option and the one you fall back to when a simple port match is too blunt.
- match dscp - classify by DSCP marking, which is how QoS policies pick out already-marked voice or video.
- match default-inspection-traffic - a special built-in match that selects the standard set of protocols the ASA knows how to inspect, using each protocol's well-known port. This is what the factory
inspection_defaultclass-map uses, and it is why the default policy just works without you listing every port. - match any - matches every packet, used when an action should apply to all traffic on an interface, such as a blanket TCP normalizer.
A class-map can also be match-all or match-any when it carries multiple match lines, which controls whether every condition must be true or just one of them. For the single-match classifier above it makes no difference, but it becomes important the moment you start combining conditions, which is exactly what Layer 7 inspection class-maps do.
Why this is called "modular"
Here is the payoff, and the reason this article sits at the front of the ASA deep-dive. The exact three-layer stack you just built for HTTP inspection is the same stack every other ASA feature uses. Nothing about the skeleton changes. Only the action inside the policy-map changes.
inspect http, inspect ftp, inspect dns. Same class-map, same service-policy.priority or police. Classify by DSCP, then queue or rate-limit.set connection conn-max and embryonic limits to blunt SYN floods.set connection advanced-options pointing at a tcp-map.Every one of those features answers the same three questions. Which traffic? A class-map. What action? A policy-map. Where? A service-policy. When you learn a new ASA feature, you are not learning a new configuration model, you are learning one new action verb to drop into a policy-map you already know how to write. That is modularity in the most practical sense: one framework, many actions.
It also explains the ASA's default configuration. Out of the box the firewall ships with a global_policy policy-map, an inspection_default class-map, and a service-policy global_policy global line. That is MPF applied to itself, running the default inspection set on every interface. We will pull that default apart in the ASA inspection engines deep-dive, but notice that even Cisco's factory config is just the same three layers you built by hand above.
Global versus interface policies
You can apply a service-policy in two scopes, and knowing the rule prevents a lot of confusion. A service-policy attached with interface dmz is an interface policy and it acts only on that interface. A service-policy attached with the keyword global acts on every interface that does not already have a matching interface policy.
The precedence rule is simple: an interface service-policy overrides the global service-policy for that specific feature and that specific interface. That is why our per-interface PLZ-DMZ-POLICY can inspect HTTP on the DMZ while the factory global_policy keeps running the default inspection set everywhere else. They coexist because they are scoped differently, and the more specific interface policy wins where they overlap. This is worth internalizing before you start layering custom inspection on top of the defaults, because a surprising number of "my inspection is not applying" tickets come down to a global policy and an interface policy quietly fighting over the same traffic.
Common MPF mistakes
Most MPF problems are not exotic. They cluster around a handful of predictable mistakes, and every one of them maps back to the three-layer model. If you keep the three questions in mind, you can usually diagnose the issue before you touch a debug command.
The first is defining a class-map and policy-map but forgetting the service-policy, so nothing is ever applied. The counters give this away instantly: there is no interface output to read because the policy is not bound. The second is a class-map that does not match the traffic you expected, which shows up as a packet 0 counter while traffic is clearly flowing. The third, and the subtlest, is an interface policy and the global policy quietly overlapping so that the more specific interface policy wins and your global rule appears to do nothing on that interface. The fix in every case is the same: read the config bottom up, confirm the service-policy is applied, then confirm the class-map is matching before you ever suspect the action.
There is one more constraint worth knowing early. A single interface can only have one service-policy at a time, and a policy-map can only be applied to one place per direction. If you need several actions on the same traffic, you stack multiple class statements inside one policy-map rather than trying to apply two service-policies to the same interface. That constraint is exactly why the framework encourages you to think of a policy-map as a container of actions rather than a single rule.
Reading an MPF config quickly
When you open an unfamiliar ASA, read MPF from the bottom up. Find the service-policy lines first, because they tell you which policy-maps are actually live and where. Then read each referenced policy-map to see the actions. Then read the class-maps to see what traffic those actions hit. Working backwards from "what is applied" to "what it matches" is far faster than reading top to bottom and trying to hold the whole thing in your head.
The verification commands follow the same logic. show service-policy interface dmz shows a single interface's live policy with counters, which is what we used above. show service-policy global shows the global policy and its counters. Both print the class-map, the action, and the per-action packet counts, so you can confirm at a glance not just that a policy exists but that it is catching traffic.
Key Takeaways
- MPF has exactly three layers. class-map = WHICH traffic, policy-map = WHAT action, service-policy = WHERE it applies. Every ASA feature you configure fits this shape.
- Nothing is live until the service-policy is applied. A class-map or policy-map on its own is just a definition. The service-policy is the on switch.
- The
packetcounter is your first troubleshooting signal.show service-policy interface dmzshowedpacket 60from real HTTP flows. Zero means your class-map is not matching. - "Modular" is literal. Inspection, QoS, connection limits, and TCP normalization all reuse the same three-layer stack, changing only the action verb inside the policy-map.
- Interface policies override the global policy for the same feature on the same interface, which is how custom inspection coexists with the factory default_inspection set.
With the framework clear, the natural next step is to see what the ASA's default inspection policy actually does and how application inspection differs from a plain ACL. Continue with the rest of the Cisco ASA series, starting with the application inspection engines that plug into the policy-map you just built.