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

blog.frankel.ch · 2026-06-28 · by Long Nguyen ·generic
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.

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. 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.

    — JGDMS on GitHub

    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 | NativeInvocationPermissionNativeMemoryPermission | | Maintain and extend permission guard hooks | guards | | SPIFFE/SPIRE zero-touch certificate management | SpiffeX509TrustManagerSpiffeX509KeyManager |

    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.

    To go further:

  • The Principle of Least Privilege and How JEP 411 Will Have a Negative Impact on Java Security
  • JEP 411: What it Means for Java’s Security Model and Why You Should Apply the Principle of Least Privilege
  • JEP 486: Permanently Disable the Security Manager
  • JGDMS source code
  • DirtyChai source code

Follow me Follow me

blog.frankel.ch · 5 images · 30 links

Comments (0)

No comments yet