Conversation
0d1943e to
23bcab1
Compare
23bcab1 to
846a4c3
Compare
846a4c3 to
dd48f9a
Compare
dd48f9a to
dafc362
Compare
PR Reviewer Guide 🔍(Review updated until commit 03cdcc4)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 03cdcc4 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 5579028
Suggestions up to commit 3ac26d1
Suggestions up to commit b5760ff
|
|
Persistent review updated to latest commit f908586 |
f908586 to
de757a3
Compare
|
Persistent review updated to latest commit de757a3 |
de757a3 to
b9f7617
Compare
|
Persistent review updated to latest commit b9f7617 |
|
Persistent review updated to latest commit b4fce9b |
b4fce9b to
8e3f614
Compare
|
Persistent review updated to latest commit 8e3f614 |
8e3f614 to
5c719f9
Compare
|
Persistent review updated to latest commit 5c719f9 |
5c719f9 to
b194efc
Compare
|
Persistent review updated to latest commit b194efc |
b194efc to
07f75d1
Compare
|
Persistent review updated to latest commit 07f75d1 |
|
Persistent review updated to latest commit ab97aab |
|
Persistent review updated to latest commit 905dbb1 |
905dbb1 to
b12e4f7
Compare
b12e4f7 to
8f6e1e1
Compare
|
Persistent review updated to latest commit 8f6e1e1 |
| byte[] decodedBytes = Base64.getDecoder().decode(encryptedString); | ||
| return new String(processWithCipher(decodedBytes, decryptCipher), StandardCharsets.UTF_8); | ||
| public EncryptionDecryptionUtil(final byte[] secretBytes) { | ||
| this.aesKey = deriveKey(secretBytes); |
There was a problem hiding this comment.
I think with this change, the upgrade path (rolling upgrade primarily) for existing clusters will not be possible, right?
There was a problem hiding this comment.
Yes, all existing OBO connections will be interrupted for up to 10 minutes after the rollout. If this maintenance window is inconvenient, I can work out a migration plan to avoid the disruption.
|
@beanuwave this is tremendous effort, thank you, a few high level comments:
Hope it make sense, thank you! |
|
@reta Thank you for pushing this through the review process - that's great! If you plan to do a second review, I'd really appreciate it if you could also take the "Reviewer call-outs" into account.
The FIPS test class abstraction is a no-brainer, since it would then follow the same style as the core project. However, I'm not sure the same general rules can be applied to production code - it's more of a case-by-case assessment.
Do you have a specific scenario in mind where a heterogeneous cluster would be advantageous and provide real-world value? Assuming it would be possible for a non-FIPS node to join a FIPS cluster, IMO that would introduce a weak link in the chain and undermine the FIPS security guarantees of the entire cluster. |
👍
It is probably could be summarized like that: how would we recommend to start with FIPS support. Only brand new clusters or gradual migrations.
Correct, tests are indeed no-brainer, the one which really clicked for me for PCKS11, I think we could have cleaner implementation, but certainly - case by case. |
|
Persistent review updated to latest commit b5760ff |
…th a StorePassword wrapper Signed-off-by: Iwan Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
b5760ff to
3ac26d1
Compare
|
Persistent review updated to latest commit 3ac26d1 |
…bout ignored trust store settings Signed-off-by: Iwan Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
|
Persistent review updated to latest commit 5579028 |
- ReactorHttpClient: flatMap(mono -> mono, parallelism) instead of ParallelFlux rails, so parallelism is a real concurrency limit — lets ResourceFocusedTests drop its FIPS-scaled parallelism/request counts - Removed sslProvider(SslProvider.JDK) (no tcnative on the classpath, JDK is already the default) - TestSecurityConfig / OnBehalfOfJwtAuthenticationTest: Randomness.createSecure() instead of new SecureRandom() - ApiTokenTest: reuse TestSecurityConfig.DEFAULT_TEST_PASSWORD as the OBO encryption key instead of the short hardcoded one Signed-off-by: Iwan Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
|
Persistent review updated to latest commit 03cdcc4 |
…replace their baselines Signed-off-by: Iwan Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
…nd test-worker JVM args into gradle/fips.gradle Signed-off-by: Iwan Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
|
@beanuwave Are you still working on this PR? If so could you please resolve conflicts? |
|
@DarshitChanpura I think this PR is now mostly useful as a historical archive, so we can probably close it. @reta To make the changes easier to review, I've split it into a stack of x5 PRs, each focused on a major topic. PRs #2, #3, and #4 are interchangeable and can be reviewed in any order. The only unresolved comment is related to OBO token wire-format break, which I'll try to fix soon. Everything else in the stack can already be reviewed.
|
Description
Category: Enhancement, New feature, Bug fix
This branch makes the Security plugin run correctly under FIPS approved-only mode (BCFIPS as the sole crypto provider). Every change traces to one of: an algorithm/format/usage that FIPS disallows (whether or not BCFIPS rejects it at runtime), a weakness surfaced during the audit, or build/packaging plumbing to make FIPS the always-available baseline.
At a glance:
FipsMode.isEnabled()(envOPENSEARCH_FIPS_MODE=true) replacesCryptoServicesRegistrar.isInApprovedOnlyMode()everywhere. Intent is decoupled from provider state and cross-checked at startup.compileOnly, shipped by core); the compile-timeFipsBuildParamsfork is gone. FIPS engages purely through thejava.securitythe JVM loads (no provider is registered in code) - the runtime launcher merges infips_java.securityonOPENSEARCH_FIPS_MODE=true, while tests swap it wholesale (-D...properties==<file>).PemKeyReaderrebuilt on BouncyCastle; TLSv1.1 dropped in FIPS mode.securityadminaccepts BCFKS/PKCS11 and now launches via core'sopensearch-cli.Key changes
OBO / JWT tokens
The OBO key path was audited end-to-end; the encryption rewrite is the FIPS trigger, the rest are weaknesses found alongside it.
EncryptionDecryptionUtil: built its cipher with the bareCipher.getInstance("AES")and a key forced to 16 bytes viaArrays.copyOf(silently truncating or zero-padding) -> now AES-256-GCM (random 12-byte IV, 128-bit tag), key derived via HKDF-SHA256. A bare"AES"transformation resolves to the JCE provider's default mode - ECB - which for data confidentiality violates NIST SP 800-38A2 (ECB is approved only for key wrapping, SP 800-38F3). The mode is never spelled out or configurable, and BC FIPS permits the ECB primitive at runtime - so this had to be caught by review, not the provider.encryption_keywould produce a nominal AES-256 key with sub-112-bit strength. A FIPS guard rejects input keying material < 32 bytes and zeroes the IKM after derivation.OnBehalfOfAuthenticatorinitializes lazily and atomically: the constructor is inert, the first token request triggers init, a failure logs once and declines - a bad key can't throw out of the constructor, propagate through config reload, and retry forever. MirrorsApiTokenAuthenticator.KeyUtils.loadKeyFromKeystore) instead of inline config, keeping the secrets out of cluster state. Relative*_keystore_pathvalues resolve against the node config dir (consistent with other security file settings). This is an SP 800-576 key-at-rest hardening (protecting keying material / limiting exposure), not a FIPS 140-31 requirement.OnBehalfOfSettings.toString()redacts thesigning_key/encryption_keyvalues so they no longer leak into logs.Keystores / TLS
PemKeyReaderrewritten onto BouncyCastle (PEMParser/JcaPEMKeyConverter/ PKCS8 decryptor) instead of raw JCE; adds BCFKS + PKCS11 and store-type auto-detection.SSLConfigConstants: both defaults are now FIPS-conditional - default store type is forced to BCFKS in FIPS, andALLOWED_SSL_PROTOCOLSdrops TLSv1.1 in FIPS. Both LDAP backends now reuseALLOWED_SSL_PROTOCOLSfor their defaultenabled_ssl_protocols.no encoding for key); SunJSSE delegates the handshake signature to the key's own provider (SunPKCS11). So when the keystore is PKCS#11,SslConfigurationbuilds theSSLContextagainst SunJSSE (SslContextBuilder.sslContextProvider(SunJSSE)).mainself-registered it at plugin load (OpenSearchSecuritySSLPlugin.tryAddSecurityProvider()->Security.addProvider(new BouncyCastleFipsProvider())); that method is removed. Providers now come solely from the activejava.securityfile (JCA lazy-loads them), so FIPS vs non-FIPS is a launch-time provider swap (BCJSSE vs SunJSSE) with no code branch - the security files are a core/distribution concern.LDAP
SNISettingTLSSocketFactorysets the ClientHello SNI before the handshake so a multi-cert server serves the right cert;HostnameVerifyingTrustManagerchecks the returned cert after. For IP targets the SNI factory early-returns (no SNI) and the trust manager is the only hostname check; for DNS it sets SNI + endpointIdentification (the trust-manager hostname check is then redundant).HostnameAwareConnectionFactorythreads the target hostname through so SNI works.create*CredentialConfigwith key aliases routes throughKeyStoreSSLContextInitializer.getKeyManagers(), which copies the private key into a fresh in-memory PKCS12 store; SunJCE protects it withPBEWithHmacSHA256AndAES_256, not available in FIPS. Fix: build BCFKS keystores from PEM viaPemKeyReader.toTruststore/toKeystoreand pass null key aliases, sokmf.init(keystore, password)is called directly and the PKCS12-copy branch is bypassed. (LDAPAuthorizationBackendpreviously usedcreateX509CredentialConfig, which hit the same path unconditionally.)Java9CLinto a sharedSocketFactoryClassLoader(resolves the JNDI socket-factory classes by name) and set it onldap2's JNDI provider config, soPrivilegedProvider's thread-context swap resolves the socket factory on reconnect. Fixes an ldap2 LDAPS reconnectClassNotFoundException.Auth hardening (found mid-audit)
HTTPSpnegoAuthenticator: no longer mutates globalSystem.setPropertydebug flags; stops logging the acceptor principal; properLoginContext.logout()+ decoded-header zeroing infinally.InternalAuthenticationBackend+PasswordHasher.getDummyHash(): not-found timing path now uses the configured hasher (closes user-enumeration side-channel under PBKDF2).PasswordValidator.FIPS_MIN_PASSWORD_LENGTH(14) now anchors both ends: FIPS raises an unsetrestapi.password_min_lengthto 14, and startup rejects a lower explicit value - otherwise the REST API accepts passwords the hasher then refuses.Randomness.createSecure()replacesnew SecureRandom()for OBO encryption, api-tokens, and user passwords - it resolves to the approved SP 800-90A4 DRBG in FIPS.UserServicegenerates 20-27 chars in FIPS (>=119 bits over the 62-char alphabet), 8-15 otherwise;char[]zeroed infinally.CLI
SecurityAdmin: accepts BCFKS/PKCS11; PKCS#11 PIN prompt. Launcher scripts now delegate to core's sharedopensearch-cli. Standalone bundle ships BCFIPS jars underdeps/.SecurityAdmin.buildPkcs11SslContextroutes PKCS#11 keys through SunJSSE client-side (same as the server TLS layer - see Keystores/TLS).Note
Reviewer call-outs
securityadminnow launches via core'sopensearch-cli- this targets the in-distribution path; the standalone bundle is no longer self-launching and is effectively deprecated.System.setProperty(disableEndpointIdentification, true)from the defaultldapbackend - now warn-only. Two reasons: (a) it aligns both backends on the same logic (ldap2never set it); (b) mutating a global JVM property from application code is bad practice - it's process-wide and load-order-dependent, so one auth-domain'sverify_hostnames: falsesilently reconfigured hostname checking for the whole JVM. Operators who need it must now set the-Dflag deliberately.verify_hostnamesandtrust_allcoupled to the same verifier? -verifyHostnames = !trustAll && <setting>, sotrust_all: trueforcesAllowAnyHostnameVerifieron top ofAllowAnyTrustManager. Chain validation and hostname matching are orthogonal concerns; collapsing them onto one code path means you can't relax one without the other and hides which layer a config change actually touches. Worth untangling so each parameter maps to exactly one verification layer.Core / distribution follow-ups
Not plugin changes - each fix belongs in core or the distribution, tracked here so it can be routed:
bctls-fipshas no socketconnectgrant. Core's basesecurity.policygrantsbc-fips/bcpkix-fipsbut notbctls-fips, so under the agent all outbound BCJSSE TLS (LDAPS, audit sinks, remote reindex/snapshot over https) is denied - and plugin policies can't grant a corelib/jar. Verified against a live LDAPS-over-FIPS cluster.LogConfiguratorsetsjava.util.logging.managerat runtime, too late for the BouncyCastle FIPS JSSE provider (which initialises JUL during bootstrap); its per-handshake INFO traces then leak toSystem.erras[WARN][stderr]spam. Fix in the distribution: set-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManageras a launch-time-Dinjvm.options, and defaultorg.bouncycastle.jssetowarnin the shippedlog4j2.properties.org.opensearch.fips.FipsModestill detects FIPS viaCryptoServicesRegistrar.isInApprovedOnlyMode()- the provider-state probe this branch replaced with the env var; it should follow.BCFipsEntropyDaemonFiltershould move into core's test framework. The local thread-filter accommodation for the "BC FIPS Entropy Daemon" thread belongs in core'sBouncyCastleThreadFilter, not this plugin's test sources.Additional notes
SecretKeyentries (engineSetKeyEntryrequiresPrivateKey) - relevant to the JWT-signing-key path; BCFKS is the only FIPS-approved store that holds secret keys.SunJGSSis deliberately retained for Kerberos/SPNEGO.SNISettingTLSSocketFactory+ theSniAwareConnectiondecorator + hostname ThreadLocal +HostnameAwareConnectionFactory) works around the JNDI LDAP provider resolving hostnames to IPs before socket creation (bcgit/bc-java#460); ldaptive 2.x's native (Netty) transport opens sockets with the real hostname, which would let this entire stack be deleted (out of scope here).Testing
The suite runs in non-FIPS mode by default. To exercise the FIPS code paths, set the environment variable before invoking Gradle:
OPENSEARCH_FIPS_MODE=true ./gradlew test integrationTestWhen set, the build swaps in the FIPS
java.securitypolicy (BCFIPS-only providers), enables-Dorg.bouncycastle.fips.approved_only=true, and points the JVM at the BCFKS truststore. FIPS-incompatible tests (BCrypt, Argon2, SAML, SSLv3, JKS/PKCS12, weak/short passwords) are auto-skipped via JUnit assumptions. Static bcrypt fixtures and their short demo passwords are rewritten to PBKDF2 and padded past the 14-char floor byFipsHashAdapter(a no-op outside FIPS), and a few timing-sensitive integ tests scale down under FIPS, where PBKDF2 logins and BCTLS handshakes are markedly slower.For a running cluster, select the FIPS-approved password hasher in
opensearch.yml(BCrypt/Argon2 are not available in approved-only mode):The demo hashes in
config/opensearch-security/internal_users.ymlare BCrypt, which won't verify under PBKDF2 - regenerate the hash for each test account (e.g. withtools/hash.sh) and replace it before applying the security config.Test securityadmin.sh with BCFKS + PKCS#11 keystores (SoftHSM)
Exercises a token-resident node TLS key (signed via SunJSSE) and
securityadminauthenticating with a PKCS#11 client key. This example is FIPS-specific, but adjusts easily to non-FIPS by registering a SunPKCS11 provider viaOPENSEARCH_JAVA_OPTS="-Djava.security.properties=$OPENSEARCH_HOME/config/java.security"instead ofOPENSEARCH_FIPS_MODE=true. All paths below are relative to$OPENSEARCH_HOME.Test LDAP authentication over LDAPS (SNI, hostname verification, mTLS)
Self-contained manual tests for the LDAP TLS changes: SNI / hostname verification, mutual TLS, and the TLS protocol floor. Authentication only - these changes don't touch authz code, so role resolution is out of scope (verify that against a real directory).
Run them against any LDAPS directory that supports mTLS (a client cert is required). The walkthrough uses a local UnboundID in-memory stand-in only because it's repeatable and trivial to set up - a convenience, not a requirement; substitute your own server anywhere it appears. Its setup lives in
LDAP_UNBOUNDID_STANDIN_GUIDE.md.TLS material is the OpenSearch install's own demo certs in
$OPENSEARCH_HOME/config/-esnode(server, SAN includeslocalhost),root-ca.pem(trust anchor),kirk(client). Run the node withOPENSEARCH_FIPS_MODE=true(omit for non-FIPS; the only observable difference is the TLS protocol floor).Test matrix. Run every scenario in all four cells - flip the backend on
type:; for FIPS setOPENSEARCH_FIPS_MODE=true(launcher loadsfips_java.security-> BCJSSE), for non-FIPS setOPENSEARCH_JAVA_OPTS="-Djava.security.properties=$OPENSEARCH_HOME/config/java.security"(BCFIPS stays declared - the FIPS installer converts the node stores to BCFKS - but TLS runs on SunJSSE). Launch-time provider swap, no code path (see Keystores / TLS). Outcomes are identical; only the protocol floor ([TLSv1.3, TLSv1.2]FIPS vs+ TLSv1.1non-FIPS) and the provider differ, so theProv*/TlsFatalAlertclass names in the excerpts are BCJSSE-only.ldap...ldap2.LDAPAuthenticationBackend2)Scenario 1 - hostname verification. Same trusted
esnodecert throughout; 1b-1d dial a name not in its SAN (echo "127.0.0.1 ldap-wrong.example.com" | sudo tee -a /etc/hosts, then sethosts: [ldap-wrong.example.com:8636]), so the only thing that can object is one of the two hostname guards: (1) ldaptive's verifier (verify_hostnames), (2) JNDI endpoint-id (-Dcom.sun.jndi.ldap.object.disableEndpointIdentification=trueinconfig/jvm.options; the plugin no longer sets it, only warns). Chain trust is valid throughout, so this isolates hostname checking - the untrusted-cert case is 2c. Apply + restart per row.verify_hostnamestruetrueDefaultHostnameVerifierfalseverify_hostnames: falsealone isn't enough)falseCleanup: remove the
/etc/hostsline + thejvm.optionsflag, restorehosts:+verify_hostnames: true. Never setdisableEndpointIdentification=truein production (it's process-wide).Scenario 2 - mTLS client authentication. Vary only the client cert / trust anchor; apply + restart per row, and restore
pemtrustedcas_filepath: root-ca.pemafter 2c. First generate the two "bad" credentials once - both self-signed, so neither chains to the demoroot-ca- intoconfig/:config.yml)pemcert/pemkey_filepath: kirk.*(baseline)pemcert/pemkey_filepath: untrusted-client.*pemtrustedcas_filepath: untrusted-ca.pemTest OnBehalfOf (OBO) token
Exercises OBO token issuance and verification against an already-running cluster, in three modes: keys inline in the dynamic config (Scenario A), held in a BCFKS keystore out of cluster state (Scenario B), or in a PKCS#12 keystore for non-FIPS builds (Scenario C). No restart needed -
-t configpushes only the dynamicon_behalf_ofblock, picked up live. Pick ONE scenario, editconfig.yml, then run the apply / issue / use steps. All paths are relative to$OPENSEARCH_HOME.Issues Resolved
Resolves RFC
Related to the series of FIPS PRs in the security plugin:
Check List
References