
NIST SP 800-207 is the vocabulary for zero trust: a subject, a resource, and a policy decision on every request.
It will not configure Express for you. The useful move is to translate each idea into a check: identify the caller, authorize the object, and keep a proof you can revoke.
The usual mistake is quoting the PDF in a design doc and then skipping the session check on an internal IP.
This page is the parts of 800-207 that map to application code. The sibling zero-trust page is the Express version of those checks.
NIST SP 800-207 is still the August 2020 final. Fifty-nine pages. As of 22 August 2026, CSRC record. Document history stops at 11 August 2020: two drafts, then Final. There is no Rev 1.
This page is the practitioner map. The sibling app-controls page is the Express deny path. Keep JWT in Express next to you when the PA must mint a blob a peer can verify, and the secure coding checklist for the rest of the request surface.
The current text is still the 2020 final
Authors on the cover: Scott Rose and Oliver Borchert at NIST, Stu Mitchell, Sean Connelly at CISA. DOI 10.6028/NIST.SP.800-207. The PDF still carries August 2020 on the title page. CSRC’s planning note from 19 April 2024 only points at unofficial Spanish and Japanese translations. That is not a technical revision.
Section 1 says the audience is enterprise security architects for civilian unclassified systems. It is not a deployment cookbook and it says so. Section 2 defines ZT as concepts that minimize uncertainty when you enforce least privilege per request on a network you treat as already hostile. ZTA is the plan: components, workflows, policies. A zero-trust enterprise is the infrastructure and the operational rules that plan produces.
OMB M-22-09, dated 26 January 2022, pointed civilian agencies at this document and at CISA’s maturity model, and set goals through Fiscal Year 2024. CISA Zero Trust Maturity Model 2.0 is the April 2023 PDF. Those memos did not rewrite 800-207. They used it. If a vendor slide says “SP 800-207 Rev 2, 2025,” ask for the CSRC URL. I could not find that revision on 22 August 2026.
Seven tenets in handler language
Section 2.1 lists seven tenets. I am translating each into a check you can put in a pull request, not restating the brochure.
- Everything that holds data or runs code is a resource. An internal admin route, a billing worker, a debug port, a Cloud Function. Inventory those URLs. A resource you did not list will not get a PEP.
- Secure the channel regardless of where the socket opened. TLS on the internal listener too. Same authn bar for a pod on 10.0.0.0/8 as for a laptop in a cafe.
- Grant per session, least privilege, one resource at a time. Passing
/api/orders/aaadoes not open/api/orders/bbb. A support role is scoped toorgId. - Policy is dynamic and uses observable state. Subject, device posture if you have it, time, resource sensitivity. A static ACL that never reads context is the old perimeter in a spreadsheet.
- Measure posture of owned and associated assets. You cannot do CDM in twenty lines of Express. You can refuse a session from a client whose
deviceTrustclaim is stale, and you can log the deny. - Authn and authz are discrete and happen before access. Two functions. 401 then 403 or 404. MFA lives at the mint, not as a slide.
- Collect state and feed it back into policy. Decision logs with
subject,resource,decision,reason. Then changedecideAccesswhen the log says a grant was wrong.
Section 2.1 also says these tenets are the ideal and an enterprise may not hit purity on day one. That is not permission to skip tenet 2 with if (isPrivateIp). It is permission to sequence: identity on every private route this sprint, object checks next, device claims when you have a source of them.
PE, PA, PEP as three functions
Section 3 splits the policy decision point into a policy engine and a policy administrator. The PEP is the gate on the data plane. The PE and PA talk to the PEP on a control plane. In a monolith those planes are function calls. In a mesh the control plane is xDS or an authorizer RPC. The names stay the same.
NIST’s words, shortened: the PE makes the grant or deny using policy plus external signals. The PA starts or tears down the path and may mint a session-specific credential. The PEP enables, watches, and ends the connection. Some products merge PE and PA. Your code can too, if you still log a decision separate from minting the cookie.
function decideAccess({ subject, resource, action, context }) {
if (!subject || !subject.userId) {
return { decision: "deny", reason: "no-subject" };
}
if (context.deviceTrust === "untrusted") {
return { decision: "deny", reason: "device" };
}
if (resource.type === "order" && resource.ownerId === subject.userId) {
return { decision: "allow", reason: "owner" };
}
if (subject.role === "support" && resource.orgId === subject.orgId && action === "read") {
return { decision: "allow", reason: "org-support" };
}
return { decision: "deny", reason: "policy" };
}
function mintSession(req, subject) {
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) {
reject(err);
return;
}
req.session.userId = subject.userId;
req.session.role = subject.role;
req.session.orgId = subject.orgId;
req.session.createdAt = Date.now();
req.session.save((saveErr) => {
if (saveErr) reject(saveErr);
else resolve({ sid: req.session.id });
});
});
});
}
async function pep(req, res, next) {
const subject = req.session && req.session.userId
? {
userId: req.session.userId,
role: req.session.role,
orgId: req.session.orgId,
}
: null;
const resource = req.order;
const action = req.method === "GET" ? "read" : "write";
const verdict = decideAccess({
subject,
resource,
action,
context: { deviceTrust: req.session && req.session.deviceTrust },
});
req.log.info({
subject: subject && subject.userId,
resource: resource && resource.id,
decision: verdict.decision,
reason: verdict.reason,
}, "pe");
if (verdict.decision !== "allow") {
res.status(404).send("Not found");
return;
}
next();
}
Identifiers stay decideAccess, mintSession, pep, subject, resource, verdict. decideAccess is the PE. mintSession is the PA. pep is the PEP. express-session 1.19.0, dated 22 January 2026, is the row store. If a second host must accept the proof, jose 6.2.10 dated 21 August 2026 verifies a blob the PA minted with iss, aud, exp, and sub. Pin algorithms. The JWT page is that verifier.
SUBJECT cookie or bearer or mTLS id | v PEP pep() on /api/orders/:orderId no subject -> deny, reason=no-subject | +----- control -----> PE decideAccess(...) | allow | deny + reason | +----- control -----> PA mintSession / destroy __Host-session row | v RESOURCE handler runs only after allow
Section 3.1 names three approaches: enhanced identity governance, micro-segmentation, and network or SDP overlays. A web app almost always starts with identity governance. The subject is the primary input. Device and environment adjust the score. Micro-segmentation and SDP are how you stop a grant to orderId from becoming a walk across the cluster. They are extra PEPs, not a substitute for decideAccess.
Inputs the trust algorithm can actually use
Section 3.3 is the trust algorithm. The PE scores a request from several feeds. You will not stand up every feed this quarter. You should know which ones you already have, and which ones you are faking with a comment.
| NIST feed | What you can wire now | What to stop inventing |
|---|---|---|
| ID management | Session userId, IdP claims, SPIFFE id | A shared X-Internal header |
| Data access policy | decideAccess plus tests | A wiki table no one runs |
| CDM / posture | Device cert, MDM claim, stale deviceTrust | User-Agent as posture |
| Threat intel | Known-bad jti, stolen-session list | A news feed in the hot path |
| Activity logs | Structured pe lines | Logging the bearer or cookie |
| PKI | mTLS to the worker | A self-signed cert you never rotate |
Section 3.3.1 describes criteria-based versus score-based algorithms, and singular versus contextual. Criteria-based is a boolean gate: missing MFA, deny. Score-based sums signals. Contextual uses recent behavior. Start criteria-based. A score you cannot explain will fail open under pressure. Log reason as a stable string so you can grep denies.
Do not put the password, the Authorization header, or the raw sid in that log. A09:2025 is the Top 10 name for logging and alerting failures. The decision line is the signal. The secret is not.
800-207A and 1800-35 without the pitch
SP 800-207A, CSRC final 13 September 2023, is the cloud-native access-control companion. Chandramouli and Butcher. It shifts the emphasis from IP and subnet toward application and service identities, and it names API gateways, sidecar proxies, and SPIFFE as the platform that can enforce those identities across locations. If your PEP is only a security group, 207A is the document that tells you that is the wrong tier.
SP 1800-35, CSRC final 10 June 2025, is the NCCoE practice guide. Fifty-five pages in the high-level PDF June 2025 on the title page. NIST’s 11 June 2025 news note says 19 example implementations and 24 collaborators. Use it to see a worked PE/PA/PEP wiring, not to copy a vendor BOM into a purchase order.
Section 5 of 800-207 is the threat chapter people skip. Subverted PE, DoS against the decision path, stolen credentials, loss of visibility, proprietary decision formats, and non-person entities that administer the ZTA. The stolen-credential case is why MFA at mint and a killable row matter more than a new diagram. The subverted-PE case is why decideAccess lives in a repo you review, not only in a SaaS you cannot diff.
Prove one per-request decision
You are proving your PE. You are not attacking a third-party ZTNA.
- Log in on your own origin. Confirm
__Host-session. - Call a private route you own. Expect 200 and a
pelog line withdecision=allowand areasonyou recognize. - Delete the cookie. Repeat. Expect 404 or 401 and
reason=no-subject. - Keep the cookie. Change
orderIdto a row you do not own. Expect 404 andreason=policy. - Grep for a LAN skip and for a second mount that bypasses
pep.
rg -n "decideAccess|function pep|isPrivateIp|X-Internal" --glob '!node_modules'
# after a 200 on your own order, your last pe line should look like:
# subject=ada resource=ord_123 decision=allow reason=owner
If the log cannot tell allow-from-owner from allow-from-support, the PE is a boolean you will regret. If there is no log, you cannot feed tenet 7. Add the line before you add another vendor agent.
Questions we keep getting
Did NIST replace 800-207 with 1800-35?
No. 1800-35 is a practice guide that shows builds consistent with the 2020 publication. CSRC still lists 800-207 as the August 2020 final. Read 1800-35 after you can point at PE, PA, and PEP in your own repo.
Do I need a policy engine product?
You need a function that can deny and a log of why. OPA, Cedar, or a SaaS PE are options once that function is painful to maintain. Starting with a SKU and no decideAccess tests is how a zero-trust page turns into a brochure.
Where does a JWT sit in PE, PA, PEP?
The PA may mint it. The PEP verifies it. The PE still decides whether that sub may touch this orderId. A verified blob is identity, not authorization. Keep the session row when only your origin needs the proof.



