Skip to content

Commit

Permalink
[Fleet] Use FIPS compliant password hashing algorithm in output preco…
Browse files Browse the repository at this point in the history
…nfiguration (#196754)

(cherry picked from commit 07eee19)
  • Loading branch information
nchaulet committed Oct 18, 2024
1 parent 9ad35b8 commit 7a9d75e
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
38 changes: 16 additions & 22 deletions x-pack/plugins/fleet/server/services/preconfiguration/outputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 7a9d75e

Please sign in to comment.