Skip to content

Commit

Permalink
Add support for the encryption key rotation to the encrypted saved ob…
Browse files Browse the repository at this point in the history
…jects. (#72420)
  • Loading branch information
azasypkin committed Oct 2, 2020
1 parent 9021c83 commit 388a905
Show file tree
Hide file tree
Showing 24 changed files with 4,486 additions and 153 deletions.
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: [] }),
}),
},
{
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`.';
}
},
}
);

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

0 comments on commit 388a905

Please sign in to comment.