Skip to content

Commit

Permalink
[Fleet] handle ESO errors in message signing key pair generation (ela…
Browse files Browse the repository at this point in the history
  • Loading branch information
joeypoon authored Nov 1, 2023
1 parent 3b0f029 commit 19359e8
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 3 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ export class FleetPlugin

public start(core: CoreStart, plugins: FleetStartDeps): FleetStartContract {
const messageSigningService = new MessageSigningService(
this.initializerContext.logger,
plugins.encryptedSavedObjects.getClient({
includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { KibanaRequest } from '@kbn/core-http-server';
import type { SavedObjectsClientContract } from '@kbn/core/server';
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { loggingSystemMock } from '@kbn/core/server/mocks';

import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants';
import { createAppContextStartContractMock } from '../../mocks';
Expand Down Expand Up @@ -64,7 +65,7 @@ describe('MessageSigningService', () => {
.getSavedObjects()
.getScopedClient({} as unknown as KibanaRequest) as jest.Mocked<SavedObjectsClientContract>;

messageSigningService = new MessageSigningService(esoClientMock);
messageSigningService = new MessageSigningService(loggingSystemMock.create(), esoClientMock);
}

describe('with encryption key configured', () => {
Expand Down Expand Up @@ -208,6 +209,33 @@ describe('MessageSigningService', () => {
expect(isVerified).toBe(true);
expect(data).toBe(message);
});

it('will retry getting keypair if ESO error', async () => {
esoClientMock.createPointInTimeFinderDecryptedAsInternalUser = jest
.fn()
.mockRejectedValueOnce(new Error('some error'))
.mockRejectedValueOnce(new Error('another error'))
.mockResolvedValueOnce({
close: jest.fn(),
find: function* asyncGenerator() {
yield { saved_objects: [] };
},
});

const generateKeyPairResponse = await messageSigningService.generateKeyPair();
expect(esoClientMock.createPointInTimeFinderDecryptedAsInternalUser).toBeCalledTimes(3);
expect(soClientMock.create).toHaveBeenLastCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, {
private_key: expect.any(String),
public_key: expect.any(String),
passphrase: expect.any(String),
});

expect(generateKeyPairResponse).toEqual({
passphrase: expect.any(String),
privateKey: expect.any(String),
publicKey: expect.any(String),
});
});
});

describe('with NO encryption key configured', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
* 2.0.
*/

import { backOff } from 'exponential-backoff';

import { generateKeyPairSync, createSign, randomBytes } from 'crypto';

import type { LoggerFactory, Logger } from '@kbn/core/server';
import type { KibanaRequest } from '@kbn/core-http-server';
import type {
SavedObjectsClientContract,
Expand Down Expand Up @@ -39,8 +42,11 @@ export interface MessageSigningServiceInterface {

export class MessageSigningService implements MessageSigningServiceInterface {
private _soClient: SavedObjectsClientContract | undefined;
private logger: Logger;

constructor(private esoClient: EncryptedSavedObjectsClient) {}
constructor(loggerFactory: LoggerFactory, private esoClient: EncryptedSavedObjectsClient) {
this.logger = loggerFactory.get('messageSigningService');
}

public get isEncryptionAvailable(): MessageSigningServiceInterface['isEncryptionAvailable'] {
return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false;
Expand Down Expand Up @@ -188,6 +194,30 @@ export class MessageSigningService implements MessageSigningServiceInterface {
return this._soClient;
}

private async getCurrentKeyPairObjWithRetry() {
let soDoc: SavedObjectsFindResult<MessageSigningKeys> | undefined;

await backOff(
async () => {
soDoc = await this.getCurrentKeyPairObj();
},
{
maxDelay: 60 * 60 * 1000, // 1 hour in milliseconds
startingDelay: 1000, // 1 second
jitter: 'full',
numOfAttempts: Infinity,
retry: (_err: Error, attempt: number) => {
// not logging the error since we don't control what's in the error and it might contain sensitive data
// ESO already logs specific caught errors before passing the error along
this.logger.warn(`failed to get message signing key pair. retrying attempt: ${attempt}`);
return true;
},
}
);

return soDoc;
}

private async getCurrentKeyPairObj(): Promise<
SavedObjectsFindResult<MessageSigningKeys> | undefined
> {
Expand All @@ -205,6 +235,10 @@ export class MessageSigningService implements MessageSigningServiceInterface {
}
finder.close();

if (soDoc?.error) {
throw soDoc.error;
}

return soDoc;
}

Expand All @@ -216,7 +250,7 @@ export class MessageSigningService implements MessageSigningServiceInterface {
}
| undefined
> {
const currentKeyPair = await this.getCurrentKeyPairObj();
const currentKeyPair = await this.getCurrentKeyPairObjWithRetry();
if (!currentKeyPair) {
return;
}
Expand Down

0 comments on commit 19359e8

Please sign in to comment.