Do not generate an ephemeral encryption key in production. (#81511)

This commit is contained in:
Aleh Zasypkin 2021-02-10 11:27:31 +01:00 committed by GitHub
parent 634c0b3424
commit 03a53b9f39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 543 additions and 238 deletions

View file

@ -26,9 +26,7 @@ beforeEach(() => {
actionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
actionsConfigUtils: mockedActionsConfig,
licenseState: mockedLicenseState,
preconfiguredActions: [

View file

@ -59,9 +59,7 @@ beforeEach(() => {
actionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
actionsConfigUtils: actionsConfigMock.create(),
licenseState: mockedLicenseState,
preconfiguredActions: [],
@ -411,9 +409,7 @@ describe('create()', () => {
const localActionTypeRegistryParams = {
licensing: licensingMock.createSetup(),
taskManager: mockTaskManager,
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
actionsConfigUtils: localConfigUtils,
licenseState: licenseStateMock.create(),
preconfiguredActions: [],

View file

@ -33,9 +33,7 @@ export function createActionTypeRegistry(): {
const actionTypeRegistry = new ActionTypeRegistry({
taskManager: taskManagerMock.createSetup(),
licensing: licensingMock.createSetup(),
taskRunnerFactory: new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
),
taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true })),
actionsConfigUtils: actionsConfigMock.create(),
licenseState: licenseStateMock.create(),
preconfiguredActions: [],

View file

@ -28,7 +28,7 @@ describe('execute()', () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
actionTypeRegistry,
isESOUsingEphemeralEncryptionKey: false,
isESOCanEncrypt: true,
preconfiguredActions: [],
});
savedObjectsClient.get.mockResolvedValueOnce({
@ -87,7 +87,7 @@ describe('execute()', () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
actionTypeRegistry: actionTypeRegistryMock.create(),
isESOUsingEphemeralEncryptionKey: false,
isESOCanEncrypt: true,
preconfiguredActions: [
{
id: '123',
@ -158,10 +158,10 @@ describe('execute()', () => {
);
});
test('throws when passing isESOUsingEphemeralEncryptionKey with true as a value', async () => {
test('throws when passing isESOCanEncrypt with false as a value', async () => {
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
isESOUsingEphemeralEncryptionKey: true,
isESOCanEncrypt: false,
actionTypeRegistry: actionTypeRegistryMock.create(),
preconfiguredActions: [],
});
@ -173,7 +173,7 @@ describe('execute()', () => {
apiKey: null,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
`"Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
@ -181,7 +181,7 @@ describe('execute()', () => {
const mockedActionTypeRegistry = actionTypeRegistryMock.create();
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
isESOUsingEphemeralEncryptionKey: false,
isESOCanEncrypt: true,
actionTypeRegistry: mockedActionTypeRegistry,
preconfiguredActions: [],
});
@ -211,7 +211,7 @@ describe('execute()', () => {
const mockedActionTypeRegistry = actionTypeRegistryMock.create();
const executeFn = createExecutionEnqueuerFunction({
taskManager: mockTaskManager,
isESOUsingEphemeralEncryptionKey: false,
isESOCanEncrypt: true,
actionTypeRegistry: mockedActionTypeRegistry,
preconfiguredActions: [
{

View file

@ -14,7 +14,7 @@ import { isSavedObjectExecutionSource } from './lib';
interface CreateExecuteFunctionOptions {
taskManager: TaskManagerStartContract;
isESOUsingEphemeralEncryptionKey: boolean;
isESOCanEncrypt: boolean;
actionTypeRegistry: ActionTypeRegistryContract;
preconfiguredActions: PreConfiguredAction[];
}
@ -33,16 +33,16 @@ export type ExecutionEnqueuer = (
export function createExecutionEnqueuerFunction({
taskManager,
actionTypeRegistry,
isESOUsingEphemeralEncryptionKey,
isESOCanEncrypt,
preconfiguredActions,
}: CreateExecuteFunctionOptions) {
return async function execute(
unsecuredSavedObjectsClient: SavedObjectsClientContract,
{ id, params, spaceId, source, apiKey }: ExecuteOptions
) {
if (isESOUsingEphemeralEncryptionKey === true) {
if (!isESOCanEncrypt) {
throw new Error(
`Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
`Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}

View file

@ -17,7 +17,7 @@ import { ActionType } from '../types';
import { actionsMock, actionsClientMock } from '../mocks';
import { pick } from 'lodash';
const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false });
const actionExecutor = new ActionExecutor({ isESOCanEncrypt: true });
const services = actionsMock.createServices();
const actionsClient = actionsClientMock.create();
@ -310,8 +310,8 @@ test('should not throws an error if actionType is preconfigured', async () => {
});
});
test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => {
const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true });
test('throws an error when passing isESOCanEncrypt with value of false', async () => {
const customActionExecutor = new ActionExecutor({ isESOCanEncrypt: false });
customActionExecutor.initialize({
logger: loggingSystemMock.create().get(),
spaces: spacesMock,
@ -325,7 +325,7 @@ test('throws an error when passing isESOUsingEphemeralEncryptionKey with value o
await expect(
customActionExecutor.execute(executeParams)
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
`"Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});

View file

@ -48,10 +48,10 @@ export type ActionExecutorContract = PublicMethodsOf<ActionExecutor>;
export class ActionExecutor {
private isInitialized = false;
private actionExecutorContext?: ActionExecutorContext;
private readonly isESOUsingEphemeralEncryptionKey: boolean;
private readonly isESOCanEncrypt: boolean;
constructor({ isESOUsingEphemeralEncryptionKey }: { isESOUsingEphemeralEncryptionKey: boolean }) {
this.isESOUsingEphemeralEncryptionKey = isESOUsingEphemeralEncryptionKey;
constructor({ isESOCanEncrypt }: { isESOCanEncrypt: boolean }) {
this.isESOCanEncrypt = isESOCanEncrypt;
}
public initialize(actionExecutorContext: ActionExecutorContext) {
@ -72,9 +72,9 @@ export class ActionExecutor {
throw new Error('ActionExecutor not initialized');
}
if (this.isESOUsingEphemeralEncryptionKey === true) {
if (!this.isESOCanEncrypt) {
throw new Error(
`Unable to execute action because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
`Unable to execute action because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}

View file

@ -84,18 +84,14 @@ beforeEach(() => {
});
test(`throws an error if factory isn't initialized`, () => {
const factory = new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
);
const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true }));
expect(() =>
factory.create({ taskInstance: mockedTaskInstance })
).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`);
});
test(`throws an error if factory is already initialized`, () => {
const factory = new TaskRunnerFactory(
new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false })
);
const factory = new TaskRunnerFactory(new ActionExecutor({ isESOCanEncrypt: true }));
factory.initialize(taskRunnerFactoryInitializerParams);
expect(() =>
factory.initialize(taskRunnerFactoryInitializerParams)

View file

@ -51,25 +51,21 @@ describe('Actions Plugin', () => {
};
});
it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => {
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, pluginsSetup);
expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true);
it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => {
await plugin.setup(coreSetup, pluginsSetup);
expect(pluginsSetup.encryptedSavedObjects.canEncrypt).toEqual(false);
expect(context.logger.get().warn).toHaveBeenCalledWith(
'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
});
describe('routeHandlerContext.getActionsClient()', () => {
it('should not throw error when ESO plugin not using a generated key', async () => {
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, {
it('should not throw error when ESO plugin has encryption key', async () => {
await plugin.setup(coreSetup, {
...pluginsSetup,
encryptedSavedObjects: {
...pluginsSetup.encryptedSavedObjects,
usingEphemeralEncryptionKey: false,
canEncrypt: true,
},
});
@ -99,10 +95,8 @@ describe('Actions Plugin', () => {
actionsContextHandler!.getActionsClient();
});
it('should throw error when ESO plugin using a generated key', async () => {
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, pluginsSetup);
it('should throw error when ESO plugin is missing encryption key', async () => {
await plugin.setup(coreSetup, pluginsSetup);
expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1);
const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0] as [
@ -123,7 +117,7 @@ describe('Actions Plugin', () => {
httpServerMock.createResponseFactory()
)) as unknown) as ActionsApiRequestHandlerContext;
expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot(
`"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
`"Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
});
@ -234,14 +228,12 @@ describe('Actions Plugin', () => {
expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true);
});
it('should not throw error when ESO plugin not using a generated key', async () => {
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, {
it('should not throw error when ESO plugin has encryption key', async () => {
await plugin.setup(coreSetup, {
...pluginsSetup,
encryptedSavedObjects: {
...pluginsSetup.encryptedSavedObjects,
usingEphemeralEncryptionKey: false,
canEncrypt: true,
},
});
const pluginStart = await plugin.start(coreStart, pluginsStart);
@ -249,17 +241,15 @@ describe('Actions Plugin', () => {
await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest());
});
it('should throw error when ESO plugin using generated key', async () => {
// coreMock.createSetup doesn't support Plugin generics
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await plugin.setup(coreSetup as any, pluginsSetup);
it('should throw error when ESO plugin is missing encryption key', async () => {
await plugin.setup(coreSetup, pluginsSetup);
const pluginStart = await plugin.start(coreStart, pluginsStart);
expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true);
expect(pluginsSetup.encryptedSavedObjects.canEncrypt).toEqual(false);
await expect(
pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest())
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
`"Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
});

View file

@ -144,7 +144,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
private security?: SecurityPluginSetup;
private eventLogService?: IEventLogService;
private eventLogger?: IEventLogger;
private isESOUsingEphemeralEncryptionKey?: boolean;
private isESOCanEncrypt?: boolean;
private readonly telemetryLogger: Logger;
private readonly preconfiguredActions: PreConfiguredAction[];
private readonly kibanaIndexConfig: { kibana: { index: string } };
@ -162,12 +162,11 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
plugins: ActionsPluginsSetup
): PluginSetupContract {
this.licenseState = new LicenseState(plugins.licensing.license$);
this.isESOUsingEphemeralEncryptionKey =
plugins.encryptedSavedObjects.usingEphemeralEncryptionKey;
this.isESOCanEncrypt = plugins.encryptedSavedObjects.canEncrypt;
if (this.isESOUsingEphemeralEncryptionKey) {
if (!this.isESOCanEncrypt) {
this.logger.warn(
'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
@ -181,7 +180,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
});
const actionExecutor = new ActionExecutor({
isESOUsingEphemeralEncryptionKey: this.isESOUsingEphemeralEncryptionKey,
isESOCanEncrypt: this.isESOCanEncrypt,
});
// get executions count
@ -270,7 +269,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
actionTypeRegistry,
taskRunnerFactory,
kibanaIndexConfig,
isESOUsingEphemeralEncryptionKey,
isESOCanEncrypt,
preconfiguredActions,
instantiateAuthorization,
getUnsecuredSavedObjectsClient,
@ -286,9 +285,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
request: KibanaRequest,
authorizationContext?: ActionExecutionSource<unknown>
) => {
if (isESOUsingEphemeralEncryptionKey === true) {
if (isESOCanEncrypt !== true) {
throw new Error(
`Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
`Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
@ -314,7 +313,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
executionEnqueuer: createExecutionEnqueuerFunction({
taskManager: plugins.taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
}),
auditLogger: this.security?.audit.asScoped(request),
@ -437,7 +436,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
): IContextProvider<ActionsRequestHandlerContext, 'actions'> => {
const {
actionTypeRegistry,
isESOUsingEphemeralEncryptionKey,
isESOCanEncrypt,
preconfiguredActions,
actionExecutor,
instantiateAuthorization,
@ -448,9 +447,9 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
const [{ savedObjects }, { taskManager }] = await core.getStartServices();
return {
getActionsClient: () => {
if (isESOUsingEphemeralEncryptionKey === true) {
if (isESOCanEncrypt !== true) {
throw new Error(
`Unable to create actions client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
`Unable to create actions client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
return new ActionsClient({
@ -468,7 +467,7 @@ export class ActionsPlugin implements Plugin<PluginSetupContract, PluginStartCon
executionEnqueuer: createExecutionEnqueuerFunction({
taskManager,
actionTypeRegistry: actionTypeRegistry!,
isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!,
isESOCanEncrypt: isESOCanEncrypt!,
preconfiguredActions,
}),
auditLogger: security?.audit.asScoped(request),

View file

@ -25,7 +25,7 @@ describe('Alerting Plugin', () => {
let coreSetup: ReturnType<typeof coreMock.createSetup>;
let pluginsSetup: jest.Mocked<AlertingPluginsSetup>;
it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => {
it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
@ -40,7 +40,7 @@ describe('Alerting Plugin', () => {
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const setupMocks = coreMock.createSetup();
// need await to test number of calls of setupMocks.status.set, becuase it is under async function which awaiting core.getStartServices()
// need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices()
await plugin.setup(setupMocks, {
licensing: licensingMock.createSetup(),
encryptedSavedObjects: encryptedSavedObjectsSetup,
@ -51,9 +51,9 @@ describe('Alerting Plugin', () => {
});
expect(setupMocks.status.set).toHaveBeenCalledTimes(1);
expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true);
expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false);
expect(context.logger.get().warn).toHaveBeenCalledWith(
'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
});
@ -110,7 +110,7 @@ describe('Alerting Plugin', () => {
describe('start()', () => {
describe('getAlertsClientWithRequest()', () => {
it('throws error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to true', async () => {
it('throws error when encryptedSavedObjects plugin is missing encryption key', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
@ -141,15 +141,15 @@ describe('Alerting Plugin', () => {
taskManager: taskManagerMock.createStart(),
});
expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true);
expect(encryptedSavedObjectsSetup.canEncrypt).toEqual(false);
expect(() =>
startContract.getAlertsClientWithRequest({} as KibanaRequest)
).toThrowErrorMatchingInlineSnapshot(
`"Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
`"Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command."`
);
});
it(`doesn't throw error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to false`, async () => {
it(`doesn't throw error when encryptedSavedObjects plugin has encryption key`, async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
@ -163,7 +163,7 @@ describe('Alerting Plugin', () => {
const encryptedSavedObjectsSetup = {
...encryptedSavedObjectsMock.createSetup(),
usingEphemeralEncryptionKey: false,
canEncrypt: true,
};
plugin.setup(coreMock.createSetup(), {
licensing: licensingMock.createSetup(),

View file

@ -153,7 +153,7 @@ export class AlertingPlugin {
private alertTypeRegistry?: AlertTypeRegistry;
private readonly taskRunnerFactory: TaskRunnerFactory;
private licenseState: ILicenseState | null = null;
private isESOUsingEphemeralEncryptionKey?: boolean;
private isESOCanEncrypt?: boolean;
private security?: SecurityPluginSetup;
private readonly alertsClientFactory: AlertsClientFactory;
private readonly telemetryLogger: Logger;
@ -189,12 +189,11 @@ export class AlertingPlugin {
};
});
this.isESOUsingEphemeralEncryptionKey =
plugins.encryptedSavedObjects.usingEphemeralEncryptionKey;
this.isESOCanEncrypt = plugins.encryptedSavedObjects.canEncrypt;
if (this.isESOUsingEphemeralEncryptionKey) {
if (!this.isESOCanEncrypt) {
this.logger.warn(
'APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
'APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
@ -311,7 +310,7 @@ export class AlertingPlugin {
public start(core: CoreStart, plugins: AlertingPluginsStart): PluginStartContract {
const {
isESOUsingEphemeralEncryptionKey,
isESOCanEncrypt,
logger,
taskRunnerFactory,
alertTypeRegistry,
@ -353,9 +352,9 @@ export class AlertingPlugin {
});
const getAlertsClientWithRequest = (request: KibanaRequest) => {
if (isESOUsingEphemeralEncryptionKey === true) {
if (isESOCanEncrypt !== true) {
throw new Error(
`Unable to create alerts client because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
`Unable to create alerts client because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.`
);
}
return alertsClientFactory!.create(request, core.savedObjects);

View file

@ -47,8 +47,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [config] = router.get.mock.calls[0];
@ -60,8 +59,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -85,12 +83,11 @@ describe('healthRoute', () => {
`);
});
it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => {
it('evaluates whether Encrypted Saved Objects is missing encryption key', async () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = true;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: false });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -129,8 +126,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -169,8 +165,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -209,8 +204,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -249,8 +243,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];
@ -291,8 +284,7 @@ describe('healthRoute', () => {
const router = httpServiceMock.createRouter();
const licenseState = licenseStateMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup();
encryptedSavedObjects.usingEphemeralEncryptionKey = false;
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
healthRoute(router, licenseState, encryptedSavedObjects);
const [, handler] = router.get.mock.calls[0];

View file

@ -55,7 +55,7 @@ export function healthRoute(
const frameworkHealth: AlertingFrameworkHealth = {
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey,
hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt,
alertingFrameworkHeath,
};

View file

@ -5,10 +5,7 @@
* 2.0.
*/
jest.mock('crypto', () => ({ randomBytes: jest.fn() }));
import { loggingSystemMock } from 'src/core/server/mocks';
import { createConfig, ConfigSchema } from './config';
import { ConfigSchema } from './config';
describe('config schema', () => {
it('generates proper defaults', () => {
@ -32,6 +29,17 @@ describe('config schema', () => {
}
`);
expect(ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true }))
.toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"keyRotation": Object {
"decryptionOnlyKeys": Array [],
},
}
`);
expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(`
Object {
"enabled": true,
@ -79,6 +87,18 @@ describe('config schema', () => {
);
});
it('should not allow `null` value for the encryption key', () => {
expect(() => ConfigSchema.validate({ encryptionKey: null })).toThrowErrorMatchingInlineSnapshot(
`"[encryptionKey]: expected value of type [string] but got [null]"`
);
expect(() =>
ConfigSchema.validate({ encryptionKey: null }, { dist: true })
).toThrowErrorMatchingInlineSnapshot(
`"[encryptionKey]: expected value of type [string] but got [null]"`
);
});
it('should throw error if any of the xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys is less than 32 characters', () => {
expect(() =>
ConfigSchema.validate({
@ -121,43 +141,3 @@ describe('config schema', () => {
);
});
});
describe('createConfig()', () => {
it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', () => {
const mockRandomBytes = jest.requireMock('crypto').randomBytes;
mockRandomBytes.mockReturnValue('ab'.repeat(16));
const logger = loggingSystemMock.create().get();
const config = createConfig(ConfigSchema.validate({}, { dist: true }), logger);
expect(config).toEqual({
enabled: true,
encryptionKey: 'ab'.repeat(16),
keyRotation: { decryptionOnlyKeys: [] },
usingEphemeralEncryptionKey: true,
});
expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.",
],
]
`);
});
it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => {
const logger = loggingSystemMock.create().get();
const config = createConfig(
ConfigSchema.validate({ encryptionKey: 'supersecret'.repeat(3) }, { dist: true }),
logger
);
expect(config).toEqual({
enabled: true,
encryptionKey: 'supersecret'.repeat(3),
keyRotation: { decryptionOnlyKeys: [] },
usingEphemeralEncryptionKey: false,
});
expect(loggingSystemMock.collect(logger).warn).toEqual([]);
});
});

View file

@ -5,11 +5,9 @@
* 2.0.
*/
import crypto from 'crypto';
import { schema, TypeOf } from '@kbn/config-schema';
import { Logger } from 'src/core/server';
export type ConfigType = ReturnType<typeof createConfig>;
export type ConfigType = TypeOf<typeof ConfigSchema>;
export const ConfigSchema = schema.object(
{
@ -33,23 +31,3 @@ export const ConfigSchema = schema.object(
},
}
);
export function createConfig(config: TypeOf<typeof ConfigSchema>, logger: Logger) {
let encryptionKey = config.encryptionKey;
const usingEphemeralEncryptionKey = encryptionKey === undefined;
if (encryptionKey === undefined) {
logger.warn(
'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' +
'To decrypt encrypted saved objects attributes after restart, ' +
'please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
encryptionKey = crypto.randomBytes(16).toString('hex');
}
return {
...config,
encryptionKey,
usingEphemeralEncryptionKey,
};
}

View file

@ -226,6 +226,72 @@ describe('#stripOrDecryptAttributes', () => {
);
});
});
describe('without encryption key', () => {
beforeEach(() => {
service = new EncryptedSavedObjectsService({
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
});
it('does not fail if none of attributes are supposed to be encrypted', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
await expect(
service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes)
).resolves.toEqual({ attributes: { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } });
});
it('does not fail if there are attributes are supposed to be encrypted, but should be stripped', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
});
await expect(
service.stripOrDecryptAttributes({ id: 'known-id', type: 'known-type-1' }, attributes)
).resolves.toEqual({ attributes: { attrTwo: 'two' } });
});
it('fails if needs to decrypt any attribute', async () => {
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set([
'attrOne',
{ key: 'attrThree', dangerouslyExposeValue: true },
]),
});
const mockUser = mockAuthenticatedUser();
const { attributes, error } = await service.stripOrDecryptAttributes(
{ type: 'known-type-1', id: 'object-id' },
{ attrOne: 'one', attrTwo: 'two', attrThree: 'three' },
undefined,
{ user: mockUser }
);
expect(attributes).toEqual({ attrTwo: 'two' });
const encryptionError = error as EncryptionError;
expect(encryptionError.attributeName).toBe('attrThree');
expect(encryptionError.message).toBe('Unable to decrypt attribute "attrThree"');
expect(encryptionError.cause).toEqual(
new Error('Decryption is disabled because of missing decryption keys.')
);
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
'attrThree',
{ type: 'known-type-1', id: 'object-id' },
mockUser
);
});
});
});
describe('#encryptAttributes', () => {
@ -465,6 +531,58 @@ describe('#encryptAttributes', () => {
mockUser
);
});
describe('without encryption key', () => {
beforeEach(() => {
service = new EncryptedSavedObjectsService({
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
});
it('does not fail if none of attributes are supposed to be encrypted', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
await expect(
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
).resolves.toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
});
it('fails if needs to encrypt any attribute', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
});
const mockUser = mockAuthenticatedUser();
await expect(
service.encryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, {
user: mockUser,
})
).rejects.toThrowError(EncryptionError);
expect(attributes).toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith(
'attrOne',
{ type: 'known-type-1', id: 'object-id' },
mockUser
);
});
});
});
describe('#decryptAttributes', () => {
@ -1099,6 +1217,88 @@ describe('#decryptAttributes', () => {
expect(decryptionOnlyCryptoTwo.decrypt).not.toHaveBeenCalled();
});
});
describe('without encryption key', () => {
beforeEach(() => {
service = new EncryptedSavedObjectsService({
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
});
it('does not fail if none of attributes are supposed to be decrypted', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service = new EncryptedSavedObjectsService({
decryptionOnlyCryptos: [],
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
await expect(
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
).resolves.toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
});
it('does not fail if can decrypt attributes with decryption only keys', async () => {
const decryptionOnlyCryptoOne = createNodeCryptMock('old-key-one');
decryptionOnlyCryptoOne.decrypt.mockImplementation(
async (encryptedOutput: string | Buffer, aad?: string) => `${encryptedOutput}||${aad}`
);
service = new EncryptedSavedObjectsService({
decryptionOnlyCryptos: [decryptionOnlyCryptoOne],
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']),
});
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null };
await expect(
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes)
).resolves.toEqual({
attrOne: 'one||["known-type-1","object-id",{"attrTwo":"two"}]',
attrTwo: 'two',
attrThree: 'three||["known-type-1","object-id",{"attrTwo":"two"}]',
attrFour: null,
});
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
['attrOne', 'attrThree'],
{ type: 'known-type-1', id: 'object-id' },
undefined
);
});
it('fails if needs to decrypt any attribute', async () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrOne']) });
const mockUser = mockAuthenticatedUser();
await expect(
service.decryptAttributes({ type: 'known-type-1', id: 'object-id' }, attributes, {
user: mockUser,
})
).rejects.toThrowError(EncryptionError);
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
'attrOne',
{ type: 'known-type-1', id: 'object-id' },
mockUser
);
});
});
});
describe('#encryptAttributesSync', () => {
@ -1283,6 +1483,58 @@ describe('#encryptAttributesSync', () => {
attrThree: 'three',
});
});
describe('without encryption key', () => {
beforeEach(() => {
service = new EncryptedSavedObjectsService({
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
});
it('does not fail if none of attributes are supposed to be encrypted', () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
expect(
service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes)
).toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
});
it('fails if needs to encrypt any attribute', () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set(['attrOne', 'attrThree']),
});
const mockUser = mockAuthenticatedUser();
expect(() =>
service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes, {
user: mockUser,
})
).toThrowError(EncryptionError);
expect(attributes).toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.encryptAttributesSuccess).not.toHaveBeenCalled();
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.encryptAttributeFailure).toHaveBeenCalledWith(
'attrOne',
{ type: 'known-type-1', id: 'object-id' },
mockUser
);
});
});
});
describe('#decryptAttributesSync', () => {
@ -1784,4 +2036,86 @@ describe('#decryptAttributesSync', () => {
expect(decryptionOnlyCryptoTwo.decryptSync).not.toHaveBeenCalled();
});
});
describe('without encryption key', () => {
beforeEach(() => {
service = new EncryptedSavedObjectsService({
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
});
it('does not fail if none of attributes are supposed to be decrypted', () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service = new EncryptedSavedObjectsService({
decryptionOnlyCryptos: [],
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrFour']) });
expect(
service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes)
).toEqual({
attrOne: 'one',
attrTwo: 'two',
attrThree: 'three',
});
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
});
it('does not fail if can decrypt attributes with decryption only keys', () => {
const decryptionOnlyCryptoOne = createNodeCryptMock('old-key-one');
decryptionOnlyCryptoOne.decryptSync.mockImplementation(
(encryptedOutput: string | Buffer, aad?: string) => `${encryptedOutput}||${aad}`
);
service = new EncryptedSavedObjectsService({
decryptionOnlyCryptos: [decryptionOnlyCryptoOne],
logger: loggingSystemMock.create().get(),
audit: mockAuditLogger,
});
service.registerType({
type: 'known-type-1',
attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']),
});
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null };
expect(
service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes)
).toEqual({
attrOne: 'one||["known-type-1","object-id",{"attrTwo":"two"}]',
attrTwo: 'two',
attrThree: 'three||["known-type-1","object-id",{"attrTwo":"two"}]',
attrFour: null,
});
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.decryptAttributesSuccess).toHaveBeenCalledWith(
['attrOne', 'attrThree'],
{ type: 'known-type-1', id: 'object-id' },
undefined
);
});
it('fails if needs to decrypt any attribute', () => {
const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' };
service.registerType({ type: 'known-type-1', attributesToEncrypt: new Set(['attrOne']) });
const mockUser = mockAuthenticatedUser();
expect(() =>
service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes, {
user: mockUser,
})
).toThrowError(EncryptionError);
expect(mockAuditLogger.decryptAttributesSuccess).not.toHaveBeenCalled();
expect(mockAuditLogger.decryptAttributeFailure).toHaveBeenCalledWith(
'attrOne',
{ type: 'known-type-1', id: 'object-id' },
mockUser
);
});
});
});

View file

@ -77,7 +77,7 @@ interface EncryptedSavedObjectsServiceOptions {
/**
* NodeCrypto instance used for both encryption and decryption.
*/
primaryCrypto: Crypto;
primaryCrypto?: Crypto;
/**
* NodeCrypto instances used ONLY for decryption (i.e. rotated encryption keys).
@ -293,12 +293,17 @@ export class EncryptedSavedObjectsService {
let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(
await this.options.primaryCrypto.encrypt(attributeValue, encryptionAAD)
);
} catch (err) {
iterator.throw!(err);
// We check this inside of the iterator to throw only if we do need to encrypt anything.
if (this.options.primaryCrypto) {
try {
iteratorResult = iterator.next(
await this.options.primaryCrypto.encrypt(attributeValue, encryptionAAD)
);
} catch (err) {
iterator.throw!(err);
}
} else {
iterator.throw!(new Error('Encryption is disabled because of missing encryption key.'));
}
}
@ -324,12 +329,17 @@ export class EncryptedSavedObjectsService {
let iteratorResult = iterator.next();
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
try {
iteratorResult = iterator.next(
this.options.primaryCrypto.encryptSync(attributeValue, encryptionAAD)
);
} catch (err) {
iterator.throw!(err);
// We check this inside of the iterator to throw only if we do need to encrypt anything.
if (this.options.primaryCrypto) {
try {
iteratorResult = iterator.next(
this.options.primaryCrypto.encryptSync(attributeValue, encryptionAAD)
);
} catch (err) {
iterator.throw!(err);
}
} else {
iterator.throw!(new Error('Encryption is disabled because of missing encryption key.'));
}
}
@ -358,7 +368,11 @@ export class EncryptedSavedObjectsService {
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
let decryptionError;
// We check this inside of the iterator to throw only if we do need to decrypt anything.
let decryptionError =
decrypters.length === 0
? new Error('Decryption is disabled because of missing decryption keys.')
: undefined;
for (const decrypter of decrypters) {
try {
iteratorResult = iterator.next(await decrypter.decrypt(attributeValue, encryptionAAD));
@ -402,7 +416,11 @@ export class EncryptedSavedObjectsService {
while (!iteratorResult.done) {
const [attributeValue, encryptionAAD] = iteratorResult.value;
let decryptionError;
// We check this inside of the iterator to throw only if we do need to decrypt anything.
let decryptionError =
decrypters.length === 0
? new Error('Decryption is disabled because of missing decryption keys.')
: undefined;
for (const decrypter of decrypters) {
try {
iteratorResult = iterator.next(decrypter.decryptSync(attributeValue, encryptionAAD));
@ -541,6 +559,9 @@ export class EncryptedSavedObjectsService {
return this.options.decryptionOnlyCryptos;
}
return [this.options.primaryCrypto, ...(this.options.decryptionOnlyCryptos ?? [])];
return [
...(this.options.primaryCrypto ? [this.options.primaryCrypto] : []),
...(this.options.decryptionOnlyCryptos ?? []),
];
}
}

View file

@ -8,11 +8,13 @@
import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin';
import { EncryptedSavedObjectsClient, EncryptedSavedObjectsClientOptions } from './saved_objects';
function createEncryptedSavedObjectsSetupMock() {
function createEncryptedSavedObjectsSetupMock(
{ canEncrypt }: { canEncrypt: boolean } = { canEncrypt: false }
) {
return {
registerType: jest.fn(),
__legacyCompat: { registerLegacyAPI: jest.fn() },
usingEphemeralEncryptionKey: true,
canEncrypt,
createMigration: jest.fn(),
} as jest.Mocked<EncryptedSavedObjectsPluginSetup>;
}

View file

@ -19,12 +19,28 @@ describe('EncryptedSavedObjects Plugin', () => {
);
expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }))
.toMatchInlineSnapshot(`
Object {
"createMigration": [Function],
"registerType": [Function],
"usingEphemeralEncryptionKey": true,
}
`);
Object {
"canEncrypt": false,
"createMigration": [Function],
"registerType": [Function],
}
`);
});
it('exposes proper contract when encryption key is set', () => {
const plugin = new EncryptedSavedObjectsPlugin(
coreMock.createPluginInitializerContext(
ConfigSchema.validate({ encryptionKey: 'z'.repeat(32) }, { dist: true })
)
);
expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() }))
.toMatchInlineSnapshot(`
Object {
"canEncrypt": true,
"createMigration": [Function],
"registerType": [Function],
}
`);
});
});

View file

@ -6,10 +6,9 @@
*/
import nodeCrypto from '@elastic/node-crypto';
import { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server';
import { TypeOf } from '@kbn/config-schema';
import { SecurityPluginSetup } from '../../security/server';
import { createConfig, ConfigSchema } from './config';
import type { Logger, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server';
import type { SecurityPluginSetup } from '../../security/server';
import type { ConfigType } from './config';
import {
EncryptedSavedObjectsService,
EncryptedSavedObjectTypeRegistration,
@ -26,8 +25,11 @@ export interface PluginsSetup {
}
export interface EncryptedSavedObjectsPluginSetup {
/**
* Indicates if Saved Object encryption is possible. Requires an encryption key to be explicitly set via `xpack.encryptedSavedObjects.encryptionKey`.
*/
canEncrypt: boolean;
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void;
usingEphemeralEncryptionKey: boolean;
createMigration: CreateEncryptedSavedObjectsMigrationFn;
}
@ -50,19 +52,24 @@ export class EncryptedSavedObjectsPlugin
}
public setup(core: CoreSetup, deps: PluginsSetup): EncryptedSavedObjectsPluginSetup {
const config = createConfig(
this.initializerContext.config.get<TypeOf<typeof ConfigSchema>>(),
this.initializerContext.logger.get('config')
const config = this.initializerContext.config.get<ConfigType>();
const canEncrypt = config.encryptionKey !== undefined;
if (!canEncrypt) {
this.logger.warn(
'Saved objects encryption key is not set. This will severely limit Kibana functionality. ' +
'Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
const primaryCrypto = config.encryptionKey
? nodeCrypto({ encryptionKey: config.encryptionKey })
: undefined;
const decryptionOnlyCryptos = config.keyRotation.decryptionOnlyKeys.map((decryptionKey) =>
nodeCrypto({ encryptionKey: decryptionKey })
);
const auditLogger = new EncryptedSavedObjectsAuditLogger(
deps.security?.audit.getLogger('encryptedSavedObjects')
);
const primaryCrypto = nodeCrypto({ encryptionKey: config.encryptionKey });
const decryptionOnlyCryptos = config.keyRotation.decryptionOnlyKeys.map((decryptionKey) =>
nodeCrypto({ encryptionKey: decryptionKey })
);
const service = Object.freeze(
new EncryptedSavedObjectsService({
primaryCrypto,
@ -94,9 +101,9 @@ export class EncryptedSavedObjectsPlugin
});
return {
canEncrypt,
registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) =>
service.registerType(typeRegistration),
usingEphemeralEncryptionKey: config.usingEphemeralEncryptionKey,
createMigration: getCreateMigration(
service,
(typeRegistration: EncryptedSavedObjectTypeRegistration) => {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ConfigSchema, createConfig } from '../config';
import { ConfigSchema, ConfigType } from '../config';
import { httpServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
import { encryptionKeyRotationServiceMock } from '../crypto/index.mock';
@ -14,7 +14,7 @@ export const routeDefinitionParamsMock = {
create: (config: Record<string, unknown> = {}) => ({
router: httpServiceMock.createRouter(),
logger: loggingSystemMock.create().get(),
config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get()),
config: ConfigSchema.validate(config) as ConfigType,
encryptionKeyRotationService: encryptionKeyRotationServiceMock.create(),
}),
};

View file

@ -4,14 +4,13 @@
"server": true,
"ui": true,
"configPath": ["xpack", "fleet"],
"requiredPlugins": ["licensing", "data"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects"],
"optionalPlugins": [
"security",
"features",
"cloud",
"usageCollection",
"home",
"encryptedSavedObjects"
"home"
],
"extraPublicDirs": ["common"],
"requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils"]

View file

@ -95,7 +95,7 @@ export interface FleetSetupDeps {
}
export interface FleetStartDeps {
encryptedSavedObjects?: EncryptedSavedObjectsPluginStart;
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
security?: SecurityPluginStart;
}
@ -255,11 +255,11 @@ export class FleetPlugin
// Conditional config routes
if (config.agents.enabled) {
const isESOUsingEphemeralEncryptionKey = !deps.encryptedSavedObjects;
if (isESOUsingEphemeralEncryptionKey) {
const isESOCanEncrypt = deps.encryptedSavedObjects.canEncrypt;
if (!isESOCanEncrypt) {
if (this.logger) {
this.logger.warn(
'Fleet APIs are disabled because the Encrypted Saved Objects plugin uses an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
'Fleet APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
);
}
} else {

View file

@ -24,7 +24,7 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re
const isProductionMode = appContextService.getIsProductionMode();
const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false;
const isTLSCheckDisabled = appContextService.getConfig()?.agents?.tlsCheckDisabled ?? false;
const isUsingEphemeralEncryptionKey = !appContextService.getEncryptedSavedObjectsSetup();
const canEncrypt = appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt === true;
const missingRequirements: GetFleetStatusResponse['missing_requirements'] = [];
if (!isAdminUserSetup) {
@ -37,7 +37,7 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re
missingRequirements.push('tls_required');
}
if (isUsingEphemeralEncryptionKey) {
if (!canEncrypt) {
missingRequirements.push('encrypted_saved_object_encryption_key_required');
}

View file

@ -44,7 +44,7 @@ export class AlertingSecurity {
return {
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
hasPermanentEncryptionKey: Boolean(encryptedSavedObjects),
hasPermanentEncryptionKey: encryptedSavedObjects?.canEncrypt === true,
};
};
}

View file

@ -18,7 +18,7 @@ describe('read_privileges route', () => {
({ clients, context } = requestContextMock.createTools());
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult());
readPrivilegesRoute(server.router, false);
readPrivilegesRoute(server.router, true);
});
describe('normal status codes', () => {

View file

@ -14,7 +14,7 @@ import { readPrivileges } from '../../privileges/read_privileges';
export const readPrivilegesRoute = (
router: SecuritySolutionPluginRouter,
usingEphemeralEncryptionKey: boolean
hasEncryptionKey: boolean
) => {
router.get(
{
@ -39,7 +39,7 @@ export const readPrivilegesRoute = (
const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index);
const privileges = merge(clusterPrivileges, {
is_authenticated: request.auth.isAuthenticated ?? false,
has_encryption_key: !usingEphemeralEncryptionKey,
has_encryption_key: hasEncryptionKey,
});
return response.ok({ body: privileges });

View file

@ -183,7 +183,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
initRoutes(
router,
config,
plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false,
plugins.encryptedSavedObjects?.canEncrypt === true,
plugins.security,
plugins.ml
);

View file

@ -47,7 +47,7 @@ import { getTimelineRoute } from '../lib/timeline/routes/get_timeline_route';
export const initRoutes = (
router: SecuritySolutionPluginRouter,
config: ConfigType,
usingEphemeralEncryptionKey: boolean,
hasEncryptionKey: boolean,
security: SetupPlugins['security'],
ml: SetupPlugins['ml']
) => {
@ -102,5 +102,5 @@ export const initRoutes = (
readTagsRoute(router);
// Privileges API to get the generic user privileges
readPrivilegesRoute(router, usingEphemeralEncryptionKey);
readPrivilegesRoute(router, hasEncryptionKey);
};