1. Separar identidade, perfil e postura
Um portátil pode apresentar um certificado aceite e continuar sem evidência atual de antimalware ou configuração exigida. Autenticação identifica uma credencial ou interlocutor; perfil e inventário ajudam a classificar o dispositivo; postura avalia requisitos concretos. No ISE estudado, unknown pode indicar falta de política correspondente ou ausência de avaliação recebida. Não o traduzas automaticamente por malware. Noncompliant refere requisitos não satisfeitos e precisa de diagnóstico e remediação. Compliant deve ser lido com política, requisitos, hora e contexto, não como garantia de ausência de compromisso. Desenha três resultados de autorização e os recursos necessários à remediação. Uma regra de acesso excessiva pode neutralizar uma avaliação de postura correta.
2. Entender a idade do resultado
Posture lease pode reutilizar o último estado conhecido e não implica nova consulta ao endpoint a cada acesso. No fluxo documentado, expirar a lease não desencadeia por si reautenticação ou reavaliação da sessão existente. Distingue isso de periodic reassessment, aplicável a clientes já conformes, e verifica suporte do método e comportamento de failover. Um período de graça pode autorizar temporariamente um dispositivo anteriormente conforme apesar de uma falha atual; não é prova de nova conformidade. Regista o risco aceite, prazo e serviços acessíveis. Para agentless posture, “sem agente permanente” não significa “sem executar código no endpoint”: o fluxo pode contactar o cliente, lançar um plug-in e removê-lo. Credenciais, conectividade e eventos de accounting fazem parte do diagnóstico.
3. Observar autenticação mútua real
Executa o laboratório com Node 25.8.0 e o gerador de certificados registado. O servidor escuta apenas em 127.0.0.1, exige certificado de cliente e valida a CA fornecida; o cliente também valida CA e nome access.test do servidor. Testa certificado ausente, emissor não confiável, certificado apenas serverAuth e nome de servidor errado. Esses pedidos não chegam ao handler HTTP. São ligações TLS 1.3 efetivas, não um modelo de handshake. Os certificados e chaves são criados num diretório temporário próprio, com permissões restritas, e removidos no fim. Não se modifica o trust store do sistema. A execução não inclui EAP, RADIUS, ISE, scan de postura nem verificação de revogação.
4. Aplicar política depois do certificado
Um registo fictício indexado pelo fingerprint liga a credencial a uma role e a dados de postura. Uma nova chave com o mesmo CN tem outro fingerprint e não fica autorizada automaticamente. Um cliente registado como reader recebe 403 para /admin apesar de o mTLS passar. Unknown, noncompliant e evidência antiga também recebem decisões próprias. O limite de 60 segundos e o relógio fixo são escolhas didáticas deste código, não valores ISE. A rota /remediate está explicitamente permitida ao cliente registado para mostrar acesso limitado. Alterar dados do registo não é medir o endpoint. Retirar uma identidade do registo devolve 403 enquanto o certificado continua criptograficamente aceite: isso não é revogação PKI.
5. Validar a transição de acesso
Para uma implantação real, testa autenticação, associação à identidade, postura, regra de autorização, aplicação no dispositivo de rede e acesso final. Planeia renovações de certificado com política de associação e recuperação; um CN igual não demonstra que o novo artefacto está autorizado. Confirma requisitos de revogação e acesso aos serviços necessários antes de declarar conformidade EAP-TLS. O RFC 9190 exige verificações que este laboratório não executa. Recolhe resultados permitidos, recusados e de remediação, incluindo sessões existentes e nova autenticação, com versões e donos. Resumo: certificado válido, postura atual e autorização efetiva precisam de evidência separada. As 28 verificações locais não substituem aceitação do supplicant, NAD, ISE e requisitos operacionais da organização.
// 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));
O certificado passa e /report responde 200; /admin devolve 403 para reader. Um certificado novo com o mesmo CN precisa de associação autorizada.
Armadilhas comuns
Unknown como malware; lease como scan novo; certificado como postura; CN igual como autorização; remoção de registo como revogação PKI.
Tópicos relacionados: EAP-TLS e certificados · Postura e remediação · Autorização de rede
Um acesso seguro exige confiança, associação à identidade, evidência de postura e política efetiva no caminho usado.
Referência: ISE 3.4 Compliance · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security