Repository navigation
[improve][pip] PIP-489: FIPS 140-3 compliance mode for Apache Pulsar - #26155
david-streamlio wants to merge 5 commits into
Conversation
|
I wanted to flag some overlap so we can keep the efforts aligned. PIP-478 has been in progress and, besides the asynchronous authentication interfaces on the client side, it covers the TLS transport aspects of Pulsar 5.0 on both the client and the server side. The dependency between the auth and the TLS parts is explained in the PIP document (PIP-478: #25890, discussion thread: https://lists.apache.org/thread/s9n9jksr9vqgn9o982zmnnkcxdcncy3f). On the TLS transport side there are more concerns than configuring the JCA provider. Stricter security compliance -- for example FIPS 140-3 Level 3 -- requires an HSM: the key material is stored in a hardware device and kept outside the application, so it never crosses into the Pulsar process. This can be achieved with the PulsarTlsFactory plugin interface introduced in PIP-478: a plugin implementation can integrate with a security solution that meets such requirements (for instance building the TLS context against an HSM via a PKCS#11 token, so the private key never leaves the device). For the simpler FIPS 140-3 Level 1 case, PIP-478 also lets you configure the JCA provider directly (for example a FIPS-validated provider such as BC-FIPS on the JDK TLS engine), and it wires the engine/provider selection through every server component and the client. PIP-478 also adds a PulsarHttpClient API. The Pulsar client today uses several HTTP clients -- for example authentication plugins such as OAuth2 that call a token endpoint -- which currently don't share a centralized configuration. The PulsarHttpClient API is added for authentication plugin implementations so that the TLS configuration of those HTTP clients can be controlled by the PulsarTlsFactory when there are special requirements, or handled through the Pulsar v5 client's TlsPolicy configuration when there are none. This keeps the TLS transport (and FIPS) configuration consistent across the client's outbound TLS, not only the binary protocol and the web/admin listeners. Given that overlap, I'd suggest that PIP-489 builds upon PIP-478 for the TLS transport aspects rather than defining a separate TLS transport configuration path. That would let PIP-489 concentrate on the broader FIPS-compliance concerns that PIP-478 intentionally leaves out of scope -- FIPS-approved algorithms in message encryption and authentication, the FIPS distribution/packaging, and a fail-fast FIPS-mode validation switch -- while reusing the TLS transport foundation. On the packaging point specifically: I don't think the Gradle migration actually lost anything essential there. BC and BC-FIPS can't co-exist on the classpath, so switching between them is really a matter of replacing the non-FIPS BC jars with the FIPS ones. The old bouncy-castle/bcfips module wasn't especially useful for that swap in practice -- a client can simply exclude the non-FIPS BC dependencies and add the FIPS ones, and on the server side it can be handled by keeping the non-FIPS and FIPS jars in separate directories and choosing which one to put on the classpath based on configuration. So the FIPS packaging story seems fairly tractable without reintroducing a dedicated swappable module. |
|
One more bit of history that may help scope PIP-489: FIPS / BouncyCastle-FIPS support in Pulsar has been partial and handled case by case in the past, rather than as a coherent, tested capability -- which is a big part of why a proper end-to-end FIPS design is worth doing. Two concrete examples:
The takeaway is that FIPS support in Pulsar has been incomplete and piecemeal: the TLS transport was never routed through a FIPS provider, and the crypto paths didn't consistently honor the FIPS provider when it was present. That's exactly the gap worth closing, and it splits naturally: PIP-478 covers the TLS-transport slice (a configurable engine plus any JCA provider via TlsPolicy.jcaProvider, and the PulsarTlsFactory plugin for HSM-backed / FIPS 140-3 Level 3 cases), and PIP-489 can cover the broader FIPS-mode concerns -- FIPS-approved algorithms in message encryption and authentication, packaging, and a fail-fast validation switch -- so the crypto paths consistently respect FIPS rather than silently falling back to non-validated providers. |
…ore/TLS-version/cipher validation and java.security guidance
|
Thanks @lhotari — this is exactly the alignment I was hoping to get before the VOTE, and I agree with the split you propose. I've updated the PIP (95ac5af) accordingly:
While revising I also tightened the One sequencing question for you: given PIP-489's phase 1 depends on PIP-478's |
… not missing machinery The first gap bullet claimed the BC/BC-FIPS swap "lost its packaging story in the Gradle migration", which reads as though the Maven-era swappable bouncy-castle-bc / bouncy-castle-bcfips NAR modules need resurrecting. They do not, and this PIP never proposed to: BC and BC-FIPS are mutually exclusive on a classpath, so the swap is a matter of shipping one set of jars instead of the other, and SecurityUtility's reflective loading already accepts either flavor. Raised on the DISCUSS thread. The bullet now concedes that mechanics up front, states explicitly that the NAR modules are not coming back, and locates the actual gap where it belongs: no supported route for the FIPS artifacts to reach a server runtime (the server distribution excludes bc-fips outright), no server-side coverage (the only consumer is a client-side JCA-swap test), and no documentation telling an operator which jars to swap. Also drops the pinned FIPS version numbers and the libs.versions.toml line reference, both of which go stale when the BouncyCastle upgrade in apache#26349 lands; the surviving citation is the distribution/server exclusion, which is the load-bearing evidence.
PIP-478 has landed since this proposal was written, and it moved or closed several of the things the Motivation section cites as current state. Reviewers who check the first claim and find it already fixed will discount the rest, so the audit is done against master rather than left to them. SecurityUtility is gone (apache#26322 deleted it with the rest of the PIP-337 TLS stack), and the PIP leaned on it in six places -- the Background paragraph establishing that Pulsar is already FIPS-capable at the JCA layer, the packaging jar-swap rationale, the EC curve allowlist, the fipsMode validator's first assertion, the High Level Design, and the integration test. Each now names JcaProviders, whose ResolvedBouncyCastleProvider(Provider, boolean fips) reports the flavour as a value rather than a static predicate. Its registration-first resolution order is recorded too: a provider an operator registered in java.security is preserved rather than shadowed by a classpath-constructed one, which is the property a FIPS deployment depends on. Motivation item 2 is closed. PulsarSslConfiguration, PulsarSslFactory and DefaultPulsarSslFactory no longer exist; every listener builds from a PulsarTlsFactory, and the broker binary listener carries engineProvider(TlsFactorySupport.engineProvider(conf.getTlsProvider())). Rewritten to what survives -- an unset tlsProvider still resolves to the native engine, because tcnative remains an unconditional pulsar-common dependency, so FIPS mode must pin the engine and drop the jar. Motivation item 3 is stale in form: the declared defaults are now empty rather than the literal Conscrypt, and unset *means* Conscrypt only through resolveWebJsseProvider, on the web listeners. The concern survives; the citations did not. Items 1, 4, 5 and 6 were re-verified and hold, with line numbers corrected. Item 1 gains the version catalog's own statement of the gap (libs.versions.toml:62-63). Adds the gap the PIP did not list: DefaultBrokerTlsFactory resolves its JSSE provider as resolveJsseProvider(null, ...), so the broker's jsseProvider and brokerClientJsseProvider keys are declared but ignored -- item 2's shape in a newer key. apache#26326 fixes it and adds the jcaProvider configuration surface that design (a) assumes, so the dependency is now stated rather than implied. Housekeeping: apache#26152 and apache#26153 are merged, not open; apache#26154 no longer claims the KeyManagerProxy half, which PIP-478 obsoleted; and the Configuration table's four pre-PIP-478 provider rows become two rows naming the keys fipsMode actually evaluates.
|
@lhotari picking this back up now that its dependency has landed. The sequencing question I left you in July — whether 489 could go through DISCUSS/VOTE in parallel I have since reconciled the document against post-478
The scope split you proposed is intact: 489 takes the TLS transport from 478's What it needs now is a review. It has none, and I have not opened a VOTE thread yet — starting one No urgency on my side — it has waited a month, another week will not hurt it. |
lhotari
left a comment
There was a problem hiding this comment.
Read the reconciled document at 1912da5dfa end to end and checked its current-state claims against master at bef7b8f9. This is the first review the PIP has had, so it is a long one; the short version is that the correction you asked me to check is right, the scope split holds, and there is more to do before a vote than the reconciliation itself.
A concrete classpath proposal for the standard distribution. I'd like Pulsar to ship the dependencies for both modes out of the box, with PULSAR_FIPS_MODE=true selecting the FIPS dependencies at startup. General Notes already mentions separate directories and startup selection (pip-489.md:633-637); I would make that the supported packaging design rather than requiring a manual jar swap or a separate tarball.
The distribution would contain three sibling directories:
lib/: dependencies shared by both modes, with neither BC flavor nor the mode-specific native TLS jars here.lib-fips/:bc-fips,bcpkix-fips,bcutil-fips, and any other dependencies selected for FIPS mode.lib-non-fips/: ordinary BC jars,netty-tcnative-boringssl-static, and Conscrypt (conscrypt-openjdk-uber).
To match the actual launcher: bin/pulsar currently appends $PULSAR_HOME/lib/* to PULSAR_CLASSPATH in its installed-distribution branch (pulsar:212-216). That block could become:
if [ -d "$PULSAR_HOME/lib" ]; then
PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib/*"
if [ "${PULSAR_FIPS_MODE:-false}" = "true" ]; then
PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib-fips/*"
else
PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib-non-fips/*"
fi
else
add_gradle_deps_to_classpath
fiThe dependency portion of the Java classpath would then be:
PULSAR_FIPS_MODE=true:
-cp "lib/*:lib-fips/*"
PULSAR_FIPS_MODE unset (or false):
-cp "lib/*:lib-non-fips/*"
These are dependency-only shorthand, not replacements for the entire -cp argument. The script still adds PULSAR_JAR, PULSAR_EXTRA_CLASSPATH, and the logging-configuration directory (pulsar:262-263), and passes the resulting PULSAR_CLASSPATH through OPTS="-cp $PULSAR_CLASSPATH $OPTS" (pulsar:321). With the distribution installed at /pulsar in Docker (Dockerfile:203), the selected dependency segment is /pulsar/lib/*:/pulsar/lib-fips/* or /pulsar/lib/*:/pulsar/lib-non-fips/*.
This also handles tcnative/BoringSSL and Conscrypt: package their jars in lib-non-fips/, and they remain in the distribution but are absent from the selected FIPS classpath. They must not also be copied into common lib/ or reintroduced through extra classpath settings. The PIP's grpc-netty-shaded/grpc-netty choice should follow the same separation so the shaded native copy is not left in the common dependencies. (We should replace grpc-netty-shaded with unshaded grpc-netty dependency in any case since the reasons to have it no longer exist.)
This sketch covers the installed distribution. The existing Gradle fallback, used when lib/ is absent, needs equivalent dependency selection for source-tree launches. The script also supplies this classpath as pulsar.functions.instance.classpath (pulsar:368); test both modes from the assembled archive, including a function runtime and any additional dependencies it loads. The provider configuration and approved-only settings described elsewhere in the PIP still apply.
The SecurityUtility.isBCFIPS() correction is right, in full. SecurityUtility is gone from the tree entirely - #26322 removed it on 2026-08-15 - ResolvedBouncyCastleProvider(Provider provider, boolean fips) is a record at JcaProviders.java:98, and the registration-first ordering is exactly what the new Background paragraph describes: an already-registered BC or BCFIPS provider wins before anything is constructed from the classpath.
One sentence worth adding while you are in that paragraph, because it turns two of your items from precautionary into necessary: the classpath fallback prefers the non-FIPS artifact - BouncyCastleProvider first, BouncyCastleFipsProvider only as a fallback. A deployment that assembles a FIPS classpath but still pulls bcprov in transitively therefore resolves silently to the non-validated provider, with nothing in the log to say so. That is the single strongest argument for both the packaging work in (b) and the fips() assertion in (g), and right now it is left implicit.
The scope split holds. Design (a) consumes TlsPolicy / PulsarTlsFactory / PulsarHttpClient and defines no TLS configuration of its own; the Configuration table is explicit that its two provider rows add no keys. The only residue is the Upgrade section's mention of the interim maintenance-branch keys (:572-573) and Alternative 6 deferring a webServiceTlsProvider default (:621-623) - both are PIP-478's to change rather than this PIP's, which is worth a clause so nobody reads them as deliverables here.
Other current-state claims I checked rather than took on trust, and that hold: the "false-confidence FIPS test" characterisation of tests/pulsar-client-test-bcfips is accurate (the module excludes the non-FIPS BouncyCastle jars but excludes neither netty-tcnative-boringssl-static nor Conscrypt, and sets no org.bouncycastle.fips.approved_only); the General Notes claim that JcaKeyStores never asks for JKS is right, with a qualifier that turns out to matter; the &s=v2: dispatch in (e) is unambiguous, for a better reason than the one given; and the full-hash-as-salt semantics in (d) are correct even though the method named does not exist.
The findings, in the order I would work them. Details are in the inline comments.
- (c)'s stated failure mode is wrong, and the opt-in is irreversible for the retention window. Old consumers do not land in "retry/DLQ paths" - the default action is
FAIL, which discards the message and never delivers or acks it. And rollback is conditioned on producer state when the constraint is really about persisted backlog, in every replicated cluster. - Phase 1's validator asserts things phase 2 delivers. On Basic auth it does worse than fail: it green-lights a broker whose password file it has just declared FIPS-clean and which cannot authenticate anyone.
fipsModescreens the configured keystore types, not the carrier the TLS stack builds - which on a PEM deployment is JKS or PKCS12, the two types the PIP rejects.- (d) collapses to deleting one
.substring(0, 2), and the API it builds its design around does not exist. - The packaging deliverable never says which
bc-fipsjar, and certified-versus-patched is the first question an auditor asks. The catalog already documents the tradeoff. The classpath example above makes startup selection concrete; inline 11 covers the packaging implications. - The reconciliation is itself about two weeks behind
master- #26326 and #26154 both merged before this commit was pushed, so the PIP no longer has any dependency on unmerged work, but five passages still say it does. - Shape questions I would settle before the key is public:
fipsModeas a boolean with an open-ended, unversioned check list;saslRoleTokenSignerAlgorithmencoding two axes in one enum. - (f) contradicts its own Motivation, and adds a hand-written SHA-1 for a hypothetical.
- Two missing Alternatives, one of which would change the shape of the whole proposal.
- Line-citation drift, plus a suggestion about citing symbols instead.
None of this is an argument against the proposal - the goal is right and most of the design is sound. Answers to the two questions at the end of your comment are in a reply on the conversation thread.
| |---|---| | ||
| | (a) TLS transport | Follows PIP-478's compatibility contract (`TlsPolicy` and the legacy keys' migration path). No default behavior change from this PIP; `fipsMode=true` only adds validation. | | ||
| | (b) Packaging | Standard distribution unchanged. FIPS variant is additive. | | ||
| | (c) Message crypto | Producer default remains SHA-1 OAEP → zero wire change until opted in. **Opt-in ordering constraint: upgrade all consumers of a topic before any producer sets `RSA_OAEP_SHA256`** — old consumers cannot unwrap SHA-256-wrapped keys (they will land in retry/DLQ paths per existing crypto-failure handling). Decrypt side reads both formats forever. | |
There was a problem hiding this comment.
[CORRECTNESS] (c)'s stated failure mode is not what the client does, and the rollback precondition cannot be satisfied
"they will land in retry/DLQ paths per existing crypto-failure handling" is not what the client does, and the difference matters for the operational contract in (c).
The default failure action is FAIL - ConsumerBuilderImpl.java:161-163 sets it when neither cryptoFailureAction nor decryptFailListener is configured - and the FAIL branch does this:
pulsar/pulsar-client/src/main/java/org/apache/pulsar/client/impl/ConsumerImpl.java
Lines 2092 to 2105 in bef7b8f
DecryptResult.discard() then returns out of messageReceived before any delivery, negative-ack, retry-topic or DLQ logic runs (ConsumerImpl.java:1478-1484). So a consumer that cannot unwrap a RSA_OAEP_SHA256 key does not dead-letter the message: it never sees it, never acks it, and the subscription's mark-delete position stops advancing past it. With ackTimeout set, unAckedMessageTracker.add turns that into an unbounded redelivery loop for the whole retention period. Neither outcome is visible as a DLQ an operator can drain.
Second, and I think more important: :585-587 makes rollback conditional on producer state, but the constraint is about persisted data. "provided no producer is still emitting RSA_OAEP_SHA256-wrapped keys" is not a condition anyone can establish - the wrapped keys are in the topic, so retention, backlog, replay from an earlier position, and (per :594-596) every geo-replicated copy keep SHA-256-wrapped messages unreadable to a rolled-back consumer for the whole retention window. There is no operational step that makes it true and no way to check it.
Both are fixable in prose, and the honest version is stronger than the current one:
- in the compatibility table at
:562, say what actually happens (the message is not delivered and the subscription stalls at it; with an ack timeout, it redelivers indefinitely) instead of "retry/DLQ paths"; - in Downgrade/Rollback, state the precondition in terms of backlog and retention across all replicated clusters, and say plainly that opting a producer into
RSA_OAEP_SHA256is not reversible until the last SHA-256-wrapped message has aged out everywhere.
If that is too sharp an edge for an opt-in, the alternative is a consumer capability signal rather than an ordering instruction - but I would rather see the ordering instruction stated accurately than replaced with machinery, given that it is opt-in and off by default.
| (P-256/P-384/P-521), RSA ≥ 2048 bits, no SHA-1 signatures — converting deep handshake | ||
| failures into actionable startup errors; | ||
| - Basic-auth password file (if that provider is enabled) contains only `$5$`/`$6$` entries; | ||
| - `saslRoleTokenSignerAlgorithm` is `HmacSHA256`/`HmacSHA256-strict` (if SASL is enabled). |
There was a problem hiding this comment.
[DESIGN] the phase-1 validator asserts phase-2 features, and on Basic auth it green-lights a broker that cannot authenticate anyone
These two checks assert features that phase 2 delivers, while (g) itself is phase 1 (:647-652).
saslRoleTokenSignerAlgorithmis introduced by (e), phase 2. In a phase-1 release the key does not exist, so this check is either dead or unsatisfiable -fipsMode=truewith SASL enabled could never start.- The Basic-auth check is worse than unsatisfiable, because it passes. (d) - phase 2 - is what teaches
AuthenticationProviderBasicto verify$5$/$6$entries. In a phase-1 release a$6$-only password file satisfies this bullet, the broker starts and logs the security-posture summary at:527-532saying the FIPS check passed, and every login then fails, becauseauthenticatestill routes the entry to the DES branch:
A validator that green-lights a broker which cannot authenticate anyone is the same class of outcome as the silent non-compliance the fail-fast design exists to prevent (:543-545) - it just fails in the other direction.
Either move these two checks into phase 2 beside the features they assert, or pull (d) and (e) into phase 1. Either way, "Each phase is independently shippable" at :652 needs replacing with the actual dependency statement - the phases are shippable in order, which is a weaker and true claim.
| - every configured keystore/truststore type is FIPS-capable — BCFKS, PKCS11, or PEM. `JKS` is | ||
| rejected (its integrity/privacy algorithms are not FIPS-approved), and `PKCS12` is rejected | ||
| and documented as unsupported (FIPS-conformant PKCS#12 requires the RFC 9879 KDF/PBMAC | ||
| profile, which the current parsing stack does not enforce); |
There was a problem hiding this comment.
[DESIGN] fipsMode screens the configured keystore types, but the carrier keystore the TLS stack builds for PEM material is JKS or PKCS12
This bullet screens the operator-facing tlsKeyStoreType / tlsTrustStoreType family. A PEM deployment - which the High Level Design explicitly blesses at :216-217 ("Keystores may be PEM, BCFKS, or PKCS11") - configures no store type at all, so it satisfies the check vacuously. But the JDK TLS path still materialises PEM material into a process-local carrier keystore, and that carrier's type is decided here:
With no jcaProvider pinned, inMemoryStoreType returns the caller's default - and there are two callers with two different defaults:
KeyStoreHolder, whichJdkSslContextsuses to build the key and trust managers, passesKeyStore.getDefaultType(), controlled by the JVM'skeystore.typesetting;TlsKeyStoreLoader.toInMemoryKeyStorepasses the literal"PKCS12".
A caller default of JKS or PKCS12, and the explicit PKCS12 carrier, would use types this bullet rejects. As written, fipsMode=true can pass on a deployment whose TLS key material is sitting in a JKS carrier - the "FIPS-shaped rather than FIPS-compliant" outcome the switch exists to prevent.
The keystore.type=bcfks guidance in the deployment guide fixes the first, because it changes KeyStore.getDefaultType(); it does not touch the second, which is a hardcoded literal. And a java.security line an operator may or may not have written is not what a fail-fast validator is for.
The fix makes the validator stronger rather than longer: have fipsMode require jcaProvider (and brokerClientJcaProvider) to be pinned to a provider that registers BCFKS, instead of only screening configured store types. Under a pinned BC-FIPS provider inMemoryStoreType selects BCFKS at both call sites and the problem disappears. It also resolves an ambiguity in the current wording: "PKCS12 is rejected" does not say whether it reaches the carrier, and if it does, the PKCS12 entry in the carrier preference list has to be unreachable in FIPS mode rather than merely second choice.
| salts are variable-length and may carry a `rounds=N$` prefix (e.g. | ||
| `$6$rounds=10000$saltsalt$hash`). The implementation passes the entire stored hash as the salt | ||
| argument (`Sha2Crypt.crypt(password, storedHash)` semantics), which commons-codec resolves | ||
| correctly, then constant-time-compares the result — mirroring how the `$apr1$` branch works but |
There was a problem hiding this comment.
[SIMPLIFICATION] (d) collapses to deleting one .substring(0, 2); the API it is designed around does not exist
Worth checking this section against commons-codec before writing any of it: Crypt.crypt(byte[], String) already is the prefix dispatcher this design proposes to build. From the bytecode of the 1.21.0 jar this build resolves (the catalog pins commons-codec = "1.22.1"; the dispatch has been there for many releases):
crypt(byte[] keyBytes, String salt):
salt == null -> Sha2Crypt.sha512Crypt(keyBytes)
salt.startsWith("$6$") -> Sha2Crypt.sha512Crypt(keyBytes, salt)
salt.startsWith("$5$") -> Sha2Crypt.sha256Crypt(keyBytes, salt)
salt.startsWith("$1$") -> Md5Crypt.md5Crypt(keyBytes, salt)
otherwise -> UnixCrypt.crypt(keyBytes, salt) // DES
The only reason $5$/$6$ do not work in Pulsar today is that the existing call truncates the salt to two characters:
So the whole of (d)'s new code is: pass encryptedPassword where encryptedPassword.substring(0, 2) is passed now. That is a much better story for the PIP than the one currently written, and it removes the need for the design paragraph at :367-373 entirely.
Two corrections that follow:
Sha2Crypt.crypt(password, storedHash)(:372) does not exist.Sha2Crypt's public surface issha256Crypt(byte[]),sha256Crypt(byte[], String),sha256Crypt(byte[], String, Random)and the threesha512Cryptoverloads - nocrypt. The semantics the sentence describes are right (Sha2Crypt.SALT_PATTERNends in.*, so passing the full stored hash as the salt works,rounds=N$included); only the entry point is wrong, and the right one isCrypt.crypt.- The same one-line change also starts accepting
$1$MD5-crypt entries, which today reach the DES branch with a 2-character salt and therefore can never verify. An entry that has always denied every password would start allowing the right one. That is a real behaviour change and belongs in the compatibility table at:563- and it is a good argument forfipsMode's startup scan rejecting$1$and$apr1$explicitly rather than by omission.
What survives from (d) is the part that is genuinely Pulsar's: the fipsMode startup scan of the password file, and the choice to validate at startup rather than per request.
| [discussion](https://lists.apache.org/thread/s9n9jksr9vqgn9o982zmnnkcxdcncy3f)) reworks the TLS | ||
| transport configuration for Pulsar 5.0 on both the client and the server. **It has since landed on | ||
| `master`**, which closes several gaps this PIP originally enumerated (noted per item below); the | ||
| remaining follow-ups, including the `jcaProvider` configuration surface, are in |
There was a problem hiding this comment.
[ACCURACY] every remaining dependency on unmerged work has merged; five passages still describe master as it stood in August
#26326 merged on 2026-08-27 and #26154 merged on 2026-08-28 - both before this reconciliation commit was pushed. The net effect is good for the PIP: it now has no dependency on unmerged work at all. But five passages still describe master as it stood in August, and one of them is a falsifiable claim about code.
- This line - "the remaining follow-ups, including the
jcaProviderconfiguration surface, are in#26326". :159-162- "the outstanding piece it does depend on is#26326, which honors thejsseProviderkeys and adds thejcaProviderones.":241-243- "onmastertoday the axis is reachable only programmatically through the v5 builder". It is not:ServiceConfiguration.getJcaProvider()is read atDefaultBrokerTlsFactory.java:186, andconf/proxy.confships a commented-outjcaProvider=key.:257-261- "DefaultBrokerTlsFactoryresolves its JSSE provider asresolveJsseProvider(null, conf.getTlsProvider()), passingnullfor the explicit axis, so the broker's ownjsseProvider/brokerClientJsseProviderkeys are declared but ignored".masterreadsDefaultBrokerTlsFactory.java:148-resolveJsseProvider(conf.getJsseProvider(), conf.getTlsProvider())- and the broker-client leg readsconf.getBrokerClientJsseProvider()a few lines below. The whole bullet, including "FIPS mode depends on that axis, so this PIP assumes#26326", can go.:501-502and:506-508- the Configuration table's two provider rows ("Declared but ignored onmaster", "Added by#26326") and the paragraph beneath it.
Worth fixing for exactly the reason the July reconciliation was worth doing: the gap list is the current-state claim a voter checks, and this one is now checkable against a tree that says something else.
| or unset; each comparison already uses `MessageDigest.isEqual` (constant-time) and continues to. | ||
| - New setting `saslRoleTokenSignerAlgorithm` = `SHA-512` (default, sign legacy / verify both) | | ||
| `HmacSHA256` (sign HMAC / verify both) | `HmacSHA256-strict` (sign and verify HMAC only; | ||
| implied by `fipsMode=true`). |
There was a problem hiding this comment.
[DESIGN] saslRoleTokenSignerAlgorithm encodes two axes in one enum, and one of its values duplicates fipsMode
saslRoleTokenSignerAlgorithm encodes two independent axes in one enum: which algorithm to sign with, and whether to accept legacy signatures. That shape does not survive a second algorithm - HmacSHA384 would need both HmacSHA384 and HmacSHA384-strict, and the value space becomes a mix of a digest name (SHA-512, which is not a MAC algorithm name), MAC names, and a policy suffix. A 3-value enum that is really 2xN is the kind of thing worth getting right before the key is public.
-strict also duplicates something the design already expresses: :398 says it is implied by fipsMode=true, and the PIP does not say whether a non-FIPS operator who wants strict mode exists. If they do, name them and give it its own boolean; if they do not, saslRoleTokenSignerAlgorithm = SHA-512 | HmacSHA256 for the signing algorithm and fipsMode for legacy acceptance covers everything with one fewer value and no suffix grammar.
Separately, and as a positive: the &s=v2: dispatch claim at :391-393 is sound, but for a better reason than the one given. The document argues from the Base64 alphabet's "output position", which is a bit indirect. The direct arguments are that SaslRoleToken.toString() emits u=<role>&i=<session>&e=<expires> - the session attribute is i, not s - so &s= occurs exactly once; that verifyAndExtract uses lastIndexOf(SIGNATURE) regardless; and that the legacy signature is Base64.getEncoder() output over a 64-byte digest, so 88 characters from A-Za-z0-9+/=, containing neither : nor &. Stating those three makes the claim robust against a future change to SaslRoleToken's attribute letters, which the current phrasing would not survive.
| crypto.** A deployment can pair a validated OpenSSL 3 FIPS provider (driving a native TLS | ||
| engine) with BC-FIPS for the Java crypto paths. Viable for a curated image, but for the | ||
| upstream distribution it doubles the validated-module boundary to document and keeps a | ||
| native TLS engine inside the audit scope; the single-module JDK-over-BC-FIPS path is |
There was a problem hiding this comment.
[DESIGN] two alternatives are missing, one of which would change the shape of the whole proposal
Two alternatives are missing, and one of them would change the shape of the whole proposal.
A validated JVM or PKCS#11 provider instead of BC-FIPS. SunPKCS11 over a CMVP-validated NSS or OpenSSL FIPS token, or a vendor JDK that ships a validated provider, is a mainstream FedRAMP path. It would avoid the BC jar swap entirely - and with it the shaded-client conflict at :290-294, the second tarball at :283-285, and the certified-versus-patched bc-fips question. The PIP treats BC-FIPS as the only Java-crypto answer without arguing why, and it is already assuming half the mechanism: PIP-478's PulsarTlsFactory PKCS#11 hook is cited at :243-245 for the Level 3 case. Even if the answer is "BC-FIPS, because the jar swap is simpler than provisioning a token", that argument is worth making where a reader can weigh it.
Documentation and tests only - no fipsMode key, no distribution variant. Given Motivation 1's own "the gap is not missing machinery" (:91-92) and "what is missing is that nothing in the project supports, exercises, or documents the swap" (:94), this is the honest minimal competitor, and nothing in the PIP currently argues against it. It is also the baseline every new surface in this proposal should be justified against - which is a useful discipline for the review, not a criticism of the goal.
While here: Alternative 5 (:618-620) reads as a strawman - nobody is proposing a fork with different defaults, and the phase-2 distribution/server-fips plus the grpc-netty substitution at :295-300 already make this PIP a partial instance of the second-artifact cost it rejects there.
| 2. **Published variant (phase 2).** A `distribution/server-fips` module producing | ||
| `apache-pulsar-<version>-fips-bin.tar.gz` from the `fipsRuntimeClasspath`, wired into CI. | ||
| Whether to *publish* this to dist.apache.org (vs. providing the build recipe) is a release- | ||
| management question flagged for the DISCUSS thread. |
There was a problem hiding this comment.
[SCOPE] make startup selection of bundled mode-specific dependencies the standard distribution path
General Notes already describes separate provider directories and selecting one at startup (pip-489.md:633-637). Could we promote that to the supported path in the standard distribution? Section (b) still specifies manual jar replacement and an optional second tarball, without committing to bundle both choices.
The review body gives the concrete launcher example: keep common dependencies in lib/, ship BC-FIPS in lib-fips/, and ordinary BC plus netty-tcnative-boringssl-static and Conscrypt in lib-non-fips/. PULSAR_FIPS_MODE=true selects lib/*:lib-fips/*; unset or false selects lib/*:lib-non-fips/*. The change appends the selected directory to PULSAR_CLASSPATH in the existing distribution branch, preserving the launcher's later additions. This supplies both modes in one distribution while keeping the other mode's jars off the selected classpath.
The directory split implements the tcnative/Conscrypt classpath exclusions; the gRPC substitution already specified in (b) needs the same mode-specific placement. Approved-only mode, provider configuration and fipsMode validation still apply. This is packaging support, not a claim that the environment variable alone establishes compliance. Please clarify whether the requirement at pip-489.md:295-308 to ship no BoringSSL object code rules out bundling inactive ordinary-mode dependencies in the same archive.
Both startup modes should be tested from the assembled archive, including provider exclusivity and a function runtime. The proposed tests (pip-489.md:463-472) omit the grpc-netty-shaded to grpc-netty substitution, which couples the function control channel to Pulsar's independently pinned Netty.
| exist in `bc-fips` — so on a FIPS classpath the class fails to load | ||
| (`NoClassDefFoundError`) even for RSA-only users. | ||
| - The `SecureRandom` is pinned to `NativePRNGNonBlocking` (line 128) rather than the BC-FIPS | ||
| DRBG (the RNG fix is one of the in-flight quick-win PRs; see *General Notes*). |
There was a problem hiding this comment.
[ACCURACY] (c)'s SecureRandom bullet describes a gap #26154 closed on 2026-08-28
MessageCryptoBc no longer pins NativePRNGNonBlocking. Since #26154 merged on 2026-08-28 it prefers the BC-FIPS DRBG whenever the BCFIPS provider is registered, and NativePRNGNonBlocking is the fallback for everyone else:
So this bullet belongs in the "already closed" column rather than the gap list, and the "(open)" beside #26154 in General Notes (:642-643) wants updating too. That shortens (c) rather than lengthening it.
One completeness point while you are in this list: the org.bouncycastle.jce.* bullet above names four imports at lines 74-77; there is a fifth immediately below, MessageCryptoBc.java:78 (org.bouncycastle.jce.spec.IESParameterSpec), equally absent from bc-fips. Since the whole point of that item is that the class fails to load, the complete list is the useful one.
| version catalog still declares `bc-fips`, `bcpkix-fips` and `bcutil-fips` — and now says so | ||
| in as many words, describing them as "Test-only in this build" because "the server distribution | ||
| excludes bc-fips and ships the non-FIPS provider, so a FIPS deployment assembles its own | ||
| classpath" (`gradle/libs.versions.toml:62-63`). The distribution bundles the non-FIPS |
There was a problem hiding this comment.
[NIT] six line citations have drifted; the rest are exact
Checked every file:line in the document against master. Six have drifted:
| the PIP says | on master |
|---|---|
libs.versions.toml 62-63 (this line) |
the quoted comment is at 63-64; 62 is bouncycastle-bcprov |
libs.versions.toml 75-77 (:274) |
the three version declarations are at 76-78; 75 is a comment |
ProxyConfiguration line 343 (:127) |
webServiceTlsProvider = "" is at 344 |
conf/proxy.conf line 138 (:128) |
conf/proxy.conf:145 |
MessageCryptoBc ECIES "lines 88, 374, 536" (:318) |
88, 397, 559 |
MessageCryptoBc SecureRandom line 128 (:323) |
the call moved and changed meaning - separate comment |
These are exact and unchanged, for what it is worth - the drift is not systematic:
distribution/server/build.gradle.kts:154 and :59
pulsar-common/build.gradle.kts:185-190
DefaultBrokerTlsFactory.java:112 (the sentence quoted from its comment is verbatim)
ServiceConfiguration:217
WebSocketProxyConfiguration:256
MessageCryptoBc:92 and :74-77
AuthenticationProviderBasic:141-149
SaslRoleTokenSigner:92-104 (and MessageDigest.isEqual at :79, as (e) says)
KubernetesRuntime:1141 and :322, 511, 601, 730, 886
pulsar-functions/runtime/build.gradle.kts:63
A suggestion rather than a request: a PIP outlives its line numbers, and this document is on its second reconciliation pass largely because of them. Citing the symbol - DefaultBrokerTlsFactory.fromServiceConfiguration, JcaKeyStores.inMemoryStoreType, AuthenticationProviderBasic.authenticate - except where the line genuinely is the point would make the next pass much cheaper, and would have survived both #26322 and #26326 untouched.
|
Thanks for picking it back up, and no need to apologise about the July thread - the sequencing question did answer itself, and nothing was sitting with me. On the On whether it is ready for a
Worth knowing before you revise, because it makes (d) much smaller: The rest are shape questions I would rather raise now than after the key is public - I would do that pass and then open the vote without waiting for further reviews. The DISCUSS thread has three messages in it and two of them are mine; a vote is more likely to draw readers out than another month of waiting. On who else could review - the concrete name is @merlimat: he reviewed PIP-478 (#25890), so he already holds the context this proposal builds on, and his +1 is binding. Past that it is honestly a question of who has time right now rather than one I can answer on anyone's behalf. What I would do in your position is nudge the DISCUSS thread with the two points that most want a second opinion - the packaging tradeoff and the scope of the The goal here is right and most of the design is sound; none of the above is an argument against the proposal. |
PIP: this PR
Motivation
Pulsar has no supported FIPS 140-3 deployment path today, despite being most of the way there at the JCA layer (
SecurityUtilityalready resolves BC vs. BC-FIPS reflectively). A full audit ofmasterfound the gaps documented in the proposal: the BC/BC-FIPS swap lost its packaging story in the Gradle migration; the broker binary listener cannot be steered off BoringSSL (tlsProvideris declared but never wired); Conscrypt is the shipped web-TLS default; several security paths use non-approved algorithms (SHA-1 OAEP and ECIES inMessageCryptoBc, MD5-crypt/DES-crypt in Basic auth, a non-HMAC construction inSaslRoleTokenSigner); and there is no FIPS documentation or meaningful FIPS test coverage.Modifications
Adds
pip/pip-489.md, proposing a supported, tested, documented FIPS mode deployment profile: TLS provider wiring for every listener (matching the proxy's existing pattern), a FIPS distribution variant / documented jar swap, afipsModefail-fast startup validator, metadata-negotiated migration toRSA_OAEP_SHA256key wrapping (withECDH_AES_KWreplacing ECIES in a later phase), SHA-2-crypt Basic auth, a dual-verify HMAC migration for the SASL role-token signer, a name-stable fix for the SHA-1-derived Kubernetes resource names, and a real FIPS TLS integration test group.Companion quick-win PRs already open (intentionally outside the PIP): #26152, #26153, #26154.
A
DISCUSSthread has been started on [email protected] referencing this PR.