Zero-Trust Architecture
Building zero-trust in practice: the NIST model, workload identity with SPIFFE, mTLS everywhere, per-request authorization, micro-segmentation, and just-in-time access.
On this page
Zero-trust replaces the assumption that the internal network is safe with a rule that every request is verified regardless of where it comes from. For a Java estate that means workload identity, mTLS everywhere, and authorization on every call rather than at the perimeter.
Key Takeaways
- Never trust network location. A request from inside the cluster is verified like any other.
- NIST SP 800-207 defines the roles: policy engine, administrator, enforcement point.
- Workload identity (SPIFFE) replaces "this IP is trusted" with "this workload is who it claims".
- Continuous authorization re-evaluates every request, so access can be withdrawn mid-session.
- Adopt incrementally — the highest-value controls first.
The principle
The motivation is the failure mode of the perimeter model: once an attacker is inside — through a compromised dependency, a phished credential, a misconfigured pod — they can reach everything, because internal traffic is trusted. Zero-trust removes that assumption, so a foothold yields only what the compromised identity was permitted.
The NIST model
SP 800-207 names three logical components:
- Policy Engine (PE) — decides whether to grant access, given identity and context.
- Policy Administrator (PA) — establishes or tears down the connection the PE authorised.
- Policy Enforcement Point (PEP) — sits in the request path and enforces the decision.
In a Java-on-Kubernetes estate these map onto familiar tools: OPA as the policy engine, a service mesh or a filter as the enforcement point, and an identity system feeding both.
Workload identity
The foundation is that each workload has a cryptographic identity, not an IP address:
# SPIRE issues each workload an SVID (SPIFFE Verifiable Identity Document)
# based on attestation — what node it runs on and what workload it is —
# rather than trusting whatever a network packet claims.
spiffeID: "spiffe://acme.com/ns/production/sa/order-service"
parentID: "spiffe://acme.com/spire/agent/k8s/node-1"
selectors:
- "k8s:ns:production"
- "k8s:sa:order-service"
- "k8s:container-image:ghcr.io/acme/order-service@sha256:..."// The identity travels in the mTLS certificate. A service reads the caller's
// SPIFFE ID from the verified client certificate, not from a header.
public String callerIdentity(X509Certificate clientCert) {
return clientCert.getSubjectAlternativeNames().stream()
.filter(san -> san.get(0).equals(6)) // URI type
.map(san -> (String) san.get(1))
.filter(uri -> uri.startsWith("spiffe://"))
.findFirst()
.orElseThrow(() -> new UnauthenticatedException("no workload identity"));
}The attestation is what makes this strong. An identity is issued based on verifiable facts about the workload — its namespace, service account, and image digest — so a compromised pod cannot claim to be a different service.
mTLS everywhere
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata: { name: default, namespace: production }
spec:
mtls: { mode: STRICT } # plaintext between workloads is rejectedEvery service-to-service call is now mutually authenticated and encrypted, with certificates rotated automatically. This is the single highest-value zero-trust control and, with a mesh, close to free — which is why it is the place to start.
Continuous authorization
Perimeter authentication decides once, at login. Zero-trust decides on every request, against current context:
@Component
public class ContextualAuthorizationFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return buildContext(exchange)
.flatMap(policyEngine::evaluate) // OPA decision per request
.flatMap(decision -> decision.allowed()
? chain.filter(exchange)
: deny(exchange, decision.reason()));
}
private Mono<AuthorizationContext> buildContext(ServerWebExchange exchange) {
// The decision considers far more than identity: where the request
// came from, what device, at what time, and how anomalous it looks
// against the user's normal behaviour.
return Mono.zip(
identity(exchange),
devicePosture(exchange),
Mono.just(exchange.getRequest().getRemoteAddress()),
anomalyScorer.score(exchange))
.map(t -> new AuthorizationContext(t.getT1(), t.getT2(), t.getT3(), t.getT4()));
}
}package acme.access
default allow := false
allow if {
input.identity.authenticated
input.device.compliant # patched, encrypted, managed
input.anomaly_score < 0.7
valid_for_resource
}
# Step-up: a sensitive action needs recent strong authentication regardless
# of an existing session.
allow if {
input.action == "transfer_funds"
input.identity.mfa_age_seconds < 300
input.device.compliant
}The consequence that distinguishes this from ordinary authorization: because every request is evaluated, access can be withdrawn mid-session. A device that falls out of compliance, an anomaly score that spikes, a credential flagged as compromised — any of these blocks the next request rather than waiting for a token to expire.
Micro-segmentation
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: order-service, namespace: production }
spec:
podSelector:
matchLabels: { app: order-service }
policyTypes: [Ingress, Egress]
ingress:
# Only the gateway and the checkout service may call orders.
- from:
- podSelector: { matchLabels: { app: api-gateway } }
- podSelector: { matchLabels: { app: checkout-service } }
egress:
# Orders may reach only its database and the payment service.
- to:
- podSelector: { matchLabels: { app: postgres } }
- podSelector: { matchLabels: { app: payment-service } }Deny-by-default network policy is the segmentation layer. Combined with mTLS identity, a compromised service can reach only the specific peers its policy permits — so lateral movement, the technique that turns one foothold into a full compromise, is blocked at the network as well as at the application.
Just-in-time access
Standing administrative access is a large attack surface. Grant it on request, scoped and expiring:
@Service
public class JustInTimeAccessService {
public AccessGrant request(String userId, String resource, String justification) {
// Elevated access is never permanent. It is requested, approved,
// granted for a window, and revoked automatically.
var approval = approvalWorkflow.require(userId, resource, justification);
return approval.granted()
? vault.issueTemporaryCredential(resource, Duration.ofHours(2))
: AccessGrant.denied(approval.reason());
}
}An engineer needing production database access requests it with a reason, a peer or manager approves, and the credential is valid for two hours. There is no standing credential to steal, and every access is attributable and time-boxed.
Adopting incrementally
The failure mode of zero-trust programmes is attempting everything at once, stalling under the complexity, and delivering nothing. Adopt in order of value. mTLS and per-request authorization at your most sensitive service give most of the benefit; the rest extends the model as capacity allows.
Maturity models describe the journey as traditional, advanced and optimal. The useful framing is simpler: each control you add shrinks what a single compromise can reach, and you can stop at whatever level matches your threat model.
What to take away
Stop trusting network location. Give workloads cryptographic identities and require mTLS between them. Authorize every request against current context, not once at login, so access can be withdrawn when conditions change. Segment the network deny-by-default, remove standing privilege with just-in-time access, and adopt all of it incrementally, highest-value first.
Frequently Asked Questions
Is zero-trust a product I can buy?
Do I have to adopt it all at once?
What is the difference between authentication and continuous authorization here?
Related tutorials
- Audit Logging & SIEM IntegrationBuilding an audit trail that stands up to scrutiny: what to record, a structured event format, hash-chained tamper evidence, Hibernate Envers, and shipping to a SIEM.
- Cryptographic Key ManagementManaging keys across their lifecycle: generation, storage in HSMs and KMS, envelope encryption, rotation without re-encrypting everything, and the key hierarchy that makes it work.
- GDPR Compliance for Java ApplicationsImplementing the parts of GDPR that reach the code: data subject access and export, consent records, erasure through anonymisation, retention jobs, and breach notification.
- SecureX — Zero-Trust Platform ArchitectureThe capstone architecture: an auth server, security gateway, OPA policy engine, secrets service, audit service and self-service portal, and how they fit together.