ldap2: SNI-aware connections and a shared JNDI socket-factory classloader - #6396
Draft
beanuwave wants to merge 3 commits into
Draft
ldap2: SNI-aware connections and a shared JNDI socket-factory classloader#6396beanuwave wants to merge 3 commits into
beanuwave wants to merge 3 commits into
Conversation
Introduces gradle/fips.gradle as the single place where FIPS mode is decided and applied to the build's test surface: which test classes exist in each mode and the JVM arguments test workers need to actually be in FIPS mode. Mode is driven by the OPENSEARCH_FIPS_MODE environment variable, surfaced to production code through the new FipsMode enum. BC FIPS artifacts move to compileOnly in both modes (they are provided by OpenSearch core), bctls-fips is added, and the securityadmin standalone bundles now ship the BC FIPS jars in deps/. Because java_test.security registers BouncyCastleFipsProvider in every test JVM -- including non-FIPS runs -- any suite that touches JCA now leaves a "BC FIPS Entropy Daemon" thread behind, which RandomizedRunner reports as a leak. BCFipsEntropyDaemonFilter covers it; the framework's BouncyCastleThreadFilter does not. It is applied to the suites that trip over it here, and reused by later FIPS work. No FIPS variant test classes exist yet, so this otherwise lands inert: the default build is unchanged and fips.gradle currently selects nothing. 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>
Replaces the isPkcs11()-style branching in the SSL configuration layer with sealed pem/jdk/pkcs11 records for both key stores and trust stores, and moves PKCS#11 dispatch into those records. Store passwords are wrapped in a StorePassword type so they are redacted in toString() rather than leaking into logs. A PKCS#11 store lives on the token rather than on disk, so the path becomes optional throughout: KeyStoreUtils loads such stores with a null stream, and error messages name the token instead of a file. PemKeyReader learns the PKCS11 store type and validates that a PKCS#11 provider is actually registered. Trust store settings that a PKCS#11 configuration ignores now produce a warning instead of being silently dropped. 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>
…ader JNDI's LDAP provider never passes the target hostname to the SSLSocketFactory it instantiates (bcgit/bc-java#460), so an ldaps connection could not present an SNI extension and servers doing name-based virtual hosting returned the wrong certificate. SNISettingTLSSocketFactory carries the hostname through a ThreadLocal for the duration of the connect and sets it on the socket's SSL parameters; SniAwareConnection and HostnameAwareConnectionFactory drive it for the pooled and unpooled paths. The Java9CL classloader that worked around the provider's inability to see ldaptive's socket factory was private to LDAPAuthorizationBackend, so a reconnect from the ldap2 backend raised ClassNotFoundException. It is extracted as SocketFactoryClassLoader and shared by both backends. LDAPAuthorizationBackend also builds its PEM credentials through a keystore rather than createX509CredentialConfig, and stops setting the global com.sun.jndi.ldap.object.disableEndpointIdentification system property, which disabled hostname verification process-wide as a side effect of one connection. 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>
Contributor
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 26ce531. ⛔ Hard block: Issues at High severity or above will block this PR from merging.
The table above displays the top 10 most important findings. Pull Requests Author(s): Please update your Pull Request according to the report above. Repository Maintainer(s): You can Thanks. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Category: Bug fix, Enhancement
Two LDAPS defects plus the TLS plumbing they need. Independent of FIPS, though
FIPS is what surfaced them.
Key changes
SNISettingTLSSocketFactorysets the ClientHello SNI before the handshake so amulti-cert server serves the right certificate;
HostnameVerifyingTrustManagerchecks the returned certificate 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 plus endpoint identification, making the trust-manager check
redundant.
HostnameAwareConnectionFactorythreads the target hostname throughso SNI works on both the pooled and unpooled paths.
This works around the JNDI LDAP provider resolving hostnames to IPs before
socket creation (bcgit/bc-java#460).
ClassNotFoundException(bug fix). TheJava9CLclassloader that lets the JNDI provider resolve ldaptive's socket factory was
private to
LDAPAuthorizationBackend, so a reconnect from the ldap2 backendfailed. Extracted as
SocketFactoryClassLoaderand set on ldap2's JNDI providerconfig, so
PrivilegedProvider's thread-context swap resolves the factory.authorization paths). ldaptive's
create*CredentialConfigwith key aliasesroutes through
KeyStoreSSLContextInitializer.getKeyManagers(), which copies theprivate key into a fresh in-memory PKCS#12 store protected by SunJCE's
PBEWithHmacSHA256AndAES_256— unavailable in FIPS. Fix: build keystores fromPEM via
PemKeyReader.toTruststore/toKeystoreand pass null key aliases, sokmf.init(keystore, password)is called directly and the PKCS#12 branch isbypassed. (
LDAPAuthorizationBackendpreviously usedcreateX509CredentialConfig, which hit that path unconditionally.)SSLConfigConstants.ALLOWED_SSL_PROTOCOLSfor theirdefault
enabled_ssl_protocols.Reviewer call-outs
System.setProperty(disableEndpointIdentification, true)from the default
ldapbackend — now warn-only. Two reasons: (a) it alignsboth backends on the same logic (
ldap2never set it); (b) mutating a globalJVM property from application code is process-wide and load-order-dependent, so
one auth domain's
verify_hostnames: falsesilently reconfigured hostnamechecking for the entire JVM. Operators who need it must now set the
-Ddeliberately. This is a behaviour change for existing configs.
verify_hostnamesandtrust_allcoupled to the same verifier?verifyHostnames = !trustAll && <setting>, sotrust_all: trueforcesAllowAnyHostnameVerifieron top ofAllowAnyTrustManager. Chain validationand hostname matching are orthogonal; collapsing them means you cannot relax one
without the other, and it hides which layer a config change actually touches.
Worth untangling (not done here).
which would let this entire SNI 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 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.pemCheck List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.