SecureX — Zero-Trust Platform Architecture
The capstone architecture: an auth server, security gateway, OPA policy engine, secrets service, audit service and self-service portal, and how they fit together.
On this page
This is the architecture the Spring Security track builds toward: a platform where every request is authenticated, authorised against policy, and recorded — assembled from the components covered individually across the preceding topics.
Key Takeaways
- Six components: auth server, gateway, policy engine, secrets, audit, portal.
- The policy engine is on every request path — it must be fast and fail closed.
- The audit service must not be on the critical path; buffer instead of blocking.
- Workload identity plus mTLS underpins everything else.
- Build the pieces specific to your domain; buy identity.
The architecture
Auth server
Spring Authorization Server issuing OIDC tokens, federating to enterprise identity providers, and enforcing MFA:
@Bean
AuthorizationServerSettings settings() {
return AuthorizationServerSettings.builder()
.issuer("https://auth.acme.com")
.build();
}
@Bean
OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(UserService users) {
return context -> {
if (!OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) return;
var user = users.findByUsername(context.getPrincipal().getName());
context.getClaims().claims(claims -> {
claims.put("tenant_id", user.tenantId());
claims.put("roles", user.roleNames());
// The policy engine uses this: an action requiring recent strong
// authentication checks how long ago MFA happened.
claims.put("mfa_at", user.lastMfaAt().getEpochSecond());
});
};
}The mfa_at claim is what enables step-up authorization downstream — a policy can require MFA within
the last five minutes for a sensitive action without the resource server needing to know how MFA
works.
Security gateway
@Component
public class PolicyEnforcementFilter implements GlobalFilter, Ordered {
private final PolicyClient opa; // localhost sidecar
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return opa.evaluate(buildInput(exchange))
.flatMap(decision -> decision.allow()
? chain.filter(exchange)
: forbidden(exchange, decision.reason()))
// Fail CLOSED. An authorisation layer that cannot decide must
// deny — unlike a rate limiter, where failing open is correct.
.onErrorResume(ex -> {
log.error("policy evaluation failed", ex);
return forbidden(exchange, "policy unavailable");
});
}
@Override public int getOrder() { return -50; } // after auth, before routing
}The gateway rejects bad traffic early, but every service still validates independently — the gateway is an optimisation, not a trust boundary.
Policy engine
package acme.access
import rego.v1
default allow := false
allow if {
input.subject.authenticated
input.subject.tenant_id == input.resource.tenant_id # tenant isolation
permitted_action
not high_risk
}
# Step-up for sensitive operations regardless of an existing session.
permitted_action if {
input.action in {"read", "list"}
some role in input.subject.roles
role in {"USER", "MANAGER", "ADMIN"}
}
permitted_action if {
input.action in {"refund", "delete"}
"ADMIN" in input.subject.roles
time.now_ns() / 1000000000 - input.subject.mfa_at < 300
}
high_risk if input.context.anomaly_score > 0.8
high_risk if input.context.device_compliant == falsePolicies live in Git, are tested with opa test in CI, and are distributed as bundles to every
sidecar. That gives reviewable, versioned authorisation with an audit trail of who changed what.
Secrets service
Vault with Kubernetes authentication and dynamic database credentials, so no service holds a long-lived password:
spring:
cloud:
vault:
authentication: KUBERNETES
kubernetes: { role: order-service }
database:
enabled: true
role: order-service-db # credentials created per workload, 1h leaseAudit service
The component that must not block:
@Component
public class AuditIngest {
// Events arrive asynchronously over Kafka. A slow SIEM or a full index
// creates a backlog, not a request failure.
@KafkaListener(topics = "audit-events", groupId = "audit-service")
public void ingest(AuditEvent event) {
var chained = event.withPreviousHash(repository.latestHash());
repository.append(chained); // append-only, hash-chained
siemForwarder.forward(chained);
}
}The design decision here is the split. Events that must never be lost — a permission change, a financial transaction — are written in the same transaction as the action by the originating service. Everything else flows through Kafka to the audit service, where a backlog degrades latency of the audit trail rather than availability of the application.
Self-service portal
The component teams underestimate. Users need to manage their own security without a support ticket: enrol MFA, view and revoke active sessions, see login history, manage API keys, review connected applications, and download their data.
Each of those is a support ticket avoided and, more importantly, a way for users to notice something wrong. A session list showing a login from an unexpected country is how account compromise gets detected.
What runs where
| Component | Critical path | Failure mode |
|---|---|---|
| Auth server | Login only | No new logins; existing tokens work |
| Gateway | Every request | Total outage — needs replicas |
| Policy engine | Every request | Fail closed — sidecar, not central |
| Secrets | Startup and lease renewal | Services fail to start or renew |
| Audit | No | Backlog, buffered |
| Portal | No | Self-service unavailable |
The sidecar decision for the policy engine follows from that table: something on every request path must not be a network hop to a shared service, because that adds latency and a shared failure domain to everything.
Building versus buying
Be clear-eyed about scope. This architecture describes perhaps eighteen months of work for a small team, and most of it duplicates products that exist.
Buy or adopt: the auth server (Keycloak, Auth0, Okta), the secrets service (Vault, cloud KMS), the SIEM. These are commodity, and building them diverts your best engineers from your domain.
Build: the policy content specific to your business rules, the audit event taxonomy for your domain, and the self-service portal, which is product surface only you can design.
The value of understanding the whole architecture is knowing what you are assembling and where the seams are — not building all of it.
What to take away
Six components, of which only the gateway and policy engine are on every request path. Workload identity and mTLS underneath, policy in Git and tested in CI, audit off the critical path and hash-chained. Buy identity and secrets management; build the policy content, audit taxonomy and portal that are specific to your domain.
Frequently Asked Questions
Should a team actually build this?
What is the hardest component to get right?
How do the components fail independently?
Related tutorials
- 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.
- Zero-Trust ArchitectureBuilding zero-trust in practice: the NIST model, workload identity with SPIFFE, mTLS everywhere, per-request authorization, micro-segmentation, and just-in-time access.
- 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.
- 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.