What a Cisco dACL Actually Is
Static access control on 802.1X switchports has a scalability problem. If every access port needs a permit/deny ACL, and each user group needs a different one, you either push dozens of ACLs to every switch in the campus (configuration sprawl, no version control) or you accept that everyone in a VLAN gets identical traffic rights, which throws away most of the value of identity-based access.
A downloadable ACL, or dACL, moves the ACL definition off the switch and into the policy server. The RADIUS Access-Accept names an ACL, the switch fetches the contents, and the switch installs those entries against that one session. When the session ends the entries go away. If you want the wider architecture first, the 802.1X complete guide covers everything this article plugs into.
There are two RADIUS mechanisms that get called "downloadable" in the field, and they are not the same thing:
ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-NAME-haship:inacl#10=permit ... repeated per ACEFilter-Id = ACL-NAMEThe rest of this article is about the first one, because that is the mechanism the exam and the ISE deployment both mean. Everything below it was captured live on a Catalyst 9000v running IOS XE 17.18.02 with a RADIUS server pushing a real dACL, so you can see exactly which parts of the description survive contact with a switch.
What the Switch Actually Applies
The lab is deliberately small: one Catalyst 9000v (SW9K) as the authenticator, one Linux endpoint (H9, 192.168.99.60) on Gi1/0/2, a second Linux host (SRV1, 192.168.99.61) on Gi1/0/3 as the thing the policy blocks, and FreeRADIUS 3.2.7 on 192.168.99.100 standing in for ISE. The RADIUS server returns one attribute at authentication time and the ACEs on request, which is precisely the ISE behavior.
The endpoint has no supplicant, so the session below authorizes by MAB after 802.1X times out. That does not change anything about the dACL path: the download is driven by the attribute in the Access-Accept, not by which method produced it.
The Session Carries a Name, Not a Policy
SW9K# show authentication sessions interface GigabitEthernet1/0/2 details
Interface: GigabitEthernet1/0/2
IIF-ID: 0x12329EBD
MAC Address: 5254.00fc.ad92
IPv6 Address: Unknown
IPv4 Address: 192.168.99.60
User-Name: 525400fcad92
Status: Authorized
Domain: DATA
Oper host mode: multi-auth
Oper control dir: both
Session timeout: 3600s (local), Remaining: 3470s
Timeout action: Reauthenticate
Common Session ID: 0C63A8C00000000E3C8930C5
Acct Session ID: 0x00000003
Handle: 0xd5000004
Current Policy: POLICY_Gi1/0/2
Local Policies:
Service Template: DEFAULT_LINKSEC_POLICY_SHOULD_SECURE (priority 150)
Security Policy: Should Secure
Server Policies:
ACS ACL: xACSACLx-IP-PL-CONTRACTORS-6a1f2b3c
Method status list:
Method State
dot1x Stopped
mab Authc SuccessThree lines matter here. Status: Authorized says the port opened. IPv4 Address: 192.168.99.60 says the switch knows which IP this session owns, which is what a dACL is bound against. And ACS ACL: xACSACLx-IP-PL-CONTRACTORS-6a1f2b3c is the dACL.
Note the name. The server sent #ACSACL#-IP-PL-CONTRACTORS-6a1f2b3c and the switch installed xACSACLx-..., swapping each # for an x because # is not legal in an IOS ACL name. When you go looking for the ACL on the switch, search for the x form; when you go looking in ISE logs, search for the # form. Getting that backwards wastes a surprising amount of time.
"Downloadable" Means a Second RADIUS Round Trip
This is the part that gets described wrongly most often. The ACEs do not ride in the Access-Accept. The switch authenticates, receives a name, and then opens a completely separate RADIUS transaction whose only purpose is to fetch that ACL. Here is the pair from the RADIUS server's debug output:
(10) Received Access-Request Id 6 from 192.168.99.12:58427 to 192.168.99.100:1812 length 297
(10) User-Name = "525400fcad92"
(10) Service-Type = Call-Check
(10) Cisco-AVPair = "audit-session-id=0C63A8C00000000E3C8930C5"
(10) Cisco-AVPair = "method=mab"
(10) Framed-IP-Address = 192.168.99.60
(10) NAS-IP-Address = 192.168.99.12
(10) NAS-Port-Id = "GigabitEthernet1/0/2"
(10) Calling-Station-Id = "52-54-00-FC-AD-92"
(10) Called-Station-Id = "52-54-00-63-E5-67"
(10) Sent Access-Accept Id 6 from 192.168.99.100:1812 to 192.168.99.12:58427 length 147
(10) Cisco-AVPair = "ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-PL-CONTRACTORS-6a1f2b3c"
(10) Reply-Message = "PingLabz dACL demo - contractor tier"
(11) Received Access-Request Id 7 from 192.168.99.12:58427 to 192.168.99.100:1812 length 143
(11) NAS-IP-Address = 192.168.99.12
(11) User-Name = "#ACSACL#-IP-PL-CONTRACTORS-6a1f2b3c"
(11) Cisco-AVPair = "aaa:service=ip_admission"
(11) Cisco-AVPair = "aaa:event=acl-download"
(11) Sent Access-Accept Id 7 from 192.168.99.100:1812 to 192.168.99.12:58427 length 221
(11) Cisco-AVPair = "ip:inacl#10=permit udp any any eq 53"
(11) Cisco-AVPair = "ip:inacl#20=permit icmp any host 192.168.99.12"
(11) Cisco-AVPair = "ip:inacl#30=deny ip any host 192.168.99.61"
(11) Cisco-AVPair = "ip:inacl#40=deny ip any any"Request 11 is the download. Its User-Name is the ACL name, it carries no endpoint identity at all, and it is tagged aaa:event=acl-download so the server can tell it apart from a user authentication. The reply is the ACE list in ip:inacl# form, which is why the inline method and the named method look the same from the switch's point of view: the named method just fetches the inline attributes on demand.
On the wire, the two exchanges are 84 milliseconds apart:
j@llmbits:~$ sudo tcpdump -i ens224 -n udp portrange 1812-1813
22:27:14.565802 IP 192.168.99.12.58427 > 192.168.99.100.1812: RADIUS, Access-Request (1), id: 0x06 length: 297
22:27:14.566683 IP 192.168.99.100.1812 > 192.168.99.12.58427: RADIUS, Access-Accept (2), id: 0x06 length: 147
22:27:14.649389 IP 192.168.99.12.58427 > 192.168.99.100.1812: RADIUS, Access-Request (1), id: 0x07 length: 143
22:27:14.649928 IP 192.168.99.100.1812 > 192.168.99.12.58427: RADIUS, Access-Accept (2), id: 0x07 length: 221
22:27:14.769879 IP 192.168.99.12.58794 > 192.168.99.100.1813: RADIUS, Accounting-Request (4), id: 0x06 length: 219
22:27:14.770589 IP 192.168.99.100.1813 > 192.168.99.12.58794: RADIUS, Accounting-Response (5), id: 0x06 length: 20The practical consequence: every authorization that references a dACL costs two RADIUS transactions, not one, and the switch counts the second one as an authorization rather than an authentication.
SW9K# show aaa servers | include RADIUS: id|Authen: request|Author: request
RADIUS: id 1, priority 1, host 192.168.99.100, auth-port 1812, acct-port 1813, hostname RAD1
Authen: request 4, timeouts 0, failover 0, retransmission 0
Author: request 3, timeouts 0, failover 0, retransmission 0Scale that across a few thousand ports reauthenticating on a timer and it is a real load consideration. It is also why a RADIUS server that is reachable but slow shows up as sessions that authorize and then filter nothing: the first transaction succeeded and the second timed out. RADIUS server unreachable in 802.1X walks the reachability side of that in more depth.
The ACL the Switch Installs
Once the download completes, the dACL exists on the switch as an ordinary named extended ACL:
SW9K# show ip access-lists xACSACLx-IP-PL-CONTRACTORS-6a1f2b3c
Extended IP access list xACSACLx-IP-PL-CONTRACTORS-6a1f2b3c
10 permit udp any any eq domain
20 permit icmp any host 192.168.99.12
30 deny ip any host 192.168.99.61
40 deny ip any anyTwo details worth noting. The sequence numbers came from the ip:inacl#<seq> attribute numbers, so the order in the policy server is the order on the switch. And eq 53 became eq domain, because the switch parses what it receives through the normal ACL parser, which means a syntax error in your policy server's ACL text becomes a parse failure on the switch, not a validation error in the GUI.
The show Command That Lies to You
Here is the trap. The port has a static pre-authentication ACL called ACL-DEFAULT, and this is what the interface command reports while the dACL is active:
SW9K# show ip access-lists interface GigabitEthernet1/0/2
Extended IP access list ACL-DEFAULT
10 permit udp any any eq bootps
20 permit udp any any eq domain
30 deny ip any anyOn IOS XE 17.18.02 that command shows only the ACL you configured on the interface. It does not show the per-session dACL, even while the dACL is the thing making the forwarding decisions. Reading this output as "the dACL did not apply" is the single most common false alarm in dACL troubleshooting.
The way to settle it is to test traffic. ACL-DEFAULT ends in deny ip any any and permits no ICMP at all, so if the port ACL were still in charge, all three of these pings would fail:
(H9) $ ping -c 3 -W 2 192.168.99.12 <-- dACL line 20, permit
64 bytes from 192.168.99.12: icmp_seq=1 ttl=254 time=96.0 ms
64 bytes from 192.168.99.12: icmp_seq=2 ttl=254 time=52.0 ms
64 bytes from 192.168.99.12: icmp_seq=3 ttl=254 time=46.9 ms
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
(H9) $ ping -c 3 -W 2 192.168.99.61 <-- dACL line 30, deny host
3 packets transmitted, 0 received, 100% packet loss, time 2072ms
(H9) $ ping -c 3 -W 2 192.168.99.100 <-- dACL line 40, deny ip any any
3 packets transmitted, 0 received, 100% packet loss, time 2083msOne target reachable and two blocked, matching the downloaded ACEs line for line. The dACL replaced the port ACL for this session. That is the check that actually proves application: pick one address the dACL permits and one it denies, and watch the difference.
dACL vs Static Port ACL
Both end up as an inbound IP ACL on an access port. Everything else about them differs.
The two are not alternatives in practice. You want both: a restrictive port ACL as the pre-authentication and fallback state, and a dACL that replaces it once the session authorizes. The port ACL is what protects the network while a device is still unauthenticated, or when the RADIUS servers are down and the switch has nothing to download.
Switch-Side Configuration
This is the configuration that produced every capture above, on IOS XE 17.18.02.
Global Configuration
aaa new-model
aaa authentication dot1x default group radius
aaa authorization network default group radius
aaa accounting dot1x default start-stop group radius
radius server RAD1
address ipv4 192.168.99.100 auth-port 1812 acct-port 1813
key PingLabzRAD123
ip radius source-interface Vlan1
radius-server vsa send authentication
radius-server vsa send accounting
aaa server radius dynamic-author
client 192.168.99.100 server-key PingLabzRAD123
auth-type any
device-tracking policy IPDT-POLICY
tracking enable
dot1x system-auth-control
epm logging
ip access-list extended ACL-DEFAULT
10 permit udp any any eq bootps
20 permit udp any any eq domain
30 deny ip any anyaaa authorization network default group radius is the line that permits the switch to accept authorization attributes at all. Without it the Access-Accept still arrives and the port still authorizes, but the dACL attribute is discarded and the session shows no ACS ACL line. radius-server vsa send authentication is the other easy miss: the dACL rides in a Cisco vendor-specific attribute, so VSA handling has to be on.
Interface Configuration
interface GigabitEthernet1/0/2
description dot1x + MAB port - H9
switchport mode access
device-tracking attach-policy IPDT-POLICY
ip access-group ACL-DEFAULT in
authentication host-mode multi-auth
authentication order dot1x mab
authentication priority dot1x mab
authentication port-control auto
authentication periodic
authentication violation restrict
mab
dot1x pae authenticator
dot1x timeout tx-period 5
spanning-tree portfastdevice-tracking attach-policy is not optional for dACLs. The switch has to know the endpoint's IP address before it can bind an IP ACL to that endpoint's traffic, and on 17.x that binding comes from SISF, not from the legacy ip device tracking command. Confirm the binding exists:
SW9K# show device-tracking database
Binding Table has 1 entries, 1 dynamic (limit 200000)
Network Layer Address Link Layer Address Interface vlan prlvl age state Time left
ARP 192.168.99.60 5254.00fc.ad92 Gi1/0/2 1 0005 4mn REACHABLE 65 sTwo more choices in that interface block earned themselves during the build. authentication host-mode multi-auth matters more than it looks: in single-host mode the first MAC seen on the port owns the session, and on a virtualized link a stray MAC from the hypervisor claimed the port, got rejected, and locked the real endpoint out for the length of the restart timer. 802.1X host modes covers when each one is the right answer. And authentication violation restrict keeps a second MAC from error-disabling the port, which is the default and is rarely what you want on an access port.
ISE-Side Configuration
The lab above used FreeRADIUS to make the attribute exchange visible. In production the same two attributes come out of ISE, configured in three places.
Define the downloadable ACL
Policy > Policy Elements > Results > Authorization > Downloadable ACLs. Create one dACL per access tier. The content field takes plain extended ACL syntax with no ACL name and no sequence numbers, because ISE adds both when it converts the entries into ip:inacl# attributes. A contractor tier looks like this:
permit udp any any eq 53
permit tcp any 10.0.10.0 0.0.0.255 eq 443
permit tcp any 10.0.10.0 0.0.0.255 eq 22
deny ip any 10.0.0.0 0.255.255.255
permit ip any anyISE validates the syntax when you save. It cannot validate intent, and ACL order still decides everything: a broad permit ip any any above a deny means the deny is unreachable.
Reference it from an authorization profile
Policy > Policy Elements > Results > Authorization > Authorization Profiles. Set Access Type ACCESS_ACCEPT, then pick the dACL under Common Tasks. The RADIUS Attributes tab at the bottom of the profile shows exactly what will be sent, and it is worth reading once: you should see the cisco-av-pair with ACS:CiscoSecure-Defined-ACL= and nothing else related to the ACL. If you also set a VLAN here, that VLAN is applied alongside the dACL, not instead of it. Dynamic VLAN assignment covers combining the two.
Bind the profile to a rule
Policy > Policy Sets > your 802.1X set > Authorization Policy. Map identity groups to profiles and leave the catch-all as DenyAccess. If you are running MAB for printers and phones, give those endpoint groups their own dACL rather than letting them inherit a user tier.
dACL Scope: Inbound Only
A dACL filters traffic from the endpoint into the network. There is no outbound direction. Traffic from a server back to the endpoint is not evaluated, which means a dACL cannot stop something from reaching a workstation, only stop the workstation from starting the conversation.
In practice that is usually enough, because most of what you want to prevent is an authenticated endpoint reaching somewhere it should not. When you genuinely need both directions, the options are a VLAN access map on the SVI, which is blunt and applies to the whole VLAN rather than per session, or TrustSec SGT policy, which stays per identity. Cisco TrustSec and SGTs covers how that fits alongside 802.1X.
Size, Caching, and CoA
Each ACE arrives as its own ip:inacl# attribute, so a large dACL means a large RADIUS packet. The download in this lab was 221 bytes for four ACEs. A 50-line dACL is over 2 KB and starts running into UDP fragmentation on paths that do not handle it cleanly, which shows up as authorizations that work on some switches and not others. Keep dACLs to tens of lines, not hundreds, and push broad segmentation into VLANs or SGTs.
The switch caches a downloaded dACL by name and version hash, which is what the trailing hex on #ACSACL#-IP-PL-CONTRACTORS-6a1f2b3c is for. Edit the ACL in ISE and the hash changes, so the next download fetches new content. This is also why editing a dACL does not affect sessions that are already up: they keep the version they downloaded until they reauthenticate.
To push a change immediately, send a CoA. aaa server radius dynamic-author in the global config above is what makes the switch listen for one, and the client address has to match the exact node sending it. Change of Authorization in 802.1X covers the reauthenticate-versus-terminate choice and how to verify a CoA landed.
Troubleshooting: dACL Not Applying
Work the symptom, not the feature. These four cover most of what actually happens.
aaa authorization network default group radius and radius-server vsa send authentication. Then confirm on the server that the Access-Accept actually carried the attribute.IPv4 Address: Unknown.show device-tracking database. If the endpoint is missing, attach a tracking policy to the port, make the endpoint send traffic, then clear the session so it reauthorizes with the IP known.show ip access-lists interface does not display per-session dACLs on 17.x.show ip access-lists xACSACLx-... for the content, then test one permitted and one denied destination from the endpoint.show aaa server radius dynamic-author, then force it with clear authentication sessions interface <port> and watch the name's hash change.When you need to see the whole exchange rather than guess at it, run the RADIUS server in debug mode and watch for the second Access-Request tagged aaa:event=acl-download. If it never arrives, the switch never parsed the attribute. If it arrives and is rejected, the policy server does not have an entry for the ACL name. Those are two very different problems and the switch alone cannot tell you which one you have. dACL not applying correctly goes through the switch-side diagnostics in more depth, and troubleshooting 802.1X with show authentication sessions covers reading session state generally.
Key Takeaways
- A dACL is a name in the Access-Accept, not a policy. The ACEs arrive in a second RADIUS Access-Request tagged
aaa:event=acl-download, roughly 84 ms later in this lab. - The switch rewrites
#tox, so ISE's#ACSACL#-IP-NAME-hashisxACSACLx-IP-NAME-hashon the switch. Search for the right form on the right side. show ip access-lists interfaceshows only the statically configured port ACL on IOS XE 17.x. Prove application with traffic against one permitted and one denied destination instead.- No device-tracking binding means no endpoint IP, which means the dACL downloads but never binds.
IPv4 Address: Unknownin the session is the tell. - Every dACL authorization costs two RADIUS transactions, and each ACE is its own attribute. Keep dACLs small and let VLANs or SGTs carry broad segmentation.
- Keep a restrictive port ACL underneath. It is what protects the port before authorization and when RADIUS is unavailable.
Everything above was captured on a Catalyst 9000v running IOS XE 17.18.02 against FreeRADIUS 3.2.7 in CML. For the rest of the port-based access control picture, including host modes, CoA, deployment phases and the failure modes that come with each, start at the 802.1X complete guide. If you want to build the same authentication path yourself without an ISE license, 802.1X with FreeRADIUS on Cisco is the full lab.