← CCNP Security: SCOR core and operations
25 / 25 · 55 MIN

Identity, posture and effective access

Separate certificate validation, posture age and authorization using local mTLS and ISE acceptance criteria.

1. Separate identity, profile and posture

A laptop may present an accepted certificate while lacking current evidence of required antimalware or configuration. Authentication identifies a credential or peer; profiling and inventory help classify the device; posture evaluates concrete requirements. In the studied ISE guide, unknown can mean no matching policy or no received assessment. Do not automatically translate it into malware. Noncompliant refers to unmet requirements and needs diagnosis and remediation. Compliant must be read with policy, requirements, time and context rather than as assurance of no compromise. Design three authorization outcomes and the resources needed for remediation. An overly broad access rule can neutralize a correct posture assessment.

2. Understand result age

Posture lease can reuse the last known state and does not imply querying the endpoint again at every access. In the documented flow, lease expiry does not itself trigger reauthentication or reassessment of an existing session. Distinguish it from periodic reassessment, applicable to already-compliant clients, and check method support and failover behavior. A grace period can temporarily authorize a previously compliant device despite a current failure; this is not evidence of renewed compliance. Record accepted risk, expiry and reachable services. For agentless posture, “no permanent agent” does not mean “no endpoint code”: the flow can contact the client, run a plug-in and remove it. Credentials, connectivity and accounting events belong in diagnosis.

3. Observe actual mutual authentication

Run the lab with Node 25.8.0 and the recorded certificate generator. The server listens only on 127.0.0.1, requires a client certificate and validates the supplied CA; the client also validates the server CA and access.test name. Test a missing certificate, untrusted issuer, serverAuth-only certificate and wrong server name. These requests do not reach the HTTP handler. These are actual TLS 1.3 connections, not a handshake model. Certificates and keys are created in an owned temporary directory with restricted permissions and removed afterward. No system trust store changes occur. Execution includes no EAP, RADIUS, ISE, posture scan or revocation checking.

4. Apply policy after the certificate

A fictional fingerprint-indexed registry binds a credential to a role and posture data. A new key with the same CN has another fingerprint and is not automatically authorized. A client registered as reader receives 403 for /admin even though mTLS passes. Unknown, noncompliant and stale evidence also receive separate decisions. The 60-second limit and fixed clock are teaching choices in this code, not ISE values. The /remediate route is explicitly allowed for the registered client to illustrate limited access. Changing registry data does not measure the endpoint. Removing an identity from the registry returns 403 while its certificate remains cryptographically accepted: that is not PKI revocation.

5. Validate the access transition

For a real deployment, test authentication, identity binding, posture, authorization rule, network-device enforcement and final access. Plan certificate renewal with binding policy and recovery; an equal CN does not establish that the new artifact is authorized. Confirm revocation requirements and access to necessary services before claiming EAP-TLS conformance. RFC 9190 requires checks this lab does not perform. Collect allowed, denied and remediation outcomes, including existing sessions and new authentication, with versions and owners. Summary: a valid certificate, current posture and effective authorization need separate evidence. The 28 local checks do not replace acceptance of supplicant, NAD, ISE and the organization’s operational requirements.

// Actual loopback mutual TLS; posture and role decisions are explicit synthetic data.
import fs from 'node:fs'import os from 'node:os'import path from 'node:path'import https from 'node:https'import {X509Certificate,createHash}from'node:crypto'import{execFileSync}from'node:child_process'import assert from'node:assert/strict'
const dir=fs.mkdtempSync(path.join(os.tmpdir,'dr-mtls-posture-'));fs.chmodSync(dir,0o700);const openssl='/usr/bin/openssl',checks=[],outcomes=[],sockets=new Set;let server,applicationRequests=0,now=1000;
const check=(name,ok)=>{assert(ok,name);checks.push(name);},read=n=>fs.readFileSync(path.join(dir,n)),write=(n,s)=>fs.writeFileSync(path.join(dir,n),s),command=args=>execFileSync(openssl,args,{cwd:dir,stdio:['ignore','pipe','pipe'],timeout:15000});
function root(n){write(n+'.cnf',`[req]\nprompt=no\ndistinguished_name=dn\nx509_extensions=ca\n[dn]\nCN=fictional-${n}\n[ca]\nbasicConstraints=critical,CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign\n`);command(['req','-x509','-newkey','rsa:2048','-nodes','-days','2','-sha256','-config',n+'.cnf','-keyout',n+'.key','-out',n+'.pem']);fs.chmodSync(path.join(dir,n+'.key'),0o600);}
function leaf(n,issuer,serial,cn,usage){command(['req','-new','-newkey','rsa:2048','-nodes','-sha256','-subj','/CN='+cn,'-keyout',n+'.key','-out',n+'.csr']);write(n+'.ext',`basicConstraints=critical,CA:FALSE\nkeyUsage=critical,digitalSignature,keyEncipherment\nextendedKeyUsage=${usage}\nsubjectAltName=DNS:${cn}\n`);command(['x509','-req','-in',n+'.csr','-CA',issuer+'.pem','-CAkey',issuer+'.key','-set_serial',String(serial),'-days','1','-sha256','-extfile',n+'.ext','-out',n+'.pem']);fs.chmodSync(path.join(dir,n+'.key'),0o600);return {key:read(n+'.key'),cert:read(n+'.pem')};}
try{
 root('rootA');root('rootB');const srv=leaf('server','rootA',10,'access.test','serverAuth'),device=leaf('device','rootA',20,'device.test','clientAuth'),replacement=leaf('replacement','rootA',21,'device.test','clientAuth'),foreign=leaf('foreign','rootB',22,'device.test','clientAuth'),wrongPurpose=leaf('wrong-purpose','rootA',23,'device.test','serverAuth');
 const fingerprint=material=>new X509Certificate(material.cert).fingerprint256,id=fingerprint(device),newId=fingerprint(replacement);
 const registry=new Map([[id,{role:'reader',posture:'compliant',observedAt:990}]]);
 server=https.createServer({...srv,ca:read('rootA.pem'),requestCert:true,rejectUnauthorized:true,minVersion:'TLSv1.3',maxVersion:'TLSv1.3'},(req,res)=>{
 applicationRequests++;const peer=req.socket.getPeerCertificate,entry=registry.get(peer.fingerprint256);let status=200,reason='allowed'
 if(!entry){status=403;reason='unregistered-certificate'}
 else if(req.url==='/remediate'){reason='limited-remediation'}
 else if(entry.posture!=='compliant'){status=403;reason='posture-'+entry.posture;}
 else if(now-entry.observedAt>60){status=403;reason='stale-posture'}
 else if(req.url!=='/report'){status=403;reason='role-denied'}
 res.writeHead(status,{'Content-Type':'application/json',Connection:'close'});res.end(JSON.stringify({reason,clientCertificateAuthorized:req.socket.authorized,clientCN:peer.subject.CN}));
 });server.on('tlsClientError',=>{});server.on('connection',s=>{sockets.add(s);s.on('close',=>sockets.delete(s));});
 await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
 async function request(label,material=device,options={}){const result=await new Promise(resolve=>{const r=https.get({host:'127.0.0.1',port:server.address.port,path:options.path||'/report',servername:options.name||'access.test',ca:options.ca||read('rootA.pem'),...(material||{}),minVersion:'TLSv1.3',maxVersion:'TLSv1.3',rejectUnauthorized:true,agent:false},res=>{const protocol=res.socket.getProtocol,serverAuthorized=res.socket.authorized,reused=res.socket.isSessionReused;let text=''res.on('data',d=>text+=d);res.on('end',=>resolve({ok:true,status:res.statusCode,protocol,serverAuthorized,reused,...JSON.parse(text)}));res.on('error',e=>resolve({ok:false,error:e.code||e.message}));});r.setTimeout(4000,=>r.destroy(Error('timeout')));r.on('error',e=>resolve({ok:false,error:e.code||e.message}));});outcomes.push({label,...result});return result;}
 check('listener binds only loopback',server.address.address==='127.0.0.1');
 const allowed=await request('trusted-registered-current');check('trusted registered client reaches report',allowed.status===200&&allowed.reason==='allowed');check('TLS1.3 actually negotiated',allowed.protocol==='TLSv1.3');check('client validates server certificate',allowed.serverAuthorized===true);check('server validates client certificate',allowed.clientCertificateAuthorized===true);
 let count=applicationRequests;const absent=await request('missing-certificate',null);check('missing client certificate rejected before HTTP',!absent.ok&&applicationRequests===count);
 const otherCA=await request('untrusted-client-issuer',foreign);check('untrusted client issuer rejected before HTTP',!otherCA.ok&&applicationRequests===count);
 const purpose=await request('wrong-client-purpose',wrongPurpose);check('server-only certificate rejected for client authentication',!purpose.ok&&applicationRequests===count);
 const badServerTrust=await request('untrusted-server-issuer',device,{ca:read('rootB.pem')});check('untrusted server issuer rejected before HTTP',!badServerTrust.ok&&applicationRequests===count);
 const badName=await request('wrong-server-name',device,{name:'other.test'});check('wrong expected server identity rejected',!badName.ok&&badName.error==='ERR_TLS_CERT_ALTNAME_INVALID'&&applicationRequests===count);
 const newUnregistered=await request('replacement-not-enrolled',replacement);check('same-CN replacement has different certificate identity',newId!==id&&new X509Certificate(device.cert).subject===new X509Certificate(replacement.cert).subject);check('valid replacement chain does not create registry authorization',newUnregistered.clientCertificateAuthorized===true&&newUnregistered.status===403&&newUnregistered.reason==='unregistered-certificate');
 const wrongRole=await request('reader-admin-path',device,{path:'/admin'});check('valid mutual TLS does not grant administrative role',wrongRole.status===403&&wrongRole.reason==='role-denied'&&wrongRole.clientCertificateAuthorized);
 registry.get(id).posture='unknown'const unknown=await request('unknown-posture');check('unknown posture is distinct from failed TLS',unknown.status===403&&unknown.reason==='posture-unknown'&&unknown.clientCertificateAuthorized);
 registry.get(id).posture='noncompliant'const badPosture=await request('noncompliant-posture');check('noncompliant posture does not invalidate the certificate',badPosture.status===403&&badPosture.reason==='posture-noncompliant'&&badPosture.clientCertificateAuthorized);
 const remediation=await request('limited-remediation',device,{path:'/remediate'});check('registered noncompliant client can use only the configured remediation path',remediation.status===200&&remediation.reason==='limited-remediation');
 registry.get(id).posture='compliant'now=1050;const boundary=await request('freshness-boundary');check('synthetic 60-second boundary remains accepted',boundary.status===200);
 now=1051;const stale=await request('stale-evidence');check('stale posture denied despite valid certificate',stale.status===403&&stale.reason==='stale-posture'&&stale.clientCertificateAuthorized);
 registry.get(id).observedAt=now;const refreshed=await request('fresh-evidence');check('new synthetic evidence restores the allowed report path',refreshed.status===200);
 registry.set(newId,{role:'reader',posture:'compliant',observedAt:now});const enrolled=await request('replacement-enrolled',replacement);check('controlled enrollment permits the replacement client',enrolled.status===200&&enrolled.clientCertificateAuthorized);
 registry.delete(id);const retired=await request('old-identity-removed');check('registry removal denies old identity at application layer',retired.status===403&&retired.reason==='unregistered-certificate');check('registry removal is not PKI revocation',retired.clientCertificateAuthorized===true);
 check('all successful HTTPS requests use fresh TLS sessions',outcomes.filter(o=>o.ok).every(o=>!o.reused));check('eleven requests reach application',applicationRequests===11);check('sixteen connections attempted',outcomes.length===16);
 const keys=fs.readdirSync(dir).filter(f=>f.endsWith('.key'));check('all generated private keys have owner-only permissions',keys.every(f=>(fs.statSync(path.join(dir,f)).mode&0o777)===0o600));
 for(const s of sockets)s.destroy;await new Promise(resolve=>server.close(resolve));check('listener closed after exercise',!server.listening);server=null;
}finally{for(const s of sockets)s.destroy;if(server?.listening)await new Promise(resolve=>server.close(resolve));fs.rmSync(dir,{recursive:true,force:true});}
check('owned temporary certificate directory removed',!fs.existsSync(dir));
console.log(JSON.stringify({passed:checks.length,failed:0,checks,node:process.version,nodeOpenSSL:process.versions.openssl,certificateTool:execFileSync(openssl,['version'],{encoding:'utf8'}).trim,scriptSHA256:createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),actualMutualTLS:true,actualTLS13:true,actualCiscoISE:false,actualEAP:false,actualRADIUS:false,actualPostureScan:false,postureDataFictional:true,revocationChecked:false,systemTrustStoreChanged:false,externalConnections:false,observations:{applicationRequests,connections:outcomes.length,postureAgeLimitSeconds:60,postureClock:'synthetic fixed values',outcomes},scope:'Actual loopback mutual TLS only. Original in-memory fingerprint registry, role and posture decisions are fictional application policy, not ISE or EAP-TLS conformance. No certificate-revocation checks; no posture scanner, Cisco deployment or system trust changes.'},null,2))
IN PRACTICE

The certificate passes and /report returns 200; /admin returns 403 for reader. A new certificate with the same CN needs authorized binding.

Common pitfalls

Unknown as malware; lease as a new scan; certificate as posture; equal CN as authorization; registry removal as PKI revocation.

Related topics: EAP-TLS and certificates · Posture and remediation · Network authorization

Take this idea with you

Secure access requires trust, identity binding, posture evidence and effective policy on the path used.

Create account

Reference: ISE 3.4 Compliance · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® and Cisco® are registered trademarks of Cisco Systems, Inc. and/or its affiliates. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Cisco. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.