"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EncryptionKeyRotationService = void 0; var _coreSavedObjectsServer = require("@kbn/core-saved-objects-server"); var _encryption_error = require("./encryption_error"); var _get_descriptor_namespace = require("../saved_objects/get_descriptor_namespace"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ /** * Service that deals with encryption key rotation matters. */ class EncryptionKeyRotationService { constructor(options) { this.options = options; } async rotate(request, { batchSize, type }) { var _this$options$securit, _this$options$securit2; const [{ savedObjects }] = await this.options.getStartServices(); const typeRegistry = savedObjects.getTypeRegistry(); // We need to retrieve all SavedObject types which have encrypted attributes, specifically // collecting those that are hidden as they are ignored by the Saved Objects client by default. this.options.logger.debug('Retrieving Saved Object types that require encryption.'); const registeredSavedObjectTypes = []; const registeredHiddenSavedObjectTypes = []; for (const knownType of typeRegistry.getAllTypes()) { if (this.options.service.isRegistered(knownType.name) && (!type || knownType.name === type)) { registeredSavedObjectTypes.push(knownType.name); if (knownType.hidden) { registeredHiddenSavedObjectTypes.push(knownType.name); } } } const result = { total: 0, successful: 0, failed: 0 }; if (registeredSavedObjectTypes.length === 0) { this.options.logger.info(type ? `Saved Object type "${type}" is not registered, encryption key rotation is not needed.` : 'There are no registered Saved Object types that can have encrypted attributes, encryption key rotation is not needed.'); return result; } this.options.logger.info(`Saved Objects with the following types [${registeredSavedObjectTypes}] will be processed.`); // We need two separate Saved Objects clients for the retrieval and update. For retrieval we // don't want to have Encrypted Saved Objects wrapper so that it doesn't strip encrypted // attributes. But for the update we want to have it so that it automatically re-encrypts // attributes with the new primary encryption key. const user = (_this$options$securit = (_this$options$securit2 = this.options.security) === null || _this$options$securit2 === void 0 ? void 0 : _this$options$securit2.authc.getCurrentUser(request)) !== null && _this$options$securit !== void 0 ? _this$options$securit : undefined; const retrieveClient = savedObjects.getScopedClient(request, { includedHiddenTypes: registeredHiddenSavedObjectTypes, excludedExtensions: [_coreSavedObjectsServer.ENCRYPTION_EXTENSION_ID] }); const updateClient = savedObjects.getScopedClient(request, { includedHiddenTypes: registeredHiddenSavedObjectTypes }); // Keeps track of object IDs that have been processed already. const processedObjectIDs = new Set(); // Until we get scroll/search_after support in Saved Objects client we have to retrieve as much objects as allowed // by the `batchSize` parameter. Instead of using paging functionality (size/from or page/perPage parameters) that // has certain performance issues and is also limited by the maximum result window setting on .kibana index // (10,000 by default) we always fetch the first page of the results sorted by the `updated_at` field. This way we // can prioritize "old" objects that have a higher chance to have been encrypted with the old encryption keys, since // all newly created or updated objects are always encrypted with the current primary key. Re-encryption of the // "old" objects with the primary key implicitly bumps up their `updated_at` field so that these objects won't be // included into the first page of the results during next iteration. Additionally we track IDs of all processed // objects so that eventually we can detect that first page consists of only objects encrypted with the current // primary key and stop iterating. // // LIMITATION: if we have a lot of "old" objects encrypted with the _unknown_ encryption key it may either // significantly slow down rotation or prevent it from happening completely since such objects will be included into // every batch we fetch and if their number is equal to or greater than `batchSize` we won't be able to process any // object. Another and more complex case when we can be hit by this limitation is when users have multiple Kibana // instances configured with different primary encryption keys, these time even "new" objects may require rotation, // but they may be included into 2+ page of the results. We can potentially detect such cases and issue a warning, // but it's not an easy task: if we detect a case when none of the objects from the very first batch cannot be // decrypted with the decryption only keys we'll need to check how many of them can be decrypted at all using all // available keys including the current primary one. // // Also theoretically if `batchSize` is less than `index.max_result_window` we could try to rely on the paging // functionality and switch to the second page, but the issue here is that objects can be deleted in the meantime // so that unprocessed objects may get into the first page and we'll miss them. We can of course oscillate between // the first and the second pages or do multiple rotation passes, but it'd complicate code significantly. let batch = 0; let maxBatches = 0; while (true) { this.options.logger.debug(`Fetching ${batchSize} objects (batch #${batch}).`); const savedObjectsToDecrypt = await retrieveClient.find({ type: registeredSavedObjectTypes, perPage: batchSize, namespaces: ['*'], sortField: 'updated_at', sortOrder: 'asc' }); // We use `total` only from the first batch just as an approximate indicator for the consumer since total number // can change from batch to batch, but it won't affect the actual processing logic. if (batch === 0) { this.options.logger.debug(`Found ${savedObjectsToDecrypt.total} objects.`); result.total = savedObjectsToDecrypt.total; // Since we process live data there is a theoretical chance that we may be getting new // objects in every batch effectively making this loop infinite. To prevent this we want to // limit a number of batches we process during single rotation request giving enough room // for the Saved Objects occasionally created during rotation. maxBatches = Math.ceil(savedObjectsToDecrypt.total * 2 / batchSize); } this.options.logger.debug(`Decrypting ${savedObjectsToDecrypt.saved_objects.length} objects (batch #${batch}).`); const savedObjectsToEncrypt = await this.getSavedObjectsToReEncrypt(savedObjectsToDecrypt.saved_objects, processedObjectIDs, typeRegistry, user); if (savedObjectsToEncrypt.length === 0) { break; } this.options.logger.debug(`Re-encrypting ${savedObjectsToEncrypt.length} objects (batch #${batch}).`); try { const succeeded = (await updateClient.bulkUpdate(savedObjectsToEncrypt)).saved_objects.filter(savedObject => !savedObject.error).length; this.options.logger.debug(`Successfully re-encrypted ${succeeded} out of ${savedObjectsToEncrypt.length} objects (batch #${batch}).`); result.successful += succeeded; result.failed += savedObjectsToEncrypt.length - succeeded; } catch (err) { this.options.logger.error(`Failed to re-encrypt saved objects (batch #${batch}): ${err.message}`); result.failed += savedObjectsToEncrypt.length; } if (savedObjectsToDecrypt.total <= batchSize || ++batch >= maxBatches) { break; } } this.options.logger.info(`Encryption key rotation is completed. ${result.successful} objects out ouf ${result.total} were successfully re-encrypted with the primary encryption key and ${result.failed} objects failed.`); return result; } /** * Takes a list of Saved Objects and tries to decrypt their attributes with the secondary encryption * keys, silently skipping those that cannot be decrypted. The objects that were decrypted with the * decryption-only keys will be returned and grouped by the namespace. * @param savedObjects Saved Objects to decrypt attributes for. * @param processedObjectIDs Set of Saved Object IDs that were already processed. * @param typeRegistry Saved Objects type registry. * @param user The user that initiated decryption. */ async getSavedObjectsToReEncrypt(savedObjects, processedObjectIDs, typeRegistry, user) { const decryptedSavedObjects = []; for (const savedObject of savedObjects) { var _savedObject$namespac; // We shouldn't process objects that we already processed during previous iterations. if (processedObjectIDs.has(savedObject.id)) { continue; } else { processedObjectIDs.add(savedObject.id); } let decryptedAttributes; try { decryptedAttributes = await this.options.service.decryptAttributes({ type: savedObject.type, id: savedObject.id, namespace: (0, _get_descriptor_namespace.getDescriptorNamespace)(typeRegistry, savedObject.type, savedObject.namespaces) }, savedObject.attributes, { omitPrimaryEncryptionKey: true, user }); } catch (err) { if (!(err instanceof _encryption_error.EncryptionError)) { throw err; } continue; } decryptedSavedObjects.push({ ...savedObject, attributes: decryptedAttributes, // `bulkUpdate` expects objects with a single `namespace`. namespace: (_savedObject$namespac = savedObject.namespaces) === null || _savedObject$namespac === void 0 ? void 0 : _savedObject$namespac[0] }); } return decryptedSavedObjects; } } exports.EncryptionKeyRotationService = EncryptionKeyRotationService;