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

[Fleet] Download Elastic GPG key during build #134861

Merged
merged 9 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dev/build/build_distributables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
40 changes: 40 additions & 0 deletions src/dev/build/tasks/fleet_download_elastic_gpg_key.ts
Original file line number Diff line number Diff line change
@@ -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;
}
},
};
5 changes: 3 additions & 2 deletions src/dev/build/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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';
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export interface FleetConfigType {
outputs?: PreconfiguredOutput[];
agentIdVerificationEnabled?: boolean;
enableExperimental?: string[];
packageVerification?: {
gpgKeyPath?: string;
};
developer?: {
disableRegistryVersionCheck?: boolean;
allowAgentUpgradeSourceUri?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/fleet/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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;
}