Most "the ACL is broken" tickets are not really broken ACLs. They are misunderstood ACLs: a packet you thought matched line 3 actually matched line 1 and got denied; a deny line further down absorbed traffic the user expected to permit; an implicit deny at the end caught a flow nobody had a permit for. The ASA always tells you exactly what happened, but you have to ask it the right way.
This article is the ACL troubleshooting reference for the Cisco ASA on software 9.x. The diagnostic of choice is packet-tracer, backed by show access-list hit counters and show asp drop. All output below is from a live ASAv in the PingLabz reference lab. For the cluster context, see the Cisco ASA pillar; for the broader ACL guide, see Cisco ASA ACL Configuration; for the packet-tracer command itself, see Cisco ASA packet-tracer Command.
The Three Tools, in the Order You Use Them
| Step | Command | Tells you |
|---|---|---|
| 1 | packet-tracer input ... | For one specific 5-tuple, did the ACL allow or drop, and which line matched. |
| 2 | show access-list NAME | Per-line hit counters and last-hit timestamps for the named ACL. |
| 3 | show asp drop | Aggregate counter of every drop reason on the ASA, including acl-drop. |
Step 1 answers "what happens to this exact packet?" Step 2 answers "is the rule I just edited actually being hit?" Step 3 answers "is the ASA dropping anything, and if so, why?"
Lab Setup
The PingLabz ASA has an OUTSIDE_IN ACL applied inbound on the outside interface. The ACL has been deliberately seeded with one bad-actor block at the top, three permits for the DMZ web server, and an explicit catch-all deny with logging at the bottom (added in addition to the implicit deny so we can see clean logging in syslog):
ASA-PERIM# show running-config access-list
access-list OUTSIDE_IN extended deny tcp host 198.51.100.99 any
access-list OUTSIDE_IN extended permit tcp any object DMZ-WEB eq www
access-list OUTSIDE_IN extended permit tcp any object DMZ-WEB eq https
access-list OUTSIDE_IN extended permit icmp any object DMZ-WEB
access-list OUTSIDE_IN extended deny ip any any logThat gives us five different troubleshooting scenarios on one ACL: an explicit deny hit, three permit lines (one cold, two warm), and a logged catch-all deny.
Scenario 1: Explicit Deny Hit
An attacker IP we have blocklisted (198.51.100.99) tries to reach the DMZ web server. We expect line 1 of the ACL to drop it.
ASA-PERIM# packet-tracer input outside tcp 198.51.100.99 51000 198.51.100.10 443
Phase: 1
Type: UN-NAT
Subtype: static
Result: ALLOW
Config:
object network DMZ-WEB
nat (dmz,outside) static 198.51.100.10
Additional Information:
NAT divert to egress interface dmz
Untranslate 198.51.100.10/443 to 192.168.50.10/443
Phase: 2
Type: ACCESS-LIST
Subtype:
Result: DROP
Config:
access-group OUTSIDE_IN in interface outside
access-list OUTSIDE_IN extended deny tcp host 198.51.100.99 any
Result:
input-interface: outside
input-status: up
input-line-status: up
output-interface: dmz
output-status: up
output-line-status: up
Action: drop
Time Taken: 52927 ns
Drop-reason: (acl-drop) Flow is denied by configured rule, Drop-location: frame snp_classify_table_lookup:6044 flow (NA)/NAPhase 2 is where the answer lives. The drop reason is acl-drop, the line that fired is printed verbatim (access-list OUTSIDE_IN extended deny tcp host 198.51.100.99 any), and the ACL name is shown via the access-group binding. No guessing required.
One detail worth noting: NAT (Phase 1) ran before the ACL (Phase 2). The packet-tracer output shows the ACL evaluating against the post-NAT (real) destination 192.168.50.10, which is why the DMZ-WEB object reference works in the permit lines. On modern ASA software the ACL always sees real addresses on both sides of the rule.
Scenario 2: Permit Hit (with a Lab Quirk at the End)
A normal user IP reaches DMZ-WEB on TCP/443. We expect line 3 (the HTTPS permit) to match.
ASA-PERIM# packet-tracer input outside tcp 198.51.100.50 51001 198.51.100.10 443
Phase: 1
Type: UN-NAT
Subtype: static
Result: ALLOW
Config:
object network DMZ-WEB
nat (dmz,outside) static 198.51.100.10
Additional Information:
NAT divert to egress interface dmz
Untranslate 198.51.100.10/443 to 192.168.50.10/443
Phase: 2
Type: ACCESS-LIST
Subtype:
Result: ALLOW
Config:
access-group OUTSIDE_IN in interface outside
Phase: 10
Type: FLOW-CREATION
Subtype:
Result: ALLOW
New flow created with id 54, packet dispatched to next module
Phase: 11
Type: INPUT-ROUTE-LOOKUP-FROM-OUTPUT-ROUTE-LOOKUP
Subtype: Resolve Preferred Egress interface
Result: ALLOW
Found next-hop 192.168.50.10 using egress ifc dmz
Result:
Action: drop
Drop-reason: (no-v4-adjacency) No valid V4 adjacency. Check ARP table (show arp) has entry for nexthop., Drop-location: frame snp_fp_adj_process_cb:256 flow (NA)/NAPhase 2 says ALLOW. The ACL did its job. The eventual Action: drop at the bottom is from no-v4-adjacency, which is a downstream problem (no ARP entry for the DMZ host in this lab), not an ACL problem.
This is the exact pattern you will hit in production all the time: the ACL is fine, the failure is somewhere else. Reading the packet-tracer in order, top-to-bottom, prevents you from blaming the ACL when the cause is one phase later.
Scenario 3: The Explicit Catch-All Deny
A legitimate user IP tries to reach DMZ-WEB on TCP/22 (SSH), which the published service does not expose. We expect line 5 (the explicit deny ip any any log) to fire.
ASA-PERIM# packet-tracer input outside tcp 198.51.100.50 51002 198.51.100.10 22
Phase: 1
Type: UN-NAT
Subtype: static
Result: ALLOW
Config:
object network DMZ-WEB
nat (dmz,outside) static 198.51.100.10
Additional Information:
NAT divert to egress interface dmz
Untranslate 198.51.100.10/22 to 192.168.50.10/22
Phase: 2
Type: ACCESS-LIST
Subtype: log
Result: DROP
Config:
access-group OUTSIDE_IN in interface outside
access-list OUTSIDE_IN extended deny ip any any log
Result:
Action: drop
Drop-reason: (acl-drop) Flow is denied by configured rule, Drop-location: frame snp_classify_table_lookup:6044 flow (NA)/NAPhase 2 says DROP, and now the Subtype: log field is populated because the matched line has the log keyword. The line that fired is the catch-all deny ip any any log, and the corresponding syslog message gets generated (%ASA-4-106023) for the security team.
Lesson: if you want visibility into what is being dropped at the implicit deny, replace it with an explicit deny with the log keyword. The ASA's implicit deny does not log.
Reading Hit Counters
After running the three packet-tracer scenarios, show access-list OUTSIDE_IN shows the per-line counters with timestamps:
ASA-PERIM# show access-list OUTSIDE_IN
access-list OUTSIDE_IN; 5 elements; name hash: 0xe01d8199
access-list OUTSIDE_IN line 1 extended deny tcp host 198.51.100.99 any (hitcnt=1) (Last Hit=00:01:11 UTC May 10 2026) 0xa0fb08eb
access-list OUTSIDE_IN line 2 extended permit tcp any object DMZ-WEB eq www (hitcnt=1) (Last Hit=23:27:43 UTC May 9 2026) 0xf18a0028
access-list OUTSIDE_IN line 2 extended permit tcp any host 192.168.50.10 eq www (hitcnt=1) (Last Hit=23:27:43 UTC May 9 2026) 0xf18a0028
access-list OUTSIDE_IN line 3 extended permit tcp any object DMZ-WEB eq https (hitcnt=2) (Last Hit=00:01:11 UTC May 10 2026) 0x1ce50e7b
access-list OUTSIDE_IN line 3 extended permit tcp any host 192.168.50.10 eq https (hitcnt=2) (Last Hit=00:01:11 UTC May 10 2026) 0x1ce50e7b
access-list OUTSIDE_IN line 4 extended permit icmp any object DMZ-WEB (hitcnt=0) 0x2cc9dc3f
access-list OUTSIDE_IN line 4 extended permit icmp any host 192.168.50.10 (hitcnt=0) 0x2cc9dc3f
access-list OUTSIDE_IN line 5 extended deny ip any any log informational interval 300 (hitcnt=1) (Last Hit=00:01:12 UTC May 10 2026) 0x2dc51227Read off the data:
- Line 1 (deny attacker): hitcnt=1. The explicit deny scenario ran exactly once.
- Line 2 (permit www): hitcnt=1. From an earlier test session.
- Line 3 (permit https): hitcnt=2. Two HTTPS allow runs.
- Line 4 (permit icmp): hitcnt=0. Never been hit. This is informational: maybe the rule was added preemptively, maybe it should be removed, but it is not failing.
- Line 5 (catchall deny): hitcnt=1. The TCP/22 drop scenario.
Each "object" line in the show output expands to its underlying real address ("host 192.168.50.10") on the next line, with the same hit count. That is how object-group expansion is rendered in the show output - one logical line, one or more underlying lines.
The real diagnostic value of show access-list is the (hitcnt=...) column. After every change to an ACL, run a few real connections, then check the counter on the line you expected to fire. If the counter does not increment, your rule is not matching. The most common reasons are wrong source/destination, wrong port, or another line above absorbing the traffic.
The Big Picture: show asp drop
For aggregate visibility into everything the ASA has dropped, ACL or otherwise, use show asp drop:
ASA-PERIM# show asp drop frame acl-drop
Flow is denied by configured rule (acl-drop) 317
Last clearing: Never317 ACL drops since the ASA last had its counters cleared. The frame acl-drop argument filters to just the acl-drop reason; without arguments you get the full table:
ASA-PERIM# show asp drop
Frame drop:
No valid V4 adjacency. Check ARP table (show arp) has entry for nexthop. (no-v4-adjacency) 3
Flow is denied by configured rule (acl-drop) 305
FP L2 rule drop (l2_acl) 16
Interface is down (interface-down) 3
Last clearing: NeverThis is the place to look when a user reports "my packet is being dropped somewhere on the firewall" but cannot say where. The drop-reason names map one-to-one with what packet-tracer prints in its Result section, so once you know the reason you can run a packet-tracer and confirm.
Useful asp drop reasons related to ACLs and policy:
| Drop reason | What it means |
|---|---|
acl-drop | Interface ACL denied the flow. |
l2_acl | Layer-2 ACL denied (Ethertype, MAC). |
nat-no-xlate-to-pat-pool | PAT pool exhausted; no port available. |
no-v4-adjacency | No ARP entry for the next-hop on the egress interface. |
rpf-violated | uRPF check failed; source routes back through a different interface. |
tcp-not-syn | TCP packet without SYN arrived for a flow that does not exist; usually a stale flow on the client. |
If acl-drop is climbing without a known cause, run capture asp_drop type asp-drop acl-drop to capture the actual dropped frames for analysis. That is the deepest level of ACL diagnostic the ASA offers.
Common ACL Bugs and How to Spot Them
- An earlier deny absorbs the permit. Symptom: the permit line is in the config but its hit count is zero. Diagnosis: run packet-tracer; phase 2 will print the actual line that fired. Fix: re-order the ACL.
- Object expansion mismatch. The permit references an object-group, but the user's traffic does not match any member of the group. Diagnosis:
show access-listexpands the group inline; compare members to the actual source/destination. Fix: add the missing member to the object-group. - ACL applied to the wrong interface or direction. Symptom: ACL exists but never matches. Diagnosis:
show running-config access-grouptells you what is bound where. Fix: re-bind correctly. - Real vs mapped IP confusion (legacy configs). ACLs from ASA software pre-8.3 used mapped IPs; modern ASA uses real IPs. Symptom: traffic works on an old ASA but breaks after upgrade. Diagnosis: packet-tracer shows the rule does not match the real address. Fix: rewrite the ACL to reference real IPs.
- Implicit deny silently dropping. Symptom: traffic just disappears, no syslog. Diagnosis: nothing in
show access-listmatches. Fix: replace the implicit deny with an explicitdeny ip any any logat the bottom of every ACL so future drops generate syslog.
The Five-Step ACL Troubleshooting Checklist
- Run
packet-tracer input INTERFACE PROTO SRC SPORT DST DPORTwith the exact 5-tuple from the user complaint. Read every phase top-to-bottom. - If phase 2 (ACCESS-LIST) is the drop, the printed line tells you the rule that fired. Compare to the rule you expected.
- If the drop is somewhere else (NAT, route-lookup, adjacency), the ACL is not the problem. Stop blaming it.
- If phase 2 says ALLOW but the user still cannot connect, run a real test connection and check
show access-listhit counters. Counter incrementing means the ACL is letting it through; the failure is downstream. - For periodic auditing, run
show asp dropto see aggregate drop counters by reason. Sudden spikes inacl-dropwith no known cause warrantcapture asp_drop.
Where to Go Next
- Cisco ASA ACL Configuration for the inbound ACL syntax and object-group structure.
- Cisco ASA packet-tracer Command for the full diagnostic walk through every phase.
- Cisco ASA Packet Flow for where the ACL phase sits relative to NAT, conn lookup, and forwarding.
- Cisco ASA Security Levels for why outside-to-DMZ even needs an ACL in the first place.
- Cisco ASA NAT Explained for the NAT phase that runs before the ACL.
- Cisco ASA pillar for the cluster index.
Key Takeaways
- ACL troubleshooting on the ASA is three commands: packet-tracer for one specific flow,
show access-listfor per-line hit counters,show asp dropfor aggregate drop reasons. - packet-tracer phase 2 prints the exact ACL line that matched, in either ALLOW or DROP. Read it before guessing.
- The implicit deny does not log. Replace it with an explicit
deny ip any any logat the bottom of every interface ACL so silent drops become visible. - Modern ASA ACLs reference real (untranslated) addresses. Configs ported from pre-8.3 ASAs that still reference mapped IPs will fail after upgrade; rewrite them to real IPs.
- If packet-tracer's phase 2 ALLOWs but the user still cannot connect, the failure is downstream of the ACL. Stop debugging the ACL.