Conversation
PR Code Analyzer ❗AI-powered 'Code-Diff-Analyzer' found issues on commit 715a21c. ⛔ 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. |
5bd14ff to
dec2ca3
Compare
d32bfbe to
094236e
Compare
PR Reviewer Guide 🔍(Review updated until commit 715a21c)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 715a21c Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 4e3396c
Suggestions up to commit f6b50ef
Suggestions up to commit 9f8fb30
Suggestions up to commit 094236e
|
094236e to
9f8fb30
Compare
|
Persistent review updated to latest commit 9f8fb30 |
|
Persistent review updated to latest commit f6b50ef |
f6b50ef to
4e3396c
Compare
|
Persistent review updated to latest commit 4e3396c |
…ypto The on-behalf-of signing/encryption secret could previously only be supplied as a base64 string in the cluster configuration. KeyUtils.loadKeyFromKeystore adds a keystore-backed alternative, configured through <prefix>_keystore_path / _keystore_type / _keystore_alias / _keystore_password / _keystore_key_password, with relative paths resolved against the node config directory. PemKeyReader.loadSecretKeyFromKeystore does the actual lookup and rejects entries that are not SecretKeys. EncryptionDecryptionUtil now derives its key lazily and fails closed, enforces a minimum input-keying-material length, and zeroizes key material after use. Its toString is redacted so the secret cannot reach a log through an accidental interpolation. BREAKING: the AES-GCM encryption format has changed, so on-behalf-of tokens issued by an earlier version can no longer be decrypted and must be reissued. 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>
Moving the encrypted roles claim from AES/ECB to AES/GCM changed its wire format. In a mixed cluster a token issued by one node and verified by another would be rejected with 401, in both directions, for as long as the upgrade takes plus one token lifetime. Decryption tries AES-GCM first and falls back to the legacy format on a tag mismatch; the JWT signature is already verified at that point, so the fallback is no forgery risk. While a node without AES-GCM support is in the cluster, encryption keeps writing the legacy format so that node can read what upgraded nodes issue. Reading stays open for one maximum token lifetime after the last such node leaves, measured per node from the cluster state change that removed it rather than from the last legacy token read. The legacy transformation is spelled out as AES/ECB/PKCS5Padding instead of relying on the provider default for "AES". Upgraded nodes are recognised by capability, not version: each node advertises the internal node attribute security.internal.obo_roles_aes_gcm, set by the plugin. Operators must not configure it; any value other than the plugin's own is refused at startup. This keeps the gates correct whatever release the change lands in or is backported to. In FIPS mode the legacy format is read but never written (NIST SP 800-38A), and the first legacy token read is logged as a warning. Pre-upgrade nodes in a FIPS cluster therefore reject tokens from upgraded nodes with 401 until they are upgraded too. The transitional code lives in LegacyRolesClaimFormat and its tests, with small hooks in EncryptionDecryptionUtil, OnBehalfOfAuthenticator, SecurityTokenManager, ClusterInfoHolder and the plugin; it is to be removed with the next major version. 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>
…erialFilter() 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>
4e3396c to
715a21c
Compare
|
Persistent review updated to latest commit 715a21c |
Description
Category: Bug fix, Enhancement
The OBO key path was audited end to end. The encryption rewrite is the FIPS trigger; the rest are weaknesses found alongside it. A second commit keeps OBO tokens valid across a rolling upgrade despite the wire-format change the rewrite introduces.
Key changes
EncryptionDecryptionUtilbuilt its cipher with a 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) with the key derived via HKDF-SHA256. A bare"AES"transformation resolves to the provider's default mode - ECB - which for data confidentiality violates NIST SP 800-38A[2] (ECB is approved only for key wrapping, SP 800-38F[3]). The mode was never spelled out or configurable, and BC FIPS permits the ECB primitive at runtime, so this had to be caught by review rather than by the provider.encryption_keywould yield 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.OnBehalfOfAuthenticatorinitialises lazily and atomically: the constructor is inert, the first token request triggers init, and a failure logs once and declines. A bad key can no longer throw out of the constructor, propagate through config reload and retry forever. MirrorsApiTokenAuthenticator.KeyUtils.loadKeyFromKeystore->PemKeyReader.loadSecretKeyFromKeystore) instead of inline config, keeping secrets out of cluster state. Relative*_keystore_pathvalues resolve against the node config dir, consistent with other security file settings. This is SP 800-57[6] key-at-rest hardening, not a FIPS 140-3[1] requirement. PKCS#12 and BCFKS both holdSecretKeyentries, JKS does not (engineSetKeyEntryrequires aPrivateKey); in FIPS mode BCFKS is the only option (Enforce FIPS-approved cryptography across hashing, TLS, tokens and the CLI #6398).OnBehalfOfSettings.toString()redactssigning_key/encryption_keyso they no longer leak into logs.encrypted_rolesclaim from AES/ECB to AES-GCM changes its wire format; in a mixed cluster a token issued by one node and verified by another would be rejected with 401, in both directions, for as long as the upgrade takes plus one token lifetime. Now:security.internal.obo_roles_aes_gcm, encryption keeps writing the legacy format so that node can read what upgraded nodes issue. The attribute is set by the plugin itself, so upgraded nodes are recognised by capability rather than version, whatever release this lands in or is backported to; a configured value other than the plugin's own is refused at startup. The switch to AES-GCM happens by itself once the last such node leaves - no restart, no config change.AES/ECB/PKCS5Paddinginstead of relying on the provider default for"AES".LegacyRolesClaimFormatand its tests, with small hooks inEncryptionDecryptionUtil,OnBehalfOfAuthenticator,SecurityTokenManager,ClusterInfoHolderand the plugin; it is to be removed with the next major version.Reviewer call-outs
OPENSEARCH_FIPS_MODE=true- the variable core's launcher uses to loadfips_java.securityand BC FIPS approved-only mode, and which the plugin has read since Validate password hashing algorithm for FIPS #6126 (PBKDF2 required) - and it fails loudly rather than as a silent loss of roles - one line in the release note. Walkthrough in Enforce FIPS-approved cryptography across hashing, TLS, tokens and the CLI #6398.Testing
./gradlew test integrationTestLegacyRolesClaimFormatTest/LegacyRolesClaimFormatWindowTestcover both formats, the write gate and the read window; the integration testLegacyRolesClaimFormatNodeAttributeTestchecks that every node carries the attribute. The first two*FipsTestsvariants of the stack land here:LegacyRolesClaimFormatFipsTestsandSecurityTokenManagerFipsTestsassert that FIPS never writes the legacy format, even with a pre-upgrade node present.Manual test: OnBehalfOf (OBO) token, non-FIPS
Exercises OBO token issuance and verification against an already-running non-FIPS cluster, in three modes: keys inline in the dynamic config (Scenario A), held in a PKCS#12 keystore out of cluster state (Scenario B), or in a BCFKS keystore (Scenario C - works on a non-FIPS node too, because the plugin still registers the BC FIPS provider at load on this branch; from #6398 on this depends on core registering the BCFIPS provider on non-FIPS nodes). 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.Negative checks (each one, then restore a working config):
usestep returns no credentials.signing_key_keystore_alias: "nope").applystill succeeds and the cluster stays healthy (_cluster/health); on the first OBO request the node logsOn-behalf-of authentication is misconfigured; OBO tokens will be rejected: ... No key found at alias 'nope' in keystoreonce, and OBO requests are declined.openssl rand 48 | base64 -w0- 64 Base64 chars, 384 bits) is rejected withSigning key size was 384 bits, which is not secure enough.encryption_key(e.g.ZW5jcnlwdGlvbktleQ==, 13 bytes) is accepted here; the 32-byte floor applies only in FIPS mode (tested in Enforce FIPS-approved cryptography across hashing, TLS, tokens and the CLI #6398).Manual test: OnBehalfOf (OBO) tokens across a rolling upgrade (3.8 -> 3.9), non-FIPS
Upgrades a three-node cluster one node at a time from the previous release (3.8) to a build of this branch (3.9), and after every step issues a token on each node and verifies it on each node - nine combinations - because the point is which version issued a token and which one verifies it. A token is always sent to a named node, never through a load balancer.
Setup (three nodes on one host,
obouserholds a role withsecurity:obo/create):200+ role; format: legacy; no attribute. Baseline - if this fails, the OBO config is wrong, not the upgrade200+ role. 3.9 -> 3.8: node 1 still writes legacy (format check on node 1: legacy). 3.8 -> 3.9: node 1 reads legacy via the fallback and logs the INFO below once. Attribute: node 1TAIL_TOKENfrom node 3, the last 3.8 node200+ role; format: AES-GCM, switched by itself - no restart, no config change. Attribute: all three. Tail window:$TAIL_TOKEN(issued by 3.8, valid 10 min) used on each node ->200+ role on all three. This is the case a naive version check gets wrongScenario M - three versions in one cluster (3.7 -> 3.9 with a 3.8 node in between). A rolling upgrade may skip a minor, and one abandoned halfway and replaced by another leaves three versions in one cluster; both are valid. This shows the gates look at every node, not just the oldest. Start a fresh cluster (stop all nodes, move the three data directories aside, start all from a 3.7 installation built like the 3.8 one), create role and
obouseragain, then per step: upgrade, wait for green, nine combinations, format check on a 3.9 node, attribute listing.200+ role; legacy; no attribute200+ role; node 3 writes legacy; attribute: node 3TAIL_TOKENfrom node 2, the last pre-upgrade node (issue against${NODES[1]})200+ role; AES-GCM;$TAIL_TOKEN->200+ role on all threeM.4 - the reserved attribute. An upgraded node refuses the attribute in its own config (unit tests cover the check, this covers that the node really does not start):
The same line on a 3.7 or 3.8 node is accepted - the check lives in the new plugin.
Check 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.