Date: 2026-07-21
Accepted
ADR-0015 shipped built-in TLS termination with two manual certificate-import modes (DB-managed, file-path-referenced) and explicitly deferred ACME auto-provisioning to a follow-up (issue #30), for two stated reasons: exposing port 80 for HTTP-01 validation is a different threat-model surface than importing material an operator already holds, and "whichever ACME client library would be used" carried an unverified GraalVM native-image compatibility question — scored ? in that ADR's own Pugh matrix.
acme4j (shred/acme4j) is the natural default choice for a Java ACME client: actively maintained (v5.1.0, 2026-04-06), 591 stars, and it already supports External Account Binding (EAB) — what ZeroSSL needs alongside Let's Encrypt, per issue #30's proposal.
Checking the specific unknown ADR-0015 flagged: acme4j's two real dependencies, jose4j (JWS/JOSE) and bcpkix-jdk18on (Bouncy Castle, PKIX), both have published entries in Oracle's official graalvm-reachability-metadata repository — the same mechanism Quarkus's native build already consumes automatically for other dependencies. That meaningfully de-risks the native-image question at the dependency level. acme4j itself ships no reachability metadata of its own, only a META-INF/services ServiceLoader registration for its provider classes (LetsEncryptAcmeProvider, etc.) — a well-solved pattern in modern GraalVM/Quarkus native builds. No GitHub issue against acme4j ever mentions GraalVM or native-image, and no public example of acme4j running inside a Quarkus native build was found. De-risked, not proven — the honest gap left is an actual native-image build attempt.
islandr already has a working answer for "do we pull in a general-purpose crypto library for one narrow, well-understood operation": TlsService.wrapPkcs1RsaKeyAsPkcs8 (added for Cloudflare Origin Certificate import) hand-encodes a minimal DER/PKCS8 envelope — derEncode/derLength helpers, ~40 lines — instead of adding Bouncy Castle as a dependency just to re-wrap a PKCS1 RSA key. islandr has no Bouncy Castle (or any general ASN.1/crypto library) dependency today.
The full RFC 8555 surface needed for HTTP-01 issuance against Let's Encrypt is narrow enough to fit the same pattern:
java.security.Signature with an EC P-256 (ES256) account key covers this; no external JOSE library needed.rest-jackson is already an installed feature); no new dependency.java.net.http.HttpClient.java.util.Base64.getUrlEncoder().withoutPadding().newNonce → newAccount → newOrder → poll authorizations → HTTP-01 challenge → finalize → download certificate chain. This is orchestration code, not cryptography.CertificationRequestInfo: subject, SubjectPublicKeyInfo, extensionRequest attribute carrying the SAN), built the same way as the existing PKCS1→PKCS8 wrapper, then signed and framed as the final CertificationRequest. Larger than the existing helper (roughly 150–250 lines including the SAN extension), but the same technique, not a new one.Hand-rolled, dependency-free ACME (RFC 8555) client, implementing exactly the HTTP-01 + Let's Encrypt subset described above — no acme4j, no Bouncy Castle, no new third-party dependency of any kind. This removes the native-image unknown outright instead of merely shrinking it, and keeps every line of a security-sensitive protocol path auditable in islandr's own codebase, consistent with the precedent already set for PKCS1 import.
Concretely:
EncryptionService, the same AES-256-GCM-at-rest pattern already used for peer private keys (ADR-0007) and the TLS private key (ADR-0015).KeyStoreProvider / reload() / CertificateUpdatedEvent mechanism from ADR-0015, alongside managed (DB-upload) and referenced (file-path) — no new hot-reload plumbing, only a new material origin.GET /.well-known/acme-challenge/{token}, serves the HTTP-01 key authorization. It is always routable but returns 404 for any token that doesn't match the currently in-flight order's expected value — there is no window where it serves anything but a 404 outside an active issuance attempt.Baseline: hand-rolled ACME client (the decision).
| Criterion (weight) | Hand-rolled (baseline) | acme4j library |
Status quo (ADR-0015 only, no ACME) |
|---|---|---|---|
| Native-image risk (5) | 0 | −1 (de-risked by dependency metadata, but empirically unverified — see Context) | +1 (no ACME code to fail) |
| New third-party dependency / supply-chain surface (4) | 0 | −1 (acme4j + jose4j + Bouncy Castle enter the dependency tree) | +1 (nothing added) |
| Implementation cost (3) | −1 (JWS signing, state machine, and CSR DER encoding all hand-written) | +1 (library handles protocol + CSR) | +1 (zero code) |
| Auditability of a security-sensitive protocol path (4) | +1 (every line is islandr's own, same review bar as existing PKCS1-wrap code) | 0 (trusted upstream, but unfamiliar-to-reviewers code in the account-key/signing path) | +1 (nothing to audit) |
| Protocol-evolution maintenance burden (3) | −1 (islandr owns RFC 8555 compliance going forward) | +1 (upstream tracks spec changes) | +1 (not applicable) |
| EAB / ZeroSSL readiness (2) | 0 (addable later, not built now — see Decision) | +1 (already supported today) | −1 (never, without building something) |
| Closes R-153 for at least one storage mode (3) | +1 | +1 | −1 (status quo — the risk this ADR exists to close) |
| Weighted total | −1 | 4 | 9 |
Notes:
acme4j scores highest numerically, largely on implementation cost and maintenance burden — the library genuinely is less code to write and upkeep. It loses on the two criteria this decision actually weighs heaviest: native-image risk remains unverified in practice despite the encouraging dependency-metadata finding, and it adds three new packages (acme4j, jose4j, Bouncy Castle) to the dependency tree of a security-sensitive TLS/crypto path in a codebase that has deliberately avoided that so far. The numeric loss on cost is accepted deliberately, the same way ADR-0015 accepted a nonzero implementation cost over "require a reverse proxy forever."AcmeService (or similarly named class) owns the RFC 8555 state machine, JWS signing, and CSR construction — the largest single addition to the tls package since ADR-0015.EncryptionService gains a third consumer, no change to the service itself.TlsService; integration tests run against Let's Encrypt's staging environment (unlimited, non-trust-chained issuance) before any production-CA code path is exercised in CI or documented for operator use.AcmeService as part of this feature's own CI, not assumed correct from the dependency-level research alone.KeyStoreProvider/reload() mechanism this ADR plugs into as a third certificate source, and R-153 (closed here for the ACME mode)EncryptionService and the at-rest secret pattern reused for the ACME account keyjose4j and bcpkix-jdk18on, the basis for this ADR's native-image risk assessment of the library alternative