Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the encryption key rotation to the encrypted saved objects. #72420

Merged
merged 9 commits into from
Oct 2, 2020
1 change: 1 addition & 0 deletions docs/setup/production.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Settings that must be the same:
xpack.security.encryptionKey //decrypting session information
xpack.reporting.encryptionKey //decrypting reports
xpack.encryptedSavedObjects.encryptionKey // decrypting saved objects
xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys // saved objects encryption key rotation, if any
--------

Separate configuration files can be used from the command line by using the `-c` flag:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ kibana_vars=(
xpack.code.security.gitHostWhitelist
xpack.code.security.gitProtocolWhitelist
xpack.encryptedSavedObjects.encryptionKey
xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys
xpack.graph.enabled
xpack.graph.canEditDrillDownUrls
xpack.graph.savePolicy
Expand Down
108 changes: 93 additions & 15 deletions x-pack/plugins/encrypted_saved_objects/server/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,60 @@

jest.mock('crypto', () => ({ randomBytes: jest.fn() }));

import { first } from 'rxjs/operators';
import { loggingSystemMock, coreMock } from 'src/core/server/mocks';
import { createConfig$, ConfigSchema } from './config';
import { loggingSystemMock } from 'src/core/server/mocks';
import { createConfig, ConfigSchema } from './config';

describe('config schema', () => {
it('generates proper defaults', () => {
expect(ConfigSchema.validate({})).toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"keyRotation": Object {
"decryptionOnlyKeys": Array [],
},
}
`);

expect(ConfigSchema.validate({}, { dist: false })).toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"keyRotation": Object {
"decryptionOnlyKeys": Array [],
},
}
`);

expect(ConfigSchema.validate({}, { dist: true })).toMatchInlineSnapshot(`
Object {
"enabled": true,
"keyRotation": Object {
"decryptionOnlyKeys": Array [],
},
}
`);
});

it('properly validates config', () => {
expect(
ConfigSchema.validate(
{
encryptionKey: 'a'.repeat(32),
keyRotation: { decryptionOnlyKeys: ['b'.repeat(32), 'c'.repeat(32)] },
},
{ dist: true }
)
).toMatchInlineSnapshot(`
Object {
"enabled": true,
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"keyRotation": Object {
"decryptionOnlyKeys": Array [
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccccccccccccccc",
],
},
}
`);
});
Expand All @@ -46,21 +77,65 @@ describe('config schema', () => {
`"[encryptionKey]: value has length [3] but it must have a minimum length of [32]."`
);
});

it('should throw error if any of the xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys is less than 32 characters', () => {
expect(() =>
ConfigSchema.validate({
keyRotation: { decryptionOnlyKeys: ['a'.repeat(32), 'b'.repeat(31)] },
})
).toThrowErrorMatchingInlineSnapshot(
`"[keyRotation.decryptionOnlyKeys.1]: value has length [31] but it must have a minimum length of [32]."`
);

expect(() =>
ConfigSchema.validate(
{ keyRotation: { decryptionOnlyKeys: ['a'.repeat(32), 'b'.repeat(31)] } },
{ dist: true }
)
).toThrowErrorMatchingInlineSnapshot(
`"[keyRotation.decryptionOnlyKeys.1]: value has length [31] but it must have a minimum length of [32]."`
);
});

it('should throw error if any of the xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys is equal to xpack.encryptedSavedObjects.encryptionKey', () => {
expect(() =>
ConfigSchema.validate({
encryptionKey: 'a'.repeat(32),
keyRotation: { decryptionOnlyKeys: ['a'.repeat(32)] },
})
).toThrowErrorMatchingInlineSnapshot(
`"\`keyRotation.decryptionOnlyKeys\` cannot contain primary encryption key specified in \`encryptionKey\`."`
);

expect(() =>
ConfigSchema.validate(
{
encryptionKey: 'a'.repeat(32),
keyRotation: { decryptionOnlyKeys: ['a'.repeat(32)] },
},
{ dist: true }
)
).toThrowErrorMatchingInlineSnapshot(
`"\`keyRotation.decryptionOnlyKeys\` cannot contain primary encryption key specified in \`encryptionKey\`."`
);
});
});

describe('createConfig$()', () => {
it('should log a warning, set xpack.encryptedSavedObjects.encryptionKey and usingEphemeralEncryptionKey=true when encryptionKey is not set', async () => {
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 contextMock = coreMock.createPluginInitializerContext({});
const config = await createConfig$(contextMock).pipe(first()).toPromise();
const logger = loggingSystemMock.create().get();
const config = createConfig(ConfigSchema.validate({}, { dist: true }), logger);
expect(config).toEqual({
config: { encryptionKey: 'ab'.repeat(16) },
enabled: true,
encryptionKey: 'ab'.repeat(16),
keyRotation: { decryptionOnlyKeys: [] },
usingEphemeralEncryptionKey: true,
});

expect(loggingSystemMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(`
expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Generating a random key for xpack.encryptedSavedObjects.encryptionKey. To be able to decrypt encrypted saved objects attributes after restart, please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml",
Expand All @@ -70,15 +145,18 @@ describe('createConfig$()', () => {
});

it('should not log a warning and set usingEphemeralEncryptionKey=false when encryptionKey is set', async () => {
const contextMock = coreMock.createPluginInitializerContext({
encryptionKey: 'supersecret',
});
const config = await createConfig$(contextMock).pipe(first()).toPromise();
const logger = loggingSystemMock.create().get();
const config = createConfig(
ConfigSchema.validate({ encryptionKey: 'supersecret'.repeat(3) }, { dist: true }),
logger
);
expect(config).toEqual({
config: { encryptionKey: 'supersecret' },
enabled: true,
encryptionKey: 'supersecret'.repeat(3),
keyRotation: { decryptionOnlyKeys: [] },
usingEphemeralEncryptionKey: false,
});

expect(loggingSystemMock.collect(contextMock.logger).warn).toEqual([]);
expect(loggingSystemMock.collect(logger).warn).toEqual([]);
});
});
71 changes: 40 additions & 31 deletions x-pack/plugins/encrypted_saved_objects/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,50 @@
*/

import crypto from 'crypto';
import { map } from 'rxjs/operators';
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext } from 'src/core/server';
import { Logger } from 'src/core/server';

export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
encryptionKey: schema.conditional(
schema.contextRef('dist'),
true,
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
});
export type ConfigType = ReturnType<typeof createConfig>;

export function createConfig$(context: PluginInitializerContext) {
return context.config.create<TypeOf<typeof ConfigSchema>>().pipe(
map((config) => {
const logger = context.logger.get('config');
export const ConfigSchema = schema.object(
{
enabled: schema.boolean({ defaultValue: true }),
encryptionKey: schema.conditional(
schema.contextRef('dist'),
true,
schema.maybe(schema.string({ minLength: 32 })),
schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) })
),
keyRotation: schema.object({
decryptionOnlyKeys: schema.arrayOf(schema.string({ minLength: 32 }), { defaultValue: [] }),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to add this to kibana_docker, and allow on ESS once this merges.

}),
},
{
validate(value) {
const decryptionOnlyKeys = value.keyRotation?.decryptionOnlyKeys ?? [];
if (value.encryptionKey && decryptionOnlyKeys.includes(value.encryptionKey)) {
return '`keyRotation.decryptionOnlyKeys` cannot contain primary encryption key specified in `encryptionKey`.';
legrego marked this conversation as resolved.
Show resolved Hide resolved
}
},
}
);

let encryptionKey = config.encryptionKey;
const usingEphemeralEncryptionKey = encryptionKey === undefined;
if (encryptionKey === undefined) {
logger.warn(
'Generating a random key for xpack.encryptedSavedObjects.encryptionKey. ' +
'To be able to decrypt encrypted saved objects attributes after restart, ' +
'please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml'
);
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 be able to decrypt encrypted saved objects attributes after restart, ' +
'please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml'
);

encryptionKey = crypto.randomBytes(16).toString('hex');
}
encryptionKey = crypto.randomBytes(16).toString('hex');
}

return {
config: { ...config, encryptionKey },
usingEphemeralEncryptionKey,
};
})
);
return {
...config,
encryptionKey,
usingEphemeralEncryptionKey,
};
}
Loading