Cisco dACL: Downloadable ACLs with ISE and 802.1X

A downloadable ACL is a name in the Access-Accept, not a policy. Here is the second RADIUS transaction that fetches it, and what the switch really applies.

Cisco dACL on a Catalyst 9000v - show access-session output with the ACS ACL name from RADIUS

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:

Named dACL (the real download)
Attribute in the Access-Accept
ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-NAME-hash
What the switch does
Opens a second RADIUS Access-Request to fetch the ACEs, then installs them as a named ACL and binds them to the session.
Who uses it
Cisco ISE by default. This is what "dACL" means in an ISE deployment.
Inline per-user ACL
Attribute in the Access-Accept
ip:inacl#10=permit ... repeated per ACE
What the switch does
Applies the entries immediately. No second request, but every ACE rides in the authentication response.
Who uses it
Non-ISE servers, and it is also the payload the named download returns.
Filter-Id
Attribute in the Access-Accept
Filter-Id = ACL-NAME
What the switch does
Applies an ACL that is already configured locally. Nothing is downloaded.
Who uses it
Standards-based servers. Not a dACL, and it keeps ACL content on every switch.

The 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 Success

Three 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: 20

The 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 0

Scale 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 any

Two 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 any

On 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 2083ms

One 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.

Where the rules live
dACLPolicy server, one copy
Port ACLEvery switch, every port
Scope
dACLPer session, per endpoint IP
Port ACLPer port, all hosts on it
Follows the user
dACLYes, any port, any switch
Port ACLNo
Change without touching switches
dACLYes, edit and send a CoA
Port ACLNo, config change everywhere
Depends on device tracking
dACLYes, needs the endpoint IP
Port ACLNo
Survives a RADIUS outage
dACLNo, new sessions get nothing
Port ACLYes, it is in the config
Visible in show run
dACLNo, session state only
Port ACLYes
Cost per authorization
dACLTwo RADIUS transactions
Port ACLNone

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 any

aaa 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 portfast

device-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 s

Two 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 any

ISE 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.

Session is Authorized, no ACS ACL line
Likely cause
The switch is not accepting authorization attributes, or VSAs are not being sent.
Check
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.
ACS ACL is named but traffic is unfiltered
Likely cause
No device-tracking binding, so there is no endpoint IP to bind the ACL to. The session shows IPv4 Address: Unknown.
Check
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.
Only the port ACL shows on the interface
Likely cause
Not a fault. show ip access-lists interface does not display per-session dACLs on 17.x.
Check
show ip access-lists xACSACLx-... for the content, then test one permitted and one denied destination from the endpoint.
Old ACL content after an ISE edit
Likely cause
Existing sessions keep the version they downloaded. No CoA arrived, or the CoA client does not match.
Check
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 # to x, so ISE's #ACSACL#-IP-NAME-hash is xACSACLx-IP-NAME-hash on the switch. Search for the right form on the right side.
  • show ip access-lists interface shows 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: Unknown in 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.

Read next