Ask a room of network engineers which IP an ASA access list should reference when you publish a server, the private real address or the public mapped one, and you will get a split decision. That split is the single most misunderstood behaviour on the Cisco ASA, and it has cost more late nights than almost anything else on the platform. The answer since ASA 8.3 is unambiguous: interface ACLs reference the real, post-translation IP, never the mapped one. This article settles it with a before-and-after on one flow, allowed with a real-IP rule and dropped with a mapped-IP rule, everything else held constant.
This article is part of the Cisco ASA cluster and is the direct follow-on to destination NAT for inbound services, where we published a DMZ web server and flagged that its ACL used the real IP. Here we prove exactly why, using detailed packet-tracer output from a live ASAv, and we contrast it with the old pre-8.3 habit that quietly breaks rules on a modern firewall.
The Rule, and the Reason Behind It
The rule is one sentence: on ASA 8.3 and later, an interface access list is evaluated after the ASA has un-NATed the packet, so the ACL must reference the real (post-translation) address of the host, not the mapped or public address a client actually sent to.
The reason is the order of operations. When an inbound packet arrives, the ASA runs un-NAT as an early phase, before it consults the interface ACL. By the time the access rule is checked, the destination address in the packet has already been rewritten from the public IP to the private real IP. So the ACL is comparing against the real IP, and your rule has to name the real IP to match. This is not a quirk; it falls straight out of the fact that un-NAT is Phase 1 and the ACL check is Phase 2, the sequence proven in the destination NAT walkthrough. Get the phase order and the ACL rule stops being something to memorise and becomes something you can derive.
The Scenario
We reuse the published DMZ server from the destination NAT lab. The ASAv maps a private DMZ nginx host at 172.20.30.80 to a public address 203.0.113.80. An outside client at 192.168.99.100 connects to the public IP on port 80. Two access lists are in play across the two tests, identical except for one address:
OUTSIDE-INpermitstcp any host 172.20.30.80 eq www, the real IP.OUTSIDE-BADpermitstcp any host 203.0.113.80 eq www, the mapped IP.
Same firewall, same NAT rule, same simulated packet. The only variable is which IP the applied ACL targets. Watch what that one difference does.
Proof A: ACL Against the Real IP Permits
With OUTSIDE-IN applied (real IP), we run a detailed packet-tracer for the inbound flow. The detailed view is the key here because it exposes the compiled forward-flow rule, including the flag that reveals what the ASA is really matching against:
FW1# packet-tracer input outside tcp 192.168.99.100 23456 203.0.113.80 80 detailed
Phase: 2
Type: ACCESS-LIST
Result: ALLOW
Config:
access-group OUTSIDE-IN in interface outside
access-list OUTSIDE-IN extended permit tcp any host 172.20.30.80 eq www
Additional Information:
Forward Flow based lookup yields rule:
in id=..., priority=13, domain=permit, deny=false
hits=4, ..., use_real_addr, flags=0x0, protocol=6
src ip/id=0.0.0.0, mask=0.0.0.0, port=0, tag=any
dst ip/id=172.20.30.80, mask=255.255.255.255, port=80, tag=anyTwo things in that compiled rule prove the point. First, the flag use_real_addr. That is the ASA telling you, in its own internal representation, that this access rule is matched against the real address of the host, not the mapped one. Second, the destination in the compiled rule: dst ip/id=172.20.30.80, the real private IP, with a 32-bit mask. Even though the client sent its packet to 203.0.113.80, the compiled ACL entry the ASA evaluates carries 172.20.30.80. Because our rule names that real IP, it matches, and the result is ALLOW.
This is the mechanism made visible. The use_real_addr flag is not something you configure; it is how post-8.3 ASA compiles every interface ACL. The firewall always matches on the real address, so your rule always has to name it.
Proof B: ACL Against the Mapped IP Drops
Now swap in OUTSIDE-BAD, which permits the mapped IP 203.0.113.80, the "obvious" choice that looks right to anyone who learned ASA before 8.3. Nothing else changes. We re-run the same packet:
FW1# packet-tracer input outside tcp 192.168.99.100 34567 203.0.113.80 80
Phase (ACCESS-LIST):
Type: ACCESS-LIST
Result: DROP
Config:
Implicit Rule
Result:
Action: drop
Drop-reason: (acl-drop) Flow is denied by configured ruleDropped. And look at which rule dropped it: the Implicit Rule, the invisible deny-all at the end of every ACL. Our OUTSIDE-BAD permit is looking for a destination of 203.0.113.80, but by the time the ACL runs, the ASA has already un-NATed the destination to 172.20.30.80. The permit never matches, so the packet falls through to the implicit deny and is dropped with acl-drop: Flow is denied by configured rule.
Restoring OUTSIDE-IN (the real-IP rule) returned the same flow to Action: allow. Same firewall, same packet, same NAT. Real IP in the ACL and the packet is allowed. Mapped IP in the ACL and it is dropped by the implicit deny. The only thing that changed was the address the ACL targeted.
Reading the Compiled Rule
The detailed packet-tracer output rewards a closer look, because the compiled forward-flow rule is the ASA showing you its own internal logic. Take the working case again: src ip/id=0.0.0.0, mask=0.0.0.0 is how the firewall represents any for the source, a zero address with a zero mask matches everything, which is exactly what our permit tcp any asked for. The destination line, dst ip/id=172.20.30.80, mask=255.255.255.255, is the real host with a full 32-bit host mask, the compiled form of host 172.20.30.80.
The priority=13 and domain=permit, deny=false fields tell you this is a permit entry and where it sits in the compiled order, and hits=4 is the live match count on that specific compiled rule. When you are chasing why a flow does or does not match, this level of detail removes all guesswork: the firewall is telling you the exact address, mask, port, and protocol it will compare the packet against, and whether the last few packets actually matched. If your intended real IP does not appear in the dst ip/id field, the compiled rule is not what you think it is, and that is where the fix lives.
Why This Failure Is So Nasty
What makes the mapped-IP mistake genuinely dangerous is not that it fails, it is how it fails. It does not throw a config error. The ASA happily accepts permit tcp any host 203.0.113.80 eq www; it is a syntactically valid rule referencing a valid address. The rule simply never matches any real traffic, because no packet ever reaches the ACL with 203.0.113.80 still in the destination field. The permit sits in the config looking correct, the connections fail, and the deny counter on the implicit rule climbs while the counter on your permit stays at zero.
That zero-hit permit is the tell. If you have configured an inbound permit for a published server and it shows no hits while the traffic is clearly arriving and failing, check the address. There is a very good chance you named the public IP out of habit, and the fix is to point the rule at the real private address instead. The show access-list hit counts and a detailed packet-tracer together diagnose this in under a minute once you know to look for it.
The Historical Habit That Breaks Rules
Where does the mapped-IP instinct come from? History. On PIX and pre-8.3 ASA, NAT and ACLs were ordered the other way: the interface ACL was evaluated against the mapped (public) address, before translation. So for years the correct answer genuinely was "reference the public IP," and a generation of engineers learned it that way, wrote it into runbooks, and taught it to the next hire.
Cisco reversed the order in 8.3, and the correct answer flipped with it. Anyone carrying the pre-8.3 habit forward onto a modern firewall writes a rule that is correct by the old rules and silently non-functional by the new ones. That is why this specific behaviour trips up experienced engineers more than beginners: the beginners never learned the old way, but the veterans have to unlearn it. If you find yourself reaching for the public IP in an inbound ACL, that reflex is a pre-8.3 muscle memory, and on 8.3-plus it is exactly backwards.
permit tcp any host 203.0.113.80 eq wwwpermit tcp any host 172.20.30.80 eq wwwHow to Never Get This Wrong Again
The durable fix is not to memorise "use the real IP." It is to internalise the phase order, because the ACL rule is just a consequence of it. Un-NAT is Phase 1, the ACL check is Phase 2, so the packet is already un-translated when the rule runs, so the rule sees the real IP, so you write the real IP. When you can derive the answer from the sequence, you will get it right even on an unfamiliar topology where you cannot remember which address is which.
For the complete per-packet sequence, where un-NAT and the interface ACL sit relative to the route lookup, connection creation, and inspection, work through the ASA packet flow article. Pair that with the destination NAT publish that set this up, and the real-IP ACL rule becomes obvious rather than surprising.
A 60-Second Diagnostic
When a published server is unreachable and you suspect the ACL, this is the fastest path to an answer. First, run show access-list OUTSIDE-IN and look at the hit count on your inbound permit. If it is zero while connections are clearly being attempted, the rule is not matching, and a mapped-IP mistake is the prime suspect. Second, run a detailed packet-tracer for the exact flow and read the ACCESS-LIST phase: an ALLOW with the real IP in the compiled dst ip/id field means the rule is right, and a DROP on the Implicit Rule means your permit never matched.
Third, confirm the address in the permit is the real private IP of the server, not its public mapped IP. That single check resolves the overwhelming majority of "I permitted it but it still fails" cases on a modern ASA. The whole loop takes under a minute once you know that the un-NAT-before-ACL ordering is what makes the real IP the correct target. It is a small habit that saves the afternoon the mapped-IP rule would otherwise cost you.
Key Takeaways
- On ASA 8.3+, interface ACLs reference the real IP, not the mapped IP. Un-NAT (Phase 1) runs before the ACL check (Phase 2), so the destination is already the real address when the rule is evaluated.
- Proof A: real IP permits. The compiled rule carries
use_real_addranddst ip=172.20.30.80; a permit for that real IP matches and the flow is allowed. - Proof B: mapped IP drops. A permit for 203.0.113.80 never matches (the packet is already un-NATed), so the Implicit Rule denies it with
acl-drop: Flow is denied by configured rule. - The failure is silent. A mapped-IP permit is valid config that never matches; the tell is a zero-hit permit while the implicit deny counter climbs.
- It is a pre-8.3 habit. Old PIX and ASA evaluated ACLs against the mapped IP before NAT; 8.3 reversed the order, so the veteran instinct is now exactly wrong.
Real IPs in your ACLs is one rule you cannot afford to get backwards. For the rest of the inbound and outbound story, from publishing servers to reading the NAT table, continue through the Cisco ASA cluster.