Three separate questions in administrative access
Authentication checks the presented identity; authorization determines what that identity may do; accounting records activity selected by configuration. A working login does not establish that all required commands are authorized or that records reached the collector. During RUN handover, use a read-only role and a change role to observe an allowed and a denied operation according to the contract. Do not resolve insufficient authorization by granting global privileges without reviewing the expected role. Retain user identity, operation, time and outcome in the evidence without exposing passwords or integration secrets.
Rejection is not unavailability
Consider the conceptual list remote, local. If the remote method returns PASS, the model stops successfully; if it returns FAIL, it stops with rejection. Only ERROR permits trying the next method in this model of documented AAA authentication semantics. An operator with rejected credentials does not automatically switch to the local account. To test recovery, deliberately distinguish service unavailability from a valid rejection and retain a recovery session. The script supplies these outcomes as inputs: it implements neither TACACS+, RADIUS, dead-server detection nor timeout behaviour. On a device, the remote group may try its servers before returning its outcome to the method list.
ACL meaning depends on its use
In the previous lesson’s filtering ACL, permit and deny were packet decisions at that point. In an ACL selecting traffic for a CoPP class, permit means the packet matches the class; the policy-map action then determines treatment. The model excludes SSH from 192.0.2.10 using deny and selects other SSH using permit for a hypothetical class whose action is drop. Thus permit may lead to discard in this use. The not-selected result is not global authorization: another class, default policy or another control may act. The exercise records only selection for this class and does not simulate the device’s complete policy.
Protect the CPU while retaining operations
CoPP handles traffic reaching the control plane; it does not replace security policy for every application transiting the device. Before changing classes or rates, identify required protocols, peers and normal and recovery behaviour. Lower CPU usage does not prove improved service if the new policy also prevents adjacencies or legitimate administration. Correlate class and drop counters with routing events, management availability and change timing. Thresholds depend on platform and workload; the model provides no production-ready rate and measures no CPU capacity. Acceptance requires observation on the target device and version.
Run the model and prepare the actual change
Save the script below as run.py and run python3 run.py /tmp/access-evidence.json. It uses only the Python standard library, with no Docker, networking or credentials. Two executions passed 23 checks across three models: ordered ACL, single CoPP-class selection and an AAA outcome sequence. JSON records inputs and expected and actual outputs, allowing a condition to be changed and the decision understood. No result establishes IOS XE execution or production acceptance. For an actual change, confirm recovery access, complete configuration, methods attached to the correct lines, authorized roles, observability and rollback. Close the window only after testing agreed normal and recovery paths and documenting what remains unexercised.
"""Original teaching models only: IPv4 ACL decisions, CoPP selection and AAA outcomes.
Run: python3 run.py /tmp/access-evidence.json
No packets sent, no devices contacted, no credentials used, no hardware emulation.
"""
import datetime,hashlib,ipaddress,json,pathlib,platform,sys
checks=[]
def check(name,actual,expected,inputs):
assert actual==expected,(name,actual,expected)
checks.append(dict(name=name,inputs=inputs,actual=actual,expected=expected))
def address_match(value,base,wildcard):
mask=(~int(ipaddress.IPv4Address(wildcard)))&0xffffffff
return (int(ipaddress.IPv4Address(value))&mask)==(int(ipaddress.IPv4Address(base))&mask)
def packet(src='192.0.2.10',dst='198.51.100.20',proto='tcp',sport=51000,dport=443,flags=):
return dict(src=src,dst=dst,proto=proto,sport=sport,dport=dport,flags=list(flags))
def rule(seq,action,proto='ip',src='0.0.0.0',sw='255.255.255.255',dst='0.0.0.0',dw='255.255.255.255',sport=None,dport=None,established=False):
assert action in ['permit','deny']
return dict(seq=seq,action=action,proto=proto,src=src,sw=sw,dst=dst,dw=dw,sport=sport,dport=dport,established=established)
def evaluate(rules,p):
for r in sorted(rules,key=lambda r:r['seq']):
if r['proto']!='ip' and r['proto']!=p['proto']:continue
if not address_match(p['src'],r['src'],r['sw']) or not address_match(p['dst'],r['dst'],r['dw']):continue
if r['sport'] is not None and p['sport']!=r['sport']:continue
if r['dport'] is not None and p['dport']!=r['dport']:continue
if r['established'] and (p['proto']!='tcp' or not(set(p['flags'])&{'ACK','RST'})):continue
return dict(action=r['action'],sequence=r['seq'])
return dict(action='deny',sequence=None)
def aaa(outcomes):
visited=[]
for name,result in outcomes:
assert result in ['PASS','FAIL','ERROR']
visited.append(name)
if result!='ERROR':return dict(result=result,visited=visited)
return dict(result='ERROR',visited=visited)
def acl_check(name,rules,p,expected):check(name,evaluate(rules,p),expected,dict(rules=rules,packet=p))
permit=rule(10,'permit','tcp','192.0.2.0','0.0.0.255','198.51.100.20','0.0.0.0',dport=443)
acl=[permit];flow=packet
acl_check('approved HTTPS request matches sequence 10',acl,flow,dict(action='permit',sequence=10))
acl_check('unapproved source reaches implicit deny',acl,packet(src='203.0.113.8'),dict(action='deny',sequence=None))
acl_check('unapproved service reaches implicit deny',acl,packet(dport=22),dict(action='deny',sequence=None))
acl_check('UDP 443 is not TCP 443',acl,packet(proto='udp'),dict(action='deny',sequence=None))
acl_check('destination outside exact host is not matched',acl,packet(dst='198.51.100.21'),dict(action='deny',sequence=None))
early_deny=rule(5,'deny','ip');acl_check('earlier deny shadows intended permit',[permit,early_deny],flow,dict(action='deny',sequence=5))
late_deny=rule(20,'deny','ip');acl_check('later deny does not override first permit',[permit,late_deny],flow,dict(action='permit',sequence=10))
reverse=packet(src='198.51.100.20',dst='192.0.2.10',sport=443,dport=51000,flags=['ACK'])
acl_check('outbound permission creates no automatic reverse permission',acl,reverse,dict(action='deny',sequence=None))
return_rule=rule(10,'permit','tcp','198.51.100.20','0.0.0.0','192.0.2.0','0.0.0.255',sport=443,established=True)
acl_check('separate return rule matches ACK and source port',[return_rule],reverse,dict(action='permit',sequence=10))
acl_check('established keyword excludes SYN-only packet',[return_rule],{**reverse,'flags':['SYN']},dict(action='deny',sequence=None))
acl_check('ACK matching does not consult session history',[return_rule],packet(src='198.51.100.20',dst='192.0.2.88',sport=443,dport=59999,flags=['ACK']),dict(action='permit',sequence=10))
for value,expected in [('192.0.2.0',True),('192.0.2.254',True),('192.0.2.3',False),('192.0.3.2',False)]:
check('noncontiguous wildcard '+value,address_match(value,'192.0.2.0','0.0.0.254'),expected,dict(address=value,base='192.0.2.0',wildcard='0.0.0.254'))
# This ACL selects untrusted SSH for one hypothetical CoPP class with a drop action.
class_acl=[rule(10,'deny','tcp','192.0.2.10','0.0.0.0',dport=22),rule(20,'permit','tcp',dport=22)]
def classification(p):
result=evaluate(class_acl,p)
return dict(acl=result,selected=result['action']=='permit',classAction='drop' if result['action']=='permit' else 'not-selected')
for label,p,seq,action,selected in [('trusted SSH',packet(dport=22),10,'deny',False),('untrusted SSH',packet(src='203.0.113.8',dport=22),20,'permit',True),('unrelated HTTPS',flow,None,'deny',False)]:
check('CoPP '+label,classification(p),dict(acl=dict(action=action,sequence=seq),selected=selected,classAction='drop'if selected else'not-selected'),dict(classAcl=class_acl,packet=p))
for label,outcomes,expected in [
('remote PASS stops',[('remote','PASS'),('local','PASS')],dict(result='PASS',visited=['remote'])),
('remote FAIL stops',[('remote','FAIL'),('local','PASS')],dict(result='FAIL',visited=['remote'])),
('remote ERROR permits local attempt',[('remote','ERROR'),('local','PASS')],dict(result='PASS',visited=['remote','local'])),
('local FAIL after remote ERROR denies',[('remote','ERROR'),('local','FAIL')],dict(result='FAIL',visited=['remote','local'])),
('all methods ERROR do not authenticate',[('remote','ERROR'),('local','ERROR')],dict(result='ERROR',visited=['remote','local']))]:check('AAA '+label,aaa(outcomes),expected,dict(outcomes=outcomes))
report=dict(checkedAt=datetime.datetime.now(datetime.timezone.utc).isoformat,python=platform.python_version,scriptSha256=hashlib.sha256(pathlib.Path(__file__).read_bytes).hexdigest,checks=checks,models=['ordered IPv4 ACL matcher','single CoPP class selector','AAA method outcome state machine'],limitations=['Teaching models, not Cisco IOS XE execution or packet-level acceptance','Only listed IPv4 fields and exact ports; no fragments, NAT order, IPv6, TCAM or complete CLI parser','CoPP class selection only; no rate, burst, CPU load, hardware queues or real policy-default verdict','AAA outcomes supplied as inputs; no server, protocol, credentials, timeout measurement or authorization implementation','No network, Docker, device changes or production service validation'])
pathlib.Path(sys.argv[1]).write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(dict(checks=len(checks),models=len(report['models']),scriptSha256=report['scriptSha256'])))
remote=FAIL stops without consulting local; remote=ERROR permits a local attempt.
Common pitfalls
Using rejection as a fallback test; confusing classification permit with acceptance; choosing rates without a baseline; closing the only session before proving recovery.
Related topics: Network assurance and diagnosis · Infrastructure security and device access
Interpret each decision in context and retain evidence of normal access, denial and recovery.
Reference: Configuring Authentication · 350-401 ENCOR v1.2, effective 2026-03-19; core component of CCNP Enterprise