Skip to content
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
3 changes: 3 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,9 @@
},
"prerelease_integrations_enabled": {
"type": "boolean"
},
"secret_storage_requirements_met": {
"type": "boolean"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ingest-download-sources": "d7edc5e588d9afa61c4b831604582891c54ef1c7",
"ingest-outputs": "b4e636b13a5d0f89f0400fb67811d4cca4736eb0",
"ingest-package-policies": "55816507db0134b8efbe0509e311a91ce7e1c6cc",
"ingest_manager_settings": "418311b03c8eda53f5d2ea6f54c1356afaa65511",
"ingest_manager_settings": "64955ef1b7a9ffa894d4bb9cf863b5602bfa6885",
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",
"kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad",
"legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8",
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/constants/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
*/

export const SECRETS_ENDPOINT_PATH = '/_fleet/secret';

export const SECRETS_MINIMUM_FLEET_SERVER_VERSION = '8.10.0';
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface BaseSettings {
export interface Settings extends BaseSettings {
id: string;
preconfigured_fields?: Array<'fleet_server_hosts'>;
secret_storage_requirements_met?: boolean;
}
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export {
MESSAGE_SIGNING_SERVICE_API_ROUTES,
// secrets
SECRETS_ENDPOINT_PATH,
SECRETS_MINIMUM_FLEET_SERVER_VERSION,
} from '../../common/constants';

export {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
fleet_server_hosts: { type: 'keyword' },
has_seen_add_data_notice: { type: 'boolean', index: false },
prerelease_integrations_enabled: { type: 'boolean' },
secret_storage_requirements_met: { type: 'boolean' },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I do not think we want to search on that so we can probably add index: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

that's weird, getting an error in ci build about this, even though the mapping was added in this pr:

info Checking if mappings are compatible
--
  | │ info [o.e.c.m.MetadataCreateIndexService] [node-01] [.kibana_mappings_check] creating index, cause [api], templates [], shards [1]/[1]
  | │ info [o.e.i.m.MapperService] [node-01] [.kibana_mappings_check] reloading search analyzers
  | │ERROR There was an issue trying to apply the extracted mappings to the existing index.
  | │ERROR ResponseError: illegal_argument_exception
  | │      	Root causes:
  | │      		illegal_argument_exception: Mapper for [ingest_manager_settings.secret_storage_requirements_met] conflicts with existing mapper:
  | │      	Cannot update parameter [index] from [true] to [false]
  | │          at SniffingTransport.request (/var/lib/buildkite-agent/builds/kb-n2-2-spot-28a62e0974e60ad7/elastic/kibana-pull-request/kibana/node_modules/@elastic/transport/src/Transport.ts:535:17)
  | │          at processTicksAndRejections (node:internal/process/task_queues:95:5)
  | │          at Indices.putMapping (/var/lib/buildkite-agent/builds/kb-n2-2-spot-28a62e0974e60ad7/elastic/kibana-pull-request/kibana/node_modules/@elastic/elasticsearch/src/api/api/indices.ts:1338:12)
  | │          at checkIncompatibleMappings (check_incompatible_mappings.ts:43:18)
  | │          at run_check_mappings_update_cli.ts:66:9
  | │          at tooling_log.ts:84:18
  | │          at description (run_check_mappings_update_cli.ts:65:7)
  | │          at run.ts:70:7
  | │          at withProcRunner (with_proc_runner.ts:29:5)
  | │          at run (run.ts:69:5)


},
},
migrations: {
Expand Down
60 changes: 60 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,66 @@ export async function getAgentsById(
);
}

// given a list of agentPolicyIds, return a map of agent version => count of agents
// this is used to get all fleet server versions
export async function getAgentVersionsForAgentPolicyIds(
esClient: ElasticsearchClient,
agentPolicyIds: string[]
): Promise<Record<string, number>> {
const versionCount: Record<string, number> = {};

if (!agentPolicyIds.length) {
return versionCount;
}

try {
const res = esClient.search<
FleetServerAgent,
Record<'agent_versions', { buckets: Array<{ key: string; doc_count: number }> }>
>({
size: 0,
track_total_hits: false,
body: {
query: {
bool: {
filter: [
{
terms: {
policy_id: agentPolicyIds,
},
},
],
},
},
aggs: {
agent_versions: {
terms: {
field: 'local_metadata.elastic.agent.version.keyword',
size: 1000,
},
},
},
},
index: AGENTS_INDEX,
ignore_unavailable: true,
});

const { aggregations } = await res;

if (aggregations && aggregations.agent_versions) {
aggregations.agent_versions.buckets.forEach((bucket) => {
versionCount[bucket.key] = bucket.doc_count;
});
}
} catch (error) {
if (error.statusCode !== 404) {
throw error;
}
}

return versionCount;
}

export async function getAgentByAccessAPIKeyId(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
Expand Down
45 changes: 44 additions & 1 deletion x-pack/plugins/fleet/server/services/fleet_server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
* 2.0.
*/

import type { ElasticsearchClient } from '@kbn/core/server';
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import semverGte from 'semver/functions/gte';
import semverCoerce from 'semver/functions/coerce';

import { FLEET_SERVER_SERVERS_INDEX } from '../../constants';
import { getAgentVersionsForAgentPolicyIds } from '../agents';

import { packagePolicyService } from '../package_policy';
/**
* Check if at least one fleet server is connected
*/
Expand All @@ -23,3 +27,42 @@ export async function hasFleetServers(esClient: ElasticsearchClient) {

return (res.hits.total as number) > 0;
}

export async function allFleetServerVersionsAreAtLeast(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract,
version: string
): Promise<boolean> {
let hasMore = true;
const policyIds = new Set<string>();
let page = 1;
while (hasMore) {
const res = await packagePolicyService.list(soClient, {
page: page++,
perPage: 20,
kuery: 'ingest-package-policies.package.name:fleet_server',
});

for (const item of res.items) {
policyIds.add(item.policy_id);
}

if (res.items.length === 0) {
hasMore = false;
}
}

const versionCounts = await getAgentVersionsForAgentPolicyIds(esClient, [...policyIds]);
const versions = Object.keys(versionCounts);

// there must be at least one fleet server agent for this check to pass
if (versions.length === 0) {
return false;
}

return _allVersionsAreAtLeast(version, versions);
}

function _allVersionsAreAtLeast(version: string, versions: string[]) {
return versions.every((v) => semverGte(semverCoerce(v)!, version));
}
11 changes: 4 additions & 7 deletions x-pack/plugins/fleet/server/services/package_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import {
extractAndUpdateSecrets,
extractAndWriteSecrets,
deleteSecretsIfNotReferenced as deleteSecrets,
isSecretStorageEnabled,
} from './secrets';

export type InputsOverride = Partial<NewPackagePolicyInput> & {
Expand Down Expand Up @@ -243,8 +244,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
}
validatePackagePolicyOrThrow(enrichedPackagePolicy, pkgInfo);

const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
if (secretsStorageEnabled) {
if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndWriteSecrets({
packagePolicy: { ...enrichedPackagePolicy, inputs },
packageInfo: pkgInfo,
Expand Down Expand Up @@ -747,8 +747,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
});
validatePackagePolicyOrThrow(packagePolicy, pkgInfo);

const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
if (secretsStorageEnabled) {
if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateSecrets({
oldPackagePolicy,
packagePolicyUpdate: { ...restOfPackagePolicy, inputs },
Expand Down Expand Up @@ -913,9 +912,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
);
if (pkgInfo) {
validatePackagePolicyOrThrow(packagePolicy, pkgInfo);
const { secretsStorage: secretsStorageEnabled } =
appContextService.getExperimentalFeatures();
if (secretsStorageEnabled) {
if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateSecrets({
oldPackagePolicy,
packagePolicyUpdate: { ...restOfPackagePolicy, inputs },
Expand Down
69 changes: 67 additions & 2 deletions x-pack/plugins/fleet/server/services/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ import type {
} from '../types';

import { FleetError } from '../errors';
import { SECRETS_ENDPOINT_PATH } from '../constants';
import { SECRETS_ENDPOINT_PATH, SECRETS_MINIMUM_FLEET_SERVER_VERSION } from '../constants';

import { retryTransientEsErrors } from './epm/elasticsearch/retry';

import { auditLoggingService } from './audit_logging';

import { appContextService } from './app_context';
import { packagePolicyService } from './package_policy';
import { settingsService } from '.';
import { allFleetServerVersionsAreAtLeast } from './fleet_server';

export async function createSecrets(opts: {
esClient: ElasticsearchClient;
Expand Down Expand Up @@ -270,10 +272,21 @@ export async function extractAndUpdateSecrets(opts: {
...createdSecrets.map(({ id }) => ({ id })),
];

const secretsToDelete: PolicySecretReference[] = [];

toDelete.forEach((secretPath) => {
// check if the previous secret is actually a secret refrerence
// it may be that secrets were not enabled at the time of creation
// in which case they are just stored as plain text
if (secretPath.value.value.isSecretRef) {
secretsToDelete.push({ id: secretPath.value.value.id });
}
});

return {
packagePolicyUpdate: policyWithSecretRefs,
secretReferences,
secretsToDelete: toDelete.map((secretPath) => ({ id: secretPath.value.value.id })),
secretsToDelete,
};
}

Expand Down Expand Up @@ -344,6 +357,58 @@ export function getPolicySecretPaths(
return [...packageLevelVarPaths, ...inputSecretPaths];
}

export async function isSecretStorageEnabled(
esClient: ElasticsearchClient,
soClient: SavedObjectsClientContract
): Promise<boolean> {
const logger = appContextService.getLogger();

// first check if the feature flag is enabled, if not secrets are disabled
const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
if (!secretsStorageEnabled) {
logger.debug('Secrets storage is disabled by feature flag');
return false;
}

// if serverless then secrets will always be supported
const isFleetServerStandalone =
appContextService.getConfig()?.internal?.fleetServerStandalone ?? false;

if (isFleetServerStandalone) {
logger.trace('Secrets storage is enabled as fleet server is standalone');
return true;
}

// now check the flag in settings to see if the fleet server requirement has already been met
// once the requirement has been met, secrets are always on
const settings = await settingsService.getSettings(soClient);

if (settings.secret_storage_requirements_met) {
logger.debug('Secrets storage already met, turned on is settings');
return true;
}

// otherwise check if we have the minimum fleet server version and enable secrets if so
if (
await allFleetServerVersionsAreAtLeast(esClient, soClient, SECRETS_MINIMUM_FLEET_SERVER_VERSION)
) {
logger.debug('Enabling secrets storage as minimum fleet server version has been met');
try {
await settingsService.saveSettings(soClient, {
secret_storage_requirements_met: true,
});
} catch (err) {
// we can suppress this error as it will be retried on the next function call
logger.warn(`Failed to save settings after enabling secrets storage: ${err.message}`);
}

return true;
}

logger.info('Secrets storage is disabled as minimum fleet server version has not been met');
return false;
}

function _getPackageLevelSecretPaths(
packagePolicy: NewPackagePolicy,
packageInfo: PackageInfo
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/types/so_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export interface SettingsSOAttributes {
prerelease_integrations_enabled: boolean;
has_seen_add_data_notice?: boolean;
fleet_server_hosts?: string[];
secret_storage_requirements_met?: boolean;
}

export interface DownloadSourceSOAttributes {
Expand Down
Loading