05 Aug 2026
13 min read
Second week of this. The idea is the same as last time: list what caught my attention in the Java ecosystem, then pick two and look at the mechanism, because the CVE number and the score never tell you anything you can use.
Between 29 July and 4 August two unrelated projects, a cryptography library and a messaging stack, disclosed around sixty CVEs between them. Line up the titles and a lot of them are the same sentence. Allocation from a declared length. An iteration count read out of a file. Recursion with no depth limit. A count field with no ceiling.
So the theme this week is what happens when the input gets to decide how much work you do.
The week in five
-
Bouncy Castle 1.85, 32 CVEs, released 28 July, reached oss-security 3 August. The JCE provider a large fraction of Java crypto quietly runs on. Thirty two CVE identifiers in one release, across ASN.1 parsing, OpenPGP, DTLS, PKCS#12 and BCFKS keystores, CMS, S/MIME, JSSE hostname verification and MLS. The release is one day before my window opens, so I am cheating slightly, but the disclosure and the discussion both happened inside it. Deep dive below.
-
Apache Qpid: 24 CVEs across four AMQP 1.0 implementations, 4 August. Six in Proton-J (through 0.34.1, fixed in 0.35.0), seven in Broker-J (through 10.0.1, fixed in 10.1.0), five in ProtonJ2 (through 1.1.0, fixed in 1.2.0) and six in Proton Dotnet. Most rated Important, most reachable pre-authentication, and really about six distinct issues repeated across the four codebases. Deep dive below.
-
CVE-2026-66755, Apache Tika, 30 July. Relative path traversal in the ISA-Tab parser. The Study Assay File Name values inside an investigation file are not validated, so a crafted dataset reads files outside its own directory with the privileges of the Tika process. Affects tika-parser-scientific-module from 1.8 through 3.3.1 and 4.0.0-alpha-1, fixed in 3.3.2 and 4.0.0-beta-1. Credited to BugQore, who also supplied the patch, with an independent discovery by Rui Heng Koh. A sibling issue, CVE-2026-66756, covers the unpack endpoint in tika-server being reachable with unsecureFeatures=false.
-
Four CVEs in Apache NiFi, 3 August, all fixed in 2.11.0. Three are authorization issues around Parameter Contexts: CVE-2026-62354 (High) let a read-only user submit proposed parameter values and trigger component validation with them, CVE-2026-68979 (Medium) skipped authorization on the components referencing a parameter when the context was updated, and CVE-2026-68980 (Low) authorized asset deletion against the Parameter Context identifier supplied in the request rather then the stored one. The fourth, CVE-2026-68981 (High), is this week’s theme in miniature: the Jersey encoding filter applied size limits to the compressed request body instead of the decompressed output.
-
Four CVEs in Apache Zeppelin, 30 July. The interesting one is CVE-2026-44617, an LDAP filter injection in LdapRealm. The input is escaped, just against the wrong specification: RFC 4514 distinguished-name escaping applied to a value going into a search filter, which needs RFC 4515 filter escaping. Two grammars, two different sets of metacharacters. Moderate, affects 0.11.1, 0.11.2 and 0.12.0, fixed in 0.12.1, and explicitly an incomplete fix of CVE-2024-31867. The others are a CSRF in REST and WebSocket handling, a path traversal in NotebookRepo, and a separate LDAP injection in ActiveDirectoryGroupRealm.
Honourable mention to CVE-2026-62391 in Apache Kyuubi, 31 July. Kyuubi has an allowlist, kyuubi.session.local.dir.allow.list, restricting which local directories a session can touch, and you get past it with unprefixed Spark config aliases instead of the properly prefixed names: the check reads the name you used, not the setting you end up modifying. Affects 1.6.0 up to 1.12.0. Same shape as the ActiveMQ composite destination bug I wrote about last week, where the authorization decision was made about a label and the effect happened on what the label expanded to.
Bouncy Castle: thirty two CVEs in one release
Bouncy Castle sits under a lot of Java that nobody thinks of as crypto code: PKI, signing, S/MIME, OpenPGP, TLS. If you ever needed an algorithm the JDK did not ship, you added bcprov and moved on. It is also where post-quantum cryptography currently lives for Java, which is why I read its source properly a few months ago while writing about how Camel is preparing for post-quantum cryptography.
Version 1.85 shipped on 28 July with expanded NIST and Korean post-quantum algorithms, hybrid certificates for quantum-safe PKI migration, a high-level CAdES API, KEM-based key management in CMS, and BLS12-381 and Taproot Schnorr signatures. It also fixes thirty two CVEs, and read as a set they fall into two groups.
The first group is this week’s theme. BKS/UBER keystore allocates from untrusted lengths before integrity check. Possible OOM from unbounded up-front allocation on definite-length read. PKCS#8 / PBES2 decryptors honour unbounded KDF cost from input. BCFKS keystore load honours unbounded KDF cost from untrusted file. OpenPGP Argon2 S2K honours attacker-chosen memory and passes. HSS public-key level count unbounded, enabling huge allocation on verify. Lazy ASN.1 sequence forcing resets nesting-depth guard. DTLS handshake reassembler allocates buffer from unchecked 24-bit length.
The keystore one is worth pausing on. The point of a keystore MAC is to establish that the file has not been tampered with, so allocating on lengths read out of the file before that MAC is verified puts the integrity check after the expensive part. It is the encrypt-then-MAC principle in allocation terms: “allocate a buffer this big” is already acting on the data.
The second group is different in kind. CMS verifySignatures returns true for SignedData with zero signers. RSA PKCS#1 verification skips last two hash bytes in NULL-omitted path. CCM-family modes write plaintext to caller buffer before tag check. Stapled OCSP response accepted without binding to the checked certificate. S/MIME validator trusts signer-asserted signingTime for path validation. JSSE hostname verifier CN-fallback enabled by default despite documented opt-in. Those are not resource issues, those are the primitive returning the wrong answer.
The zero signers one takes ten seconds to explain. verifySignatures walks the signers, checks each, and reports success if none failed. Hand it a SignedData with no signers and nothing fails, so it returns true. Vacuous truth: a universal statement over an empty set holds, and validation phrased as “no element failed” rather then “at least one passed and none failed” lets empty input through. Same shape as the Cedar-Java equals() issue from last week.
Why I find it interesting
Two things, and neither is the individual bugs.
The first is the split between the two groups, because they need different responses from you. The allocation ones are availability: bad, but they fail loudly and you can often bound them at a layer above. The second group is silent. A signature check returning true, a hostname verifier falling back to CN, plaintext handed to your buffer before the tag is checked. Nothing throws, nothing logs, and the security property you designed around is simply not there. If you are triaging thirty two CVEs with limited time, that distinction is the one that matters, not the severity ordering.
The second is the attribution, and specifically how hard it is to find.
Alan Coopersmith posted the release to oss-security on 3 August with the CVE list. Peter Gutmann replied the next day asking whether some new code analysis tool was behind the volume, guessing AI and noting there would be an interesting backstory. Coopersmith answered that he had not seen anything in the announcements from the Bouncy Castle folks about that.
The answer is in the announcement. Version 1.85 “represents a significant hardening of the APIs as a result of the help the team has received from people using advanced AI-based coding analysis tools.” That is the tail of a sentence whose subject is standards coverage and preparing for the next generation of cryptographic security, in a paragraph selling the release. Two people read that page carefully enough to post it to a security list and ask a question about it, and the sentence answering the question did not register with either of them.
I do not think that is anybody being careless. It is a question of where the information lives. Thirty two CVEs, and the only statement of how they were found is prose in a feature announcement, so it is not in the advisories, not in the per-CVE credits, not in anything a tool or a reader scanning the CVE list would pick up. Compare last week’s ActiveMQ advisory, which carried “Claude and Ada Logics” in the credit field, where credit normally goes: structured, per-CVE, and impossible to miss. Same category of fact, and only one of the two formats survives contact with a reader.
That matters more as the volume grows. If AI-assisted analysis is going to be a significant share of how bugs get found, the provenance is worth recording where provenance is recorded, and it is the sort of thing I had in mind writing about the CVE stigma back in February. A count of thirty two mostly tells you how closely a library has been looked at. The how is the part that lets you interpret the count, and this week it was there and still effectively unreadable.
Apache Qpid: when the wire decides the allocation
On 4 August Qpid published twenty four advisories at once. Unbounded symbol value caching appears as CVE-2026-66257 in Proton-J, CVE-2026-68074 in Broker-J, CVE-2026-67588 in ProtonJ2 and CVE-2026-67465 in Proton Dotnet. Type size and count handling leading to excessive allocation appears as CVE-2026-66273, CVE-2026-68060, CVE-2026-67589 and CVE-2026-67551. Unbounded type nesting leading to a StackOverflowError appears as CVE-2026-66274, CVE-2026-68073, CVE-2026-67590 and CVE-2026-67552. Flow control windows, disposition ranges and transfer frames per delivery repeat across the same set. Robbie Gemmell reported on Proton-J, Daniil Kirilyuk on Broker-J, Timothy A. Bish on ProtonJ2.
One fact about AMQP 1.0 explains all of it. It is a self-describing binary type system: every value is a constructor byte saying what type follows, then a size or count field for anything variable-length, then the bytes. A list is a constructor, a size, a count of elements, then the elements, each of which is itself a constructor with possibly another size and count. Symbols, the interned ASCII strings AMQP uses for names, get cached by the decoder so repeated names cost nothing.
Each of those is a reasonable design, and each hands the sender a lever.
The size field is the peer telling you how big to make the buffer, so allocate first and read second and a four-byte field asks for four gigabytes. The count field is the peer telling you how many elements to expect, so a preallocated array is sized by the sender even when the payload is a hundred bytes. A list can contain a list, and a recursive descent decoder turns nesting depth into stack depth. Symbol caching inserts a sender-chosen key into a map that never evicts.
The timing matters as much as the mechanism. AMQP negotiates its protocol header, opens a connection and exchanges performatives before SASL authentication completes, so the decoder is parsing peer-controlled structures before anyone has proven who they are. That is why most of these are reachable pre-authentication.
Why I find it interesting
Because of where the constraint actually lives.
Four implementations, two of them different generations of the same idea, one a broker, one on a different runtime with a different memory model, written at different times. The same six issues turn up in all of them. That is a good signal that the thing to fix is not any one decoder but the assumption the format encourages: AMQP 1.0 makes the wire authoritative on how much memory to allocate and how deep to recurse, and the spec does not require an implementation to impose a ceiling, so the natural reading of the document produces a decoder without one.
That puts it in a bug class family Java developers already know. XML entity expansion, where the document says how many times to expand. Zip bombs, and NiFi’s gzip filter from the same week, where the compressed size is a lie about the decompressed size. ASN.1 length prefixes, which is where a third of the Bouncy Castle CVEs live. ObjectInputStream reading a declared array length and allocating it before reading an element. The rule is short: a length field is a request, not a fact. Anything from an untrusted source that controls allocation size, loop iterations, recursion depth or cache growth needs a ceiling that comes from your configuration.
The one I would go and look at in my own code is the symbol cache, because it is the least obvious. A memoisation cache does not look like an attack surface, its an optimisation. But an unbounded cache keyed on peer-supplied input grows on demand, and Java is full of them: interned strings, class-name to metadata maps, regex pattern caches, JSON field-name caches. If the key comes from the network and the map has no eviction policy, you have a slow OOM with extra steps.
What I take away from this week
The first is a checklist rather then a moral. Find the places where a number from outside decides how much work happens. Buffer allocation from a declared length. Array preallocation from a declared count. Loop bounds from an iteration count in a file, which is the PKCS#12, PBES2 and BCFKS case. Recursion driven by input structure. Cache insertion keyed on input. Decompression limited on the compressed side. Every serious item in this post is one of those six.
The second is that two items this week, Zeppelin and Kyuubi, were labelled as incomplete fixes of earlier CVEs, and both are cases where the check and the effect are separated by a translation step. Zeppelin escapes, but the value crosses from one LDAP grammar into another between the escaping and the query. Kyuubi checks a config name, but alias resolution happens after the check. When something gets renamed, rewritten or expanded between validation and use, that gap is where the next report comes from, and it is worth looking for deliberately.
The third is about arrival times. Bouncy Castle arrives through whatever pulls in bcprov, which for many people is Keycloak or a PDF signing library. Qpid Proton-J arrives through the ActiveMQ and Artemis AMQP clients, through Camel’s AMQP component, through anything speaking AMQP 1.0 from the JVM. Tika arrives through Solr, through Camel, through most content extraction pipelines. You get these fixes when a BOM moves, and on an unmaintained line you do not get them at all.
Bouncy Castle is the one I would not wait on. The resource exhaustion half can ride a BOM bump, but the correctness half changes what your verification calls actually mean. If you pin bcprov directly, 1.85 is worth doing on your own schedule.
See you next week.
References
- Bouncy Castle 1.85 release fixes 32 CVEs - Alan Coopersmith, oss-security
- Re: Bouncy Castle 1.85 release fixes 32 CVEs - Peter Gutmann, oss-security
- Re: Bouncy Castle 1.85 release fixes 32 CVEs - Alan Coopersmith, oss-security
- New Release: Bouncy Castle Java 1.85 - Bouncy Castle
- CVE-2026-66257: Apache Qpid Proton-J, unbounded symbol value caching - oss-security
- CVE-2026-66273: Apache Qpid Proton-J, type size/count handling can lead to excessive allocation - oss-security
- CVE-2026-66274: Apache Qpid Proton-J, unbounded type nesting can lead to pre-authentication stackoverflow - oss-security
- CVE-2026-66275: Apache Qpid Proton-J, incoming session flow control window can be exceeded - oss-security
- CVE-2026-68060: Apache Qpid Broker-J, type size/count handling can lead to excessive allocation - oss-security
- CVE-2026-68080: Apache Qpid Broker-J, unbounded echo flow responses can lead to denial of service - oss-security
- CVE-2026-67588: Apache Qpid ProtonJ2, unbounded symbol value caching - oss-security
- CVE-2026-66755: Apache Tika, arbitrary local file read in ISArchiveParser - oss-security
- CVE-2026-66756: Apache Tika, unpack endpoint in tika-server allows configuration with unsecureFeatures=false - oss-security
- CVE-2026-44617: Apache Zeppelin, LDAP filter injection in LdapRealm, incomplete fix of CVE-2024-31867 - oss-security
- CVE-2026-62391: Apache Kyuubi, allow.list bypass via unprefixed Spark file-conf aliases - oss-security
- CVE-2026-68979: Apache NiFi, missing authorization for components referenced by Parameter Context updates - oss-security
- CVE-2026-62354: Apache NiFi, incorrect authorization for Parameter Context validation requests - oss-security
- CVE-2026-68980: Apache NiFi, authorization bypass for Parameter Context asset deletion - oss-security
- CVE-2026-68981: Apache NiFi, uncontrolled resource consumption through decompression of HTTP requests - oss-security
- oss-security daily archive - Openwall
29 Jul 2026
12 min read
I read a lot of security news and most of it is useless. Not wrong, just useless: a CVE number, a CVSS score, a vendor advisory link, “patch immediately”. You close the tab and you have learned nothing. The interesting part of a vulnerability is almost never the score, its the mechanism. How does a broker end up skipping an ACL check because you gave a destination the right kind of name? What happens when the library whose entire job is to answer yes or no gets equals() backwards?
So I am going to try something. Once a week I will list the vulnerabilities in the Java ecosystem that caught my attention, then pick a couple and dig into them properly: the advisory, the mechanism, and what I think the actual lesson is. Not a feed, there are better places for that.
This week has a theme, and I did not go looking for it. Almost everything that landed between 22 and 28 July was an authorization failure of one kind or another. A broker that forgot to check an ACL. An authorization policy engine with three separate ways to get the wrong answer. A SOAP stack accepting serialized objects from anybody who can reach a port. It was a bad week for saying no.
The week in five
-
CVE-2026-66713, Apache Axis2/Java, CVSS 9.8, 28 July. Unsafe Java deserialization in the Tribes-based clustering component. An unauthenticated attacker with network access to the clustering port sends a crafted serialized object, it gets deserialized in Axis2ChannelListener#messageReceived, and that is remote code execution. Clustering is off by default, which is the only reason this is not a much bigger story. The fix in 2.0.1 is to remove the clustering feature entirely. Not harden it, remove it.
-
CVE-2026-61487, Apache ActiveMQ, 27 July. An authenticated low-privilege user bypasses per-destination write ACLs by sending to a temporary composite destination whose physical name is a comma-separated list of real queues. Rated Important. Affects everything before 5.19.9 and 6.0.0 through 6.2.7, fixed in 5.19.9, 6.2.8 and 6.3.0. Deep dive below, and look at the credit line while you are there.
-
CVE-2026-55771, CVE-2026-55772 and CVE-2026-55773, Cedar-Java, CVSS 8.8 each, 28 July. Three vulnerabilities in the Java binding for the Cedar authorization policy language: a policy injection, a type confusion across the Java to Rust boundary, and an incorrect equality comparison. Affects everything before 2.3.6, 3.1.2 through 3.4.0, and 4.0.0 through 4.8.9. Fixed in 2.3.6, 3.4.1 and 4.9.0. Deep dive below.
-
CVE-2026-66390 and CVE-2026-66391, Apache Wicket, 27 July. The first is an XSS: crafted Link URL strings can break out of the surrounding JavaScript sequence, because the URL is interpolated into a script context without being neutralised for that context. Rated Important, affects 9.0.0 through 9.23.0 and 10.0.0 through 10.9.0, fixed in 10.10.0. The second, filed the same day by the same reporter, is leaked and missing CSP headers. I like that pairing a lot: one bug lets you inject script, the sibling bug is the defence in depth that should have limited the damage not being properly applied. They found the hole and then found that the safety net had a hole too.
-
CVE-2026-59878, Apache ActiveMQ AMQP, 27 July. Improper input validation in the AMQP NIO connector: an unauthenticated remote attacker sends a negative frame size value, NIO threads crash, and the thread pool gets exhausted. Moderate severity, same affected and fixed versions as the ACL bug above, reported by zx (Jace). A negative length field crashing a network protocol parser in 2026 is a comforting reminder that the classics never really go away.
ActiveMQ: the name is not the resource
Some background, because this bug only makes sense if you know two ActiveMQ features that are individually reasonable.
The first is composite destinations. ActiveMQ lets you address more than one destination with a single name by comma separating them, so publishing to A,B,C fans the message out to queues A, B and C. It is a genuinely useful thing for integration work and I have used it plenty.
The second is temporary destinations. These are created on the fly, typically for request and reply patterns where a consumer needs somewhere to receive the answer. Because they are owned by the connection that created them and disappear when it goes away, they are treated differently by the authorization layer. You do not write an ACL entry for a queue that will exist for 200 milliseconds and then never again.
Put the two together and you get the bug. Create a destination that is marked temporary but whose physical name is A,B,C, where A, B and C are real queues you have no write permission on. The broker looks at the destination, sees the temporary flag, and takes the path that skips the per-destination write ACL. Then the composite expansion runs and your message is delivered to all three queues.
The authorization decision was made about the wrapper, and the delivery was performed on the expansion. A composite destination is not one destination, its N destinations wearing a single name, and the check happened while it still looked like one thing.
Why I find it interesting
The generalisable lesson is that you have to authorize the effect and not the label. Any time a system has a naming mechanism that expands, aliases, globs or otherwise resolves one identifier into several resources, the authorization check has to happen after resolution, on the actual set. Checking before is checking a string. This is the same shape as path traversal (the path you validated is not the file you opened), as SSRF via redirect (the host you allowed is not the host you fetched), and as the Tomcat and Reactor Netty redirect bugs from earlier this year where the credential was authorized for one host and then sent to another. The resource you checked has to be the resource you touched, and every layer of indirection between those two moments is a place for them to diverge.
The other detail worth pausing on is the credit line. The advisory credits the discovery to Claude and Ada Logics.
Ada Logics is a security firm that does a lot of open source auditing and fuzzing work, much of it funded through OpenSSF and CNCF programmes. So what that line means in practice is a professional audit team using an AI model as part of their workflow, and the Apache Security Team crediting both in the advisory as a matter of routine. No announcement, no blog post, no controversy. Just a line in an advisory on oss-security.
I find that more significant than any single one of these bugs. Back in February I wrote about the CVE stigma and how AI-assisted discovery was going to change the volume and the culture around vulnerability reporting, and at the time the examples were still notable enough to be news. Five months later it is an unremarkable attribution in a routine Apache advisory. Steve Poole made a similar point recently after seven jackson-databind CVEs landed in a single day from AI assisted analysis, and his line has stayed with me: the absence of CVEs was never evidence of safety, it was evidence of silence.
For a project like ActiveMQ this is fine, it has a real security team and a release process. My worry, and I have said this before, is the enormous middle of the ecosystem that has neither. The rate of finding is going up much faster then the capacity to triage and fix.
Cedar-Java: three ways to get the wrong answer
Cedar is the authorization policy language AWS open sourced. The engine is written in Rust and there are bindings for other languages, of which CedarJava is the Java one. You describe who can do what in Cedar’s policy language, you hand the engine a request, it tells you permit or forbid. It exists specifically so that you do not have to hand-roll authorization logic, which is exactly the kind of code everybody gets wrong.
Three CVEs landed on 28 July, all rated 8.8, all in the Java binding rather then the Rust core.
CVE-2026-55773, policy injection. Improper input handling lets attacker-controlled data end up influencing the policy itself rather then just being data the policy is evaluated against. This is the authorization equivalent of SQL injection and the mitigation is the same as it has been for twenty five years: policies are code, user input is data, and the two must never be joined with string concatenation.
CVE-2026-55772, type confusion. This one is the most interesting technically. Cedar’s serialization format, the one used to pass values from Java across the FFI boundary into the Rust evaluator, reserves two JSON key names: __entity and __extn. They mark a value as an entity reference or an extension value rather then a plain record. CedarJava did not validate that keys in a CedarMap avoided those reserved names. So if your service builds a CedarMap from anything user-controlled, and request headers or resource metadata are the obvious candidates, an attacker who can choose a key name can inject __entity and get the Rust evaluator to interpret a plain record as an entity reference. The conditions are that the integrating service builds a map from user-controlled data and a policy references that value in a condition, which is not exotic.
CVE-2026-55771, incorrect equality comparison. The advisory itself is vague, it says only that under certain circumstances this could lead to incorrect equality comparisons. Secondary reporting attributes it to inverted logic in EntityIdentifier.equals(), returning true when the argument is null and false in cases where it should return true. I have not verified that against the source, so treat the specific mechanism as reported rather then confirmed, but the shape is clear enough.
Why I find it interesting
Start with the obvious one and then the useful one.
The obvious one is the irony, and I do not think it is only irony. Cedar exists so that applications stop writing their own authorization logic, on the entirely correct theory that hand-rolled authorization is where bugs live. And then the binding layer ships a policy injection, a type confusion and a broken equals(). This is not an argument against using a policy engine, you should absolutely use one. It is a reminder that adopting a library moves the risk, it does not delete it, and the place it moves to is the glue you wrote around the library plus the glue the library wrote around its own core.
The useful lesson is about the type confusion, and it is one I think Java developers systematically underrate: a reserved key in a user-controlled map is an injection vector. __entity and __extn are in-band signalling. They are metadata living in the same namespace as the data, distinguished only by a naming convention, and a naming convention is not a boundary. This is precisely the shape of prototype pollution in JavaScript with __proto__, and of every YAML tag and JSON $type deserialization bug we have all been fixing for years. If your serialization format has magic keys and you build objects in that format out of anything that came from a request, you have to filter for the magic keys. The library should have done it here, but the integrating service is the one holding the untrusted input.
The second lesson is that the FFI boundary is a trust boundary and it does not look like one. Java to Rust in the same process feels like a function call, so nobody treats it like a protocol. But it is a protocol, with a wire format and a parser on the other side, and the Rust evaluator trusted the Java side to have produced well-formed input. Everything we know about validating input at the edges applies to internal boundaries too, and internal boundaries are much easier to forget about precisely because they do not have a socket in front of them.
What I take away from this week
Two things, plus a note about timing.
The first is that all three of the serious bugs this week are the same failure at different altitudes: a decision was made about a representation rather then about the thing it represents. ActiveMQ authorized a destination name instead of the queues it expanded into. Cedar interpreted a record as an entity because it carried the right key. Axis2 turned bytes into live objects because they arrived on the clustering port and the port was the only credential. In each case the check was performed against something that stood in for the real resource, and the substitution was attacker-influenced.
The second is the pattern I noticed last week too and which is becoming a genuine trend: deletion as a fix. Axis2’s answer to a deserialization RCE in clustering is to remove clustering in 2.0.1. Logback did the same thing in June, taking out Janino-based conditional configuration entirely rather then patching the denylist for the fourth time. When a capability’s security history reads as a list instead of an incident, taking the feature out is usually the honest answer, even though somebody’s upgrade gets painful.
The note about timing: none of this will reach most Java teams this week. It reaches you when a BOM moves. Camel, Spring Boot, Quarkus and everything else pin these versions transitively, so the practical arrival date for the ActiveMQ and Wicket fixes is whenever the next framework release picks them up, and for anything on an EOL line it is never. If you run ActiveMQ directly, though, 5.19.9 or 6.2.8 is worth doing on your own schedule rather then waiting. An authorization bypass available to any authenticated user is exactly the kind of thing that sits quietly in an environment where you assumed the ACLs were doing the work.
See you next week.
References
06 May 2026
9 min read
Post-Quantum Cryptography has moved from research papers to production standards. NIST finalized FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA) in August 2024, and the Java ecosystem is catching up fast. JDK 24 shipped with native ML-KEM support via the javax.crypto.KEM API (JEP 496), and JDK 27 will bring PQC directly into TLS with JEP 527.
As Chair of the Apache Camel PMC, I wanted to start preparing Camel for this transition early. Over the past weeks I have been working on the core SSL/TLS infrastructure and building proof-of-concept examples that show Camel can negotiate PQC TLS handshakes across three different JDK versions.
In this post I will walk through what we changed in Camel’s core, why it matters, and how you can try it yourself.
Why PQC matters for integration
If you are building enterprise integrations, chances are you are moving sensitive data between systems over TLS. The “harvest now, decrypt later” threat is real: adversaries can record encrypted traffic today and decrypt it once a sufficiently powerful quantum computer becomes available. For regulated industries, the migration deadline is approaching fast. NSA’s CNSA 2.0 guidance requires PQC for TLS in national security systems by 2033.
For Apache Camel users, this means the framework they use to wire systems together needs to support PQC at the transport layer. That is the direction we are heading, though there is still a good amount of work ahead.
What changed in Camel core
The PQC work started in Camel 4.19.0 and continued into 4.20. It focuses on making PQC TLS configuration as simple as adding a few properties.
Named Groups and Signature Schemes
TLS 1.3 uses named groups to negotiate key exchange algorithms. PQC key exchange requires new named groups like X25519MLKEM768, a hybrid that combines classical X25519 with the post-quantum ML-KEM-768 algorithm. Both run together, so security is maintained even if one is broken.
Before these changes, there was no way to configure named groups through Camel’s SSL configuration. We added namedGroups and signatureSchemes to both SSLContextParameters and the camel.ssl.* configuration properties:
camel.ssl.namedGroups=X25519MLKEM768,x25519
camel.ssl.signatureSchemes=ML-DSA,ECDSA,RSA
We also added include/exclude filters for fine-grained control:
camel.ssl.namedGroupsInclude=X25519MLKEM768,x25519
camel.ssl.namedGroupsExclude=ffdhe2048
Auto-configuration on JDK 25+
On JDK 25 and later (where X25519MLKEM768 is available in the JVM), Camel will automatically reorder the named groups to prefer PQC key exchange. If you have not explicitly configured named groups, Camel sets the ordering to:
X25519MLKEM768, x25519, secp256r1, secp384r1, [JVM defaults]
No configuration required. If your JVM supports it, Camel prefers PQC. You will see a log line confirming it:
INFO SSLContextParameters - Auto-configured PQC named groups: [X25519MLKEM768, x25519, secp256r1, secp384r1, ...]
This only kicks in when you have not set namedGroups or namedGroupsFilter explicitly, so it will not override your choices.
Self-signed certificates and provider selection
For development and testing, we added camel.ssl.selfSigned=true, which generates a self-signed certificate at startup so you can enable HTTPS without providing a keystore. Combined with camel.ssl.trustAllCertificates=true, this gives you a working TLS setup with zero external files.
We also added camel.ssl.provider so you can select a specific JSSE provider. This is what makes PQC work on JDK 21 and 24 through BouncyCastle:
camel.ssl.provider=BCJSSE
Tracking issues
- CAMEL-23154 - Add PQC named groups support to SSLContextParameters
- CAMEL-23158 - Add PQC named groups and signature schemes to SSL configuration properties
- CAMEL-23159 - Add signatureSchemes to SSLContextParameters
Three proof-of-concept examples
To validate that the approach works in practice, I built three self-contained examples that each perform a real PQC TLS 1.3 handshake using X25519MLKEM768. The examples are available in the camel-pqc-tls repository.
Each example creates an SSLServerSocket and an SSLSocket, performs a TLS 1.3 handshake with PQC key exchange, and verifies the result. No external servers, no mock objects. A real handshake that either succeeds or fails.
All three examples use the same camel.ssl.* property-based configuration. The JDK version and provider differ, but the approach is consistent.
JDK 27: Native PQC in TLS
JDK 27 adds PQC named groups directly to SunJSSE via JEP 527. This is the cleanest path: no third-party TLS providers, no security property workarounds. Everything is built into the JDK.
The application bootstrap is minimal:
public class PQCSSLContextApplication {
public static void main(String[] args) throws Exception {
Main main = new Main();
main.run(args);
}
}
The entire PQC configuration lives in application.properties:
camel.server.enabled=true
camel.server.port=8443
camel.server.useGlobalSslContextParameters=true
camel.ssl.enabled=true
camel.ssl.secureSocketProtocol=TLSv1.3
camel.ssl.selfSigned=true
camel.ssl.trustAllCertificates=true
camel.ssl.namedGroups=X25519MLKEM768,x25519
camel.ssl.cipherSuites=TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256
That is it. No keystores to generate, no shell scripts to run. Camel generates a self-signed certificate at startup, configures PQC named groups, and serves HTTPS on port 8443.
The handshake verification in the route uses Groovy to access Camel’s global SSLContext:
def sslCtx = camelContext.getSSLContextParameters().createSSLContext(camelContext)
def serverSocket = sslCtx.getServerSocketFactory().createServerSocket(0)
def clientSocket = sslCtx.getSocketFactory().createSocket("localhost", serverSocket.getLocalPort())
def sslParams = clientSocket.getSSLParameters()
sslParams.setNamedGroups(["X25519MLKEM768"] as String[])
clientSocket.setSSLParameters(sslParams)
clientSocket.startHandshake()
The client offers only X25519MLKEM768 with no classical fallback, so a successful handshake definitively proves PQC was negotiated.
JDK 21 and JDK 24: BouncyCastle JSSE
For JDK 21 and 24, the JDK’s SunJSSE does not yet support PQC named groups in TLS. BouncyCastle’s JSSE provider (BCJSSE) bridges this gap.
The configuration is almost identical to JDK 27, with two additions: camel.ssl.provider=BCJSSE to select the BouncyCastle TLS provider, and secp256r1 in the named groups for ECDSA certificate verification:
camel.server.enabled=true
camel.server.port=8443
camel.server.useGlobalSslContextParameters=true
camel.ssl.enabled=true
camel.ssl.provider=BCJSSE
camel.ssl.selfSigned=true
camel.ssl.trustAllCertificates=true
camel.ssl.secureSocketProtocol=TLSv1.3
camel.ssl.namedGroups=X25519MLKEM768,secp256r1
The bootstrap class still needs to register the BouncyCastle providers before Camel starts, and remove ECDH from jdk.tls.disabledAlgorithms (which BCJSSE interprets broadly, preventing EC credentials from working):
public class PQCSSLContextApplication {
public static void main(String[] args) throws Exception {
String disabled = Security.getProperty("jdk.tls.disabledAlgorithms");
if (disabled != null) {
disabled = disabled.replaceAll(",\\s*ECDH\\b", "");
Security.setProperty("jdk.tls.disabledAlgorithms", disabled);
}
Security.addProvider(new BouncyCastleProvider());
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Main main = new Main();
main.run(args);
}
}
The route itself uses camelContext.getSSLContextParameters().createSSLContext(camelContext) to get the SSLContext, just like the JDK 27 example. The only difference is that on JDK 21, the standard SSLParameters.setNamedGroups() API does not exist yet, so the verification route uses BouncyCastle’s BCSSLParameters to set named groups on the client socket:
def sslCtx = camelContext.getSSLContextParameters().createSSLContext(camelContext)
def serverSocket = sslCtx.getServerSocketFactory().createServerSocket(0)
// ...
def bcClientParams = new org.bouncycastle.jsse.BCSSLParameters()
bcClientParams.setNamedGroups(["X25519MLKEM768", "secp256r1"] as String[])
((org.bouncycastle.jsse.BCSSLSocket) cs).setParameters(bcClientParams)
cs.startHandshake()
Earlier versions of these examples had to generate certificates programmatically, build keystores in memory, and create the SSLContext manually in Groovy. All of that is now handled by Camel’s camel.ssl.* configuration.
JDK 24 bonus: native ML-KEM
What makes the JDK 24 example unique is the /api/verify-kem endpoint, which exercises JDK 24’s native javax.crypto.KEM API directly. This endpoint runs cross-provider interoperability tests between the JDK’s built-in ML-KEM implementation and BouncyCastle’s, including key re-encoding through standard X.509/PKCS#8 formats.
One detail worth noting: cross-provider key interoperability requires re-encoding keys through standard formats. When you generate an ML-KEM key pair with one provider and pass the key objects directly to another provider’s KEM.getInstance(), you get an InvalidKeyException. The fix is to export the keys via getEncoded() and re-import them through X509EncodedKeySpec and PKCS8EncodedKeySpec:
def bcKf = KeyFactory.getInstance("ML-KEM", "BC")
def reEncodedPub = bcKf.generatePublic(
new X509EncodedKeySpec(jdkKp.getPublic().getEncoded()))
def reEncodedPriv = bcKf.generatePrivate(
new PKCS8EncodedKeySpec(jdkKp.getPrivate().getEncoded()))
This works because both providers use the same standard key encoding formats, even though their internal key representations differ.
Verifying PQC at the network level
Running the examples and seeing pqcVerified: true is one thing. Verifying at the network level that PQC key exchange actually happened is another.
You can use tshark to capture and inspect the TLS handshake:
tshark -i lo -f "tcp port 43567" -Y "tls.handshake" \
-V -o tls.keylog_file:/dev/null 2>/dev/null | \
grep -E "(Handshake Protocol|named_group|key_share|supported_groups)"
In the capture you will see the TLS 1.3 HelloRetryRequest flow. The client’s first ClientHello advertises X25519MLKEM768 in its supported_groups extension. The server agrees on the PQC group and sends a HelloRetryRequest asking the client to provide a key share for X25519MLKEM768. The client’s second ClientHello includes a 1216-byte key share, which is the combined X25519 (32 bytes) + ML-KEM-768 (1184 bytes) public key. That 1184-byte ML-KEM component is the unmistakable fingerprint of a post-quantum key exchange.
Side-by-side comparison
| Aspect |
JDK 27 Native |
JDK 24 + BouncyCastle |
JDK 21 + BouncyCastle |
| TLS provider |
SunJSSE |
BCJSSE 1.83 |
BCJSSE 1.83 |
| Configuration |
camel.ssl.* properties |
camel.ssl.* properties |
camel.ssl.* properties |
| Certificates |
camel.ssl.selfSigned=true |
camel.ssl.selfSigned=true |
camel.ssl.selfSigned=true |
| Native ML-KEM (KEM API) |
Yes |
Yes (JEP 496) |
No |
| REST transport |
HTTPS on 8443 |
HTTPS on 8443 |
HTTPS on 8443 |
| Extra dependencies |
None |
bcprov, bctls |
bcprov, bctls |
Running the examples
All three examples follow the same pattern: select the right JDK, build, and run.
JDK 27
cd pqc-ssl-context
sdk use java 27.ea.11-open
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
JDK 24
cd pqc-kem-jdk24
sdk use java 24.0.1-tem
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
curl -k https://localhost:8443/api/verify-kem
JDK 21
cd pqc-ssl-context-jdk21
sdk use java 21.0.10-tem
mvn clean compile exec:exec
curl -k https://localhost:8443/api/verify-pqc
All three return "pqcVerified": true when the PQC TLS handshake succeeds.
What is not done yet
I want to be clear: this work is not finished. What we have so far is the foundation, and there are real gaps remaining.
The changes so far are in Camel’s core SSL layer. Individual Camel components that use TLS (HTTP, Kafka, JMS, AMQP, and many others) still need to adopt these settings for PQC to actually work end-to-end with each connector. Some components delegate to their own TLS stacks (Netty, Vert.x, the Kafka client library) and will need specific integration work to pass through the PQC named groups. We have started this for Netty, which now has a PQC fallback that auto-applies named groups, but the rest is still ahead of us.
The camel.ssl.selfSigned=true option is useful for demos and development, but production deployments need proper certificates and keystores. The property-based configuration (camel.ssl.keyStore, camel.ssl.trustStore) is already there for that.
The auto-configuration on JDK 25+ is a nice default, but it only helps when the JVM and the remote peer both support PQC named groups. In practice, most peers today will not, and the hybrid approach (X25519MLKEM768 falling back to X25519) handles that gracefully. But we have not tested this against a wide variety of real-world TLS endpoints yet.
The BouncyCastle JSSE path (JDK 21/24) works but requires a bootstrap class to register providers and tweak security properties. We would like to make this smoother, perhaps through auto-detection or a Camel extension, but that is not built yet.
Finally, there is the question of PQC at the application layer (signing, encryption, key encapsulation), not just the transport layer. The camel-pqc component covers some of this, but integrating PQC signatures and key management into Camel’s broader security model is a longer-term effort.
Contributions are welcome if any of this interests you.
References
16 Feb 2026
9 min read
If you’ve ever tried to report a security vulnerability to an open source project, you know the feeling. You find something real, you write a detailed report, you follow responsible disclosure, and then… silence. Or worse, pushback. The maintainer tells you it’s not a real issue. The ticket gets closed. Sometimes you get a reply that feels almost hostile, as if you just insulted someone’s work instead of trying to help.
I’ve been involved in open source for a long time and I’ve seen this pattern play out more times than I’d like to admit. There’s a strange dynamic around CVEs in our industry: reporting a vulnerability should be a normal, healthy part of software development, but instead it often feels like an accusation. And for the maintainer on the receiving end, having a CVE allocated against their project can feel like a mark of shame.
This needs to change. And it needs to change fast, because the world around us is changing faster than our culture can keep up.
The reporting nightmare
Let’s talk about what it actually looks like to report a vulnerability to a project. The experience varies wildly depending on the project, but the friction is surprisingly common even in well-established ones.
First you need to figure out if the project even has a security policy. Some projects have a SECURITY.md file, some have a dedicated email, some have nothing at all. You might end up opening a public GitHub issue because there’s no private channel, which kind of defeats the purpose of responsible disclosure.
Then comes the triage. Many maintainers, especially in the smaller projects, are volunteers. They have day jobs. They maintain software because they care about it, not because they’re paid to handle security reports. So your report might sit for weeks. If the maintainer doesn’t agree with your assessment, you’re stuck in a back-and-forth that can drag on for months. I’ve seen cases where a perfectly valid vulnerability was dismissed simply because the maintainer didn’t consider the attack scenario realistic enough.
And the CVE system itself doesn’t help. It was designed in a world where software came from vendors with support contracts and SLAs. When you apply it to open source, the assumptions break down. There’s no commercial relationship, no obligation to respond, no dedicated security team. But the expectations from downstream users and scanners remain the same: if there’s a CVE, it must be fixed, and it must be fixed now.
The result is a system that creates pressure without providing resources. Maintainers feel attacked, reporters feel ignored, and the actual security of the software suffers.
The shame problem
Here’s something that doesn’t get discussed enough: many projects treat a CVE as a failure rather then a finding. There’s a cultural undercurrent that says having vulnerabilities in your code means you wrote bad code, that it reflects poorly on you as a developer.
This is wrong, and it’s counterproductive.
Every non-trivial piece of software has bugs. Some of those bugs have security implications. This is not a moral failure, its a statistical certainty. The Linux kernel gets CVEs. OpenSSL gets CVEs. The JDK gets CVEs. These are some of the most reviewed, most tested codebases on the planet. If they have vulnerabilities, your project will too.
But the stigma persists. I’ve seen maintainers downplay severity scores, argue that a bug isn’t exploitable when it clearly is, or simply refuse to acknowledge the issue. In some cases the motivation is understandable: a high-severity CVE can trigger automated alerts across thousands of organizations, creating a firestorm of support requests that a volunteer maintainer is simply not equipped to handle. But ignoring the problem doesn’t make it go away.
The Daniel Stenberg situation with curl is a good example of the other side of this coin. He has been vocal about the flood of bogus CVE reports, many of them AI-generated, that waste maintainer time. He estimates that about 20% of all security submissions to curl are now AI-generated noise, and the rate of genuine vulnerabilities has dropped to around 5%. For every real vulnerability, there are four fake ones. Each fake one consumes hours of expert time to disprove. This is a real problem, and it’s getting worse.
But the solution isn’t to build higher walls around vulnerability reporting. The solution is to build better processes and, more importantly, a better culture.
AI is changing the game whether we like it or not
Here’s where things get interesting and, honestly, a bit uncomfortable for our industry.
In the last couple of years AI systems have gone from theoretical vulnerability discovery to actually finding real zero-day vulnerabilities in production software. This isn’t hype. The evidence is concrete and growing.
Google’s Big Sleep project, a collaboration between Project Zero and DeepMind, found an exploitable stack buffer underflow in SQLite in late 2024. The Project Zero team noted that human researchers couldn’t rediscover the same vulnerability using traditional fuzzing even after 150 CPU hours of testing. Since then, Big Sleep has found over 20 vulnerabilities across projects like FFmpeg and ImageMagick, each discovered and reproduced by the AI agent without human intervention.
Anthropic published findings showing Claude discovering previously unknown vulnerabilities, including in GhostScript, where the model took a creative approach by reading through the Git commit history after more traditional analysis methods failed.
In late 2025 and early 2026, AI systems autonomously discovered zero-day vulnerabilities in Node.js and React. Not toy projects. Not contrived examples. Two of the most widely deployed pieces of JavaScript infrastructure in the world. The vulnerabilities were real, the exploits worked, and patches were necessary.
Researchers from the University of Illinois showed that teams of LLM agents working together could exploit zero-day vulnerabilities with meaningful success rates. Trend Micro’s AESIR platform has contributed to the discovery of 21 CVEs across NVIDIA, Tencent, and MLflow since mid-2025.
The trajectory is clear: AI-discovered CVEs jumped from around 300 in 2023 to over 450 in 2024, and exceeded 1,000 in 2025. This is not slowing down.
The FFmpeg wake-up call
The FFmpeg controversy in late 2025 perfectly illustrates the collision between old culture and new technology. Google’s Big Sleep found 13 vulnerabilities in FFmpeg alone. The volunteer maintainers were understandably frustrated: a trillion-dollar company was using AI to find bugs in their code and then dropping reports on them with a 90-day disclosure countdown, without providing patches or funding.
FFmpeg’s maintainers called some of the findings “CVE slop” and asked Google to either fund the project or stop burdening volunteers with security reports. One maintainer described a bug in a LucasArts Smush codec, an issue affecting only early versions of a 1990s game, flagged as a “medium-impact” security vulnerability. Nick Wellnhofer resigned as maintainer of libxml2, citing the unsustainable workload of addressing security reports without compensation.
The maintainers have a point. But the uncomfortable truth is that those vulnerabilities still exist in the code. The fact that finding them is now cheap and fast doesn’t make them less real.
We need a new culture
So where does this leave us? I think we need to rethink our relationship with security vulnerabilities from the ground up. Here what I believe needs to change.
Vulnerabilities are not failures
We need to stop treating CVEs as marks of shame. A vulnerability report should be treated like a bug report: a normal part of the software lifecycle. The projects that handle CVEs well, with transparency, clear communication, and timely fixes, should be seen as more trustworthy, not less.
The current system wasn’t designed for the open source world. We need better processes for triaging reports, especially now that AI tools can generate them at scale. The OpenSSF and OWASP are working on this, but progress is slow. We need clear guidelines for what constitutes a valid report, better tooling for maintainers to handle volume, and a way to distinguish between genuine findings and noise.
Funding must follow expectations
If the industry expects open source maintainers to handle security reports with the same rigor as commercial vendors, then funding needs to follow. You can’t demand enterprise-grade security response from volunteers working on their spare time. Organizations that depend on open source software need to invest in the projects they rely on. This means direct funding, dedicated security resources, or at minimum, contributing patches alongside vulnerability reports.
Security education needs an update
Most developers learn about security as a set of rules: don’t use eval, sanitize your inputs, use parameterized queries. This is necessary but not sufficient. We need to teach developers that vulnerabilities are inevitable, that finding them is good, and that the process of fixing them is a skill worth developing. Security should be part of the development culture, not an external audit that happens once a year.
Prepare for the AI flood
AI-powered vulnerability discovery is here and it’s only going to accelerate. Bruce Schneier has noted that the latest models can analyze substantial codebases and produce candidate vulnerabilities in hours or minutes, fundamentally altering the economics of vulnerability discovery. Multi-agent systems where specialized LLMs collaborate on code analysis, exploit development, and verification are outperforming single-model approaches.
This means every project, regardless of size, will face an increasing volume of vulnerability reports. We need to build the infrastructure, both technical and cultural, to handle this. That includes better automated triage, clearer severity standards, and a shared understanding that a rising CVE count doesn’t mean software is getting worse. It means we’re getting better at finding problems.
Conclusion
The security landscape is shifting under our feet. AI tools are finding real vulnerabilities in real software at a pace that human researchers can’t match. Our vulnerability reporting and handling processes, built for a slower era, are showing cracks everywhere.
The answer isn’t to dismiss the findings or shoot the messenger. It’s to grow up as an industry. Treat vulnerabilities as the normal engineering challenge they are. Fund the maintainers who keep critical infrastructure running. Reform the systems that create perverse incentives. And prepare for a world where the volume of discovered vulnerabilities will only increase.
We’ve been treating CVEs as something to be ashamed of. It’s time to start treating them as something to be proud of fixing.
References
15 Oct 2025
4 min read
In the rapidly evolving landscape of AI-powered applications, the ability to process and understand documents has become increasingly crucial. Whether you’re dealing with PDFs, Word documents, or PowerPoint presentations, extracting meaningful insights from unstructured data is a challenge many developers face daily.
In this post, we’ll explore how Apache Camel’s new AI components enable developers to build sophisticated RAG (Retrieval Augmented Generation) pipelines with minimal code. We’ll combine the power of Docling for document conversion with LangChain4j for AI orchestration, all orchestrated through Camel’s YAML DSL.
The Challenge: Document Intelligence at Scale
Companies are drowning in documents. Legal firms process contracts, healthcare providers manage medical records, and financial institutions analyze reports. The traditional approach of manual document review simply doesn’t scale.
So this a possible space where we could apply RAG and Apache Camel. The steps:
- Convert documents from any format to structured text
- Extract key insights and summaries
- Answer questions about document content
- Process documents in real-time as they arrive
This is where the combination of Docling and LangChain4j shines, and Apache Camel provides the perfect integration layer to bring them together.
Meet the Components
Camel-Docling: Enterprise Document Conversion
The camel-docling component integrates IBM’s Docling library, an AI-powered document parser that can handle various formats including PDF, Word, PowerPoint, and more. What makes Docling special is its ability to preserve document structure while converting to clean Markdown, HTML, or JSON.
Key features:
- Multiple Operations: Convert to Markdown, HTML, JSON, or extract structured data
- Flexible Deployment: Works with both CLI and API (docling-serve) modes
- Content Control: Return content directly in the message body or as file paths
- OCR Support: Handle scanned documents with optical character recognition
Camel-LangChain4j: AI Orchestration Made Simple
The camel-langchain4j-chat component provides seamless integration with Large Language Models through the LangChain4j framework. It supports various LLM providers including OpenAI, Ollama, and more.
Perfect for:
- Document analysis and summarization
- Question-answering systems
- Content generation
- RAG implementations
Building a RAG Pipeline with YAML
Let’s walk through a complete example that demonstrates the power of combining these components. Our goal is to create a system that automatically processes documents, analyzes them with AI, and generates comprehensive reports: a classic example.
Architecture Overview
The flow is straightforward:
- Watch a directory for new documents
- Convert documents to Markdown using Docling
- Send the converted content to an LLM for analysis
- Generate a comprehensive analysis report
- Clean up processed files
All of this is defined declaratively in YAML, making it easy to understand and modify.
Setting Up the Infrastructure
First, we need our services running. Thanks to camel infra command, this is pretty simple:
# Start Docling (if camel infra supports it)
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel infra run docling
# Start Ollama (if camel infra supports it)
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel infra run ollama
Or we could use docker
# Start Docling-Serve
$ docker run -d -p 5001:5001 --name docling-serve ghcr.io/docling-project/docling-serve:latest
# Start Ollama
$ docker run -d -p 11434:11434 --name ollama ollama/ollama:latest
# Pull orca-mini model
$ docker exec -it ollama ollama pull orca-mini
We could also use docker-compose:
$ docker compose up -d
$ docker exec -it ollama ollama pull orca-mini
Configuring the Chat Model
We use a Groovy script bean to configure our LangChain4j chat model:
- beans:
- name: chatModel
type: "#class:dev.langchain4j.model.ollama.OllamaChatModel"
scriptLanguage: groovy
script: |
import dev.langchain4j.model.ollama.OllamaChatModel
import static java.time.Duration.ofSeconds
return OllamaChatModel.builder()
.baseUrl("{{ollama.base.url}}")
.modelName("{{ollama.model.name}}")
.temperature(0.3)
.timeout(ofSeconds(120))
.build()
Notice how we use property placeholders ({{ollama.base.url}}) which Camel automatically resolves. This makes the configuration flexible and environment-agnostic.
The Main RAG Route
Here’s where the magic happens. The route watches for documents, processes them through Docling, and analyzes them with our LLM:
- route:
id: document-analysis-workflow
from:
uri: file:{{documents.directory}}
parameters:
include: ".*\\.(pdf|docx|pptx|html|md)"
noop: true
idempotent: true
steps:
- log: "Processing document: ${header.CamelFileName}"
# Convert GenericFile to file path
- setBody:
simple: "${body.file.absolutePath}"
# Convert to Markdown
- to:
uri: docling:CONVERT_TO_MARKDOWN
parameters:
useDoclingServe: true
doclingServeUrl: "{{docling.serve.url}}"
contentInBody: true
# Prepare AI prompt
- setBody:
simple: |
You are a helpful document analysis assistant. Please analyze
the following document and provide:
1. A brief summary (2-3 sentences)
2. Key topics and main points
3. Any important findings or conclusions
Document content:
${exchangeProperty.convertedMarkdown}
# Get AI analysis
- to:
uri: langchain4j-chat:analysis
parameters:
chatModel: "#chatModel"
Interactive Q&A API
We also provide an HTTP endpoint for asking questions about documents:
- route:
id: document-qa-api
from:
uri: platform-http:/api/ask
steps:
# Find latest document
# Convert with Docling
# Prepare RAG prompt with user question
# Get answer from LLM
This enables interactive workflows:
$ curl -X POST http://localhost:8080/api/ask \
-d "What are the main topics in this document?"
Future Enhancements
Possible developments could be:
- Vector Storage Integration: Combine with camel-langchain4j-embeddings to store document chunks in vector databases for more sophisticated retrieval.
- Multi-Model Workflows: Use different models for different tasks - fast models for classification, powerful models for analysis.
- Streaming Responses: For long documents, stream LLM responses back to the client as they’re generated.
- Custom Tools: Integrate camel-langchain4j-tools to give the LLM access to external data sources.
Try It Yourself
The complete example is available in the Apache Camel repository under camel-jbang-examples/docling-langchain4j-rag. To run it:
$ jbang -Dcamel.jbang.version=4.16.0-SNAPSHOT camel@apache/camel run \
--fresh \
--dep=camel:docling \
--dep=camel:langchain4j-chat \
--dep=camel:platform-http \
--dep=dev.langchain4j:langchain4j:1.6.0 \
--dep=dev.langchain4j:langchain4j-ollama:1.6.0 \
--properties=application.properties \
docling-langchain4j-rag.yaml
Don’t forget to copy the sample.md into the documents directory!
Watch the logs as your document is processed, analyzed, and cleaned up automatically!
Conclusion
The combination of Apache Camel’s integration capabilities, Docling’s document conversion power, and LangChain4j’s AI orchestration creates a compelling platform for building intelligent document processing systems.
What makes this especially powerful is the declarative nature of the solution. The entire workflow is defined in ~350 lines of readable YAML, making it easy to understand, modify, and extend.
We’d love to hear about what you build with these components. Share your experiences on the Apache Camel mailing list or join us on Zulip chat!
Stay tuned for more examples combining Camel’s growing AI component ecosystem. The future of integration is intelligent, and we’re just getting started.
Happy integrating!