DEV Community

Deniss Larka
Deniss Larka

Posted on

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. Authentication is mandatory — the server env can't be
built without it — and the connection is encrypted either way: bring your own TLS context, or
the server generates an ephemeral self-signed certificate at startup. 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)                  // optional — omit it, get an ephemeral self-signed cert
        .authenticator(myAuthenticator)   // SASL/PLAIN — required
        .build();                         // refuses to build without an authenticator
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 could historically 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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)