mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
f36c984cd8
commit
f22cd7219b
2 changed files with 98 additions and 23 deletions
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue