Security Baked Into the JVM: why fork Apache River and OpenJDK?

The more distributed a system, the harder it is to secure. Code crosses JVM boundaries. Objects are serialized across trust boundaries. Third-party proxies run inside your process. The usual answer is a network firewall. It helps, but it operates at the wrong level. Java 17 deprecated the SecurityManager, Java 24 put the final nail in its coffin. Most developers didn’t notice.

The more distributed a system, the harder it is to secure. Code crosses JVM boundaries. Objects are serialized across trust boundaries. Third-party proxies run inside your process. The usual answer is a network firewall. It helps, but it operates at the wrong level.

Java 17 deprecated the SecurityManager, Java 24 put the final nail in its coffin. Most developers didn’t notice. Applications that load and execute remote code did: the JVM no longer has a built-in mechanism to restrict what that code can do.

The SecurityManager removal forced a rethink of the whole security architecture from scratch. That turned out to be an opportunity. The authorization problem and the service-discovery problem are separate. Each has a dedicated project. They are designed to be used together.

  • DirtyChai: a community fork of OpenJDK that restores Java’s authorization infrastructure; required to run JGDMS.
  • JGDMS: a security-hardened fork of Apache River, providing dynamically-discoverable microservices over IPv6.

DirtyChai scales vertically. JGDMS scales horizontally.

DirtyChai adds virtual thread support and a lock-free policy engine to a single JVM. JGDMS makes a fleet of those JVMs discoverable, self-registering, and self-healing.

In this series, I want to explore how the two projects push security into every layer of the platform. Each post covers one layer: from codebase auditing to identity to authorization to performance.

This post focuses on the two projects: what they are, what they are not, and a first taste of deployment.

JGDMS

JGDMS is described in its own project descriptor as:

Infrastructure for providing secured micro services, that are dynamically discoverable and searchable over IPv6 networks.

Most RPC frameworks stop at calling a remote method. JGDMS goes further: services announce themselves on IPv6 networks, clients find them by capability rather than hard-wired address, and trust is established cryptographically before any code runs. Three concepts drive the design:

Jini-model service discovery

lease-based registrations, multicast and unicast lookup, event-driven service notifications.

JERI

a pluggable, constraint-based RPC layer that supersedes standard Java RMI with pluggable transport, per-method security requirements, and authenticated dispatch.

Defence-in-depth security

hardened deserialization, TLSv1.3 transport, Java authorization, proxy trust verification, and a codebase safety pipeline that analyzes third-party bytecode before it is ever loaded.

What JGDMS Is Not

JGDMS is not a sandbox for running untrusted code. It will not safely isolate malicious bytecode. The goal is the opposite: prevent untrusted code from ever being loaded. LoadClassPermission is the gate; SCAP (covered in Part 2) pre-analyzes JARs before any client deserializes an object from them. If you need to run code you don’t trust, you need a different tool.

Running on bare OpenJDK is not supported. On standard OpenJDK ≤ 23, virtual threads get an AccessControlContext with no permissions when SecurityManager is enabled: they simply don’t work in a security context. On OpenJDK 24+, SecurityManager was removed entirely. DirtyChai is the only supported runtime JDK.

Building is a different story. JGDMS is compile-time compatible with standard OpenJDK: you can build with any standard OpenJDK toolchain. DirtyChai is binary compatible with software compiled on OpenJDK, so no recompilation is needed when switching runtimes.

DirtyChai

DirtyChai is a community fork of OpenJDK that restores, improves, and extends Java’s authorization infrastructure: the SecurityManager, AccessController, and ProtectionDomain APIs that OpenJDK deprecated in Java 17 and removed entirely in Java 24.

DirtyChai mascot: decorative tough chai mug in a hard hat with a SPIFFE badge

Without these APIs you cannot restrict what code from a particular source can do once it is loaded. DirtyChai’s goal is not a sandbox. Its goal isn’t to run untrusted code safely, but to ensure trusted but independent parties operate only within their declared and granted privileges.

Feature Source

Prevent loading of untrusted code

LoadClassPermission

Break deserialization gadget attack chains

SerialObjectPermission

Block native code injection

Maintain and extend permission guard hooks

guards

SPIFFE/SPIRE zero-touch certificate management

A minimal JGDMS deployment

In three steps, we define a remote interface, configure the launcher, and start the service. Pay attention to Step 2 below: authentication, encryption, and hardened deserialization are declared in the deployment configuration and bound to every call by the exporter — never written into the service API. The interface stays a plain Remote interface, so the same service redeploys under different security postures (relaxed in dev, mutual TLS in production) without touching a line of code. A call that can’t satisfy those constraints fails before a single byte leaves the client JVM. Standard Java RMI has nothing like this; security there is opt-in, scattered through application code.

The quickest path is a ServiceStarter configuration.

Step 1: Define the service interface

public interface HelloService extends Remote {
    String greet(String name) throws RemoteException;
}

That is the entire API: a contract, nothing else. No security code, no constraints — those are a deployment concern, declared next.

Step 2: Write the ServiceStarter configuration

hello-service.config
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
import net.jini.constraint.*;
import net.jini.core.constraint.*;

com.sun.jini.start {
    serviceDescriptors = new ServiceDescriptor[] {
        new NonActivatableServiceDescriptor(
            "file:hello-service-impl.jar",          (1)
            "file:hello-service-dl.jar",            (2)
            "net.example.HelloServiceImpl",        (3)
            new String[]{ "hello-service.config" }  (4)
        )
    };
}

net.example.HelloServiceImpl {

    private methodConstraints = new BasicMethodConstraints(   (5)
        new InvocationConstraints(
            new InvocationConstraint[] {
                ServerAuthentication.YES,    // server presents a valid certificate
                ClientAuthentication.YES,    // client must authenticate
                Confidentiality.YES,         // TLSv1.3 encryption
                Integrity.YES,               // MAC-covered
                AtomicInputValidation.YES    // hardened deserialization of every argument
            },
            null));

    serverExporter = new BasicJeriExporter(
        SslServerEndpoint.getInstance(0),             (6)
        new BasicILFactory(methodConstraints, null)   (7)
    );
}
1 Implementation JAR
2 Downloadable proxy JAR
3 Implementation class
4 Configuration passed to the service
5 Security requirements — declared in deployment config, not in the service API
6 TLS on a random port
7 Bind the constraints to every exported call

Step 3: Launch

java -Djava.security.policy=start-service.policy \
     -jar lib/start.jar hello-service.config

The service self-registers with the Jini Lookup Service at lookup.example.org:4160. Any client that finds the lookup service can discover HelloService by interface type, without hard-wired addresses or service registry configuration.

The constraints declared in the configuration travel with the proxy and are enforced at the client, before any I/O:

Constraints declared in config, enforced at the call boundary

Conclusion

The quick start shows security enforced at the call level: authentication, encryption, and hardened deserialization, all declared in configuration and enforced at the call boundary. That is one layer. There is another: controlling what code gets loaded in the first place. In the next post, I’ll describe SCAP, the pipeline that audits third-party JARs before any client ever deserializes an object from them.