SecureX — Zero-Trust Security Platform
An OAuth 2.1 authorization server with MFA and WebAuthn, a gateway that enforces OPA policy on every request, dynamic credentials from Vault, and a hash-chained audit trail — the project that turns the Spring Security roadmap into a running platform.
Stack at a glance
- Identity
- Spring Authorization ServerSpring Security 6WebAuthn4JOpenSAML
- Policy
- Open Policy AgentRegoSpring Cloud Gateway
- Data
- PostgreSQLRedisFlywayHibernate Envers
- Ops
- HashiCorp VaultKubernetesSPIFFE/SPIREPrometheusGrafana
On this page
Most security tutorials show one mechanism in isolation — here is JWT, here is OAuth, here is CSRF. SecureX is the opposite: a platform where those mechanisms have to work together, and where the interesting problems are the seams between them.
It is deliberately the project you should mostly not build in production. Buy your identity provider. The value here is understanding what you are buying, where the trust boundaries are, and which parts genuinely are yours to build — the policy content, the audit taxonomy, and the self-service surface.
Key Takeaways
- Six components, of which only the gateway and policy engine are on every request path.
- Policy is Rego in Git, tested in CI, distributed as bundles to sidecars.
- The policy layer fails closed — unlike a rate limiter, which should fail open.
- Audit is off the critical path, buffered, and hash-chained.
- Workload identity plus mTLS underneath everything else.
Architecture
Module breakdown
| Module | Builds | Hours |
|---|---|---|
| Auth server core | OAuth 2.1, OIDC, PKCE, refresh rotation | 8 |
| MFA | TOTP enrolment, recovery codes, trusted devices | 6 |
| WebAuthn | Passkey registration and assertion | 5 |
| Identity brokering | SAML and OIDC federation per tenant | 6 |
| Security gateway | Routing, token validation, rate limiting | 4 |
| Policy engine | OPA integration, Rego policies, CI tests | 6 |
| Secrets service | Vault Kubernetes auth, dynamic DB credentials | 4 |
| Audit service | Event ingest, hash chain, SIEM export | 4 |
| Self-service portal | MFA, sessions, API keys, data export | 2 |
Authorization server
@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());
// Enables step-up authorization downstream: a policy can require
// MFA within the last five minutes without knowing how MFA works.
claims.put("mfa_at", user.lastMfaAt().getEpochSecond());
});
};
}Every client requires PKCE and refresh-token rotation, per OAuth 2.1. Authorizations persist in PostgreSQL rather than memory, so a deploy does not sign everyone out — the single most important difference between the getting-started configuration and something deployable.
Policy as code
package securex.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
}
permitted_action if {
input.action in {"read", "list"}
some role in input.subject.roles
role in {"USER", "MANAGER", "ADMIN"}
}
# Sensitive actions require recent strong authentication regardless of session.
permitted_action if {
input.action in {"refund", "delete", "export"}
"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 == falsetest_admin_can_refund_with_recent_mfa if {
allow with input as {
"action": "refund",
"subject": {"authenticated": true, "roles": ["ADMIN"], "tenant_id": "t1",
"mfa_at": time.now_ns() / 1000000000 - 60},
"resource": {"tenant_id": "t1"},
"context": {"anomaly_score": 0.1, "device_compliant": true}
}
}
test_admin_cannot_refund_with_stale_mfa if {
not allow with input as {
"action": "refund",
"subject": {"authenticated": true, "roles": ["ADMIN"], "tenant_id": "t1",
"mfa_at": time.now_ns() / 1000000000 - 3600},
"resource": {"tenant_id": "t1"},
"context": {"anomaly_score": 0.1, "device_compliant": true}
}
}Policies live in Git, opa test runs in CI, and a bundle is published to every sidecar. Authorization
becomes reviewable in a pull request with a full history of who changed what — which is exactly what
an auditor asks for and what scattered if statements cannot provide.
Enforcement
@Component
public class PolicyEnforcementFilter implements GlobalFilter, Ordered {
private final PolicyClient opa; // localhost sidecar, sub-millisecond
@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 authorization layer that cannot decide must
// deny — the opposite of a rate limiter, where failing open
// is usually right.
.onErrorResume(ex -> {
log.error("policy evaluation failed", ex);
return forbidden(exchange, "policy unavailable");
});
}
@Override public int getOrder() { return -50; }
}The sidecar decision follows from being on every request. A network hop to a central policy service would add latency to everything and create a shared failure domain; a sidecar evaluates locally against a cached bundle.
Audit
Events that must never be lost — a permission change, an MFA disable — are written in the same transaction as the action. Everything else flows through Kafka to the audit service, so a slow SIEM creates a backlog rather than a request failure.
An hourly job walks the chain and alerts on any break.
Self-service portal
The component teams consistently underestimate, and the one that most reduces support load. Users enrol MFA, view and revoke active sessions with device and location, review login history, manage API keys, and export their data.
The session list is quietly the highest-value security feature in the platform: it is how a user notices a login from a country they have never visited, which is how account compromise actually gets detected.
What is on the critical path
| Component | Every request? | Failure impact |
|---|---|---|
| Auth server | Login only | No new logins; existing tokens work |
| Gateway | Yes | Total outage — needs replicas |
| Policy engine | Yes | Fails closed — sidecar, not central |
| Vault | Startup and lease renewal | Services fail to start or renew |
| Audit | No | Backlog, buffered |
| Portal | No | Self-service unavailable |
Designing this table before writing code is the exercise. It determines where replicas go, what fails open versus closed, and which components can tolerate a backlog — and those decisions are much more expensive to change later.
Working through it
Forty-five hours, and the order matters: auth server first because everything depends on tokens, then the gateway and policy engine because they are the architecture, then MFA, then audit, then the portal.
Build the policy tests alongside the policies from the start. Retrofitting tests onto Rego you wrote weeks ago is considerably less pleasant than writing them together, and policy is exactly the code where an untested change is dangerous.
What you end up with
A platform where every request is authenticated, authorised against reviewable policy, and recorded in a tamper-evident trail — and, more usefully, a clear understanding of which of those parts you should buy rather than build. That judgement is what the project is really for.