[Fleet] Download Elastic GPG key during build (#134861)

* add build step to download gpg key

* add gpg path to config

* add getGpgKey method

* getGpgKey reads config directly

* improve logging

* return undefined on error

* log error code instead of msg

* perform checksum check on GPG key

* fail build if GPG key download fails
This commit is contained in:
Mark Hopkin 2022-06-23 15:51:59 +01:00 committed by GitHub
parent 31db97850d
commit 69caa311bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 2 deletions

View file

@ -88,6 +88,7 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions
await run(Tasks.CleanTypescript);
await run(Tasks.CleanExtraFilesFromModules);
await run(Tasks.CleanEmptyFolders);
await run(Tasks.FleetDownloadElasticGpgKey);
await run(Tasks.BundleFleetPackages);
}

View file

@ -0,0 +1,40 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Task, downloadToDisk } from '../lib';
const BUNDLED_KEYS_DIR = 'x-pack/plugins/fleet/target/keys';
const ARTIFACTS_URL = 'https://artifacts.elastic.co/';
const GPG_KEY_NAME = 'GPG-KEY-elasticsearch';
const GPG_KEY_SHA512 =
'84ee193cc337344d9a7da9021daf3f5ede83f5f1ab049d169f3634921529dcd096abf7a91eec7f26f3a6913e5e38f88f69a5e2ce79ad155d46edc75705a648c6';
export const FleetDownloadElasticGpgKey: Task = {
description: 'Downloading Elastic GPG key for Fleet',
async run(config, log, build) {
const gpgKeyUrl = ARTIFACTS_URL + GPG_KEY_NAME;
const destination = build.resolvePath(BUNDLED_KEYS_DIR, GPG_KEY_NAME);
log.info(`Downloading Elastic GPG key from ${gpgKeyUrl} to ${destination}`);
try {
await downloadToDisk({
log,
url: gpgKeyUrl,
destination,
shaChecksum: GPG_KEY_SHA512,
shaAlgorithm: 'sha512',
skipChecksumCheck: false,
maxAttempts: 3,
});
} catch (error) {
log.error(`Error downloading Elastic GPG key from ${gpgKeyUrl} to ${destination}`);
throw error;
}
},
};

View file

@ -7,8 +7,8 @@
*/
export * from './bin';
export * from './build_kibana_platform_plugins';
export * from './build_kibana_example_plugins';
export * from './build_kibana_platform_plugins';
export * from './build_packages_task';
export * from './bundle_fleet_packages';
export * from './clean_tasks';
@ -18,6 +18,7 @@ export * from './create_archives_task';
export * from './create_empty_dirs_and_files_task';
export * from './create_readme_task';
export * from './download_cloud_dependencies';
export * from './fleet_download_elastic_gpg_key';
export * from './generate_packages_optimized_assets';
export * from './install_dependencies_task';
export * from './license_file_task';
@ -27,11 +28,11 @@ export * from './os_packages';
export * from './package_json';
export * from './patch_native_modules_task';
export * from './path_length_task';
export * from './replace_favicon';
export * from './transpile_babel_task';
export * from './uuid_verification_task';
export * from './verify_env_task';
export * from './write_sha_sums_task';
export * from './replace_favicon';
// @ts-expect-error this module can't be TS because it ends up pulling x-pack into Kibana
export { InstallChromium } from './install_chromium';

View file

@ -33,6 +33,9 @@ export interface FleetConfigType {
outputs?: PreconfiguredOutput[];
agentIdVerificationEnabled?: boolean;
enableExperimental?: string[];
packageVerification?: {
gpgKeyPath?: string;
};
developer?: {
disableRegistryVersionCheck?: boolean;
allowAgentUpgradeSourceUri?: boolean;

View file

@ -47,6 +47,7 @@ export type {
export { AgentNotFoundError, FleetUnauthorizedError } from './errors';
const DEFAULT_BUNDLED_PACKAGE_LOCATION = path.join(__dirname, '../target/bundled_packages');
const DEFAULT_GPG_KEY_PATH = path.join(__dirname, '../target/keys/GPG-KEY-elasticsearch');
export const config: PluginConfigDescriptor = {
exposeToBrowser: {
@ -143,6 +144,9 @@ export const config: PluginConfigDescriptor = {
allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }),
bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }),
}),
packageVerification: schema.object({
gpgKeyPath: schema.string({ defaultValue: DEFAULT_GPG_KEY_PATH }),
}),
/**
* For internal use. A list of string values (comma delimited) that will enable experimental
* type of functionality that is not yet released.

View file

@ -0,0 +1,58 @@
/*
* 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.
*/
import { readFile } from 'fs/promises';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
const mockLoggerFactory = loggingSystemMock.create();
const mockLogger = mockLoggerFactory.get('mock logger');
import { getGpgKeyOrUndefined, _readGpgKey } from './package_verification';
const mockGetConfig = jest.fn();
jest.mock('../../app_context', () => ({
appContextService: {
getConfig: () => mockGetConfig(),
getLogger: () => mockLogger,
},
}));
jest.mock('fs/promises', () => ({
readFile: jest.fn(),
}));
const mockedReadFile = readFile as jest.MockedFunction<typeof readFile>;
beforeEach(() => {
jest.resetAllMocks();
});
describe('getGpgKeyOrUndefined', () => {
it('should cache the gpg key after reading file once', async () => {
const keyContent = 'this is the gpg key';
mockedReadFile.mockResolvedValue(Buffer.from(keyContent));
mockGetConfig.mockReturnValue({ packageVerification: { gpgKeyPath: 'somePath' } });
expect(await getGpgKeyOrUndefined()).toEqual(keyContent);
expect(await getGpgKeyOrUndefined()).toEqual(keyContent);
expect(mockedReadFile).toHaveBeenCalledWith('somePath');
expect(mockedReadFile).toHaveBeenCalledTimes(1);
});
});
describe('_readGpgKey', () => {
it('should return undefined if the key file isnt configured', async () => {
mockedReadFile.mockResolvedValue(Buffer.from('this is the gpg key'));
mockGetConfig.mockReturnValue({ packageVerification: {} });
expect(await _readGpgKey()).toEqual(undefined);
});
it('should return undefined if there is an error reading the file', async () => {
mockedReadFile.mockRejectedValue(new Error('some error'));
mockGetConfig.mockReturnValue({ packageVerification: { gpgKeyPath: 'somePath' } });
expect(await _readGpgKey()).toEqual(undefined);
expect(mockedReadFile).toHaveBeenCalledWith('somePath');
});
});

View file

@ -0,0 +1,42 @@
/*
* 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.
*/
import { readFile } from 'fs/promises';
import { appContextService } from '../../app_context';
let cachedKey: string | undefined | null = null;
export async function getGpgKeyOrUndefined(): Promise<string | undefined> {
if (cachedKey !== null) return cachedKey;
cachedKey = await _readGpgKey();
return cachedKey;
}
export async function _readGpgKey(): Promise<string | undefined> {
const config = appContextService.getConfig();
const logger = appContextService.getLogger();
const gpgKeyPath = config?.packageVerification?.gpgKeyPath;
if (!gpgKeyPath) {
logger.warn('GPG key path not configured at "xpack.fleet.packageVerification.gpgKeyPath"');
return undefined;
}
let buffer: Buffer;
try {
buffer = await readFile(gpgKeyPath);
} catch (e) {
logger.warn(`Unable to retrieve GPG key from '${gpgKeyPath}': ${e.code}`);
return undefined;
}
const key = buffer.toString();
cachedKey = key;
return key;
}