[8.x] [Fleet] Use FIPS compliant password hashing algorithm in output preconfiguration (#196754) (#196865)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Fleet] Use FIPS compliant password hashing algorithm in output
preconfiguration
(#196754)](https://github.com/elastic/kibana/pull/196754)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nicolas
Chaulet","email":"nicolas.chaulet@elastic.co"},"sourceCommit":{"committedDate":"2024-10-18T12:32:18Z","message":"[Fleet]
Use FIPS compliant password hashing algorithm in output preconfiguration
(#196754)","sha":"07eee1924c4a5f54d5bad7950d2688e4f0dc11ed","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Fleet","v9.0.0","backport:prev-minor","v8.16.0"],"title":"[Fleet]
Use FIPS compliant password hashing algorithm in output
preconfiguration","number":196754,"url":"https://github.com/elastic/kibana/pull/196754","mergeCommit":{"message":"[Fleet]
Use FIPS compliant password hashing algorithm in output preconfiguration
(#196754)","sha":"07eee1924c4a5f54d5bad7950d2688e4f0dc11ed"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196754","number":196754,"mergeCommit":{"message":"[Fleet]
Use FIPS compliant password hashing algorithm in output preconfiguration
(#196754)","sha":"07eee1924c4a5f54d5bad7950d2688e4f0dc11ed"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Nicolas Chaulet <nicolas.chaulet@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-19 01:20:55 +11:00 committed by GitHub
parent f36c984cd8
commit f22cd7219b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 23 deletions

View file

@ -53,7 +53,10 @@ const spyAgentPolicyServicBumpAllAgentPoliciesForOutput = jest.spyOn(
);
describe('output preconfiguration', () => {
let logstashSecretHash: string;
beforeEach(async () => {
logstashSecretHash = await hashSecret('secretKey');
const internalSoClientWithoutSpaceExtension = savedObjectsClientMock.create();
jest
.mocked(appContextService.getInternalUserSOClientWithoutSpaceExtension)
@ -120,7 +123,7 @@ describe('output preconfiguration', () => {
id: 'existing-logstash-output-with-secrets-2',
is_default: false,
is_default_monitoring: false,
name: 'Logstash Output With Secrets 2',
name: 'Logstash Output With Secrets ',
type: 'logstash',
hosts: ['test:4343'],
is_preconfigured: true,
@ -130,6 +133,34 @@ describe('output preconfiguration', () => {
},
},
},
{
id: 'existing-logstash-output-with-secrets-3-outdatded-hash',
is_default: false,
is_default_monitoring: false,
name: 'Logstash Output With Secrets 3',
type: 'logstash',
hosts: ['test:4343'],
is_preconfigured: true,
secrets: {
ssl: {
key: { id: 'test456', hash: 'test456:outdatedhash' },
},
},
},
{
id: 'existing-logstash-output-with-secrets-4-hash',
is_default: false,
is_default_monitoring: false,
name: 'Logstash Output With Secrets 4',
type: 'logstash',
hosts: ['test:4343'],
is_preconfigured: true,
secrets: {
ssl: {
key: { id: 'test123', hash: logstashSecretHash },
},
},
},
{
id: 'existing-kafka-output-1',
is_default: false,
@ -689,6 +720,56 @@ describe('output preconfiguration', () => {
expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled();
});
it('should update output if a preconfigured logstash output with secrets exists and hash algorithm changed', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredOutputs(soClient, esClient, [
{
id: 'existing-logstash-output-with-secrets-3-outdatded-hash',
is_default: false,
is_default_monitoring: false,
name: 'Logstash Output With Secrets 3',
type: 'logstash',
hosts: ['test:4343'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'secretKey', // no change
},
},
},
]);
expect(mockedOutputService.create).not.toBeCalled();
expect(mockedOutputService.update).toBeCalled();
expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled();
});
it('should not update output if a preconfigured logstash output with secrets exists and hash algorithm did not changed', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredOutputs(soClient, esClient, [
{
id: 'existing-logstash-output-with-secrets-4-hash',
is_default: false,
is_default_monitoring: false,
name: 'Logstash Output With Secrets 4',
type: 'logstash',
hosts: ['test:4343'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'secretKey', // no change
},
},
},
]);
expect(mockedOutputService.create).not.toBeCalled();
expect(mockedOutputService.update).not.toBeCalled();
expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled();
});
it('should update output if a preconfigured kafka output with plain value secrets exists and did not change', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import crypto from 'crypto';
import crypto from 'node:crypto';
import utils from 'node:util';
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import { isEqual } from 'lodash';
import { safeDump } from 'js-yaml';
const pbkdf2Async = utils.promisify(crypto.pbkdf2);
import type {
PreconfiguredOutput,
Output,
@ -142,32 +145,23 @@ export async function createOrUpdatePreconfiguredOutputs(
// Values recommended by NodeJS documentation
const keyLength = 64;
const saltLength = 16;
// N=2^14 (16 MiB), r=8 (1024 bytes), p=5
const scryptParams = {
cost: 16384,
blockSize: 8,
parallelization: 5,
};
const maxIteration = 100000;
export async function hashSecret(secret: string) {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(saltLength).toString('hex');
crypto.scrypt(secret, salt, keyLength, scryptParams, (err, derivedKey) => {
if (err) reject(err);
resolve(`${salt}:${derivedKey.toString('hex')}`);
});
});
const salt = crypto.randomBytes(saltLength).toString('hex');
const derivedKey = await pbkdf2Async(secret, salt, maxIteration, keyLength, 'sha512');
return `${salt}:${derivedKey.toString('hex')}`;
}
async function verifySecret(hash: string, secret: string) {
return new Promise((resolve, reject) => {
const [salt, key] = hash.split(':');
crypto.scrypt(secret, salt, keyLength, scryptParams, (err, derivedKey) => {
if (err) reject(err);
resolve(crypto.timingSafeEqual(Buffer.from(key, 'hex'), derivedKey));
});
});
const [salt, key] = hash.split(':');
const derivedKey = await pbkdf2Async(secret, salt, maxIteration, keyLength, 'sha512');
const keyBuffer = Buffer.from(key, 'hex');
if (keyBuffer.length !== derivedKey.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(key, 'hex'), derivedKey);
}
async function hashSecrets(output: PreconfiguredOutput) {