Quarkus + GraalVM Advanced Obfuscation
The Problem
Java bytecode is a weak boundary. Even without source, a normal JVM artifact still leaves a lot of useful structure behind. GraalVM Native Image already helps: you ship machine code, not class files, and closed-world analysis strips unreachable code. GraalVM 25 adds another layer - Advanced Obfuscation (AO) - which replaces meaningful module, package, class, method, field, and source-file names with opaque symbols across application code and third-party dependencies.
The interesting question was never "does -H:AdvancedObfuscation work on Hello World?" It was whether AO would survive a real Quarkus service. There was no public write-up of anyone getting that working with a CDI-heavy Quarkus stack, so github.com/get-backbone/quarkus-graalvm-ao was created as the working reproduction.
The Distribution Model
Backbone Community needs runnable platform services. Users should be able to start auth, actor, notification, and document locally - not inherit a crippled source-only stub - and build their own domain services against that runtime. When they're ready to ship, a commercial licence unlocks the rest: full source code access to modify the backing services, CI/CD release pipelines, infrastructure automation, AWS deployments, security, scale, and compliance features.
The open surface is the local toolchain, domain-service scaffolding, SDK APIs, and the runnable platform images. The proprietary service implementations stay closed.
Private service source (IP)
│ โผ
Quarkus native build (Oracle GraalVM 25)
│ โผ
Advanced Obfuscation
│ โผ
Opaque Linux native image
│ โผ
Public Community distribution (development-only)
AO is only the binary distribution boundary. Community still needs a signed licence, and the platform adds its own integrity and entitlement checks - so the free tier stays something you develop against, not a way to run production for free.
AO turns native compilation from "just a deployment format" into a practical distribution boundary. Not a security boundary. Not DRM. A skilled attacker with time and the right tools can still reverse-engineer native binaries - but that's a different bar from unzipping a JAR and reading class files. "Here is a runnable service whose implementation isn't sitting there as readable Java" is a much more useful open-source story for a commercial platform product than a stripped demo.
First Attempt: Build Succeeded, Quarkus Died
AO is experimental and Oracle-GraalVM-only. Quarkus's native ecosystem is Mandrel-first, so this isn't a well-trodden path. A real service was pointed at Oracle GraalVM 25 with:
-H:AdvancedObfuscation=export-mapping
The native build completed. Startup then fell over inside SmallRye Fault Tolerance with an NPE. The stack trace was partially obfuscated, which made the evening more educational than it needed to be.
export-mapping produces a JSON map from original names to obfuscated ones (and lets you deobfuscate stack traces later). That was enough to see what was going on: AO isn't just cosmetic for anyone poking at the binary with a disassembler. Anything that depends on runtime names - Class#getName(), reflective lookup, CDI bean resolution, FT frames - can change behaviour when those names change.
The Preserve Problem
-H:Preserve looked right, but Oracle's docs point at -H:Preserve as the carve-out. On a Quarkus classpath, Preserve is the wrong tool. Preserve is a reachability mechanism as well as a rename exclusion. Broad package preservation started dragging optional and otherwise-unreachable types into the image - logging bridges, Kotlin metadata, MicroProfile Metrics, deployment classes, and friends. The build then failed with missing classes. Narrower Preserve lists still pulled optionals in.
The difference:
- What was wanted: If this type is already reachable, keep its name.
- What Preserve gave: Make this code available even if analysis didn't find it, and don't rename it.
For Quarkus, that second behaviour felt like a bit of a sledgehammer.
The Fix: Invert-Target Reflection Metadata
GraalVM's AO rules are clearer once you look for them: classes registered for reflection in reachability metadata aren't obfuscated. Reflection registration is rename protection. It isn't, by itself, a force-include of the whole package.
The pattern that worked was conditional reflect-config.json entries - what ended up being called invert-target registrations:
{
"name": "io.smallrye.faulttolerance.…",
"condition": {
"typeReachable": "io.smallrye.faulttolerance.…"
}
}
If the type is reachable, AO leaves the name alone. If it isn't reachable, the registration is a no-op and doesn't suddenly pull half of Quarkus into the image. That distinction - Preserve vs conditional reflection - was the entire unlock.
Beyond Fault Tolerance
Once FT started cleanly, other name-sensitive framework edges showed up:
| Symptom | What fixed it |
|---|---|
| SmallRye FT NPE / mangled FT frames | FT invert-target generator |
NoSuchMethodException on Arjuna *EnvironmentBean |
Narrow Arjuna suffix allowlist (blanket com.arjuna.** OOMed / timed out the AO compile) |
| CDI: No bean found for an obfuscated Vert.x type | Vert.x invert-target generator |
| Redis / Mutiny: Micrometer Vert.x binder NPE under AO |
quarkus.micrometer.binder.vertx.enabled=false
|
Build Details
The Quarkus runtime under test was Quarkus 3.36.1 on Oracle GraalVM 25, running a production-like stack: Quarkus ArC CDI, SmallRye Fault Tolerance, Redis cache, Postgres/Hibernate, SmallRye Health + Micrometer/Prometheus, backbone-kit metrics, logging, throttle components.
Small generators were used to rebuild the three reflect carve-outs before each AO build. The Linux/glibc native image comes out of Oracle GraalVM 25 with Advanced Obfuscation enabled. Build time is the main cost - an AO native build of a Backbone service took around 20 minutes. Oracle documents AO as typically adding ~20-50% to native-image time, with no runtime performance or memory overhead.
What Ships
Community platform services are distributed as protected native images on GHCR. You can pull and run them locally with the Community edition of Backbone. The proprietary Java source doesn't come with them.
The same approach is documented end-to-end in the demo public repo so anyone can reproduce the mechanism, without needing the full Backbone tree: get-backbone/quarkus-graalvm-ao
task compose:up # JVM path
task ao:build # regenerates reflect-config, then AO native (~15m)
task ao:image
task ao:run
task ao:smoke
Caveats
- AO is experimental and Oracle-GraalVM-only. Behaviour can shift between releases; pin versions and test the obfuscated image.
- AO isn't encryption, and it doesn't make reverse engineering impossible. It does turn "read the implementation" into specialist native-binary work - Ghidra/IDA territory, opaque symbols, closed-world stripping - rather than an afternoon with a Java decompiler.
- Keep the mapping file with the build so you can deobfuscate production stack traces. Don't ship it in the image.
- Watch the SBOM. Oracle warns that embedded class-level SBOM data can re-expose original symbol names under AO; export the SBOM or disable embedding when confidentiality matters.
- Quarkus CLI footgun:
-Dquarkus.native.additional-build-args=…replaces the property rather than merging with it. If you also need--initialize-at-run-time=…(which was needed for jansi), put both flags on the same CLI list, or you'll silently drop one.
What This Enables for Backbone Community
Community (free, development-only)
├── local Floci / Postgres / Redis backends
├── AO native platform services (auth, actor, notification, document)
├── domain-service scaffolding (auth, throttling, logging, metrics baked in)
├── BFF API + stateless reference UI (Quarkus dev mode)
└── open SDK surface + local tooling
licence when ready to ship
โผ
source code + CI/CD + IaC + AWS deploy + enterprise scale, security, compliance
Users get something they can actually run and build against. They don't get the commercial platform implementation as public Java source just because Community distribution is public. That's a much more valuable open-source boundary for this kind of product than a sales demo or video pitch before upgrading.
Takeaway
What's missing from the Oracle Advanced Obfuscation docs is an account of making AO work with a real Quarkus application - including CDI and SmallRye Fault Tolerance - without resorting to -H:Preserve and watching the native build eat optional dependencies.
The short version: Don't preserve Quarkus. Register the few name-sensitive types with conditional reflection metadata, and let AO rename everything else.
Start here if you're about to spend a day discovering that the hard way: get-backbone/quarkus-graalvm-ao
Comments
No comments yet. Start the discussion.