This is where the ASA stops being a Layer 4 gate and starts reading your web traffic. In this article we build a custom Layer 7 HTTP inspection policy that blocks a specific URL by regular expression, and then we prove it works with a real before-and-after test from a real Linux client: one URL returns instantly, the other is dropped mid-connection and hangs until it times out. No hand-waving, no simulated output. A real regex, a real request, a real block, captured on an ASAv 9.24 in CML. This is part of our Cisco ASA series and it goes well beyond the inspection engines overview, from listing what the engines do to making one drop traffic you choose.
Custom Layer 7 inspection is the payoff of everything in the ASA inspection story. The default policy inspects HTTP but does nothing with the contents. Once you attach your own inspection policy-map, the firewall can match on the URI, headers, methods, and body of every web request crossing it, and drop the ones you name. That is content control enforced at the firewall, and it is built on the same Modular Policy Framework you have seen throughout this series.
The goal: block one URI, allow the rest
Our target is simple and realistic. A published nginx server sits in the DMZ. We want normal pages to load, but any request whose URI contains the string secret should be dropped at the firewall before it ever reaches the server. The client should not get a polite error page; the connection should simply die, because we are dropping it at Layer 7, not returning an HTTP status.
Building this takes four objects, layered from the regex up to the service-policy. Each one answers a narrower question than the last.
regex BLOCK-URI ".*secret.*"
class-map type inspect http match-all PLZ-BADURI
match request uri regex BLOCK-URI
policy-map type inspect http PLZ-HTTP-INSPECT
parameters
class PLZ-BADURI
drop-connection log
policy-map PLZ-DMZ-POLICY
class PLZ-HTTP-CLASS
no inspect http
inspect http PLZ-HTTP-INSPECT
service-policy PLZ-DMZ-POLICY interface dmzRead it from the top. The regex defines the pattern to hunt for, .*secret.*, which matches any URI containing the string secret anywhere in it. The class-map type inspect http is a Layer 7 classifier: it matches HTTP requests whose URI matches that regex. The policy-map type inspect http is the Layer 7 policy: for the PLZ-BADURI class, it drops the connection and logs it. Finally, the ordinary Layer 4 policy-map swaps its plain inspect http for inspect http PLZ-HTTP-INSPECT, wiring our custom engine into the existing DMZ service-policy.
The nesting is the important idea. A Layer 4 policy-map cannot match a URI; only a type inspect http policy-map can. So you build the Layer 7 policy separately and then reference it as the action inside the Layer 4 policy. This is the ASA's way of layering deep inspection on top of the basic MPF stack from our Modular Policy Framework walkthrough.
The money capture: before and after from a real client
With the policy applied, we ran two requests from a Debian client against the published server. The first asks for a normal page. The second asks for a URI containing secret. We used curl with a format string that prints the HTTP status code and the total time, so the difference is impossible to miss.
j@llmbits:~$ curl -w '%{http_code} in %{time_total}s' http://203.0.113.80/index.html
normal /index.html -> HTTP 200 in 0.010053s <-- allowed, fast
j@llmbits:~$ curl -w '%{http_code} in %{time_total}s' http://203.0.113.80/secret/data.html
blocked /secret -> HTTP 000 in 8.003106s <-- connection DROPPED (000, then timeout)Look at the two lines side by side. The normal page returns HTTP 200 in about ten milliseconds, a clean, fast fetch. The secret URL returns HTTP 000, which is curl's way of saying it never got any HTTP response at all, and it takes eight full seconds to say so. That eight-second gap is the client waiting on a connection the ASA silently killed. There is no error page, no redirect, no reset that curl could interpret as a status; the connection is simply gone, and curl gives up only when its own timeout expires. That is exactly what drop-connection does, and the contrast between 0.01 seconds and 8 seconds is the whole proof in a single screen.
The ASA side: the drop counter and the syslog
The client saw a hang. The firewall saw everything. First, the inspection policy's own counters, filtered to the lines that matter.
FW1# show service-policy inspect http | include Inspect:|drop
Inspect: http PLZ-HTTP-INSPECT, packet 16, drop 1, reset-drop 0
drop-connection log, packet 1The engine name confirms our custom policy is the active one: Inspect: http PLZ-HTTP-INSPECT, not the plain default. drop 1 is the single dropped connection, the secret request. drop-connection log, packet 1 confirms the specific action fired exactly once. One request matched, one connection dropped. That is a clean, unambiguous counter, and it is the firewall-side twin of the eight-second hang the client experienced.
Now the logs, which are where L7 inspection becomes genuinely impressive, because the ASA is reading inside the HTTP and telling you what it saw.
FW1# show log
%ASA-5-304001: 192.168.99.100 Accessed URL 172.20.30.80:http://203.0.113.80/index.html
%ASA-5-304001: 192.168.99.100 Accessed URL 172.20.30.80:http://203.0.113.80/secret/data.html
%ASA-5-415006: HTTP - matched Class 23: PLZ-BADURI in policy-map PLZ-HTTP-INSPECT, URI matched -
Dropping connection from outside:192.168.99.100/52990 to dmz:172.20.30.80/80
%ASA-4-507003: tcp flow from outside:192.168.99.100/52990 to dmz:172.20.30.80/80 terminated by
inspection engine, reason - disconnected, dropped packet.
%ASA-6-302014: Teardown TCP connection 31 ... Flow closed by inspectionRead those five lines as a story. The two %ASA-5-304001 lines log both URLs the client accessed, the normal one and the secret one. That alone proves the ASA is parsing the HTTP request line, because it knows the full URL, not just the destination IP and port. Then %ASA-5-415006 fires on the secret request only, naming the matched class, PLZ-BADURI, and the policy-map, and states plainly that the URI matched and it is dropping the connection. The %ASA-4-507003 line records the TCP flow being terminated by the inspection engine, and %ASA-6-302014 tears the connection down. The firewall saw both URLs, decided on one, and killed it, all logged in order.
That %ASA-5-304001 pair is the detail worth sitting with. The ASA logged the exact URL of a request it allowed and the exact URL of a request it blocked. It is reading the application layer of every web request that crosses it. For a security team that is both a powerful audit capability and a privacy consideration worth being deliberate about.
What you can match in an http inspect class-map
We matched a URI, but that is one option among many. A Layer 7 HTTP inspection class-map can match on nearly every meaningful part of an HTTP transaction, which is what makes it a real content-control tool rather than a URL blocklist.
match request uri regex - the path and query, what we used to block secret.match request header - match on any header field, such as Host or Content-Type, by regex.match request body / match response body - inspect payload content within a length limit.match request method - allow GET/POST and block risky verbs like PUT, DELETE, or TRACE.match request header user-agent regex - block or flag specific clients and tools.Each of these becomes a match line inside a class-map type inspect http, and each class gets an action in the policy-map type inspect http: drop-connection, reset, or log. Combine several with match-all or match-any and you can express policies like "drop any POST to a URI containing admin from a curl user-agent" in a handful of lines.
Choosing the action: drop, reset, or log
We used drop-connection log, which silently discards the connection and writes a syslog entry. It is worth knowing the alternatives, because the action changes what the client experiences. drop-connection silently kills the flow, which is why our curl hung for eight seconds rather than getting an immediate error. reset sends a TCP reset, so the client fails fast instead of waiting for a timeout, which is friendlier when you want the user to know quickly but tells an attacker that something is actively filtering. log on its own takes no action and only records the match, which is the right first step when you are validating a regex before you let it drop production traffic.
A sane rollout is to deploy a new match with log only, watch the %ASA-5-415006 entries to confirm it fires on exactly the traffic you intend and nothing else, and only then switch the action to drop-connection or reset. A regex like .*secret.* is broad on purpose here, but in production an overly greedy pattern can catch far more than you meant, and the log-first approach is how you find that out before it becomes an outage.
Understanding the regex
The pattern .*secret.* deserves a closer look, because the regex is the part most likely to bite you. In ASA regex syntax, . matches any single character and * means zero or more of the preceding element, so .* matches any run of characters including an empty one. Wrapping secret in .* on both sides therefore means "the string secret appears anywhere in the URI", which is what caught /secret/data.html. It would equally catch /my-secret-plans or /documents/topsecret.pdf, because the match is a substring, not a path component.
That breadth is a feature when you want it and a hazard when you do not. If your intent was strictly the /secret/ directory, a tighter pattern anchored to the path, something closer to /secret/.*, expresses that intent and avoids collateral matches on unrelated URLs that merely contain the letters. The ASA also supports character classes, alternation with the pipe, and quantifiers, so you can build precise patterns. The discipline is to write the narrowest regex that covers your case, then confirm with log-only mode that it fires on exactly what you meant. A regex that is too greedy is the single most common way an L7 policy causes an outage, because it silently blocks pages nobody thought to test.
Why not just use an ACL or a proxy?
A fair question: if you want to block a URL, why not use a web proxy or a Layer 4 ACL? The answer clarifies what this feature is for. An ACL cannot do this at all, because a URI lives in the HTTP payload and an ACL only sees IP addresses and ports. Every request to the DMZ server hits the same IP and port 80, so an ACL can only allow or block the whole server, never a single path. A dedicated web proxy or next-generation firewall can absolutely do URL filtering, often with richer categorization and TLS interception, and if that is your primary requirement those tools are purpose-built for it.
ASA L7 inspection sits in between. It is not a full web-filtering platform, and it does not decrypt TLS, so it only sees inside plain HTTP. What it gives you is the ability to enforce targeted content rules directly on the firewall you already have, in the same MPF framework you already know, with no extra appliance in the path. For blocking a known-bad URI, restricting HTTP methods on a published server, or catching a specific user-agent, it is exactly the right tool, and the fact that it drops and logs with real precision is what makes it trustworthy for those jobs.
Where this sits in the packet flow
It helps to know when this drop happens relative to everything else the ASA does. The ACL permits the connection first, the connection is built, and only then does the HTTP inspection engine parse the request and decide to drop it. That is why the two %ASA-5-304001 URL-access logs appear before the drop: the connection was allowed and established, the request was read, and the drop came at Layer 7 after all the Layer 3 and 4 checks had already passed. Our ASA packet flow article lays out that ordering in full, and it is the mental model that explains why an L7 drop looks like a hang rather than a clean deny.
Key Takeaways
- Custom L7 HTTP inspection reads inside the request. A
regex, aclass-map type inspect http, and apolicy-map type inspect httplet the ASA match and drop on the URI, headers, method, or body. - The before-and-after is unambiguous. A normal page returned
HTTP 200 in 0.01s; the blocked URL returnedHTTP 000 in 8sas the dropped connection hung to timeout. - The firewall logs both URLs. Two
%ASA-5-304001entries recorded the allowed and blocked URLs, proving the ASA parses the HTTP request line, and%ASA-5-415006named the matched class on the drop. - The counter confirms it.
show service-policy inspect httpshoweddrop 1under the customPLZ-HTTP-INSPECTengine, the firewall-side twin of the client's hang. - Roll out with
logfirst. Validate a regex in log-only mode before switching todrop-connectionorreset, because a greedy pattern catches more than you expect.
You have now made the ASA drop web traffic by content, the deepest thing a firewall inspection engine does. Explore the rest of the Cisco ASA series for the surrounding pieces, revisit the inspection engines overview for the wider catalog, and read the packet flow article to see exactly where this drop lands in the ASA's processing order.