Quarkus + GraalVM Advanced Obfuscation
Quarkus + GraalVM Advanced Obfuscation
How to open-source your runtime without open-sourcing your IP 如何在不开源知识产权(IP)的前提下开源你的运行时
I’ve spent the last 18 months building Backbone, a production platform bootstrap for SaaS startup engineering teams. I recently open-sourced the Community edition. Community is meant to be genuinely usable for local development: run the platform services, scaffold product domain services, build against a real runtime. What I didn’t want to do is open source the entire codebase. That’s an awkward distribution problem. Java bytecode is a weak boundary. Even without source, a normal JVM artifact still leaves a lot of useful structure behind. 在过去的 18 个月里,我一直在构建 Backbone,这是一个为 SaaS 初创公司工程团队提供的生产平台引导程序。最近,我开源了它的社区版(Community edition)。社区版旨在真正服务于本地开发:运行平台服务、搭建产品领域服务骨架,并基于真实的运行时进行构建。但我不想开源整个代码库,这是一个棘手的发布难题。Java 字节码的边界非常脆弱,即使没有源代码,普通的 JVM 制品依然会保留大量有用的结构信息。
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. GraalVM Native Image 已经提供了一定帮助:你发布的是机器码而非类文件,且“封闭世界分析”(closed-world analysis)会剔除不可达代码。GraalVM 25 新增了一层——高级混淆(Advanced Obfuscation, AO),它将应用程序代码及第三方依赖中具有明确含义的模块、包、类、方法、字段和源文件名,替换为晦涩难懂的符号。
The interesting question was never “does -H:AdvancedObfuscation work on Hello World?” It was whether AO would survive a real Quarkus service. I couldn’t find a public write-up of anyone getting that working with a CDI-heavy Quarkus stack, so I tried it. The working reproduction is here: github.com/get-backbone/quarkus-graalvm-ao
真正有趣的问题从来不是“-H:AdvancedObfuscation 能否在 Hello World 上运行?”,而是 AO 能否在真实的 Quarkus 服务中存活。我找不到任何关于在重度依赖 CDI 的 Quarkus 技术栈中实现这一点的公开文档,所以我亲自尝试了。可运行的复现代码在这里:github.com/get-backbone/quarkus-graalvm-ao
The problem 问题所在
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. But Community isn’t the production platform. Extending core services and deploying to AWS sit behind that licence. The open surface is the local toolchain, domain-service scaffolding, SDK APIs, and the runnable platform images. The proprietary service implementations stay closed. Backbone 社区版需要可运行的平台服务。用户应该能够在本地启动认证、Actor、通知和文档服务——而不是继承一个残缺的、仅有源码的存根——并基于该运行时构建自己的领域服务。当他们准备发布时,商业许可证将解锁其余功能:修改后端服务的完整源代码访问权限、CI/CD 发布流水线、基础设施自动化、AWS 部署、安全、扩展性以及合规性功能。但社区版并非生产平台,扩展核心服务和部署到 AWS 需要商业授权。开放的部分仅限于本地工具链、领域服务骨架、SDK API 和可运行的平台镜像,而专有的服务实现则保持闭源。
So the distribution model I wanted looked like this: 因此,我想要的发布模型如下:
Private service source (IP) │ ▼ Quarkus native build (Oracle GraalVM 25) │ ▼ Advanced Obfuscation │ ▼ Opaque Linux native image │ ▼ Public Community distribution (development-only) 私有服务源码 (IP) │ ▼ Quarkus 原生构建 (Oracle GraalVM 25) │ ▼ 高级混淆 (AO) │ ▼ 晦涩的 Linux 原生镜像 │ ▼ 公共社区版发布 (仅限开发使用)
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. AO 仅仅是二进制发布的边界。社区版仍然需要签名的许可证,且平台增加了自身的完整性和授权检查——因此免费层级仅供开发使用,而非免费运行生产环境的途径。AO 将原生编译从“仅仅是一种部署格式”转变为一种实用的发布边界。它不是安全边界,也不是 DRM。熟练的攻击者如果有足够的时间和合适的工具,仍然可以对原生二进制文件进行逆向工程——但这与解压 JAR 包并阅读类文件相比,门槛完全不同。“这是一个可运行的服务,但其实现并非以可读的 Java 代码形式存在”,对于商业平台产品而言,这比一个被删减的演示版更有意义。
First attempt: build succeeded, Quarkus died 第一次尝试:构建成功,但 Quarkus 崩溃了
AO is experimental and Oracle-GraalVM-only. Quarkus’s native ecosystem is Mandrel-first, so this isn’t a well-trodden path. I pointed a real service 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 :/
AO 目前处于实验阶段,且仅支持 Oracle GraalVM。Quarkus 的原生生态系统以 Mandrel 为主,所以这条路并不平坦。我使用 -H:AdvancedObfuscation=export-mapping 将一个真实服务指向了 Oracle GraalVM 25。原生构建完成了,但在启动时,SmallRye Fault Tolerance 内部抛出了 NPE(空指针异常)。堆栈跟踪被部分混淆了,这让那个晚上变得比预想中更具“教育意义” :/
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.
export-mapping 会生成一个从原始名称到混淆名称的 JSON 映射(并允许你稍后对堆栈跟踪进行反混淆)。这足以让我看清发生了什么:对于任何使用反汇编程序探测二进制文件的人来说,AO 不仅仅是表面功夫。任何依赖运行时名称的东西——如 Class#getName()、反射查找、CDI Bean 解析、FT 帧——在名称改变时都可能改变行为。
-H:Preserve looked right but wasn’t -H:Preserve 看起来是对的,但其实不然
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. I wanted: If this type is already reachable, keep its name. Preserve gave me: 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.
Oracle 的文档指出 -H:Preserve 是排除项。但在 Quarkus 类路径上,Preserve 是错误的工具。Preserve 既是一种可达性机制,也是一种重命名排除机制。大范围的包保留开始将可选的、原本不可达的类型拖入镜像中——例如日志桥接、Kotlin 元数据、MicroProfile Metrics、部署类等。构建随后因缺少类而失败。更窄的 Preserve 列表依然会引入可选依赖。我想要的是:如果该类型已经是可达的,则保留其名称。而 Preserve 给我的却是:即使分析未发现该代码,也要使其可用,且不要重命名它。对于 Quarkus 来说,后一种行为显得有些用力过猛了。
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 I ended up calling invert-target registrations:
一旦你深入研究,GraalVM 的 AO 规则就清晰了:在可达性元数据中注册为反射的类不会被混淆。反射注册即是重命名保护,它本身并不会强制包含整个包。行之有效的模式是条件化的 reflect-config.json 条目——我最终将其称为“反向目标注册”(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. 如果该类型是可达的,AO 会保留其名称。如果不可达,该注册就是空操作,不会突然将半个 Quarkus 拖入镜像。这种区别——Preserve 与条件反射——是解决问题的关键。
It didn’t stop at Fault Tolerance. Once FT started cleanly, other name-sensitive framework edges showed up. 问题并没有止步于 Fault Tolerance。一旦 FT 顺利启动,其他对名称敏感的框架边缘问题也随之显现。
| 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 | Redis / Mutiny Vert.x invert-target generator |
| Micrometer Vert.x binder NPE under AO | quarkus.micrometer.binder.vertx.enabled=false |
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, and Mutiny. 测试所用的 Quarkus 运行时为 Quarkus 3.36.1,运行在 Oracle GraalVM 25 上,技术栈包括:Quarkus ArC CDI、SmallRye Fault Tolerance、Redis 和 Mutiny。