diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json
index 3b9e60124b034..2b8756ea0cb46 100644
--- a/x-pack/plugins/monitoring/kibana.json
+++ b/x-pack/plugins/monitoring/kibana.json
@@ -11,7 +11,8 @@
"kibanaLegacy",
"triggers_actions_ui",
"alerts",
- "actions"
+ "actions",
+ "encryptedSavedObjects"
],
"optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"],
"server": true,
diff --git a/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx
new file mode 100644
index 0000000000000..918c0b5c9b609
--- /dev/null
+++ b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx
@@ -0,0 +1,137 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiSpacer, EuiLink, EuiCode, EuiText } from '@elastic/eui';
+import { Legacy } from '../../legacy_shims';
+import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+
+export interface AlertingFrameworkHealth {
+ isSufficientlySecure: boolean;
+ hasPermanentEncryptionKey: boolean;
+}
+
+const showTlsAndEncryptionError = () => {
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+
+ Legacy.shims.toastNotifications.addWarning({
+ title: toMountPoint(
+
+ ),
+ text: toMountPoint(
+
+
+ {i18n.translate('xpack.monitoring.healthCheck.tlsAndEncryptionError', {
+ defaultMessage: `You must enable Transport Layer Security between Kibana and Elasticsearch
+ and configure an encryption key in your kibana.yml file to use the Alerting feature.`,
+ })}
+
+
+
+ {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', {
+ defaultMessage: 'Learn how.',
+ })}
+
+
+ ),
+ });
+};
+
+const showEncryptionError = () => {
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+
+ Legacy.shims.toastNotifications.addWarning(
+ {
+ title: toMountPoint(
+
+ ),
+ text: toMountPoint(
+
+ {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorBeforeKey', {
+ defaultMessage: 'To create an alert, set a value for ',
+ })}
+
+ {'xpack.encryptedSavedObjects.encryptionKey'}
+
+ {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAfterKey', {
+ defaultMessage: ' in your kibana.yml file. ',
+ })}
+
+ {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', {
+ defaultMessage: 'Learn how.',
+ })}
+
+
+ ),
+ },
+ {}
+ );
+};
+
+const showTlsError = () => {
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+
+ Legacy.shims.toastNotifications.addWarning({
+ title: toMountPoint(
+
+ ),
+ text: toMountPoint(
+
+ {i18n.translate('xpack.monitoring.healthCheck.tlsError', {
+ defaultMessage:
+ 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ',
+ })}
+
+ {i18n.translate('xpack.monitoring.healthCheck.tlsErrorAction', {
+ defaultMessage: 'Learn how to enable TLS.',
+ })}
+
+
+ ),
+ });
+};
+
+export const showSecurityToast = (alertingHealth: AlertingFrameworkHealth) => {
+ const { isSufficientlySecure, hasPermanentEncryptionKey } = alertingHealth;
+ if (
+ Array.isArray(alertingHealth) ||
+ (!alertingHealth.hasOwnProperty('isSufficientlySecure') &&
+ !alertingHealth.hasOwnProperty('hasPermanentEncryptionKey'))
+ ) {
+ return;
+ }
+
+ if (!isSufficientlySecure && !hasPermanentEncryptionKey) {
+ showTlsAndEncryptionError();
+ } else if (!isSufficientlySecure) {
+ showTlsError();
+ } else if (!hasPermanentEncryptionKey) {
+ showEncryptionError();
+ }
+};
diff --git a/x-pack/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js
index f3eadcaf9831b..5173984dbe868 100644
--- a/x-pack/plugins/monitoring/public/services/clusters.js
+++ b/x-pack/plugins/monitoring/public/services/clusters.js
@@ -7,6 +7,7 @@
import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler';
import { Legacy } from '../legacy_shims';
import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants';
+import { showSecurityToast } from '../alerts/lib/security_toasts';
function formatClusters(clusters) {
return clusters.map(formatCluster);
@@ -66,7 +67,8 @@ export function monitoringClustersProvider($injector) {
return getClusters().then((clusters) => {
if (clusters.length) {
return ensureAlertsEnabled()
- .then(() => {
+ .then(({ data }) => {
+ showSecurityToast(data);
once = true;
return clusters;
})
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts
new file mode 100644
index 0000000000000..047b14bd37fbc
--- /dev/null
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { RequestHandlerContext } from 'kibana/server';
+import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server';
+
+export interface AlertingFrameworkHealth {
+ isSufficientlySecure: boolean;
+ hasPermanentEncryptionKey: boolean;
+}
+
+export interface XPackUsageSecurity {
+ security?: {
+ enabled?: boolean;
+ ssl?: {
+ http?: {
+ enabled?: boolean;
+ };
+ };
+ };
+}
+
+export class AlertingSecurity {
+ public static readonly getSecurityHealth = async (
+ context: RequestHandlerContext,
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
+ ): Promise => {
+ const {
+ security: {
+ enabled: isSecurityEnabled = false,
+ ssl: { http: { enabled: isTLSEnabled = false } = {} } = {},
+ } = {},
+ }: XPackUsageSecurity = await context.core.elasticsearch.legacy.client.callAsInternalUser(
+ 'transport.request',
+ {
+ method: 'GET',
+ path: '/_xpack/usage',
+ }
+ );
+
+ return {
+ isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
+ hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey,
+ };
+ };
+}
diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts
index 5f358badde401..39ec5fe1ffaa7 100644
--- a/x-pack/plugins/monitoring/server/plugin.ts
+++ b/x-pack/plugins/monitoring/server/plugin.ts
@@ -203,6 +203,7 @@ export class Plugin {
requireUIRoutes(this.monitoringCore, {
router,
licenseService: this.licenseService,
+ encryptedSavedObjects: plugins.encryptedSavedObjects,
});
initInfraSource(config, plugins.infra);
}
diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
index 1d83644fce756..6077591ef6d4e 100644
--- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
+++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts
@@ -10,19 +10,37 @@ import { AlertsFactory } from '../../../../alerts';
import { RouteDependencies } from '../../../../types';
import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants';
import { ActionResult } from '../../../../../../actions/common';
-// import { fetchDefaultEmailAddress } from '../../../../lib/alerts/fetch_default_email_address';
+import { AlertingSecurity } from '../../../../lib/elasticsearch/verify_alerting_security';
const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log';
-export function enableAlertsRoute(server: any, npRoute: RouteDependencies) {
+export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) {
npRoute.router.post(
{
path: '/api/monitoring/v1/alerts/enable',
options: { tags: ['access:monitoring'] },
validate: false,
},
- async (context, request, response) => {
+ async (context, _request, response) => {
try {
+ const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService));
+
+ if (alerts.length) {
+ const {
+ isSufficientlySecure,
+ hasPermanentEncryptionKey,
+ } = await AlertingSecurity.getSecurityHealth(context, npRoute.encryptedSavedObjects);
+
+ if (!isSufficientlySecure || !hasPermanentEncryptionKey) {
+ return response.ok({
+ body: {
+ isSufficientlySecure,
+ hasPermanentEncryptionKey,
+ },
+ });
+ }
+ }
+
const alertsClient = context.alerting?.getAlertsClient();
const actionsClient = context.actions?.getActionsClient();
const types = context.actions?.listTypes();
@@ -58,7 +76,6 @@ export function enableAlertsRoute(server: any, npRoute: RouteDependencies) {
},
];
- const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService));
const createdAlerts = await Promise.all(
alerts.map(
async (alert) => await alert.createIfDoesNotExist(alertsClient, actionsClient, actions)
diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts
index 0c346c8082475..1e7a5acb33644 100644
--- a/x-pack/plugins/monitoring/server/types.ts
+++ b/x-pack/plugins/monitoring/server/types.ts
@@ -16,6 +16,7 @@ import {
import { InfraPluginSetup } from '../../infra/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server';
+import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server';
export interface MonitoringLicenseService {
refresh: () => Promise;
@@ -36,6 +37,7 @@ export interface LegacyAPI {
}
export interface PluginsSetup {
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup;
usageCollection?: UsageCollectionSetup;
licensing: LicensingPluginSetup;
@@ -56,6 +58,7 @@ export interface MonitoringCoreConfig {
export interface RouteDependencies {
router: IRouter;
licenseService: MonitoringLicenseService;
+ encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
}
export interface MonitoringCore {