DEV Community

The JDK's forgotten JMX protocol

The JDK's forgotten JMX protocol

Every Java engineer who has connected JConsole - or JDK Mission Control - to a server in another network segment knows the ritual. Open the JMX port. Discover that RMI quietly opened a second port - random by default. Pin it with a system property nobody remembers without searching. File a firewall ticket for both. Wait.

What fewer people know: the JMX specification shipped with a second remote transport that has none of these problems. One socket, one port, TLS underneath if you want it. It's called JMXMP - the JMX Messaging Protocol.

It lost for the least mysterious reason in software - RMI shipped by default, JMXMP was a separate download, and defaults win - and its reference implementation has been effectively abandoned since around 2008. Yet, it never quite died. Code that refuses to die usually knows something.

I didn't set out to resurrect it. I fell into it.

The port dance, briefly

The default remote JMX stack rides on RMI. The connection URL tells you most of the story:

service:jmx:rmi:///jndi/rmi://host:1099/jmxrmi

I'll spare you the full anatomy behind that URL - there's a JNDI lookup in it, and that second, dynamically assigned port from the ritual above; few people ever learn the details, which is rather the point. Dynamic ports were a reasonable design for 1999's flat networks. Between today's firewalls, NAT, and containers, they're friction - not because RMI is bad, but because the network it was designed for no longer exists.

The JMXMP URL:

service:jmx:jmxmp://host:9875

One socket. TCP in, TCP out. That's the whole networking story.

How I ended up in this codebase

I maintain JConsoleBooster, a modernized JConsole. It shipped fine for years on the 2008-era JMXMP jar - the one historically distributed as jmxremote_optional / jmx-optional, out of Sun's OpenDMK project, republished over the years by several parties because people kept needing single-socket JMX.

Then I moved the app to a jlink-built runtime. An automatic module from 2008 does not cooperate with the module system: no module-info, split-package hazards, services declared in ways jlink can't see.

The choices were: abandon the transport, or take responsibility for the code. So I forked it and took the code apart - 87 files of 2007 Sun code. I won't pretend I read every line myself; it's 2026, and AI did much of the first-pass reading. The rule that mattered: nothing got deleted, changed, or believed without a proof - a test, a zero-references check, an API diff.

What's inside a library frozen in 2008

Some of what I found is what you'd expect from code written for a platform that no longer exists. Some of it surprised me:

  • Dead RMI plumbing - in the socket-protocol library. Proxy references, unmarshalling helpers, an RMI exporter. The generic connector framework was written to serve both transports, and the RMI half stayed behind like scaffolding nobody removed. I deleted it after proving zero references.

  • Plaintext defaults. Encryption and authentication existed - as options, off unless asked. Normal for 2007; indefensible for something you point at a JVM in production today.

  • An authorization check that fails open on current JDKs. It was written for the SecurityManager era: it asks the runtime for the caller's identity through an API that, on a modern JDK, always answers "nobody" - and it treats "nobody" as "security isn't enabled here, let it through." On the JVMs of its time, correct. On today's, an allow-all with extra steps.

  • And the part that made the effort worth it: the protocol itself is fine. Message-based, single connection, SASL negotiation for auth, TLS for transport - the design decisions Sun made hold up. The packaging aged. The idea didn't.

That last point matters. This isn't a rewrite-because-old-code-is-bad story. The original authors solved the right problem well; the code just outlived the platform assumptions around it.

The second life

druvu-lib-jmxmp 2.0.0, on Maven Central, Java 21 baseline - the same protocol, newborn. Three changes carry the release; everything else is detail.

1. It's a real Java module - and still a drop-in

The monolith is now three JPMS modules (com.druvu.jmxmp.common / server / client), and jlink treats them as first-class citizens. The public API - javax.management.remote.jmxmp and friends - is kept frozen, verified by a snapshot test that diffs the exported surface against the original. If your code compiled against jmxremote_optional, it compiles against this.

2. The server refuses to run open

TLS and SASL authentication are mandatory - the server env can't be built without them. This costs adoption convenience, and I decided that's the right trade: nobody exposes a management port to the world on purpose, but unauthenticated JMX endpoints keep showing up in the wild by accident, and they're a documented entry point for cryptominers. This library's answer is that the accident can't happen.

Map<String, ?> env = JmxmpServerSecurity.builder()
    .tls(sslContext)
    .authenticator(myAuthenticator) // SASL/PLAIN over TLS
    .build(); // refuses to build without both

Mandatory authentication buys something besides keeping strangers out. Plenty of teams use JMX operations as the low-ceremony alternative to building an admin UI: change a setting at runtime, flip a feature toggle, trigger a business operation. It works - until something changes in production and nobody can say who changed it, or when.

With every connection authenticated, the operation knows its caller:

@Override
public void switchFeature(String name, boolean on) {
    String operator = Subject.current()
        .getPrincipals().iterator().next().getName();
    log.info("{} switched feature '{}' {}", operator, name, on ? "on" : "off");
    features.put(name, on);
}
// log: "john switched feature 'beta-checkout' on"

The connector runs each request inside the authenticated subject's context, so Subject.current() in your MBean is the operator who issued the call - an audit trail with no extra plumbing.

3. It fails closed

The SecurityManager-era identity lookup is gone; the server reads the authenticated subject from Subject.current(), and a request with no subject is refused, not waved through.

Deserialization - the classic JMX attack surface - goes through a deny-by-default filter (JmxmpSerialFilter): allow-listed classes deserialize, everything else doesn't, before any application code sees the bytes.

Authorization beyond authentication is there if you need it: a small RBAC SPI with a programmatic policy builder, default-deny.

The precise diffs - what was removed, what each fix looks like - are in the repo history and release notes. I'd rather link them than paraphrase them.

Did it survive contact with reality?

The test matrix runs the connector in every module arrangement - both sides modular, both on the classpath, mixed. That matrix earned its keep: it caught the freshly-modularized library silently behaving as a classpath citizen in one spot, where a Class.forName reached across a module seam it had no right to cross. "It compiles with a module-info" and "it's modular" are different claims; only a test can tell them apart.

And it runs in production of a sort: JConsoleBooster ships on it - every remote connection the app makes goes through this library.

Where it stands on security claims

Deliberately narrow. I'm not going to call the library "secure" - that's an adjective, not a fact. The facts: the fail-open path is fixed, transport encryption and authentication are non-optional, deserialization is allow-listed, and the delta for each is inspectable commit by commit.

It's maintained by one person, best-effort, latest-version-only - the support model is spelled out in SECURITY.md. GitHub private vulnerability reporting is switched on. If you can break it, I want to know.

When you should not use this

  • An SSH tunnel to the default RMI connector already works for you. Then it works. This is for when the tunnel is the friction.

  • You're below Java 21. The library assumes a modern JDK; the old republications exist and stay where they are.

  • You need anonymous local monitoring. JConsole attaching to a local PID doesn't involve remote JMX at all - nothing to change.

  • Your daily client is JDK Mission Control. JMC speaks the default RMI connector; teaching it JMXMP is a plugin dance this library doesn't attempt to solve. Stock JConsole can be coaxed into it with the connector on its classpath; JConsoleBooster speaks it out of the box.

  • Your security model lives in the old access-file / SecurityManager mechanics. That machinery is gone here, not emulated; migrating means adopting the SASL + RBAC model.

Coordinates

<dependency>
    <groupId>com.druvu</groupId>
    <artifactId>druvu-lib-jmxmp</artifactId>
    <version>2.0.0</version>
</dependency>

Repo: github.com/DenissLarka/druvu-lib-jmxmp - issues and the pinned "what next" thread are the feedback channel. If you're running JMXMP somewhere unusual, I'd genuinely like to hear what it's doing there.

Comments

No comments yet. Start the discussion.