Zero trust is an architecture in which no request is trusted because of where it comes from. A laptop on the office network, a pod inside the cluster and a VPN user all have to prove who they are, from what device or workload, and why they need this particular resource. A policy decision is then made for that request, and repeated as conditions change. The idea replaces the perimeter model, in which anything inside the firewall was implicitly trusted. That model failed because phishing, stolen credentials and compromised build pipelines put attackers inside the perimeter, where flat networks let them move sideways to whatever they wanted.
This article builds zero trust from first principles using the vocabulary of NIST SP 800-207, the most widely cited reference architecture. It covers the policy engine, administrator and enforcement point; identity for people, devices and workloads; a worked policy in Rego with the enforcement code that calls it; one request traced end to end; the failure modes that make zero trust programmes stall; and a practical rollout sequence. Zero trust is not a product you buy. It is a set of decisions about where trust is evaluated, and most of the work is plumbing signals into those decisions.
From perimeter trust to per-request decisions
NIST SP 800-207, published in 2020, defines zero trust around a few tenets. All data sources and computing services are resources. All communication is secured regardless of network location, so the internal network gets the same encryption and authentication as the internet. Access is granted per session, with the least privilege needed for the task. Access decisions use dynamic policy that considers the identity of the client, the application, the requesting asset and environmental attributes. The organisation monitors the security posture of every asset it owns. Authentication and authorization are strictly enforced before access, and re-evaluated during it. And the organisation collects as much information as it can about assets, traffic and requests to improve its policy.
In engineering terms, three changes follow. Network location stops being a credential. Being on the VPN or in a subnet grants nothing by itself. The unit of authorization shrinks from "the network" to "this subject, doing this action, on this resource, now". Decisions become continuous: a session that was fine at 09:00 can be cut at 09:15 because the device stopped reporting disk encryption or the account was flagged for impossible travel.
From perimeter trust to per-request decisions
NIST SP 800-207, published in 2020, defines zero trust around a few tenets. All data sources and computing services are resources. All communication is secured regardless of network location, so the internal network gets the same encryption and authentication as the internet. Access is granted per session, with the least privilege needed for the task. Access decisions use dynamic policy that considers the identity of the client, the application, the requesting asset and environmental attributes. The organisation monitors the security posture of every asset it owns. Authentication and authorization are strictly enforced before access, and re-evaluated during it. And the organisation collects as much information as it can about assets, traffic and requests to improve its policy.
In engineering terms, three changes follow. Network location stops being a credential. Being on the VPN or in a subnet grants nothing by itself. The unit of authorization shrinks from "the network" to "this subject, doing this action, on this resource, now". Decisions become continuous: a session that was fine at 09:00 can be cut at 09:15 because the device stopped reporting disk encryption or the account was flagged for impossible travel.
The logical architecture: PE, PA and PEP
SP 800-207 splits the architecture into a control plane and a data plane. The Policy Engine (PE) makes the decision: given a request and the current signals, allow, deny or require more proof. The Policy Administrator (PA) carries out the decision, for example by issuing a short-lived session token or telling a gateway to open a path, and closes the path when the decision is revoked. Together they form the policy decision point. The Policy Enforcement Point (PEP) sits in the data path in front of the resource: an identity-aware proxy for web apps, an API gateway, a service mesh sidecar, or a database proxy. Nothing reaches the resource except through a PEP.
The PE is only as good as its inputs. NIST lists them as policy information sources: the identity provider (who the user is, group membership, MFA strength), device management and endpoint detection (OS patch level, disk encryption, EDR health), threat intelligence, activity logs, data classification, and the public key infrastructure that issues device and workload certificates. Much of a real programme is integration work: getting those signals to the PE fresh enough to matter, and in a form a policy can test.
Keeping the control plane separate matters for resilience and security. PEPs can cache decisions for a short time, so a brief PE outage does not take down every application. Compromising a single application does not give an attacker the ability to change policy.
Identity for people and devices
For people, the foundation is a single identity provider with strong authentication. Phishing-resistant methods, such as FIDO2 security keys or platform passkeys built on WebAuthn, bind the login to the real site's origin, so a look-alike domain cannot replay it. One-time codes and push approvals can be phished or fatigue-bombed. US federal guidance, OMB memorandum M-22-09, requires phishing-resistant MFA for agency staff for exactly this reason. Sessions should be short and refreshable, so a revoked user or a non-compliant device loses access within minutes rather than at the end of a day-long token.
Devices need identities too. A managed laptop gets a certificate whose private key lives in its TPM or secure enclave, and the device management system reports posture against that identity. The PE can then distinguish "Alice on her managed, encrypted, patched laptop" from "Alice's password typed on an unknown machine", and grant different access to each. Tokens issued after this check should be validated carefully at every hop; JWT validation covers the checks that are most often skipped, and the OAuth 2.0 PKCE flow shows how browser and mobile clients obtain tokens safely.
Workload identity and service-to-service trust
Service-to-service traffic is where perimeter thinking survives longest, in the form of "anything in the cluster can call anything". Zero trust gives every workload a cryptographic identity and authorizes calls between identities rather than IP addresses. SPIFFE is the open standard for this. A workload's identity is a URI such as spiffe://prod.example.com/ns/payments/sa/ledger, delivered as a short-lived X.509 certificate or JWT, called an SVID. SPIRE, the reference implementation, issues SVIDs only after attesting the workload: it checks the node with cloud instance identity or a TPM, and checks the process with signals such as its Kubernetes service account. A stolen certificate expires within an hour or less.
With identities in place, every connection uses mutual TLS. Both sides present certificates, so the server knows which workload is calling and the client knows it reached the real server; see mTLS for service-to-service traffic. A service mesh can do this transparently in sidecars and enforce rules such as "only the ledger service may call the payments database proxy". Network microsegmentation still has value as defence in depth: firewall rules that allow only expected flows limit damage when an identity check is misconfigured. But identity, not the subnet, is the primary control.
Writing the policy: Rego and an enforcement point
Policies work best as code: versioned, reviewed and tested like any other code. Open Policy Agent (OPA) is a common policy engine, and Rego is its language. The policy below uses an input document that we define ourselves, which the PEP builds for each request. It combines role-based rules with attribute checks on device, MFA freshness, data classification and risk; RBAC and ABAC explains the two models it mixes.
package zt.authz
# Input built by the PEP:
# {"subject": {"id": "u123", "roles": ["finance-analyst"], "mfa_age_s": 600},
# "device": {"managed": true, "disk_encrypted": true, "os_patch_age_d": 9},
# "action": "read", "resource": {"app": "ledger", "classification": "confidential"},
# "risk": {"score": 20}}
default decision := {"allow": false, "reason": "no rule matched"}
healthy_device if {
input.device.managed
input.device.disk_encrypted
input.device.os_patch_age_d <= 30
}
role_allows if {
some role in input.subject.roles
some perm in data.roles[role]
perm.app == input.resource.app
perm.action == input.action
}
decision := {"allow": true, "reason": "ok"} if {
role_allows
healthy_device
input.risk.score < 50
input.subject.mfa_age_s <= 3600
}
decision := {"allow": false, "step_up": true, "reason": "mfa too old"} if {
role_allows
healthy_device
input.risk.score < 50
input.subject.mfa_age_s > 3600
input.resource.classification == "confidential"
}
decision := {"allow": false, "reason": "mfa too old for this resource"} if {
role_allows
healthy_device
input.risk.score < 50
input.subject.mfa_age_s > 3600
input.resource.classification != "confidential"
}The PEP asks OPA for the decision over its REST API and enforces the result. The two parts that matter most are the cache, which keeps latency low, and the failure behaviour, which decides what happens when the engine is unreachable.
import time, requests
CACHE, TTL_S = {}, 30 # short TTL bounds how long a revoked grant survives
def decide(inp: dict) -> dict:
key = (inp["subject"]["id"], inp["device"].get("id"), inp["action"], inp["resource"]["app"])
hit = CACHE.get(key)
if hit and time.monotonic() - hit[0] < TTL_S:
return hit[1]
try:
r = requests.post("http://127.0.0.1:8181/v1/data/zt/authz/decision",
json={"input": inp}, timeout=0.05)
r.raise_for_status()
result = r.json().get("result", {"allow": False, "reason": "undefined"})
except requests.RequestException:
result = {"allow": False, "reason": "policy engine unavailable"} # fail closed
CACHE[key] = (time.monotonic(), result)
audit_log(inp, result) # every decision, allow and deny, goes to the SIEM
return result
Worked example: one request, end to end
A finance analyst opens the ledger app from a hotel at 08:40. The identity-aware proxy (the PEP) has no session, so it redirects to the identity provider, where they sign in with a passkey. The proxy now holds an ID token with mfa_age_s of 0, and a device certificate from the laptop's TPM. It asks the device management API for posture: managed, encrypted, patched nine days ago. The risk service scores the session at 35 because the IP address is new, but the passkey proves who they are. The proxy builds the input document, OPA returns allow, and the proxy forwards the request with a signed header carrying the user's identity. The ledger app trusts that header only because it arrives over mTLS from the proxy's SPIFFE identity.
At 10:05 the analyst exports a confidential report. The MFA is now 85 minutes old, so the second rule fires and the proxy asks them to touch their key again; the export goes through. At 10:20 EDR reports that real-time protection was disabled on the laptop. Posture flips, and within one cache TTL, 30 seconds here, the next request is denied with a message telling the analyst how to fix it. Behind the app, the ledger service calls the database proxy over mTLS. The mesh allows spiffe://prod.example.com/ns/payments/sa/ledger to reach it and nothing else, so a compromised reporting pod completes the mTLS handshake, proving it is the reporting service, and is then refused by the mesh's authorization policy. Every one of these decisions, including the denials, lands in the SIEM with the inputs that produced it.
Failure modes
- Fail-open policy engines. A PEP that allows traffic when the PE is unreachable makes an outage or a denial-of-service attack an authorization bypass. Fail closed, run the PE close to the PEP (OPA as a sidecar or local daemon), and keep short-lived caches for availability.
- Caches that outlive revocation. A 24-hour decision cache means a fired employee keeps access for a day. Keep TTLs short, and push revocation events to PEPs for high-value resources.
- Coarse policy dressed as zero trust. Replacing the VPN with a proxy that admits every authenticated employee to every app moves the perimeter without shrinking it. The test is whether a compromised account can reach systems unrelated to that person's job.
- Unverified signals. Posture reported by the device itself and never attested can be faked. Prefer signals bound to hardware keys and collected server-side.
- No break-glass path. When the identity provider is down, nobody can log in to fix it. Keep audited emergency accounts with hardware keys, stored offline and tested.
- Policy sprawl. Thousands of hand-written exceptions become unreviewable. Keep policy in Git with tests, owners and expiry dates on exceptions.
Rolling it out: maturity and trade-offs
CISA's Zero Trust Maturity Model, version 2.0 from 2023, is a practical yardstick. It scores five pillars, identity, devices, networks, applications and workloads, and data, plus cross-cutting capabilities for visibility and analytics, automation and orchestration, and governance. Each is rated through four stages: traditional, initial, advanced and optimal. Few organisations need optimal everywhere; the aim is to move the riskiest gaps first.
A sequence that works: inventory users, devices, applications and data flows, because you cannot write policy for things you cannot name. Consolidate identity and roll out phishing-resistant MFA. Put an identity-aware proxy in front of the most sensitive internal web apps and retire their network-level access. Issue workload identities and turn on mTLS service by service, starting in permissive mode and switching to enforcing once logs show no unexpected callers. Finally, feed device posture and risk into decisions. The trade-off throughout is friction and latency against blast radius: every check adds milliseconds and an occasional prompt, and every skipped check is a path an attacker can use.
What to do next
- List your ten most sensitive applications and how a user reaches each today; mark any that rely on network location alone.
- Enforce phishing-resistant MFA for administrators first, then for everyone.
- Put one sensitive internal app behind an identity-aware proxy with a device-posture check, and remove its VPN route.
- Issue SPIFFE identities to two services that talk to each other, enable mTLS in permissive mode, review the logs, then enforce.
- Move access rules into a policy engine with tests in CI, fail closed, and cap decision caches at minutes.
- Send every allow and deny to the SIEM, and run a tabletop exercise in which an account is compromised: measure how far it could reach.