Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Expert~45 hoursUpdated

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

The gateway and policy engine sit on every request. The audit service observes without blocking, so a backlog degrades the trail rather than the platform.

Module breakdown

ModuleBuildsHours
Auth server coreOAuth 2.1, OIDC, PKCE, refresh rotation8
MFATOTP enrolment, recovery codes, trusted devices6
WebAuthnPasskey registration and assertion5
Identity brokeringSAML and OIDC federation per tenant6
Security gatewayRouting, token validation, rate limiting4
Policy engineOPA integration, Rego policies, CI tests6
Secrets serviceVault Kubernetes auth, dynamic DB credentials4
Audit serviceEvent ingest, hash chain, SIEM export4
Self-service portalMFA, sessions, API keys, data export2

Authorization server

TokenCustomizer.java
@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

policies/access.rego
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 == false
policies/access_test.rego
test_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

PolicyEnforcementFilter.java
@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

Hash chaining makes tampering detectable rather than preventing it — combined with append-only permissions, silent alteration becomes very hard.

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

ComponentEvery request?Failure impact
Auth serverLogin onlyNo new logins; existing tokens work
GatewayYesTotal outage — needs replicas
Policy engineYesFails closed — sidecar, not central
VaultStartup and lease renewalServices fail to start or renew
AuditNoBacklog, buffered
PortalNoSelf-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.