Contractors need one thing on the network: the portal they work in. They don't need file shares, printers or the management range. The cleanest way to enforce that with Cisco ISE is a single authorization result that does two jobs. It puts the contractor in their own VLAN and pushes a downloadable ACL (dACL) that only allows the portal.
In this lab that result works on the first try, at least on paper. The contractor authenticates, lands in VLAN 862 and gets the dACL. HTTPS to the portal's IP address works. Then they type the portal's name into a browser and nothing happens. Nobody wrote a DNS rule, so the dACL's implicit deny ip any any drops every lookup.
I built this in my home lab with Cisco ISE 3.5 and a Cisco IOSv-L2 15.2 access switch. All the output below is real. Addresses and MACs are documentation values, and secrets are placeholders.
If you want the theory first, read the Cisco dACL Guide: Downloadable ACLs with ISE and 802.1X. If a dACL isn't showing up on the port at all, that's a different problem: see dACL Not Applying in 802.1X. This lab is about a dACL that applies perfectly and still breaks the user.
The lab
| Device | Role | Addressing |
|---|---|---|
| ise35 | Cisco ISE 3.5, RADIUS server | 192.0.2.10 |
| acc-sw3 | 802.1X authenticator, gateway for VLANs 862 and 890 | Gi0/0 10.86.253.10/24 in VRF MGMT; Vlan862 10.86.62.1/24; Vlan890 10.86.90.1/24 |
| ctr-01 | Contractor laptop with a PEAP supplicant | 10.86.62.21/24, MAC 00:00:5E:00:53:62, on Gi0/1, resolver 10.86.90.53 |
| srv-web | Contractor portal and DNS server | HTTPS on 10.86.90.10, DNS on 10.86.90.53, on Gi0/2 |
The switch has three VLANs:
- VLAN 999 PARKING is the configured access VLAN on Gi0/1. It has no SVI. An unauthorized contractor sits here and goes nowhere.
- VLAN 862 CONTRACTORS is where ISE puts the contractor.
- VLAN 890 SERVERS holds the portal and the DNS server.
The switch routes between 862 and 890, so without a dACL the contractor could reach anything in the server VLAN. The dACL is the only thing that narrows it down to the portal.
ISE side
The ISE side is four objects plus one rule. The 802.1X basics (network device, PEAP, the Wired-Access policy set) carry over from the first lab in this series, ISE 802.1X Lab: Fixing 11007 "Could not locate Network Device". The rule matches on the user identity group rather than a literal value, for the same reason as in the second lab, ISE MAB Lab: Why a Known Printer Still Gets 15039 DenyAccess.
- Network device
acc-sw3at 10.86.253.10/32 with the RADIUS shared secret<RADIUS_SHARED_SECRET>. - User identity group
Wired-Contractors, with an internal userctr-01(password<CONTRACTOR_PASSWORD>). - Downloadable ACL
CTR-WEB-ONLY. This is the version that broke things:
permit tcp any host 10.86.90.10 eq 443
deny ip any any

- Authorization profile
Contractor-VLAN862-WEB. It'sACCESS_ACCEPTwith DACL Name set toCTR-WEB-ONLYand VLAN set to tag 1,862. One result does both jobs.

Then one authorization rule at the top of the Wired-Access policy set:
- Contractors-Web-Only:
IdentityGroup·Name EQUALS User Identity Groups:Wired-Contractors→Contractor-VLAN862-WEB

A side note: ISE won't let you write the source
My first draft of the dACL wrote the server as the source of the rule, the way you'd write the return direction on a router ACL. ISE refused to save it:
While creating DACL, the keyword 'Any' must be the source in all ACE in DACL. Once the DACL is pushed, the 'Any' in the source is replaced with the IP address of the client that is connecting to the switch.
That's worth understanding. A dACL is always written from the client's point of view, inbound on the port. The source is always "the client", so ISE makes you write any. The switch fills in the client's address when it applies the dACL, so the switch has to know the client's IP. That's what device tracking is for. On this IOSv-L2 image, device tracking turned on by itself when I enabled 802.1X on the port.
Switch side
This is the AAA and port config. The shared secret is a placeholder.
aaa new-model
radius server ISE-1
address ipv4 192.0.2.10 auth-port 1812 acct-port 1813
key <RADIUS_SHARED_SECRET>
aaa group server radius ISE
server name ISE-1
ip vrf forwarding MGMT
ip radius source-interface GigabitEthernet0/0
aaa authentication dot1x default group ISE
aaa authorization network default group ISE
aaa accounting dot1x default start-stop group ISE
radius-server attribute 6 on-for-login-auth
radius-server attribute 8 include-in-access-req
radius-server vsa send accounting
radius-server vsa send authentication
dot1x system-auth-control
!
interface GigabitEthernet0/1
description ctr-01 contractor (802.1X)
switchport mode access
switchport access vlan 999
authentication port-control auto
dot1x pae authenticator
dot1x timeout tx-period 10
spanning-tree portfast edge
Two lines matter most for dACLs:
aaa authorization network default group ISElets the switch accept the VLAN and the dACL name from ISE. Without it you get neither.radius-server vsa send authenticationlets the Cisco AV pairs through. The dACL name arrives as a Cisco AV pair.
Break it: the name never resolves
The contractor authenticates with PEAP. The switch shows the session is authorized, in VLAN 862, with the dACL applied:
acc-sw3#show authentication sessions interface Gi0/1 details
Interface: GigabitEthernet0/1
MAC Address: 0000.5e00.5362
IPv4 Address: 10.86.62.21
User-Name: ctr-01
Status: Authorized
Domain: DATA
Oper host mode: single-host
Server Policies:
Vlan Group: Vlan: 862
ACS ACL: xACSACLx-IP-CTR-WEB-ONLY-6ae3f12e
Method status list:
Method State
dot1x Authc Success
Everything a ticket would ask for checks out: right user, right VLAN, right dACL. Now the contractor tries to use it:
ctr-01$ cat /etc/resolv.conf
nameserver 10.86.90.53
ctr-01$ nslookup portal.lab.example
;; connection timed out; no servers could be reached
ctr-01$ curl -sk -m 8 https://portal.lab.example/ -o /dev/null -w "%{http_code}\n"
000
curl exit code: 28
ctr-01$ curl -sk -m 5 https://10.86.90.10/ -o /dev/null -w "%{http_code}\n"
200
The portal works by IP and fails by name. The lookup didn't come back with "not found". It timed out, so the query never got an answer.
The DNS server's log confirms the query never arrived:
srv-web$ grep -c query /tmp/dns.log
0
What the switch is enforcing
acc-sw3#show ip access-lists interface Gi0/1
permit tcp any host 10.86.90.10 eq 443 (11 matches)
deny ip any any (8 matches)
The deny counter is climbing. Every DNS query from the contractor is a UDP packet to 10.86.90.53 port 53, and nothing above the final line matches it.
The same command also prints Extended IP access list Auth-Default-ACL, which does permit DNS. Don't let it fool you. That's the switch's built-in default ACL, not the contractor's dACL. The authorized session's traffic hit the dACL, and the dACL dropped it.
What ISE sees
From ISE's side, nothing is wrong. Live Logs shows a clean 5200 for ctr-01 and a separate 5232 DACL Download Succeeded line for the dACL:

The authentication detail shows the result ISE sent:

- Event:
5200 Authentication succeeded - Authentication Protocol:
PEAP (EAP-MSCHAPv2) - Authorization Policy:
Wired-Access >> Contractors-Web-Only - Authorization Result:
Contractor-VLAN862-WEB - Result:
Tunnel-Private-Group-ID (tag=1) 862andcisco-av-pair ACS:CiscoSecure-Defined-ACL=#ACSACL#-IP-CTR-WEB-ONLY-6ae3f12e - Steps:
11022 Added the dACL specified in the Authorization Profile
That last AV pair carries only the dACL's name. The switch then asks ISE for the contents in a second RADIUS request, which is the 5232 line. Its detail shows exactly what the switch got:

ip:inacl#1=permit tcp any host 10.86.90.10 eq 443
ip:inacl#2=deny ip any any
So ISE, the switch and the contractor all agree. The policy is doing exactly what it says. What it says is wrong.
Fix it: let the contractor resolve names
The portal is only usable by name, so the contractor needs DNS to the one resolver they use. The fix is to add DNS to the dACL above the HTTPS line. Add TCP 53 as well as UDP, for large responses.
permit udp any host 10.86.90.53 eq domain
permit tcp any host 10.86.90.53 eq domain
permit tcp any host 10.86.90.10 eq 443
deny ip any any

Saving the dACL doesn't fix the user
This is the part that catches people. I saved the dACL in ISE and tested again straight away. It still failed:
ctr-01$ nslookup portal.lab.example
;; connection timed out; no servers could be reached
acc-sw3#show authentication sessions interface Gi0/1 details | include ACS ACL
ACS ACL: xACSACLx-IP-CTR-WEB-ONLY-6ae3f12e
The session still has the old dACL, with the same -6ae3f12e suffix. The switch downloaded the contents once, when the session came up, and it doesn't go back to ISE until the session re-authenticates. In production you'd send a CoA reauth from the session in ISE. In the lab I logged the supplicant off and on again.
The suffix on the name is how you tell the versions apart. ISE changes it whenever the dACL contents change:
acc-sw3#show authentication sessions interface Gi0/1 details
User-Name: ctr-01
Status: Authorized
Vlan Group: Vlan: 862
ACS ACL: xACSACLx-IP-CTR-WEB-ONLY-6ae3f66d
dot1x Authc Success
-6ae3f12e became -6ae3f66d, so this session has the new contents.
Prove it
Contractor
ctr-01$ nslookup -type=a portal.lab.example
Server: 10.86.90.53
Address: 10.86.90.53:53
Name: portal.lab.example
Address: 10.86.90.10
ctr-01$ curl -sk -m 8 https://portal.lab.example/ -o /dev/null -w "%{http_code}\n"
200
The name resolves and the portal loads by name. The dACL is still doing its job for everything else. Plain HTTP to the same server and ping are both dropped:
ctr-01$ curl -s -m 5 http://10.86.90.10/ -o /dev/null -w "%{http_code}\n"
000
curl exit code: 28
ctr-01$ ping -c 3 -W 1 10.86.90.10
3 packets transmitted, 0 packets received, 100% packet loss
Switch
acc-sw3#show ip access-lists interface Gi0/1
permit udp any host 10.86.90.53 eq domain (6 matches)
permit tcp any host 10.86.90.53 eq domain
permit tcp any host 10.86.90.10 eq 443 (11 matches)
deny ip any any (8 matches)
The new DNS line is matching.
ISE
Live Logs shows the new session, with a second 5232 download for the new suffix:


ip:inacl#1=permit udp any host 10.86.90.53 eq domain
ip:inacl#2=permit tcp any host 10.86.90.53 eq domain
ip:inacl#3=permit tcp any host 10.86.90.10 eq 443
ip:inacl#4=deny ip any any
Checklist for "authorized, right VLAN, but the app doesn't work"
When ISE says the session is fine and the user says it isn't:
- Run
show authentication sessions interface <port> details. Check Vlan Group and ACS ACL. If the dACL is missing, that's a different fault. Start with theaaa authorization networkandradius-server vsa sendlines. - Run
show ip access-lists interface <port>and watch which line's counter moves while the user retries. A climbingdeny ip any anyis your answer. - Write down every service the application really depends on before you write the dACL: DNS, sometimes DHCP renewals, any SSO or identity provider, and the app itself. "Web only" almost always means "web and DNS".
- In ISE, open the 5232 line, not just the 5200. Its
inaclentries are the exact ACEs the switch received. - After editing a dACL, compare the name suffix on the switch. Same suffix means the session still has the old contents, so reauthenticate the session (CoA) before you retest.
- Write every ACE with
anyas the source. ISE won't save anything else, and the switch fills in the client address.
If you've done per-user ACLs with FreeRADIUS before, FreeRADIUS Filter-Id Lab: Per-User ACLs on a Cisco Switch is the local-ACL version of the same idea. For how ISE hands VLANs to the switch, see Dynamic VLAN Assignment with 802.1X and Cisco ISE.