Hardened dACL Configuration

1. Overview

This document defines the hardened downloadable ACLs (dACLs) for Linux research workstations. The design principle is zero-trust: block all internal network access, permit only internet and essential infrastructure.

The default permit ip any any dACL is NOT acceptable for production. It allows lateral movement and violates the principle of least privilege.

2. dACL Design Principles

Principle Implementation

Deny Internal First

Block RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) before any permits

Least Privilege

Only permit traffic explicitly required for operations

Defense in Depth

dACL + UFW + network segmentation = layered security

64 ACE Limit

Stay within Cisco’s recommended ACE count for switch performance

Document Everything

Every ACE must have a business justification

3. Current State vs Target State

Current dACL (INSECURE):

Extended IP access list xACSACLx-IP-LINUX_EAPTLS_PERMIT_ALL-69680320 (per-user)
    1 permit ip any any    <-- WIDE OPEN!

This allows the workstation to reach any internal system. A compromised endpoint can pivot to domain controllers, file servers, and other critical infrastructure.

Attribute Current Target

dACL Name

LINUX_EAPTLS_PERMIT_ALL

LINUX_RESEARCH_HARDENED

Internal Access

Full (permit any any)

Blocked (deny RFC1918)

Internet Access

Full

HTTP/HTTPS/SSH only

Infrastructure

Full

DNS, NTP, ISE only

ACE Count

1

~15

4. Hardened dACL Definition

4.1. LINUX_RESEARCH_HARDENED

! =============================================================================
! dACL: LINUX_RESEARCH_HARDENED
! Purpose: Zero-trust access for Linux research workstations
! Author: InfoSec Team
! Created: 2026-01-22
! ACE Count: 15 (well within 64 ACE limit)
! =============================================================================

! -----------------------------------------------------------------------------
! SECTION 1: BLOCK INTERNAL NETWORKS (MUST BE FIRST)
! Prevents lateral movement to internal systems
! -----------------------------------------------------------------------------
remark === DENY RFC1918 - No Lateral Movement ===
deny ip any 10.0.0.0 0.255.255.255           ! (1)
deny ip any 172.16.0.0 0.15.255.255          ! (2)
deny ip any 192.168.0.0 0.0.255.255          ! (3)

! -----------------------------------------------------------------------------
! SECTION 2: PERMIT INFRASTRUCTURE (Minimal Required)
! Only essential services for workstation operation
! -----------------------------------------------------------------------------
remark === DNS ===
permit udp any host 10.50.1.50 eq 53         ! (4)

remark === NTP ===
permit udp any any eq 123                     ! (5)

remark === ISE Posture ===
permit tcp any host 10.50.1.21 eq 8443       ! (6)
permit tcp any host 10.50.1.21 eq 8905       ! (7)

! -----------------------------------------------------------------------------
! SECTION 3: PERMIT INTERNET ACCESS
! Research requires external connectivity
! -----------------------------------------------------------------------------
remark === External Traffic ===
permit tcp any any eq 80                      ! (8)
permit tcp any any eq 443                     ! (9)
permit tcp any any eq 22                      ! (10)

! -----------------------------------------------------------------------------
! SECTION 4: IMPLICIT DENY WITH LOGGING
! Catch and log any unexpected traffic
! -----------------------------------------------------------------------------
remark === Deny All Other ===
deny ip any any log                           ! (11)
1 Block all 10.x.x.x (Class A private)
2 Block all 172.16-31.x.x (Class B private)
3 Block all 192.168.x.x (Class C private)
4 DNS to domain controller only
5 NTP for time sync (required for Kerberos)
6 ISE admin/posture portal
7 ISE posture agent communication
8 HTTP for redirects and package repos
9 HTTPS for updates, research, cloud services
10 SSH/SFTP for research collaboration
11 Log denied traffic for security monitoring

Why deny before permit?

ACLs are processed top-to-bottom. By denying RFC1918 first, we ensure internal traffic is blocked even if a later rule accidentally permits it.

dACL Processing Flow

4.2. ACE Summary Table

# ACE Purpose Direction Risk if Removed

1-3

deny ip any <RFC1918>

Block internal networks

Outbound

CRITICAL - Lateral movement

4

permit udp any host <DC> eq 53

DNS resolution

Outbound

No name resolution

5

permit udp any any eq 123

NTP time sync

Outbound

Kerberos failures

6-7

permit tcp any host <ISE> eq 8443,8905

ISE posture

Outbound

Posture fails

8-9

permit tcp any any eq 80,443

Web/HTTPS

Outbound

No internet

10

permit tcp any any eq 22

SSH/SFTP

Outbound

No remote access

11

deny ip any any log

Catch-all deny

Both

Implicit deny anyway

5. ISE Configuration Steps

5.1. Step 1: Create the dACL

Path: Policy → Policy Elements → Results → Authorization → Downloadable ACLs

  1. Click Add

  2. Configure:

    Field Value

    Name

    LINUX_RESEARCH_HARDENED

    Description

    Zero-trust dACL for Linux research workstations - blocks RFC1918, permits internet

    DACL Content

    (paste ACL from above, without remarks)

  3. Click Submit

5.2. Step 2: Update Authorization Profile

Do not delete the old profile yet! Create a new one and test before replacing.

Path: Policy → Policy Elements → Results → Authorization → Authorization Profiles

  1. Find or create profile: Linux_Research_EAP_TLS

  2. Configure:

    Field Value

    Name

    Linux_Research_EAP_TLS

    Access Type

    ACCESS_ACCEPT

    DACL Name

    LINUX_RESEARCH_HARDENED

    VLAN

    40 (Research VLAN)

    Reauthentication Timer

    3600 (1 hour)

    Reauthentication Connectivity

    RADIUS Request

Reauth Timer is CRITICAL!

Without a reauth timer, a session persists indefinitely. If posture status changes (e.g., ClamAV stops), the workstation keeps its current access until manually cleared.

Session timeout:  N/A     <-- BAD: No reauth
Session timeout:  3600    <-- GOOD: Revalidates hourly

5.3. Step 3: Verify Policy Configuration

5.3.1. Policy Sets

$ uv run netapi ise get-policy-sets

                              Policy Sets
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓
┃ Name               ┃ ID                                   ┃ State   ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩
│ Domus-Wired 802.1X │ 2c2e6b05-a29c-46a4-a0ee-7fcea4853e47 │ enabled │  (1)
│ ...                │ ...                                  │ ...     │
└────────────────────┴──────────────────────────────────────┴─────────┘
1 Linux workstations hit this policy set

5.3.2. Policy Set Condition

$ uv run netapi ise get-policy-set "Domus-Wired 802.1X"

  State              enabled
  Rank               3
  Hit Count          276
  Service            Default Network Access
  Description        Wired 802.1X closed mode authentication

Condition:
  Attribute          NAS-Port-Type                                    (1)
  Operator           equals
  Value              Ethernet                                         (2)
1 RADIUS attribute from NAD (switch)
2 Matches wired Ethernet ports only

5.3.3. Certificate Authentication Profile

$ uv run netapi ise get-cert-profile "AD_Cert_Profile"

  Description               Certificate authentication for AD-joined Linux workstations
  Certificate Attribute     SUBJECT_COMMON_NAME                      (1)
  Username From             CERTIFICATE                              (2)
  Match Mode                NEVER
1 Extracts CN from certificate subject (e.g., modestus-p50.inside.domusdigitalis.dev)
2 Username derived from certificate, not user input

5.3.4. Authentication Rules

$ uv run netapi ise get-auth-rules "Domus-Wired 802.1X"

    #    Rule Name                   Identity Source       If Fail    If Not Found    State
    0    EAP_TLS_Certificate_Auth    AD_Cert_Profile       REJECT     REJECT          enabled  (1)
    1    Default                     All_User_ID_Stores    REJECT     REJECT          enabled
1 Certificate-based authentication against AD certificate profile

5.3.5. Authorization Rules

$ uv run netapi ise get-authz-rules "Domus-Wired 802.1X"

    #    Rule Name                     Profile(s)                    SGT    State
    0    Linux_EAPTLS_Test             Linux_EAPTLS_Permit           -      enabled  (1)
    1    Linux_Posture_Compliant       Linux_Posture_Compliant       -      enabled
    2    Linux_Posture_NonCompliant    Linux_Posture_NonCompliant    -      enabled
    3    Linux_Posture_Unknown         Linux_Posture_Unknown         -      enabled
    4    Default                       DenyAccess                    -      enabled  (2)
1 Matches EAP-TLS cert auth → Linux_EAPTLS_Permit profile (VLAN 40 + hardened dACL + 3600s reauth)
2 Unmatched endpoints denied (closed mode)

6. Validation Evidence (2026-01-22)

6.1. dACL and Reauth Timer via netapi

# Create hardened dACL
$ uv run netapi ise create-dacl LINUX_RESEARCH_HARDENED \
    --file /tmp/LINUX_RESEARCH_HARDENED.txt \
    --descr "Zero-trust dACL for Linux research workstations - blocks RFC1918, permits internet"
Creating DACL: LINUX_RESEARCH_HARDENED
  OK (ID: 2416bc00-f805-11f0-b76e-52c54a1d1f56)

# Update profile with dACL
$ uv run netapi ise update-authz-profile "Linux_EAPTLS_Permit" \
    --dacl LINUX_RESEARCH_HARDENED
✓ Updated authorization profile: Linux_EAPTLS_Permit
  DACL: LINUX_RESEARCH_HARDENED

# Set reauth timer (1 hour)
$ uv run netapi ise update-authz-profile "Linux_EAPTLS_Permit" \
    --reauth-timer 3600
✓ Updated authorization profile: Linux_EAPTLS_Permit
  Reauth Timer: 3600s (60 min)

6.2. ISE Profile Verification

$ uv run netapi ise get-authz-profile "Linux_EAPTLS_Permit"
 name        Linux_EAPTLS_Permit
 vlan        {'nameID': 'RESEARCH_VLAN', 'tagID': 1}
 reauth      {'timer': 3600, 'connectivity': 'RADIUS_REQUEST'}   (1)
 daclName    LINUX_RESEARCH_HARDENED                              (2)
1 Reauth timer set to 3600s with RADIUS_REQUEST connectivity
2 Hardened dACL applied

6.3. Session Verification

$ uv run netapi ios exec "show access-session interface GigabitEthernet1/0/5 detail" \
    | grep -E "User-Name|Status|Vlan|ACS ACL|IPv4|MAC|Session timeout"
          MAC Address:  c85b.76c6.5962
         IPv4 Address:  10.50.40.101
            User-Name:  modestus-p50.inside.domusdigitalis.dev
               Status:  Authorized
      Session timeout:  3600s (server), Remaining: 3584s               (1)
           Vlan Group:  Vlan: 40
      Security Status:  Link Unsecure
              ACS ACL:  xACSACLx-IP-LINUX_RESEARCH_HARDENED-6972e00d   (2)
1 Reauth timer pushed from ISE - session will reauthenticate hourly
2 Hardened dACL successfully applied

6.4. ACL on Switch

$ uv run netapi ios exec "show ip access-list xACSACLx-IP-LINUX_RESEARCH_HARDENED-6972e00d"
Extended IP access list xACSACLx-IP-LINUX_RESEARCH_HARDENED-6972e00d (per-user)
    1 deny ip any 10.0.0.0 0.255.255.255
    2 deny ip any 172.16.0.0 0.15.255.255
    3 deny ip any 192.168.0.0 0.0.255.255
    4 permit udp any host 10.50.1.50 eq domain
    5 permit udp any any eq ntp
    6 permit tcp any host 10.50.1.21 eq 8443
    7 permit tcp any host 10.50.1.21 eq 8905
    8 permit tcp any any eq www
    9 permit tcp any any eq 443
    10 permit tcp any any eq 22
    11 deny ip any any log

6.5. Connectivity Test from P50

# Internal BLOCKED (RFC1918 denied)
$ ping -c 3 10.50.1.1
PING 10.50.1.1 (10.50.1.1) 56(84) bytes of data.
--- 10.50.1.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2056ms   (1)

# Internet WORKS (HTTPS permitted)
$ curl -sI https://google.com | head -1
HTTP/2 301   (2)
1 Internal traffic blocked by dACL - zero-trust enforced
2 Internet connectivity confirmed

7. Rollback Plan

If the hardened dACL breaks connectivity:

  1. SSH to switch (or console)

  2. Clear the session to force re-auth with old profile:

    clear access-session interface GigabitEthernet1/0/5
  3. Revert ISE authorization policy to old profile

  4. Investigate and fix dACL

8. netapi Commands

8.1. ISE Policy Query Commands

# Load secrets
dsource d000 dev/network

# Policy Sets and Rules
uv run netapi ise get-policy-sets
uv run netapi ise get-policy-set "Domus-Wired 802.1X"
uv run netapi ise get-auth-rules "Domus-Wired 802.1X"
uv run netapi ise get-authz-rules "Domus-Wired 802.1X"

# Certificate Profile
uv run netapi ise get-cert-profiles
uv run netapi ise get-cert-profile "AD_Cert_Profile"

# Authorization Profiles and dACLs
uv run netapi ise get-authz-profiles
uv run netapi ise get-authz-profile "Linux_EAPTLS_Permit"
uv run netapi ise get-dacls
uv run netapi ise get-dacl "LINUX_RESEARCH_HARDENED"

8.2. ISE Configuration Commands

# Create dACL from file
uv run netapi ise create-dacl LINUX_RESEARCH_HARDENED \
  --file /path/to/dacl.txt \
  --descr "Zero-trust dACL - blocks RFC1918, permits internet"

# Update profile with dACL
uv run netapi ise update-authz-profile "Linux_EAPTLS_Permit" \
  --dacl LINUX_RESEARCH_HARDENED

# Set reauth timer (seconds)
uv run netapi ise update-authz-profile "Linux_EAPTLS_Permit" \
  --reauth-timer 3600

# Verify profile
uv run netapi ise get-authz-profile "Linux_EAPTLS_Permit"

8.3. Switch Session Verification

# Check session with dACL and timeout (elegant one-liner)
INT="GigabitEthernet1/0/5" && \
uv run netapi ios exec "show access-session interface $INT detail" \
  | grep -E "User-Name|Status|Vlan|ACS ACL|IPv4|MAC|Session timeout" && echo "---" && \
ACL=$(uv run netapi ios exec "show access-session interface $INT detail" \
  | awk '/ACS ACL/{print $3}') && \
uv run netapi ios exec "show ip access-list $ACL"

# Force re-authentication (use 'run' not 'exec' for clear commands)
uv run netapi ios run "clear access-session interface GigabitEthernet1/0/5"

# Check ISE live logs
uv run netapi ise mnt session C8:5B:76:C6:59:62

exec vs run: Use netapi ios exec for show commands, netapi ios run for privileged exec commands like clear.

--reauth-timer options:

  • --reauth-timer 3600 - Set timer in seconds (3600 = 1 hour)

  • --reauth-connectivity - DEFAULT, RADIUS_REQUEST, or MAINTAIN_CONNECTIVITY (default: RADIUS_REQUEST)

8.4. Change of Authorization (CoA) Configuration

To enable ISE to send Change of Authorization (CoA) commands to switches, the switch must:

  1. Have CoA clients configured - ISE PSN nodes as RADIUS clients

  2. Use correct shared secret - Must match NAD configuration in ISE

  3. Allow UDP 1700 - Firewall rules must permit ISE → Switch on UDP 1700

8.4.1. Check Current CoA Configuration

# Check if CoA is configured
uv run netapi ios exec "show run | section dynamic-author"

# Expected output:
# aaa server radius dynamic-author
#  client 10.50.1.20 server-key Cisco123!ISE   <-- ISE PSN 1
#  client 10.50.1.21 server-key Cisco123!ISE   <-- ISE PSN 2
#  auth-type any

# Check NAD configuration in ISE
netapi ise get-nad --name "Home-3560CX-01"

8.4.2. Configure CoA Using Heredoc Method

When configuring switches via netapi ios run, netmiko may timeout detecting the prompt, but commands still execute successfully. Use heredoc with || true to handle timeouts gracefully:

# Create configuration script using heredoc
cat >| /tmp/configure-coa.sh << 'EOF'
#!/bin/bash
# Note: Use --timeout 5 and || true to ignore netmiko prompt timeout
# Commands execute successfully even if netmiko times out
uv run netapi ios run --timeout 5 "configure terminal" || true
uv run netapi ios run --timeout 5 "aaa server radius dynamic-author" || true
uv run netapi ios run --timeout 5 "client 10.50.1.20 server-key Cisco123\!ISE" || true
uv run netapi ios run --timeout 5 "client 10.50.1.21 server-key Cisco123\!ISE" || true
uv run netapi ios run --timeout 5 "exit" || true
uv run netapi ios run --timeout 5 "end" || true
uv run netapi ios run --timeout 5 "write memory" || true
EOF

# Execute the script
chmod +x /tmp/configure-coa.sh
/tmp/configure-coa.sh

# Verify configuration applied
uv run netapi ios exec "show run | section dynamic-author"

Why || true?

Netmiko may fail to detect the switch prompt after configuration commands, causing ReadTimeout errors. However, the commands DO execute successfully (you’ll see "Building configuration…​ [OK]"). Using || true allows the script to continue despite these harmless timeouts.

8.4.3. Remove Invalid CoA Clients

# Remove a misconfigured or non-existent CoA client
cat >| /tmp/remove-coa-client.sh << 'EOF'
#!/bin/bash
uv run netapi ios run --timeout 5 "configure terminal" || true
uv run netapi ios run --timeout 5 "aaa server radius dynamic-author" || true
uv run netapi ios run --timeout 5 "no client 10.50.1.22" || true
uv run netapi ios run --timeout 5 "exit" || true
uv run netapi ios run --timeout 5 "end" || true
uv run netapi ios run --timeout 5 "write memory" || true
EOF

chmod +x /tmp/remove-coa-client.sh
/tmp/remove-coa-client.sh

# Verify removal
uv run netapi ios exec "show run | section dynamic-author"

8.4.4. Test CoA

# Send CoA Reauth to endpoint
netapi ise mnt coa c8:5b:76:c6:59:62

# If successful:
# ✓ CoA Reauth sent to c8:5b:76:c6:59:62

# If it fails:
# Error: [404] CoA Reauth failed for C8:5B:76:C6:59:62: Session not found...
# check ISE can reach NAD on UDP 1700

# Verify new dACL applied after CoA
netapi ise mnt auth-logs c8:5b:76:c6:59:62 --limit 1

CoA Common Issues:

  1. Shared secret mismatch - Check switch config vs ISE NAD config

  2. Firewall blocking UDP 1700 - Verify ISE can reach switch

  3. Wrong ISE node IP - Ensure PSN IPs are configured, not PAN

  4. Session not found - Session may have expired, try clearing on switch instead:

    uv run netapi ios run "clear access-session interface GigabitEthernet1/0/5"

8.5. Updating an In-Use dACL

When a dACL is actively referenced by an authorization profile, ISE prevents deletion to avoid breaking active sessions. The safe workflow is:

# Step 1: Find which authorization profile is using the dACL
uv run netapi ise get-authz-profiles | grep -i linux

# Output:
# │ Linux_EAPTLS_Permit          │ 94aab910-f18b-11f0-b76e-52c54a1d1f56 │
# │ Linux_Posture_Compliant      │ 0027b090-f1ae-11f0-b76e-52c54a1d1f56 │

# Step 2: Create fixed ACL content using heredoc
cat > /tmp/LINUX_RESEARCH_ZERO_TRUST.txt << 'EOF'
deny icmp any 10.0.0.0 0.255.255.255
deny icmp any 172.16.0.0 0.15.255.255
deny icmp any 192.168.0.0 0.0.255.255
permit icmp any any
permit udp any host 10.50.1.1 eq 53
permit udp any host 10.50.1.50 eq 53
permit udp any eq 53 any gt 1023
permit udp any host 10.50.1.1 eq 123
permit udp any eq 123 any gt 1023
permit tcp any host 10.50.1.50 eq 88
permit udp any host 10.50.1.50 eq 88
permit tcp any host 10.50.1.50 eq 389
permit tcp any host 10.50.1.50 eq 636
permit tcp any host 10.50.1.50 eq 445
permit tcp any host 10.50.1.21 eq 8443
permit tcp any host 10.50.1.21 eq 8905
permit tcp any eq 22 10.50.0.0 0.0.255.255 gt 1023
permit tcp any any eq 80
permit tcp any eq 80 any gt 1023
permit tcp any any eq 443
permit tcp any eq 443 any gt 1023
deny ip any 10.0.0.0 0.255.255.255
deny ip any 172.16.0.0 0.15.255.255
deny ip any 192.168.0.0 0.0.255.255
EOF

# Step 3: Create V2 dACL (can't delete original - it's in use)
uv run netapi ise create-dacl LINUX_RESEARCH_ZERO_TRUST_V2 \
  --file /tmp/LINUX_RESEARCH_ZERO_TRUST.txt \
  --descr "Zero-trust ACL for domain-joined Linux workstations - ICMP hardened V2"

# Output:
# Creating DACL: LINUX_RESEARCH_ZERO_TRUST_V2
#   OK (ID: 2254dfe0-fbb7-11f0-9bb2-fafc6167f873)

# Step 4: Update authorization profile to use V2
uv run netapi ise update-authz-profile "Linux_EAPTLS_Permit" \
  --dacl LINUX_RESEARCH_ZERO_TRUST_V2

# Output:
# ✓ Updated authorization profile: Linux_EAPTLS_Permit
#   DACL: LINUX_RESEARCH_ZERO_TRUST_V2

# Step 5: Verify current session still has old ACL
uv run netapi ios exec "show access-s int g1/0/5 d" | grep "ACS ACL"

# Output:
#               ACS ACL:  xACSACLx-IP-LINUX_RESEARCH_ZERO_TRUST-69790b85
# ^-- Still old ACL! Session is already active

# Step 6: Trigger CoA to apply new ACL
uv run netapi ise mnt coa 10:60:4b:e9:7e:ed

# Output:
# ✓ CoA Reauth sent to 10:60:4b:e9:7e:ed

# Step 7: Verify new ACL is applied
uv run netapi ios exec "show access-s int g1/0/5 d" | grep "ACS ACL"

# Output:
#               ACS ACL:  xACSACLx-IP-LINUX_RESEARCH_ZERO_TRUST_V2-<new-hash>
# ^-- NEW ACL applied!

# Step 8: Delete old dACL (now safe since not in use)
uv run netapi ise delete-dacl LINUX_RESEARCH_ZERO_TRUST

# Step 9: Optionally rename V2 to production name
# (Requires repeating steps 3-7 with final name)

Why this workflow matters:

  1. ISE protects in-use resources - Prevents accidental deletion of active ACLs

  2. No service disruption - Create new version before removing old

  3. CoA is required - Profile updates don’t affect active sessions automatically

  4. Verify before cleanup - Always confirm new ACL is applied before deleting old

Common mistake: Trying to delete an in-use dACL results in:

Error: [500] - The resource named '...' cannot be deleted since it is in use
and referenced within the system configuration.

This is by design - follow the versioned workflow above.

9. Testing Zero-Trust dACL

After applying a zero-trust dACL, validation is CRITICAL to ensure it’s blocking lateral movement while allowing required services.

9.1. Automated Test Script

Create a comprehensive test script to validate zero-trust restrictions:

# Create test script locally
cat > /tmp/test-zero-trust.sh << 'EOF'
#!/bin/bash

echo "=== Testing PERMITTED traffic ==="
echo ""

echo "Test 1: Internet ICMP (8.8.8.8)"
timeout 3 ping -c 2 8.8.8.8 && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 2: DNS to pfSense (10.50.1.1)"
timeout 3 dig google.com @10.50.1.1 +short && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 3: DNS to DC (10.50.1.50)"
timeout 3 dig google.com @10.50.1.50 +short && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 4: HTTPS to internet"
timeout 3 curl -sI https://google.com | head -1 && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 5: Kerberos to DC (10.50.1.50:88)"
timeout 3 bash -c '</dev/tcp/10.50.1.50/88' 2>/dev/null && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 6: LDAP to DC (10.50.1.50:389)"
timeout 3 bash -c '</dev/tcp/10.50.1.50/389' 2>/dev/null && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 7: LDAPS to DC (10.50.1.50:636)"
timeout 3 bash -c '</dev/tcp/10.50.1.50/636' 2>/dev/null && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "Test 8: SMB to DC (10.50.1.50:445)"
timeout 3 bash -c '</dev/tcp/10.50.1.50/445' 2>/dev/null && echo "✓ PASS" || echo "✗ FAIL"
echo ""

echo "=== Testing BLOCKED traffic (should timeout) ==="
echo ""

echo "Test 9: Ping to pfSense (10.50.1.1) - should FAIL"
timeout 3 ping -c 2 10.50.1.1 && echo "✗ SECURITY ISSUE: Internal ICMP allowed!" || echo "✓ CORRECTLY BLOCKED"
echo ""

echo "Test 10: SSH to switch (10.50.1.10:22) - should FAIL"
timeout 3 bash -c '</dev/tcp/10.50.1.10/22' 2>/dev/null && echo "✗ SECURITY ISSUE: Lateral movement allowed!" || echo "✓ CORRECTLY BLOCKED"
echo ""

echo "Test 11: HTTP to internal server (10.50.2.1:80) - should FAIL"
timeout 3 bash -c '</dev/tcp/10.50.2.1/80' 2>/dev/null && echo "✗ SECURITY ISSUE: Internal access allowed!" || echo "✓ CORRECTLY BLOCKED"
echo ""

echo "Test 12: RDP to Windows host (10.50.3.10:3389) - should FAIL"
timeout 3 bash -c '</dev/tcp/10.50.3.10/3389' 2>/dev/null && echo "✗ SECURITY ISSUE: RDP lateral movement!" || echo "✓ CORRECTLY BLOCKED"
EOF

# Deploy and run on target workstation
scp /tmp/test-zero-trust.sh <target-host>:/tmp/
ssh <target-host> 'chmod +x /tmp/test-zero-trust.sh && /tmp/test-zero-trust.sh'

Why /dev/tcp instead of nc?

  • Built into bash, no extra packages needed

  • Works on minimal installations

  • Syntax: bash -c '</dev/tcp/HOST/PORT'

9.2. Expected Results

Test Expected Result Security Impact if Failed

Internet ICMP

✓ PASS

Updates/troubleshooting broken

DNS (pfSense/DC)

✓ PASS

Name resolution broken

HTTPS to internet

✓ PASS

Updates/repos broken

Kerberos (88)

✓ PASS

AD authentication fails

LDAP (389/636)

✓ PASS

Group policy fails

SMB to DC (445)

✓ PASS

Cert enrollment fails

Ping to internal

✓ BLOCKED

CRITICAL - Lateral movement possible

SSH to switch

✓ BLOCKED

CRITICAL - Infrastructure access

HTTP to internal

✓ BLOCKED

CRITICAL - Internal service access

RDP to Windows

✓ BLOCKED

CRITICAL - Workstation compromise

If any "should FAIL" tests pass, your zero-trust ACL is BROKEN!

This indicates lateral movement is possible - the workstation can reach internal systems it shouldn’t.

9.3. Validation Success (2026-01-27)

After applying LINUX_RESEARCH_ZERO_TRUST_V2 with ICMP hardening to modestus-p50:

❯ ssh modestus-p50 '/tmp/test-zero-trust.sh'

=== Testing PERMITTED traffic ===

Test 1: Internet ICMP (8.8.8.8)
✓ PASS

Test 2: DNS to pfSense (10.50.1.1)
✓ PASS

Test 3: DNS to DC (10.50.1.50)
✓ PASS

Test 4: HTTPS to internet
✓ PASS

Test 5: Kerberos to DC (10.50.1.50:88)
✓ PASS

Test 6: LDAP to DC (10.50.1.50:389)
✓ PASS

Test 7: LDAPS to DC (10.50.1.50:636)
✓ PASS

Test 8: SMB to DC (10.50.1.50:445)
✓ PASS

=== Testing BLOCKED traffic (should timeout) ===

Test 9: Ping to pfSense (10.50.1.1) - should FAIL
✓ CORRECTLY BLOCKED   <-- ICMP HARDENING WORKING!

Test 10: SSH to switch (10.50.1.10:22) - should FAIL
✓ CORRECTLY BLOCKED

Test 11: HTTP to internal server (10.50.2.1:80) - should FAIL
✓ CORRECTLY BLOCKED

Test 12: RDP to Windows host (10.50.3.10:3389) - should FAIL
✓ CORRECTLY BLOCKED

Key Success Indicators:

All 8 permitted tests PASS - Workstation can reach required services ✅ All 4 blocked tests CORRECTLY BLOCKED - Zero-trust isolation working ✅ Test 9 fixed - RFC1918 ICMP denies prevent internal reconnaissance ✅ External ICMP works - Troubleshooting capability preserved

ACL Applied:

# show access-session interface g1/0/5 detail
ACS ACL:  xACSACLx-IP-LINUX_RESEARCH_ZERO_TRUST_V2-6979132a

# show ip access-list xACSACLx-IP-LINUX_RESEARCH_ZERO_TRUST_V2-6979132a
1 deny icmp any 10.0.0.0 0.255.255.255      <-- RFC1918 ICMP denied FIRST
2 deny icmp any 172.16.0.0 0.15.255.255
3 deny icmp any 192.168.0.0 0.0.255.255
4 permit icmp any any                        <-- External ICMP still allowed

Status: ✅ READY FOR HOME DEPLOYMENT

9.4. Domain Requirements Validation

Beyond zero-trust isolation, domain-joined workstations have additional requirements. Use this script to validate all prerequisites:

cat > /tmp/test-requirements.sh << 'EOF'
#!/bin/bash

echo "=== Zero-Trust Requirements Validation ==="
echo ""

# Test 1: Domain Join
echo "Test 1: Domain Join Status"
if realm list | grep -q "configured: kerberos-member"; then
    echo "✓ PASS - Domain joined to $(realm list | grep 'realm-name' | awk '{print $2}')"
else
    echo "✗ FAIL - Not domain joined"
fi
echo ""

# Test 2: DNS Resolution (Internal)
echo "Test 2: DNS Resolution (Internal)"
if host dc-01.inside.domusdigitalis.dev 10.50.1.50 >/dev/null 2>&1; then
    echo "✓ PASS - Can resolve internal DNS"
else
    echo "✗ FAIL - Cannot resolve internal DNS"
fi
echo ""

# Test 3: DNS Resolution (External)
echo "Test 3: DNS Resolution (External)"
if host google.com 10.50.1.1 >/dev/null 2>&1; then
    echo "✓ PASS - Can resolve external DNS"
else
    echo "✗ FAIL - Cannot resolve external DNS"
fi
echo ""

# Test 4: Kerberos TGT
echo "Test 4: Kerberos Ticket"
if klist -s 2>/dev/null; then
    echo "✓ PASS - Valid Kerberos ticket"
    klist | grep "Default principal" || true
else
    echo "✗ FAIL - No valid Kerberos ticket (expected via SSH)"
fi
echo ""

# Test 5: LDAP to DC
echo "Test 5: LDAP Connectivity"
if timeout 3 bash -c '</dev/tcp/10.50.1.50/389' 2>/dev/null; then
    echo "✓ PASS - LDAP reachable"
else
    echo "✗ FAIL - LDAP not reachable"
fi
echo ""

# Test 6: LDAPS to DC
echo "Test 6: LDAPS Connectivity"
if timeout 3 bash -c '</dev/tcp/10.50.1.50/636' 2>/dev/null; then
    echo "✓ PASS - LDAPS reachable"
else
    echo "✗ FAIL - LDAPS not reachable"
fi
echo ""

# Test 7: SMB to DC (for cert enrollment)
echo "Test 7: SMB to DC (Certificate Enrollment)"
if timeout 3 bash -c '</dev/tcp/10.50.1.50/445' 2>/dev/null; then
    echo "✓ PASS - SMB reachable for certs"
else
    echo "✗ FAIL - SMB not reachable"
fi
echo ""

# Test 8: Internet Connectivity
echo "Test 8: Internet HTTPS"
if timeout 3 curl -sI https://google.com >/dev/null 2>&1; then
    echo "✓ PASS - Internet accessible"
else
    echo "✗ FAIL - Internet not accessible"
fi
echo ""

# Test 9: ISE Posture Access
echo "Test 9: ISE Posture Portal"
if timeout 3 bash -c '</dev/tcp/10.50.1.21/8443' 2>/dev/null; then
    echo "✓ PASS - ISE posture reachable"
else
    echo "✗ FAIL - ISE posture not reachable"
fi
echo ""

# Test 10: Zero-Trust Validation (Internal blocked)
echo "Test 10: Zero-Trust Isolation (Lateral Movement)"
if timeout 3 bash -c '</dev/tcp/10.50.1.10/22' 2>/dev/null; then
    echo "✗ SECURITY ISSUE - Can reach switch (lateral movement!)"
else
    echo "✓ PASS - Internal lateral movement blocked"
fi
echo ""

echo "=== Summary ==="
echo "All critical requirements validated for zero-trust workstation"
EOF

chmod +x /tmp/test-requirements.sh
scp /tmp/test-requirements.sh modestus-p50:/tmp/
ssh modestus-p50 '/tmp/test-requirements.sh'

Expected output (2026-01-27):

=== Zero-Trust Requirements Validation ===

Test 1: Domain Join Status
✓ PASS - Domain joined to INSIDE.DOMUSDIGITALIS.DEV

Test 2: DNS Resolution (Internal)
✓ PASS - Can resolve internal DNS

Test 3: DNS Resolution (External)
✓ PASS - Can resolve external DNS

Test 4: Kerberos Ticket
✗ FAIL - No valid Kerberos ticket (expected via SSH)

Test 5: LDAP Connectivity
✓ PASS - LDAP reachable

Test 6: LDAPS Connectivity
✓ PASS - LDAPS reachable

Test 7: SMB to DC (Certificate Enrollment)
✓ PASS - SMB reachable for certs

Test 8: Internet HTTPS
✓ PASS - Internet accessible

Test 9: ISE Posture Portal
✗ FAIL - ISE posture not reachable

Test 10: Zero-Trust Isolation (Lateral Movement)
✓ PASS - Internal lateral movement blocked

=== Summary ===
All critical requirements validated for zero-trust workstation

Expected failures via SSH:

  • Test 4 (Kerberos): No interactive session - ticket won’t be visible via SSH

  • Test 9 (ISE Posture): May fail if posture services aren’t running

Critical tests that MUST pass:

  • Tests 1-3, 5-8, 10 - All domain and zero-trust functionality

9.5. Common Test Failures and Fixes

9.5.1. Issue: Internal ICMP Allowed (Test 9 fails)

Problem:

Test 9: Ping to pfSense (10.50.1.1) - should FAIL
✗ SECURITY ISSUE: Internal ICMP allowed!

Root cause: ACL has permit icmp any any BEFORE RFC1918 denies. First match wins - internal ICMP is permitted before it can be blocked.

The CORRECT Fix: Add RFC1918 ICMP denies BEFORE the general ICMP permit:

# Create fixed ACL using heredoc
cat > /tmp/LINUX_RESEARCH_ZERO_TRUST.txt << 'EOF'
deny icmp any 10.0.0.0 0.255.255.255
deny icmp any 172.16.0.0 0.15.255.255
deny icmp any 192.168.0.0 0.0.255.255
permit icmp any any
permit udp any host 10.50.1.1 eq 53
permit udp any host 10.50.1.50 eq 53
permit udp any eq 53 any gt 1023
permit udp any host 10.50.1.1 eq 123
permit udp any eq 123 any gt 1023
permit tcp any host 10.50.1.50 eq 88
permit udp any host 10.50.1.50 eq 88
permit tcp any host 10.50.1.50 eq 389
permit tcp any host 10.50.1.50 eq 636
permit tcp any host 10.50.1.50 eq 445
permit tcp any host 10.50.1.21 eq 8443
permit tcp any host 10.50.1.21 eq 8905
permit tcp any eq 22 10.50.0.0 0.0.255.255 gt 1023
permit tcp any any eq 80
permit tcp any eq 80 any gt 1023
permit tcp any any eq 443
permit tcp any eq 443 any gt 1023
deny ip any 10.0.0.0 0.255.255.255
deny ip any 172.16.0.0 0.15.255.255
deny ip any 192.168.0.0 0.0.255.255
EOF

# Upload to ISE
uv run netapi ise create-dacl LINUX_RESEARCH_ZERO_TRUST \
  --file /tmp/LINUX_RESEARCH_ZERO_TRUST.txt \
  --descr "Zero-trust ACL for domain-joined Linux workstations - ICMP hardened"

# Trigger CoA to apply immediately
uv run netapi ise mnt coa <mac-address>

Why this works:

  • ICMP to RFC1918 destinations is denied FIRST (lines 1-3)

  • ICMP to external destinations (8.8.8.8) is permitted (line 4)

  • All other IP traffic to RFC1918 is denied at the end (lines 21-23)

  • Keeps external ICMP for troubleshooting while blocking internal reconnaissance

Do NOT simply remove permit icmp any any!

External ICMP (like ping 8.8.8.8) is valuable for troubleshooting. The correct fix is to deny RFC1918 ICMP specifically, allowing external ICMP to work.

9.5.2. Issue: Lateral Movement Allowed (Tests 10-12 fail)

Problem: Workstation can reach internal systems (switches, servers, workstations).

Root cause: RFC1918 denies are not at the END of the ACL, or specific permits came after them.

Fix: Ensure RFC1918 denies are the LAST rules before implicit deny:

# All permits FIRST
permit tcp any host 10.50.1.50 eq 88
permit tcp any host 10.50.1.50 eq 389
# ... other permits ...

# RFC1918 denies LAST (just before implicit deny)
deny ip any 10.0.0.0 0.255.255.255
deny ip any 172.16.0.0 0.15.255.255
deny ip any 192.168.0.0 0.0.255.255

9.6. Deployment Checklist for HOME

Step Action Status

1

Create test workstation in isolated VLAN

2

Apply zero-trust dACL to test profile

3

Run automated test script

4

Verify ALL "should FAIL" tests actually fail

5

Verify ALL "should PASS" tests actually pass

6

Test domain join and cert enrollment

7

Test application access (Epic, VPN, etc.)

8

Document any additional permits needed

9

Apply to pilot group (5-10 workstations)

10

Monitor for 48 hours, check ISE denied logs

11

Roll out to production incrementally

NEVER deploy to production without thorough testing!

A broken dACL can:

  • Block users from Epic/critical applications

  • Prevent certificate renewal

  • Break domain authentication

  • Lock admins out of workstations

Always test in isolated environment first.

9.7. ISE Monitoring During Rollout

Check for denied traffic that should be permitted:

# Check ISE live logs for denies
netapi ise mnt auth-logs <mac-address> --limit 20

# Query DataConnect for denied ACL hits (if available)
netapi ise dc query "SELECT * FROM radius_accounting WHERE acl_name LIKE '%ZERO_TRUST%'"

# Check switch for ACL hit counts
uv run netapi ios exec "show ip access-list xACSACLx-IP-LINUX_RESEARCH_ZERO_TRUST-<hash>"

Look for high deny counts on specific rules - may indicate a legitimate service being blocked.