Your app has a login page, a firewall, and a VPN for the admin panel. None of that stops an attacker who’s already inside — a leaked API key, a compromised laptop on your office Wi-Fi, a dependency that phones home. Zero trust is the answer to that specific failure mode: every request gets checked, every time, regardless of where it came from. This guide translates the official NIST and CISA zero trust frameworks into code you can actually ship — token validation, service-to-service auth, and gateway policy — for developers building web apps, not just enterprise network admins reading a compliance checklist.
Key Takeaways
- Zero trust, per NIST SP 800-207, removes implicit trust from network location entirely — every request is authenticated and authorized on its own merits, session by session.
- PASETO tokens close a real class of JWT bugs (algorithm confusion, 'alg: none') by hardcoding the crypto to the token version instead of trusting a header field.
- mTLS plus a workload identity system (SPIFFE/SPIRE) replaces static API keys for service-to-service calls, so a leaked key can't be replayed from an untrusted host.
- CISA's Zero Trust Maturity Model gives you four stages — Traditional, Initial, Advanced, Optimal — across five pillars, so you don't have to rebuild everything on day one.
- The riskiest zero trust mistake in web apps isn't the crypto — it's trusting a token's claims without re-verifying device posture and session freshness on every privileged action.
Prerequisites
Before you start, you should have:
- A working understanding of HTTP authentication (sessions, cookies, bearer tokens)
- Some exposure to an API gateway or reverse proxy (Nginx, Envoy, Kong, or a cloud equivalent)
- Basic TLS/PKI concepts — certificates, certificate authorities, and what a handshake actually does
- Node.js 18+ if you want to run the code examples locally (
npm install jose paseto)
This article covers zero trust for application-layer decisions — auth, sessions, service-to-service calls, and gateway policy. It doesn’t cover network segmentation hardware, SASE deployments, or endpoint device management, which are covered by CISA’s Devices and Networks pillars and usually owned by an infrastructure team, not app developers.
What Does “Zero Trust” Actually Mean for a Web App?
The term gets thrown around loosely, so it’s worth pinning down the actual source. NIST defines it in Special Publication 800-207, published in August 2020 and still the current baseline as of 2026 (a companion document, SP 800-207A, extends it to cloud-native multi-cloud access control but doesn’t replace it). NIST describes zero trust as a set of principles that move defense “from static, network-based perimeters to focus on users, assets, and resources.” No implicit trust is granted based on physical or network location, or on who owns the device.
That’s abstract until you translate it into what a web developer actually does differently. NIST lists seven tenets in the publication. Here they are, with the web-dev translation next to each one:
- All data sources and computing services are considered resources. Your internal admin API, your cron job’s database connection, your CI pipeline’s deploy hook — all of it needs an access decision, not just your public-facing endpoints.
- All communication is secured regardless of network location. TLS everywhere, including calls between two services sitting in the same VPC. “It’s internal” is not a security control.
- Access to individual enterprise resources is granted on a per-session basis. A token that’s valid means it’s valid for this session, this request pattern — not “logged in, therefore trusted for the next 30 days.”
- Access is determined by dynamic policy — the observable state of client identity, application, and the requesting asset, plus behavioral and environmental attributes. In practice: your authz decision can and should factor in device posture, IP reputation, and time-of-day risk scoring, not just “does this JWT have a valid signature.”
- The enterprise monitors and measures the integrity and security posture of all owned and associated assets. If you don’t log auth decisions and can’t answer “which service called this endpoint at 3am,” you don’t have zero trust — you have vibes.
- All resource authentication and authorization are dynamic and strictly enforced before access is allowed. No standing credentials that skip the check. Every call re-proves itself.
- The enterprise collects as much information as possible about the current state of assets, network infrastructure, and communications, and uses it to improve its security posture. This is the feedback loop — logs and telemetry feed back into policy, not just into a dashboard nobody reads.
Notice what’s missing from that list: nothing about firewalls, VPNs, or network topology as the primary control. That’s the actual shift. Your VPC’s private subnet is not a trust boundary under this model — it’s just where the servers happen to be.
NIST SP 800-207 has not had a full-document revision since its August 2020 release. There’s no “Zero Trust Architecture 2.0” — SP 800-207A (2023) is a companion for cloud-native, multi-cloud access control, and it supplements rather than supersedes the base document. If you see a vendor citing “the new NIST zero trust standard,” ask which SP number they mean. CISA’s Zero Trust Maturity Model is still on Version 2.0, published April 2023 — no newer version has shipped as of this writing.
Why Can’t My Login Page and Firewall Just Handle This?
Because they answer the wrong question. A login page answers “who are you, right now, at this one moment” and then usually hands you a session cookie or a long-lived token that’s trusted for the rest of the day. A firewall answers “are you coming from an IP range I’ve decided to trust.” Neither one asks the question zero trust cares about: “should this specific request, from this specific caller, in this current state, be allowed to touch this specific resource?”
Picture the failure case. A contractor’s laptop gets a malware infection. It’s on the corporate VPN, so it’s “inside the perimeter.” From that machine, an attacker pivots to your internal admin dashboard — no firewall rule stops them, because the firewall already decided that IP range is trusted. If your admin dashboard’s only gate was network location, you’re done. If it independently re-verifies the user’s identity, checks the device’s posture, and re-runs an access decision for that specific action, the pivot stalls.
This is exactly the “assume breach” framing that industry practice has built on top of NIST’s tenets (NIST itself doesn’t use the phrase “assume breach,” but tenet 5 — continuous monitoring of asset posture — and tenet 6 — strict enforcement before every access — describe the same behavior). You design as if the attacker is already past your outer defenses, because eventually they will be. The question stops being “how do I keep them out” and becomes “how little can they do once they’re in.”
Micro-segmentation is the network-layer expression of that same idea — instead of one flat internal network where any service can talk to any other service, you cut it into small zones where a compromised container can reach the two services it actually needs and nothing else. For web apps specifically, this usually means: your payments service doesn’t get network-level access to your marketing analytics database, even though they’re both “internal.”
How Do the NIST Components Map to Your Stack?
SP 800-207 defines a logical architecture with three core pieces, and mapping them onto real infrastructure makes the abstract model concrete:
- Policy Engine (PE) — makes the actual allow/deny decision by running a trust algorithm over identity, device, and risk signals. In a web app, this is your authorization service or policy-as-code layer — something like Open Policy Agent, a custom authz microservice, or your IdP’s fine-grained authorization API.
- Policy Administrator (PA) — executes the PE’s decision, issuing or revoking the session token/credential and telling the enforcement point to open or close the connection. NIST groups PE and PA together as the “Policy Decision Point” (PDP).
- Policy Enforcement Point (PEP) — the actual gatekeeper that sits in the request path, enabling, monitoring, and terminating the connection between the caller and the resource. This is your API gateway, your service mesh sidecar, or middleware sitting in front of a route handler.
Image Prompt: A premium hand-drawn isometric vector doodle illustration on a warm cream paper background. A client device sends a request through a labeled gateway box marked “PEP,” which connects via a dashed line up to a decision box marked “Policy Engine” checking identity, device, and risk signals, next to a box marked “Policy Administrator” issuing a short-lived token, then the request flows down into a locked database/API icon. Clean black outlines, hand-drawn arrows showing the request path, soft pastel yellow and blue highlight accents, small handwritten labels near each box. No text beyond short box labels, no watermark. Square 1:1.
In a typical Node.js or Python API, this usually shakes out as: a gateway (Kong, Envoy, or a cloud API Gateway) acting as the PEP, a token verification and policy-evaluation layer as the PE, and your identity provider’s token-issuing endpoint acting as the PA. The important part isn’t matching the exact NIST vocabulary in your code — it’s making sure these are three genuinely separate concerns, so a bug in your route handler can’t accidentally grant access that the policy layer never approved.
JWT vs PASETO: Which Token Format Fits a Zero Trust Session Model?
Zero trust’s third tenet says access is granted per session, not per login. That makes your token format a load-bearing security decision, not a library choice you make once and forget.
JWTs (JSON Web Tokens) are the default almost everywhere, and for good reason — huge ecosystem, every framework supports them, JWKS rotation is well understood. But JWT’s design lets the token itself declare which algorithm to use to verify it, via the alg header. That flexibility is where most JWT vulnerabilities live: algorithm confusion attacks (tricking a verifier expecting RS256 into accepting a token signed with the public key as an HMAC secret) and the infamous alg: none bypass, where older or misconfigured libraries would accept an unsigned token. Modern libraries patch these, but “patched by the library” is a weaker guarantee than “structurally impossible.”
This is industry practice, not something NIST specifies — the framework doesn’t mandate a token format. But it’s worth citing precisely: OWASP’s JWT security guidance and multiple CVEs against JWT libraries document these exact failure classes.
Here’s a JWT verification example using the jose library, pinned to a single expected algorithm and issuer — the defensive pattern that closes the algorithm-confusion hole:
import { createRemoteJWKSet, jwtVerify } from "jose";
// Fetch and cache the identity provider's public keys automatically
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json")
);
export async function verifyRequestToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "orders-api", // reject tokens minted for a different service
algorithms: ["RS256"], // pin the algorithm — never trust the header alone
clockTolerance: 5, // seconds of drift allowed; keep this tight
});
return payload;
}PASETO (Platform-Agnostic Security Tokens) exists specifically to remove that footgun. Instead of a negotiable alg header, the cryptographic algorithm is baked into the token’s version string (v4.local or v4.public, for example) — there’s no field for an attacker to manipulate because there’s no choice to manipulate. It’s a newer, smaller ecosystem than JWT, so check library maturity for your stack before committing to it for a production auth flow.
import { V4 } from "paseto";
import { readFileSync } from "fs";
const publicKey = readFileSync("./keys/paseto_public.pem");
export async function verifyPasetoToken(token: string) {
// V4.verify rejects malformed or mismatched tokens outright —
// there's no algorithm field to spoof in the first place
const payload = await V4.verify(token, publicKey, {
assertion: "orders-api",
});
return payload;
}If you’re already deep in a JWT-based identity provider (Auth0, Okta, Cognito, Keycloak), migrating to PASETO for user-facing session tokens is usually not worth the churn — pin your algorithms, keep tokens short-lived, and verify audience/issuer strictly. PASETO earns its keep most clearly for internal, service-issued tokens you fully control on both ends, where you’re not locked into an existing JWT-based IdP.
How Do I Enforce Least Privilege at the API Gateway?
The Policy Enforcement Point is where least privilege becomes a real, checkable rule instead of a design principle in a slide deck. The gateway should evaluate a policy against the caller’s verified identity and the specific resource being requested — not just check “is there a valid token” and then let the route handler sort out the rest.
Open Policy Agent’s Rego language is a common way to express this, whether it’s wired into Envoy, Kong, or a custom middleware:
package zero_trust.authz
import future.keywords.in
default allow := false
# A user can only read their own order records, and only from
# a device that has passed a recent posture check
allow if {
input.method == "GET"
input.path == ["orders", input.order_id]
input.token.claims.sub == input.resource_owner_id
input.token.claims.device_posture == "compliant"
time_since_last_check_in_minutes < 30
}
time_since_last_check_in_minutes := (time.now_ns() / 1000000000 - input.token.claims.posture_checked_at) / 60The point of writing it this way is that the rule reads like the actual security requirement: this specific user, on this specific verified device, within this specific freshness window, for this specific resource they own. Compare that to a typical if (req.user) { next() } middleware check, which answers “is someone logged in” and nothing else. That’s the difference between authentication and zero trust authorization — one confirms an identity exists, the other confirms that identity is allowed to do this exact thing right now.
Don’t let “least privilege” quietly become “least privilege at signup, then whatever the role had eight months ago.” Roles rot. Someone gets temporary admin access for an incident, and it never gets revoked. Bake an access review into your policy layer — either time-boxed grants that expire automatically, or a scheduled job that flags permissions nobody’s used in 90 days.
How Should Services Authenticate to Each Other?
Most of the zero trust conversation focuses on the human logging in. But in a modern web app, most requests are service-to-service: your API calling your payments processor’s internal service, your worker queue calling your database proxy, your frontend’s server-side rendering layer calling three internal APIs per page load. If those calls are secured with a static API key sitting in an environment variable, you’ve built a zero trust perimeter around the user and left the back door on a spring latch.
Mutual TLS (mTLS) is the standard answer here, and it maps directly onto tenet 2 — all communication secured regardless of network location. Both sides of the connection present a certificate; both sides verify the other’s certificate against a trusted CA before any application data moves. A leaked static token can be replayed from anywhere; a stolen mTLS private key still needs the corresponding certificate chain to be trusted by the CA your services actually validate against, and short-lived certificates limit how long a stolen one stays useful.
Here’s what that looks like at the Envoy proxy layer, requiring client certificates on the inbound side of a service:
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
require_client_certificate: true
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "/etc/certs/server-cert.pem" }
private_key: { filename: "/etc/certs/server-key.pem" }
validation_context:
trusted_ca: { filename: "/etc/certs/spire-bundle.pem" }That trusted_ca pointing at a SPIRE bundle is the piece that turns manual cert management into workload identity. Rotating mTLS certificates by hand across dozens of services is how teams quietly give up on mTLS six months in. SPIFFE (Secure Production Identity Framework For Everyone) and its reference implementation SPIRE issue short-lived, automatically-rotated identity documents (SVIDs) to each workload based on attested properties of the workload itself — which Kubernetes namespace it’s running in, which node, which service account — rather than a credential someone typed into a secrets manager once and forgot about.
Image Prompt: A premium hand-drawn isometric vector doodle illustration on a warm cream paper background. Several small isometric server-rack boxes grouped into three fenced zones labeled with simple icons (a lock, a coin, a chart), each server box holding a tiny hand-drawn certificate/badge icon, dashed connection lines drawn only between boxes that are allowed to talk to each other, with a few red “X” marks on blocked connection attempts between zones. Clean black outlines, soft pastel green and coral highlight accents, small hand-drawn padlock doodles at zone boundaries. No text, no watermark. Square 1:1.
An mTLS handshake without workload identity attestation just proves “this caller has a valid certificate,” not “this caller is the specific service it claims to be.” If your CA will sign a certificate for any workload that asks, you’ve built strong crypto around a weak identity check. SPIFFE/SPIRE’s attestation step — verifying platform-level facts about the workload before issuing its identity — is what actually closes that gap.
How Do I Handle Session Management Under Zero Trust?
Long-lived sessions are the most common way teams accidentally undo everything else they’ve built. A 30-day refresh token that never gets re-verified against device posture is, functionally, an implicit trust grant — exactly what tenet 1 exists to eliminate. The fix isn’t “log everyone out constantly,” it’s shortening the trust window and adding cheap, frequent re-checks instead of one expensive check that lasts a month.
A pattern that works well in practice:
- Short-lived access tokens (5–15 minutes), long enough to avoid hammering your token endpoint, short enough that a leaked token has a small blast radius.
- Refresh tokens with rotation — each refresh issues a new refresh token and invalidates the old one. If an old, already-rotated refresh token gets used again, that’s a strong signal of token theft, and the whole token family should be revoked immediately.
- Step-up re-authentication for sensitive actions — viewing your own order history is a low-stakes read; changing the payout bank account is not. The second one should trigger a fresh authentication check even mid-session, regardless of how recently the user logged in.
- Device posture checks tied to token issuance, not just at login. If your device management tooling flags a laptop as out-of-compliance three hours into a session, that session’s next token refresh should fail, not sail through on the strength of the original login.
This is where tenet 4 — dynamic policy based on observable state — actually earns its keep. A static session timeout is a blunt instrument. A policy that factors in “has this device’s posture check gone stale” or “is this request pattern consistent with the last 50 requests from this session” gives you something closer to continuous verification without forcing a re-login every five minutes.
What Does CISA’s Maturity Model Tell Me About Where to Start?
Reading NIST’s tenets can feel like being handed a finished cathedral’s blueprint when you’re trying to fix a leaky roof. That’s what CISA’s Zero Trust Maturity Model is actually for — it doesn’t add new principles on top of NIST’s; it gives you a staged way to get there. Version 2.0 (April 2023, still current as of this writing) organizes the work into five pillars — Identity, Devices, Networks, Applications and Workloads, and Data — plus three cross-cutting capabilities that run through all five: Visibility and Analytics, Automation and Orchestration, and Governance. Each pillar is scored across four stages: Traditional, Initial, Advanced, and Optimal.
For a web development team, not a federal agency network, here’s the honest, practical read on where those pillars map to work you’d actually schedule:
| Pillar | Traditional (where most apps start) | Advanced (a realistic 6–12 month target) |
|---|---|---|
| Identity | Static passwords, long-lived sessions, roles rarely reviewed | MFA enforced, short-lived tokens, automated access reviews |
| Applications & Workloads | Flat internal network, static API keys between services | Gateway-enforced per-request authz, mTLS + workload identity |
| Data | Access controlled at the database/table level only | Field-level access policy tied to request context, not just role |
| Networks | One internal network, perimeter firewall as primary control | Micro-segmented zones, encrypted internal traffic by default |
| Devices | No posture checks on API callers | Device compliance signal fed into the policy engine |
Image Prompt: A premium hand-drawn isometric vector doodle infographic on a warm cream paper background. Five small isometric pillar towers standing side by side, each labeled with a simple hand-drawn icon (a key for Identity, a laptop for Devices, a network node for Networks, a gear-in-a-box for Applications and Workloads, a filing folder for Data), each tower built from four visibly stacked blocks rising in height and drawn with slightly brighter pastel color at the top block to suggest progress, faint dashed arrows climbing alongside each tower, small hand-drawn checkmarks on the top block of each tower. Clean black outlines, soft pastel blue, coral, and mustard highlight accents, hand-drawn shading. No text, no watermark. Square 1:1.
The decision framework that actually matters here: don’t try to hit Optimal on all five pillars simultaneously. Pick the pillar where a breach would hurt the most — for most web apps, that’s Identity (because credential compromise is still the most common initial access vector) or Applications and Workloads (because that’s where lateral movement between your own services happens) — and push that one pillar from Traditional to Advanced before spreading effort thin across all five. CISA’s own guidance frames this as an iterative, multi-year journey for large agencies; for a startup-sized web team, a realistic version of that is “one pillar meaningfully improved per quarter,” not a single big-bang rewrite.
If you’re implementing this for the first time: pin your JWT algorithms and shorten your token lifetimes first (cheap, high-impact, Identity pillar). Then move to gateway-enforced per-resource authorization (Applications and Workloads pillar). mTLS and workload identity for internal services is real work — schedule it as its own project, not a side task bolted onto a sprint.
Summary
- Zero trust, as defined in NIST SP 800-207, removes network location as a trust signal entirely — every request is authenticated and authorized on its own, per session, based on dynamic policy rather than standing credentials.
- The three logical components — Policy Engine, Policy Administrator, Policy Enforcement Point — map onto real infrastructure as your authz service, token-issuing identity provider, and API gateway or mesh sidecar respectively.
- PASETO removes JWT’s algorithm-confusion attack surface by hardcoding crypto to the token version, but pinned, well-configured JWTs remain a defensible choice, especially with an existing IdP.
- Service-to-service calls need mTLS backed by workload identity (SPIFFE/SPIRE), not static API keys — the crypto alone doesn’t help if any workload can get a certificate signed.
- CISA’s Zero Trust Maturity Model (five pillars, four stages) is a staged roadmap, not a checklist to complete overnight — pick the pillar with the highest breach impact and push it from Traditional toward Advanced first.
Frequently Asked Questions
Is zero trust just a marketing term, or is there an actual technical standard behind it?
There’s a real standard: NIST Special Publication 800-207, published August 2020, defines zero trust architecture with seven specific tenets and a logical component model (Policy Engine, Policy Administrator, Policy Enforcement Point). CISA’s Zero Trust Maturity Model builds a staged implementation roadmap on top of it. Vendor marketing around “zero trust” products varies wildly in how closely it tracks the actual NIST tenets, so it’s worth checking a specific product claim against the source document.
Do I need to replace my firewall and VPN to adopt zero trust?
Not necessarily, and not as a first step. Zero trust shifts the primary trust decision away from network location, but firewalls and network segmentation still have a role as one layer among several — they’re just no longer the thing your access decisions rest on. Most teams layer zero trust identity and policy checks on top of their existing network infrastructure rather than ripping it out.
Should I use JWT or PASETO for a new project?
If you’re integrating with an existing identity provider (Auth0, Okta, Cognito, Keycloak), stick with JWT — pin the algorithm, keep tokens short-lived, and verify issuer/audience strictly. Consider PASETO for internal, service-issued tokens where you control both the issuer and the verifier and want to remove the algorithm-negotiation attack surface entirely. This is industry practice built on top of JWT’s known weaknesses, not a NIST or CISA recommendation — neither framework specifies a token format.
How does zero trust handle a user’s device, not just their identity?
This falls under CISA’s Devices pillar. In practice, it means feeding device posture — is the OS patched, is disk encryption on, has the device checked in recently with your MDM or EDR tool — into the same policy decision that evaluates the user’s identity and the resource being requested. A valid login from a non-compliant device should get a different (usually more restricted) outcome than the same login from a compliant one.
Is zero trust only relevant for large enterprises with dedicated security teams?
No — the principles apply at any scale, and arguably matter more for smaller teams who can’t rely on a large security operations center to catch what perimeter defenses miss. The scale of implementation differs: a five-person startup isn’t standing up a full SPIFFE/SPIRE deployment on day one, but pinning JWT algorithms, shortening token lifetimes, and enforcing least-privilege gateway policy are achievable regardless of team size.
What to Read Next
- How to Secure Your WordPress Site in 2026 — a lower-level look at hardening a specific, widely-deployed web application against the same class of unauthorized-access threats.
- How to Prevent Image Hotlinking in 2026 — a narrower resource-access-control problem that follows the same “verify every request” logic covered here.
- GitHub Actions Secrets: Security Best Practices — credential handling in your CI/CD pipeline, which is exactly the kind of “internal, therefore trusted” surface zero trust asks you to stop assuming is safe.



