[Fleet] Add ssl fields to agent binary source settings (#213211)

closes https://github.com/elastic/kibana/issues/207324
follow up of https://github.com/elastic/kibana/issues/207322

## Summary
Add ssl fields to agent binary source settings. The new fields allow
users to set a TLS connection to the agent binary source uri.
- The cert key will be stored either as an encrypted SO or a secret
(latter option will be available once fleet server will have this
functionality: https://github.com/elastic/fleet-server/issues/4470).
- The secret field is only available when the feature flag
`enableSSLSecrets` is enabled, otherwise the cert key is saved as an
encrypted SO.

<details>
  <summary>Screenshots</summary>
<img width="809" alt="Screenshot 2025-03-11 at 14 53 44"
src="https://github.com/user-attachments/assets/e93a04cf-c699-4e13-8cb6-870986197f92"
/>
<img width="804" alt="Screenshot 2025-03-11 at 14 53 34"
src="https://github.com/user-attachments/assets/c2c13c8f-e65c-4843-a538-d317e1359bf0"
/>



Generated policy:
<img width="797" alt="Screenshot 2025-03-06 at 17 43 02"
src="https://github.com/user-attachments/assets/12411fea-9a8b-4ee9-aa7c-123c6aefea4a"
/>

</details>

### Checklist
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2025-03-12 09:50:32 +01:00 committed by GitHub
parent ed7178674c
commit 382630ecd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 3462 additions and 689 deletions

View file

@ -8703,6 +8703,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -8807,6 +8856,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -8846,6 +8944,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9015,6 +9162,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9113,6 +9309,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9152,6 +9397,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -16374,8 +16668,56 @@
"download": {
"additionalProperties": false,
"properties": {
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"key"
],
"type": "object"
}
},
"type": "object"
},
"sourceURI": {
"type": "string"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
},
"renegotiation": {
"type": "string"
},
"verification_mode": {
"type": "string"
}
},
"type": "object"
}
},
"required": [

View file

@ -8703,6 +8703,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -8807,6 +8856,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -8846,6 +8944,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9015,6 +9162,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9113,6 +9309,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -9152,6 +9397,55 @@
"description": "The ID of the proxy to use for this download source. See the proxies API for more information.",
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -16374,8 +16668,56 @@
"download": {
"additionalProperties": false,
"properties": {
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"key": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"key"
],
"type": "object"
}
},
"type": "object"
},
"sourceURI": {
"type": "string"
},
"ssl": {
"additionalProperties": false,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"key": {
"type": "string"
},
"renegotiation": {
"type": "string"
},
"verification_mode": {
"type": "string"
}
},
"type": "object"
}
},
"required": [

View file

@ -12995,6 +12995,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -13065,6 +13095,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- name
- host
@ -13094,6 +13154,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -13208,6 +13298,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -13273,6 +13393,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- name
- host
@ -13302,6 +13452,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -17976,8 +18156,40 @@ paths:
additionalProperties: false
type: object
properties:
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
additionalProperties: false
type: object
properties:
id:
type: string
required:
- key
sourceURI:
type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
renegotiation:
type: string
verification_mode:
type: string
required:
- sourceURI
features:

View file

@ -15226,6 +15226,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -15295,6 +15325,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- name
- host
@ -15324,6 +15384,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -15436,6 +15526,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -15500,6 +15620,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- name
- host
@ -15529,6 +15679,36 @@ paths:
description: The ID of the proxy to use for this download source. See the proxies API for more information.
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
required:
- id
- name
@ -20195,8 +20375,40 @@ paths:
additionalProperties: false
type: object
properties:
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
key:
additionalProperties: false
type: object
properties:
id:
type: string
required:
- key
sourceURI:
type: string
ssl:
additionalProperties: false
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
key:
type: string
renegotiation:
type: string
verification_mode:
type: string
required:
- sourceURI
features:

View file

@ -2105,6 +2105,7 @@
}
},
"ingest-download-sources": {
"dynamic": false,
"properties": {
"host": {
"type": "keyword"

View file

@ -125,7 +125,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"infrastructure-monitoring-log-view": "5f86709d3c27aed7a8379153b08ee5d3d90d77f5",
"infrastructure-ui-source": "113182d6895764378dfe7fa9fa027244f3a457c4",
"ingest-agent-policies": "cfe66f4aeca8f53b26bd4ddb0e956de1637d774e",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-download-sources": "5be99940d6b5f9121b2fd279708d14e2bc0bde26",
"ingest-outputs": "6743521f501bd77b1523dbb1df48d7c47fdad529",
"ingest-package-policies": "6a80000fdf2544f2485b0c6a51ecc434b6a12987",
"ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505",

View file

@ -8,9 +8,7 @@
import type { SecurityRoleDescriptor } from '@elastic/elasticsearch/lib/api/types';
import type { agentPolicyStatuses } from '../../constants';
import type { MonitoringType, PolicySecretReference, ValueOf } from '..';
import type { SOSecret } from '..';
import type { BaseSSLSecrets, MonitoringType, PolicySecretReference, ValueOf } from '..';
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
import type { Output } from './output';
@ -179,6 +177,11 @@ export interface FullAgentPolicyMonitoring {
};
};
}
export interface FullAgentPolicyDownload {
sourceURI: string;
ssl?: BaseSSLConfig;
secrets?: BaseSSLSecrets;
}
export interface FullAgentPolicy {
id: string;
@ -198,7 +201,7 @@ export interface FullAgentPolicy {
revision?: number;
agent?: {
monitoring: FullAgentPolicyMonitoring;
download: { sourceURI: string };
download: FullAgentPolicyDownload;
features: Record<string, { enabled: boolean }>;
protection?: {
enabled: boolean;
@ -239,9 +242,7 @@ export interface FullAgentPolicyFleetConfig {
proxy_url?: string;
proxy_headers?: any;
ssl?: BaseSSLConfig;
secrets?: {
ssl?: { key?: SOSecret };
};
secrets?: BaseSSLSecrets;
}
export interface FullAgentPolicyKibanaConfig {

View file

@ -5,11 +5,19 @@
* 2.0.
*/
import type { BaseSSLSecrets } from './secret';
export interface DownloadSourceBase {
name: string;
host: string;
is_default: boolean;
proxy_id?: string | null;
ssl?: {
certificate_authorities?: string[];
certificate?: string;
key?: string;
};
secrets?: BaseSSLSecrets;
}
export type DownloadSource = DownloadSourceBase & {

View file

@ -6,7 +6,7 @@
*/
import type { outputType } from '../../constants';
import type { ValueOf } from '..';
import type { BaseSSLSecrets, ValueOf } from '..';
import type { kafkaAuthType, kafkaCompressionType, kafkaSaslMechanism } from '../../constants';
import type { kafkaPartitionType } from '../../constants';
import type { kafkaTopicWhenType } from '../../constants';
@ -47,11 +47,7 @@ interface NewBaseOutput {
proxy_id?: string | null;
shipper?: ShipperOutput | null;
allow_edit?: string[];
secrets?: {
ssl?: {
key?: SOSecret;
};
};
secrets?: BaseSSLSecrets;
preset?: OutputPreset;
}
@ -62,13 +58,7 @@ export interface NewElasticsearchOutput extends NewBaseOutput {
export interface NewRemoteElasticsearchOutput extends NewBaseOutput {
type: OutputType['RemoteElasticsearch'];
service_token?: string | null;
secrets?: {
service_token?: SOSecret;
kibana_api_key?: SOSecret;
ssl?: {
key?: SOSecret;
};
};
secrets?: RemoteESOutputSecrets;
sync_integrations?: boolean;
kibana_url?: string | null;
kibana_api_key?: string | null;
@ -76,11 +66,6 @@ export interface NewRemoteElasticsearchOutput extends NewBaseOutput {
export interface NewLogstashOutput extends NewBaseOutput {
type: OutputType['Logstash'];
secrets?: {
ssl?: {
key?: SOSecret;
};
};
}
export type NewOutput =
@ -140,10 +125,13 @@ export interface KafkaOutput extends NewBaseOutput {
timeout?: number;
broker_timeout?: number;
required_acks?: ValueOf<KafkaAcknowledgeReliabilityLevel>;
secrets?: {
password?: SOSecret;
ssl?: {
key?: SOSecret;
};
};
secrets?: KafkaOutputSecrets;
}
interface KafkaOutputSecrets extends BaseSSLSecrets {
password?: SOSecret;
}
interface RemoteESOutputSecrets extends BaseSSLSecrets {
service_token?: SOSecret;
kibana_api_key?: SOSecret;
}

View file

@ -45,3 +45,7 @@ export interface DeletedSecretReference {
id: string;
deleted: boolean;
}
export interface BaseSSLSecrets {
ssl?: { key?: SOSecret };
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { DownloadSourceBase, DownloadSource } from '../models';
import type { DownloadSourceBase, DownloadSource, BaseSSLSecrets } from '../models';
import type { ListResult } from './common';
@ -31,6 +31,12 @@ export interface PutDownloadSourceRequest {
name: string;
host: string;
is_default?: boolean;
ssl?: {
certificate_authorities?: string[];
certificate?: string;
key?: string;
};
secrets?: BaseSSLSecrets;
};
}
@ -41,6 +47,12 @@ export interface PostDownloadSourceRequest {
host: string;
is_default?: boolean;
proxy_id?: string | null;
ssl?: {
certificate_authorities?: string[];
certificate?: string;
key?: string;
};
secrets?: BaseSSLSecrets;
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useMemo, useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiComboBox,
@ -29,10 +29,12 @@ import { i18n } from '@kbn/i18n';
import type { DownloadSource, FleetProxy } from '../../../../types';
import { MAX_FLYOUT_WIDTH } from '../../../../constants';
import { useBreadcrumbs, useStartServices } from '../../../../hooks';
import { useBreadcrumbs, useFleetStatus, useStartServices } from '../../../../hooks';
import { ProxyWarning } from '../fleet_proxies_table/proxy_warning';
import { ExperimentalFeaturesService } from '../../../../services';
import { useDowloadSourceFlyoutForm } from './use_download_source_flyout_form';
import { SSLFormSection } from './ssl_form_section';
export interface EditDownloadSourceFlyoutProps {
downloadSource?: DownloadSource;
@ -53,6 +55,59 @@ export const EditDownloadSourceFlyout: React.FunctionComponent<EditDownloadSourc
() => proxies.map((proxy) => ({ value: proxy.id, label: proxy.name })),
[proxies]
);
const [isFirstLoad, setIsFirstLoad] = React.useState(true);
const [secretsToggleState, setSecretsToggleState] = useState<'disabled' | true | false>(true);
const useSecretsStorage = secretsToggleState === true;
const [isConvertedToSecret, setIsConvertedToSecret] = React.useState({
sslKey: false,
});
const { enableSSLSecrets } = ExperimentalFeaturesService.get();
const fleetStatus = useFleetStatus();
if (fleetStatus.isSecretsStorageEnabled !== undefined && secretsToggleState === 'disabled') {
setSecretsToggleState(fleetStatus.isSecretsStorageEnabled);
}
const onToggleSecretStorage = (secretEnabled: boolean) => {
if (secretsToggleState === 'disabled') {
return;
}
setSecretsToggleState(secretEnabled);
};
useEffect(() => {
if (!isFirstLoad) return;
setIsFirstLoad(false);
// populate the secret input with the value of the plain input in order to re-save the output with secret storage
if (useSecretsStorage && enableSSLSecrets) {
if (inputs.sslKeyInput.value && !inputs.sslKeySecretInput.value) {
inputs.sslKeySecretInput.setValue(inputs.sslKeyInput.value);
inputs.sslKeyInput.clear();
setIsConvertedToSecret({ ...isConvertedToSecret, sslKey: true });
}
}
}, [
useSecretsStorage,
inputs.sslKeyInput,
inputs.sslKeySecretInput,
isFirstLoad,
setIsFirstLoad,
isConvertedToSecret,
enableSSLSecrets,
]);
const onToggleSecretAndClearValue = (secretEnabled: boolean) => {
if (secretEnabled) {
inputs.sslKeyInput.clear();
} else {
inputs.sslKeySecretInput.setValue('');
}
setIsConvertedToSecret({ sslKey: false });
onToggleSecretStorage(secretEnabled);
};
return (
<EuiFlyout onClose={onClose} maxWidth={MAX_FLYOUT_WIDTH}>
<EuiFlyoutHeader hasBorder={true}>
@ -184,6 +239,13 @@ export const EditDownloadSourceFlyout: React.FunctionComponent<EditDownloadSourc
}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<SSLFormSection
inputs={inputs}
useSecretsStorage={enableSSLSecrets && useSecretsStorage}
isConvertedToSecret={isConvertedToSecret.sslKey}
onToggleSecretAndClearValue={onToggleSecretAndClearValue}
/>
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -0,0 +1,147 @@
/*
* 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 React from 'react';
import { EuiTextArea, EuiFormRow, EuiCallOut, EuiSpacer, EuiPanel, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { MultiRowInput } from '../multi_row_input';
import { SecretFormRow } from '../edit_output_flyout/output_form_secret_form_row';
import type { DownlaodSourceFormInputsType } from './use_download_source_flyout_form';
interface Props {
inputs: DownlaodSourceFormInputsType;
useSecretsStorage: boolean;
isConvertedToSecret: boolean;
onToggleSecretAndClearValue: (secretEnabled: boolean) => void;
}
export const SSLFormSection: React.FunctionComponent<Props> = (props) => {
const { inputs, useSecretsStorage, isConvertedToSecret, onToggleSecretAndClearValue } = props;
return (
<>
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
<EuiTitle size="s">
<h3 id="FleetEditOutputFlyoutKafkaAuthenticationTitle">
<FormattedMessage
id="xpack.fleet.settings.editDownloadSourcesFlyout.sslAuthenticationTitle"
defaultMessage="Authentication"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiCallOut
title={i18n.translate('xpack.fleet.editDownloadSourcesFlyout.sslWarningCallout', {
defaultMessage:
'Invalid settings can prevent Elastic Agent from being able to upgrade. If this happens, you will need to provide valid credentials.',
})}
color="warning"
iconType="warning"
/>
<EuiSpacer size="m" />
<MultiRowInput
placeholder={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslCertificateAuthoritiesInputPlaceholder',
{
defaultMessage: 'Specify certificate authority',
}
)}
label={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslCertificateAuthoritiesInputLabel',
{
defaultMessage: 'Server SSL certificate authorities (optional)',
}
)}
multiline={true}
sortable={false}
{...inputs.sslCertificateAuthoritiesInput.props}
/>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editDownloadSourcesFlyout.sslCertificateInputLabel"
defaultMessage="Client SSL certificate"
/>
}
{...inputs.sslCertificateInput.formRowProps}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslCertificateInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslCertificateInputPlaceholder',
{
defaultMessage: 'Specify ssl certificate',
}
)}
/>
</EuiFormRow>
{!useSecretsStorage ? (
<SecretFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editDownloadSourcesFlyout.sslKeyInputLabel"
defaultMessage="Client SSL certificate key"
/>
}
{...inputs.sslKeyInput.formRowProps}
useSecretsStorage={useSecretsStorage}
onToggleSecretStorage={onToggleSecretAndClearValue}
disabled={!useSecretsStorage}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslKeyInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslKeyInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
) : (
<SecretFormRow
fullWidth
title={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslKeySecretInputTitle',
{
defaultMessage: 'Client SSL certificate key',
}
)}
{...inputs.sslKeySecretInput.formRowProps}
useSecretsStorage={useSecretsStorage}
isConvertedToSecret={isConvertedToSecret}
onToggleSecretStorage={onToggleSecretAndClearValue}
cancelEdit={inputs.sslKeySecretInput.cancelEdit}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslKeySecretInput.props}
data-test-subj="sslKeySecretInput"
placeholder={i18n.translate(
'xpack.fleet.settings.editDownloadSourcesFlyout.sslKeySecretInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
)}
</EuiPanel>
</>
);
};

View file

@ -9,6 +9,7 @@ import { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useSecretInput, useComboInput } from '../../../../hooks';
import {
sendPostDownloadSource,
useInput,
@ -20,8 +21,21 @@ import {
import type { DownloadSource, PostDownloadSourceRequest } from '../../../../types';
import { useConfirmModal } from '../../hooks/use_confirm_modal';
import type { DownloadSourceBase } from '../../../../../../../common/types';
import { confirmUpdate } from './confirm_update';
export interface DownlaodSourceFormInputsType {
nameInput: ReturnType<typeof useInput>;
defaultDownloadSourceInput: ReturnType<typeof useSwitchInput>;
hostInput: ReturnType<typeof useInput>;
proxyIdInput: ReturnType<typeof useInput>;
sslCertificateInput: ReturnType<typeof useInput>;
sslKeyInput: ReturnType<typeof useInput>;
sslKeySecretInput: ReturnType<typeof useSecretInput>;
sslCertificateAuthoritiesInput: ReturnType<typeof useComboInput>;
}
export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource?: DownloadSource) {
const authz = useAuthz();
const [isLoading, setIsloading] = useState(false);
@ -41,11 +55,34 @@ export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource
const proxyIdInput = useInput(downloadSource?.proxy_id ?? '', () => undefined, isEditDisabled);
const sslCertificateAuthoritiesInput = useComboInput(
'sslCertificateAuthoritiesComboxBox',
downloadSource?.ssl?.certificate_authorities ?? [],
undefined,
undefined
);
const sslCertificateInput = useInput(
downloadSource?.ssl?.certificate ?? '',
undefined,
undefined
);
const sslKeyInput = useInput(downloadSource?.ssl?.key ?? '', undefined, undefined);
const sslKeySecretInput = useSecretInput(
(downloadSource as DownloadSourceBase)?.secrets?.ssl?.key,
undefined,
undefined
);
const inputs = {
nameInput,
hostInput,
defaultDownloadSourceInput,
proxyIdInput,
sslCertificateInput,
sslKeyInput,
sslCertificateAuthoritiesInput,
sslKeySecretInput,
};
const hasChanged = Object.values(inputs).some((input) => input.hasChanged);
@ -54,8 +91,12 @@ export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource
const nameInputValid = nameInput.validate();
const hostValid = hostInput.validate();
return nameInputValid && hostValid;
}, [nameInput, hostInput]);
const sslCertificateValid = sslCertificateInput.validate();
const sslKeyValid = sslKeyInput.validate();
const sslKeySecretValid = sslKeySecretInput.validate();
return nameInputValid && hostValid && sslCertificateValid && sslKeyValid && sslKeySecretValid;
}, [nameInput, hostInput, sslCertificateInput, sslKeyInput, sslKeySecretInput]);
const submit = useCallback(async () => {
try {
@ -69,6 +110,19 @@ export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource
host: hostInput.value.trim(),
is_default: defaultDownloadSourceInput.value,
proxy_id: proxyIdInput.value || null,
ssl: {
certificate: sslCertificateInput.value,
key: sslKeyInput.value || undefined,
certificate_authorities: sslCertificateAuthoritiesInput.value.filter((val) => val !== ''),
},
...(!sslKeyInput.value &&
sslKeySecretInput.value && {
secrets: {
ssl: {
key: sslKeySecretInput.value || undefined,
},
},
}),
};
if (downloadSource) {
@ -109,6 +163,10 @@ export function useDowloadSourceFlyoutForm(onSuccess: () => void, downloadSource
notifications.toasts,
onSuccess,
proxyIdInput.value,
sslCertificateAuthoritiesInput.value,
sslCertificateInput.value,
sslKeyInput.value,
sslKeySecretInput.value,
validate,
]);

View file

@ -7,37 +7,67 @@
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE } from '../../constants';
import type { AgentPolicy, DownloadSource } from '../../types';
import type { AgentPolicy } from '../../types';
import { getSourceUriForAgentPolicy } from './source_uri_utils';
import { appContextService } from '../../services/app_context';
const soClientMock = savedObjectsClientMock.create();
import { getDownloadSourceForAgentPolicy } from './source_uri_utils';
jest.mock('../download_source', () => {
return {
downloadSourceService: {
getDefaultDownloadSourceId: async () => 'default-download-source-id',
get: async (soClient: any, id: string): Promise<DownloadSource> => {
if (id === 'test-ds-1') {
return {
id: 'test-ds-1',
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
};
}
return {
id: 'default-download-source-id',
jest.mock('../../services/app_context');
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
...securityMock.createSetup(),
}));
function getMockedSoClient(options: { id?: string; sameName?: boolean } = {}) {
const soClientMock = savedObjectsClientMock.create();
soClientMock.get.mockImplementation(async (type: string, id: string) => {
switch (id) {
case 'test-ds-1': {
return mockDownloadSourceSO('test-ds-1', {
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
});
}
case 'default-download-source-id': {
return mockDownloadSourceSO('default-download-source-id', {
is_default: true,
name: 'Default host',
host: 'http://default-registry.co',
};
});
}
default:
throw new Error('not found: ' + id);
}
});
soClientMock.find.mockResolvedValue({
saved_objects: [
{
id: 'default-download-source-id',
is_default: true,
attributes: {
download_source_id: 'test-source-id',
},
},
},
};
});
{
id: 'test-ds-1',
attributes: {
download_source_id: 'test-ds-1',
},
},
],
} as any);
mockedAppContextService.getInternalUserSOClient.mockReturnValue(soClientMock);
return soClientMock;
}
function mockDownloadSourceSO(id: string, attributes: any = {}) {
return {
@ -51,47 +81,10 @@ function mockDownloadSourceSO(id: string, attributes: any = {}) {
};
}
describe('helpers', () => {
beforeEach(() => {
soClientMock.get.mockImplementation(async (type: string, id: string) => {
switch (id) {
case 'test-ds-1': {
return mockDownloadSourceSO('test-ds-1', {
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
});
}
case 'default-download-source-id': {
return mockDownloadSourceSO('default-download-source-id', {
is_default: true,
name: 'Default host',
host: 'http://default-registry.co',
});
}
default:
throw new Error('not found: ' + id);
}
});
soClientMock.find.mockResolvedValue({
saved_objects: [
{
id: 'default-download-source-id',
is_default: true,
attributes: {
download_source_id: 'test-source-id',
},
},
{
id: 'test-ds-1',
attributes: {
download_source_id: 'test-ds-1',
},
},
],
} as any);
});
describe('getSourceUriForAgentPolicy', () => {
it('should return the source_uri set on an agent policy ', async () => {
beforeEach(() => {});
describe('getDownloadSourceForAgentPolicy', () => {
it('should return the dowload source object set on an agent policy ', async () => {
const soClient = getMockedSoClient();
const agentPolicy: AgentPolicy = {
id: 'agent-policy-id',
status: 'active',
@ -106,11 +99,16 @@ describe('helpers', () => {
is_protected: false,
};
expect(await getSourceUriForAgentPolicy(soClientMock, agentPolicy)).toEqual({
expect(await getDownloadSourceForAgentPolicy(soClient, agentPolicy)).toEqual({
host: 'http://custom-registry-test',
id: 'test-ds-1',
is_default: false,
name: 'Test',
});
});
it('should return the default source_uri if there is none set on the agent policy ', async () => {
it('should return the default download source object if there is none set on the agent policy ', async () => {
const soClient = getMockedSoClient();
const agentPolicy: AgentPolicy = {
id: 'agent-policy-id',
status: 'active',
@ -124,8 +122,11 @@ describe('helpers', () => {
is_protected: false,
};
expect(await getSourceUriForAgentPolicy(soClientMock, agentPolicy)).toEqual({
expect(await getDownloadSourceForAgentPolicy(soClient, agentPolicy)).toEqual({
host: 'http://default-registry.co',
id: 'default-download-source-id',
is_default: true,
name: 'Default host',
});
});
});

View file

@ -8,13 +8,13 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { downloadSourceService } from '../../services';
import type { AgentPolicy } from '../../types';
import type { AgentPolicy, DownloadSource } from '../../types';
import { FleetError, DownloadSourceNotFound } from '../../errors';
export const getSourceUriForAgentPolicy = async (
export const getDownloadSourceForAgentPolicy = async (
soClient: SavedObjectsClientContract,
agentPolicy: AgentPolicy
) => {
): Promise<DownloadSource> => {
const defaultDownloadSourceId = await downloadSourceService.getDefaultDownloadSourceId(soClient);
if (!defaultDownloadSourceId) {
@ -25,5 +25,5 @@ export const getSourceUriForAgentPolicy = async (
if (!downloadSource) {
throw new DownloadSourceNotFound(`Download source host not found ${downloadSourceId}`);
}
return { host: downloadSource.host, proxy_id: downloadSource.proxy_id };
return downloadSource;
};

View file

@ -8,6 +8,8 @@
import type { RequestHandler } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import Boom from '@hapi/boom';
import type {
GetOneDownloadSourcesRequestSchema,
PutDownloadSourcesRequestSchema,
@ -19,10 +21,17 @@ import type {
DeleteDownloadSourceResponse,
PutDownloadSourceResponse,
GetDownloadSourceResponse,
DownloadSource,
} from '../../../common/types';
import { downloadSourceService } from '../../services/download_source';
import { agentPolicyService } from '../../services';
function ensureNoDuplicateSecrets(downloadSource: Partial<DownloadSource>) {
if (downloadSource.ssl?.key && downloadSource.secrets?.ssl?.key) {
throw Boom.badRequest('Cannot specify both ssl.key and secrets.ssl.key');
}
}
export const getDownloadSourcesHandler: RequestHandler = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
const downloadSources = await downloadSourceService.list(soClient);
@ -52,7 +61,7 @@ export const getOneDownloadSourcesHandler: RequestHandler<
} catch (error) {
if (error.isBoom && error.output.statusCode === 404) {
return response.notFound({
body: { message: `Download source ${request.params.sourceId} not found` },
body: { message: `Agent binary source ${request.params.sourceId} not found` },
});
}
@ -68,15 +77,16 @@ export const putDownloadSourcesHandler: RequestHandler<
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
const esClient = coreContext.elasticsearch.client.asInternalUser;
ensureNoDuplicateSecrets(request.body);
try {
await downloadSourceService.update(soClient, request.params.sourceId, request.body);
await downloadSourceService.update(soClient, esClient, request.params.sourceId, request.body);
const downloadSource = await downloadSourceService.get(soClient, request.params.sourceId);
if (downloadSource.is_default) {
await agentPolicyService.bumpAllAgentPolicies(esClient);
} else {
await agentPolicyService.bumpAllAgentPoliciesForDownloadSource(esClient, downloadSource.id);
}
const body: PutDownloadSourceResponse = {
item: downloadSource,
};
@ -102,11 +112,13 @@ export const postDownloadSourcesHandler: RequestHandler<
const soClient = coreContext.savedObjects.client;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const { id, ...data } = request.body;
const downloadSource = await downloadSourceService.create(soClient, data, { id });
ensureNoDuplicateSecrets(data);
const downloadSource = await downloadSourceService.create(soClient, esClient, data, { id });
if (downloadSource.is_default) {
await agentPolicyService.bumpAllAgentPolicies(esClient);
}
const body: GetOneDownloadSourceResponse = {
item: downloadSource,
};
@ -129,7 +141,7 @@ export const deleteDownloadSourcesHandler: RequestHandler<
} catch (error) {
if (error.isBoom && error.output.statusCode === 404) {
return response.notFound({
body: { message: `Donwload source ${request.params.sourceId} not found` },
body: { message: `Agent binary source ${request.params.sourceId} not found` },
});
}

View file

@ -1129,6 +1129,7 @@ export const getSavedObjectTypes = (
importableAndExportable: false,
},
mappings: {
dynamic: false,
properties: {
source_id: { type: 'keyword', index: false },
name: { type: 'keyword' },
@ -1137,6 +1138,16 @@ export const getSavedObjectTypes = (
proxy_id: { type: 'keyword' },
},
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {},
},
],
},
},
},
[FLEET_SERVER_HOST_SAVED_OBJECT_TYPE]: {
name: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
@ -1297,4 +1308,10 @@ export function registerEncryptedSavedObjects(
// enforceRandomId allows to create an SO with an arbitrary id
enforceRandomId: false,
});
encryptedSavedObjects.registerType({
type: DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
attributesToEncrypt: new Set([{ key: 'ssl', dangerouslyExposeValue: true }]),
// enforceRandomId allows to create an SO with an arbitrary id
enforceRandomId: false,
});
}

View file

@ -20,9 +20,11 @@ import { getFleetServerHostsForAgentPolicy } from '../fleet_server_host';
import {
generateFleetConfig,
getBinarySourceSettings,
getFullAgentPolicy,
getFullMonitoringSettings,
transformOutputToFullPolicyOutput,
generateFleetServerOutputSSLConfig,
} from './full_agent_policy';
import { getMonitoringPermissions } from './monitoring_permissions';
@ -126,6 +128,23 @@ jest.mock('../download_source', () => {
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
};
} else if (id === 'test-ds-secrets') {
return {
id: 'test-ds-1',
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
secrets: {
ssl: {
key: 'KEY1',
},
},
};
}
return {
@ -685,7 +704,7 @@ describe('getFullAgentPolicy', () => {
expect(agentPolicy).toMatchSnapshot();
});
it('should return the sourceURI from the agent policy', async () => {
it('should return agent binary sourceURI and ssl options from the agent policy', async () => {
mockAgentPolicy({
namespace: 'default',
revision: 1,
@ -710,6 +729,53 @@ describe('getFullAgentPolicy', () => {
agent: {
download: {
sourceURI: 'http://custom-registry-test',
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
},
monitoring: {
namespace: 'default',
use_output: 'default',
enabled: true,
logs: false,
metrics: true,
traces: false,
},
},
});
});
it('should return agent binary with secrets if there are any present', async () => {
mockAgentPolicy({
namespace: 'default',
revision: 1,
monitoring_enabled: ['metrics'],
download_source_id: 'test-ds-secrets',
});
const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy');
expect(agentPolicy).toMatchObject({
id: 'agent-policy',
outputs: {
default: {
type: 'elasticsearch',
hosts: ['http://127.0.0.1:9201'],
},
},
inputs: [],
revision: 1,
fleet: {
hosts: ['http://fleetserver:8220'],
},
agent: {
download: {
sourceURI: 'http://custom-registry-test',
secrets: {
ssl: {
key: 'KEY1',
},
},
},
monitoring: {
namespace: 'default',
@ -1591,20 +1657,14 @@ describe('generateFleetConfig', () => {
outputs
);
expect(res).toMatchInlineSnapshot(`
Object {
"hosts": Array [
"https://test.fr",
],
"ssl": Object {
"certificate": "my-cert",
"certificate_authorities": Array [
"/tmp/ssl/ca.crt",
],
"key": "my-key",
},
}
`);
expect(res).toEqual({
hosts: ['https://test.fr'],
ssl: {
certificate: 'my-cert',
certificate_authorities: ['/tmp/ssl/ca.crt'],
key: 'my-key',
},
});
});
it('should generate ssl config when a default remote_elasticsearch output has ssl options', () => {
@ -1641,20 +1701,14 @@ describe('generateFleetConfig', () => {
outputs
);
expect(res).toMatchInlineSnapshot(`
Object {
"hosts": Array [
"https://test.fr",
],
"ssl": Object {
"certificate": "my-cert",
"certificate_authorities": Array [
"/tmp/ssl/ca.crt",
],
"key": "my-key",
},
}
`);
expect(res).toEqual({
hosts: ['https://test.fr'],
ssl: {
certificate: 'my-cert',
certificate_authorities: ['/tmp/ssl/ca.crt'],
key: 'my-key',
},
});
});
it('should generate ssl config when a ES custom output has ssl options', () => {
@ -1679,7 +1733,7 @@ describe('generateFleetConfig', () => {
},
secrets: {
ssl: {
key: 'my-key',
key: { id: 'my-key' },
},
},
},
@ -1695,24 +1749,18 @@ describe('generateFleetConfig', () => {
outputs
);
expect(res).toMatchInlineSnapshot(`
Object {
"hosts": Array [
"https://test.fr",
],
"secrets": Object {
"ssl": Object {
"key": "my-key",
},
expect(res).toEqual({
hosts: ['https://test.fr'],
secrets: {
ssl: {
key: { id: 'my-key' },
},
"ssl": Object {
"certificate": "my-cert",
"certificate_authorities": Array [
"/tmp/ssl/ca.crt",
],
},
}
`);
},
ssl: {
certificate: 'my-cert',
certificate_authorities: ['/tmp/ssl/ca.crt'],
},
});
});
it('should generate ssl config when a remote_elasticsearch custom output has ssl options', () => {
@ -1737,7 +1785,7 @@ describe('generateFleetConfig', () => {
},
secrets: {
ssl: {
key: 'my-key',
key: { id: 'my-key' },
},
},
},
@ -1753,23 +1801,318 @@ describe('generateFleetConfig', () => {
outputs
);
expect(res).toMatchInlineSnapshot(`
Object {
"hosts": Array [
"https://test.fr",
],
"secrets": Object {
"ssl": Object {
"key": "my-key",
expect(res).toEqual({
hosts: ['https://test.fr'],
secrets: {
ssl: {
key: {
id: 'my-key',
},
},
"ssl": Object {
"certificate": "my-cert",
"certificate_authorities": Array [
"/tmp/ssl/ca.crt",
],
},
ssl: {
certificate: 'my-cert',
certificate_authorities: ['/tmp/ssl/ca.crt'],
},
});
});
it('should use secrets key if both keys are present', () => {
const outputs = [
{
id: 'output-1',
name: 'Output 1',
type: 'elasticsearch',
is_default: true,
hosts: ['http://test.fr:9200'],
},
{
id: 'output-2',
name: 'Output 2',
type: 'remote_elasticsearch',
is_default_monitoring: false,
hosts: ['http://test.fr:9200'],
is_default: false,
ssl: {
certificate_authorities: ['/tmp/ssl/ca.crt'],
certificate: 'my-cert',
key: { id: 'my-key' },
},
}
`);
secrets: {
ssl: {
key: { id: 'my-secret-key' },
},
},
},
] as any;
const agentPolicyWithCustomOutput = { ...agentPolicy, data_output_id: 'output-2' };
const res = generateFleetConfig(
agentPolicyWithCustomOutput,
{
host_urls: ['https://test.fr'],
} as any,
[],
outputs
);
expect(res).toEqual({
hosts: ['https://test.fr'],
secrets: {
ssl: {
key: {
id: 'my-secret-key',
},
},
},
ssl: {
certificate: 'my-cert',
certificate_authorities: ['/tmp/ssl/ca.crt'],
},
});
});
});
describe('generateFleetServerOutputSSLConfig', () => {
const baseFleetServerHost = {
name: 'default Fleet Server',
id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c',
is_default: true,
host_urls: ['http://fleetserver:8220'],
is_preconfigured: false,
} as any;
it('should return undefined if no fleetServerHost is passed', () => {
const res = generateFleetServerOutputSSLConfig(undefined);
expect(res).toEqual(undefined);
});
it('should return undefined if fleetServerHost has no ssl and no secrets', () => {
const res = generateFleetServerOutputSSLConfig(baseFleetServerHost);
expect(res).toEqual(undefined);
});
it('should generate a bootstrap output if there are ES ssl fields', () => {
const fleetServerHost = {
...baseFleetServerHost,
ssl: {
certificate_authorities: ['/tmp/ssl/ca.crt'],
certificate: 'my-cert',
key: 'my-key',
es_certificate_authorities: ['/tmp/ssl/es-ca.crt'],
es_certificate: 'my-es-cert',
es_key: 'my-es-key',
},
};
const res = generateFleetServerOutputSSLConfig(fleetServerHost);
expect(res).toEqual({
'fleetserver-output-93f74c0-e876-11ea-b7d3-8b2acec6f75c': {
ssl: {
certificate: 'my-es-cert',
certificate_authorities: ['/tmp/ssl/es-ca.crt'],
key: 'my-es-key',
},
type: 'elasticsearch',
},
});
});
it('should generate a bootstrap output if there are ES secrets fields', () => {
const fleetServerHost = {
...baseFleetServerHost,
secrets: {
ssl: {
key: 'my-key',
es_key: 'my-es-key',
},
},
};
const res = generateFleetServerOutputSSLConfig(fleetServerHost);
expect(res).toEqual({
'fleetserver-output-93f74c0-e876-11ea-b7d3-8b2acec6f75c': {
secrets: {
ssl: {
key: 'my-es-key',
},
},
type: 'elasticsearch',
},
});
});
it('should generate a bootstrap output if there are both secrets and ES ssl fields', () => {
const fleetServerHost = {
...baseFleetServerHost,
ssl: {
es_certificate_authorities: ['/tmp/ssl/es-ca.crt'],
es_certificate: 'my-es-cert',
},
secrets: {
ssl: {
key: { id: 'my-key' },
es_key: { id: 'my-es-key' },
},
},
};
const res = generateFleetServerOutputSSLConfig(fleetServerHost);
expect(res).toEqual({
'fleetserver-output-93f74c0-e876-11ea-b7d3-8b2acec6f75c': {
ssl: {
certificate_authorities: ['/tmp/ssl/es-ca.crt'],
certificate: 'my-es-cert',
},
secrets: {
ssl: {
key: { id: 'my-es-key' },
},
},
type: 'elasticsearch',
},
});
});
it('should use secrets key if the key is present in both ways', () => {
const fleetServerHost = {
...baseFleetServerHost,
ssl: {
es_certificate_authorities: ['/tmp/ssl/es-ca.crt'],
es_certificate: 'my-es-cert',
es_key: { id: 'my-es-key' },
},
secrets: {
ssl: {
key: { id: 'my-key' },
es_key: { id: 'my-secret-es-key' },
},
},
};
const res = generateFleetServerOutputSSLConfig(fleetServerHost);
expect(res).toEqual({
'fleetserver-output-93f74c0-e876-11ea-b7d3-8b2acec6f75c': {
ssl: {
certificate_authorities: ['/tmp/ssl/es-ca.crt'],
certificate: 'my-es-cert',
},
secrets: {
ssl: {
key: { id: 'my-secret-es-key' },
},
},
type: 'elasticsearch',
},
});
});
});
describe('getBinarySourceSettings', () => {
const downloadSource = {
id: 'test-ds-1',
is_default: false,
name: 'Test',
host: 'http://custom-registry-test',
} as any;
it('should return sourceURI for agent download config', () => {
expect(getBinarySourceSettings(downloadSource, null)).toEqual({
sourceURI: 'http://custom-registry-test',
});
});
it('should return agent download config with ssl options if present', () => {
const downloadSourceSSL = {
...downloadSource,
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
};
expect(getBinarySourceSettings(downloadSourceSSL, null)).toEqual({
sourceURI: 'http://custom-registry-test',
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
});
});
it('should return agent download config when there is a proxy', () => {
expect(getBinarySourceSettings(downloadSource, 'http://proxy_uri.it')).toEqual({
proxy_url: 'http://proxy_uri.it',
sourceURI: 'http://custom-registry-test',
});
});
it('should return agent download config with secrets if present', () => {
const downloadSourceSecrets = {
...downloadSource,
secrets: {
ssl: {
key: { id: 'keyid' },
},
},
};
expect(getBinarySourceSettings(downloadSourceSecrets, null)).toEqual({
sourceURI: 'http://custom-registry-test',
secrets: {
ssl: {
key: { id: 'keyid' },
},
},
});
});
it('should return agent download config with secrets and ssl if present', () => {
const downloadSourceSecrets = {
...downloadSource,
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
},
secrets: {
ssl: {
key: { id: 'keyid' },
},
},
};
expect(getBinarySourceSettings(downloadSourceSecrets, null)).toEqual({
sourceURI: 'http://custom-registry-test',
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
},
secrets: {
ssl: {
key: { id: 'keyid' },
},
},
});
});
it('should return agent download config using secrets key if both keys are present', () => {
const downloadSourceSecrets = {
...downloadSource,
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: { id: 'keyid' },
},
secrets: {
ssl: {
key: { id: 'secretkeyid' },
},
},
};
expect(getBinarySourceSettings(downloadSourceSecrets, null)).toEqual({
sourceURI: 'http://custom-registry-test',
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
},
secrets: {
ssl: {
key: { id: 'secretkeyid' },
},
},
});
});
});

View file

@ -28,6 +28,8 @@ import type {
AgentPolicy,
} from '../../types';
import type {
DownloadSource,
FullAgentPolicyDownload,
FullAgentPolicyInput,
FullAgentPolicyMonitoring,
FullAgentPolicyOutputPermissions,
@ -45,7 +47,11 @@ import { getPackageInfo } from '../epm/packages';
import { pkgToPkgKey, splitPkgKey } from '../epm/registry';
import { appContextService } from '../app_context';
import { getFleetServerHostsSecretReferences, getOutputSecretReferences } from '../secrets';
import {
getFleetServerHostsSecretReferences,
getOutputSecretReferences,
getDownloadSourceSecretReferences,
} from '../secrets';
import { getMonitoringPermissions } from './monitoring_permissions';
import { storedPackagePoliciesToAgentInputs } from '.';
@ -90,7 +96,7 @@ export async function getFullAgentPolicy(
dataOutput,
fleetServerHost,
monitoringOutput,
downloadSourceUri,
downloadSource,
downloadSourceProxyUri,
} = await fetchRelatedSavedObjects(soClient, agentPolicy);
// Build up an in-memory object for looking up Package Info, so we don't have
@ -159,7 +165,9 @@ export async function getFullAgentPolicy(
const fleetserverHostSecretReferences = fleetServerHost
? getFleetServerHostsSecretReferences(fleetServerHost)
: [];
const downloadSourceSecretReferences = downloadSource
? getDownloadSourceSecretReferences(downloadSource)
: [];
const packagePolicySecretReferences = (agentPolicy?.package_policies || []).flatMap(
(policy) => policy.secret_references || []
);
@ -181,14 +189,12 @@ export async function getFullAgentPolicy(
secret_references: [
...outputSecretReferences,
...fleetserverHostSecretReferences,
...downloadSourceSecretReferences,
...packagePolicySecretReferences,
],
revision: agentPolicy.revision,
agent: {
download: {
sourceURI: downloadSourceUri,
...(downloadSourceProxyUri ? { proxy_url: downloadSourceProxyUri } : {}),
},
download: getBinarySourceSettings(downloadSource, downloadSourceProxyUri),
monitoring: getFullMonitoringSettings(agentPolicy, monitoringOutput),
features,
protection: {
@ -377,14 +383,13 @@ export function generateFleetConfig(
}),
};
}
// if both ssl.es_key and secrets.ssl.es_key are present, prefer the secrets'
if (output?.secrets) {
config.secrets = {
ssl: {
...(output.secrets?.ssl?.key &&
!output?.ssl?.key && {
key: output.secrets.ssl.key,
}),
...(output.secrets?.ssl?.key && {
key: output.secrets.ssl.key,
}),
},
};
}
@ -440,13 +445,12 @@ function generateSSLConfigForFleetServerInput(fleetServerHost: FleetServerHost)
}),
};
}
// if both ssl.key and secrets.ssl.key are present, prefer the secrets'
if (fleetServerHost?.secrets) {
inputConfig.secrets = {
...(fleetServerHost?.secrets?.ssl?.key &&
!fleetServerHost?.ssl?.key && {
ssl: { key: fleetServerHost.secrets?.ssl?.key },
}),
...(fleetServerHost?.secrets?.ssl?.key && {
ssl: { key: fleetServerHost.secrets?.ssl?.key },
}),
};
}
return inputConfig;
@ -633,7 +637,7 @@ export function transformOutputToFullPolicyOutput(
// Generate the SSL configs for fleet server connection to ES
// Corresponding to --fleet-server-es-ca, --fleet-server-es-cert, --fleet-server-es-cert-key cli options
// This function generates a `bootstrap output` to be sent directly to elastic-agent
function generateFleetServerOutputSSLConfig(fleetServerHost: FleetServerHost | undefined):
export function generateFleetServerOutputSSLConfig(fleetServerHost: FleetServerHost | undefined):
| {
[key: string]: FullAgentPolicyOutput;
}
@ -655,12 +659,12 @@ function generateFleetServerOutputSSLConfig(fleetServerHost: FleetServerHost | u
}),
};
}
// if both ssl.es_key and secrets.ssl.es_key are present, prefer the secrets'
if (fleetServerHost?.secrets) {
outputConfig.secrets = {
...(fleetServerHost?.secrets?.ssl?.es_key &&
!fleetServerHost?.ssl?.es_key && {
ssl: { key: fleetServerHost.secrets?.ssl?.es_key },
}),
...(fleetServerHost?.secrets?.ssl?.es_key && {
ssl: { key: fleetServerHost.secrets?.ssl?.es_key },
}),
};
}
@ -794,3 +798,38 @@ function buildShipperQueueData(shipper: ShipperOutput) {
};
}
/* eslint-enable @typescript-eslint/naming-convention */
export function getBinarySourceSettings(
downloadSource: DownloadSource,
downloadSourceProxyUri: string | null
) {
const config: FullAgentPolicyDownload = {
sourceURI: downloadSource.host,
...(downloadSourceProxyUri ? { proxy_url: downloadSourceProxyUri } : {}),
};
if (downloadSource?.ssl) {
config.ssl = {
...(downloadSource.ssl?.certificate_authorities && {
certificate_authorities: downloadSource.ssl.certificate_authorities,
}),
...(downloadSource.ssl?.certificate && {
certificate: downloadSource.ssl.certificate,
}),
...(downloadSource.ssl?.key &&
!downloadSource?.secrets?.ssl?.key && {
key: downloadSource.ssl.key,
}),
};
}
// if both ssl.es_key and secrets.ssl.key are present, prefer the secrets'
if (downloadSource?.secrets) {
config.secrets = {
ssl: {
...(downloadSource.secrets?.ssl?.key && {
key: downloadSource.secrets.ssl.key,
}),
},
};
}
return config;
}

View file

@ -11,7 +11,7 @@ import { uniq } from 'lodash';
import type { AgentPolicy } from '../../types';
import { outputService } from '../output';
import { getSourceUriForAgentPolicy } from '../../routes/agent/source_uri_utils';
import { getDownloadSourceForAgentPolicy } from '../../routes/agent/source_uri_utils';
import { getFleetServerHostsForAgentPolicy } from '../fleet_server_host';
import { appContextService } from '../app_context';
@ -46,18 +46,18 @@ export async function fetchRelatedSavedObjects(
}, []),
]);
const [outputs, { host: downloadSourceUri, proxy_id: downloadSourceProxyId }, fleetServerHosts] =
await Promise.all([
outputService.bulkGet(outputIds, { ignoreNotFound: true }),
getSourceUriForAgentPolicy(soClient, agentPolicy),
getFleetServerHostsForAgentPolicy(soClient, agentPolicy).catch((err) => {
appContextService
.getLogger()
?.warn(`Unable to get fleet server hosts for policy ${agentPolicy?.id}: ${err.message}`);
const [outputs, downloadSource, fleetServerHosts] = await Promise.all([
outputService.bulkGet(outputIds, { ignoreNotFound: true }),
getDownloadSourceForAgentPolicy(soClient, agentPolicy),
getFleetServerHostsForAgentPolicy(soClient, agentPolicy).catch((err) => {
appContextService
.getLogger()
?.warn(`Unable to get fleet server hosts for policy ${agentPolicy?.id}: ${err.message}`);
return undefined;
}),
]);
return undefined;
}),
]);
const { proxy_id: downloadSourceProxyId } = downloadSource;
const dataOutput = outputs.find((output) => output.id === dataOutputId);
if (!dataOutput) {
@ -92,7 +92,7 @@ export async function fetchRelatedSavedObjects(
proxies,
dataOutput,
monitoringOutput,
downloadSourceUri,
downloadSource,
downloadSourceProxyUri,
fleetServerHost: fleetServerHosts,
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
@ -140,19 +140,29 @@ describe('Download Service', () => {
beforeEach(() => {
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
jest
.mocked(appContextService.getExperimentalFeatures)
.mockReturnValue({ useSpaceAwareness: true } as any);
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: true,
} as any);
});
afterEach(() => {
mockedAgentPolicyService.list.mockClear();
mockedAgentPolicyService.hasAPMIntegration.mockClear();
mockedAgentPolicyService.removeDefaultSourceFromAll.mockReset();
mockedAppContextService.getInternalUserSOClient.mockReset();
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReset();
});
const esClient = elasticsearchServiceMock.createInternalClient();
describe('create', () => {
it('work with a predefined id', async () => {
const soClient = getMockedSoClient();
await downloadSourceService.create(
soClient,
esClient,
{
host: 'http://test.co',
is_default: false,
@ -175,6 +185,7 @@ describe('Download Service', () => {
await downloadSourceService.create(
soClient,
esClient,
{
is_default: true,
name: 'Test',
@ -191,7 +202,7 @@ describe('Download Service', () => {
defaultDownloadSourceId: 'existing-default-download-source',
});
await downloadSourceService.create(soClient, {
await downloadSourceService.create(soClient, esClient, {
is_default: true,
name: 'New default host',
host: 'http://test.co',
@ -204,6 +215,41 @@ describe('Download Service', () => {
{ is_default: false }
);
});
it('should throw if encryptedSavedObject is not configured', async () => {
const soClient = getMockedSoClient();
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: false,
} as any);
await expect(
downloadSourceService.create(
soClient,
esClient,
{
is_default: true,
name: 'Test',
host: 'http://test.co',
},
{ id: 'download-source-test' }
)
).rejects.toThrow(`Agent binary source needs encrypted saved object api key to be set`);
});
it('should work if encryptedSavedObject is configured', async () => {
const soClient = getMockedSoClient();
await downloadSourceService.create(
soClient,
esClient,
{
is_default: true,
name: 'Test',
host: 'http://test.co',
},
{ id: 'download-source-test' }
);
expect(soClient.create).toBeCalled();
});
});
describe('update', () => {
@ -212,7 +258,7 @@ describe('Download Service', () => {
defaultDownloadSourceId: 'existing-default-download-source',
});
await downloadSourceService.update(soClient, 'download-source-test', {
await downloadSourceService.update(soClient, esClient, 'download-source-test', {
is_default: true,
name: 'New default',
host: 'http://test.co',
@ -237,7 +283,7 @@ describe('Download Service', () => {
defaultDownloadSourceId: 'existing-default-download-source',
});
await downloadSourceService.update(soClient, 'existing-default-download-source', {
await downloadSourceService.update(soClient, esClient, 'existing-default-download-source', {
is_default: true,
name: 'Test',
host: 'http://test.co',

View file

@ -4,7 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { omit } from 'lodash';
import type {
ElasticsearchClient,
KibanaRequest,
SavedObjectsClientContract,
} from '@kbn/core/server';
import type { SavedObject } from '@kbn/core/server';
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
@ -15,30 +20,60 @@ import {
DEFAULT_DOWNLOAD_SOURCE_ID,
} from '../constants';
import type { DownloadSource, DownloadSourceSOAttributes, DownloadSourceBase } from '../types';
import { DownloadSourceError, FleetError } from '../errors';
import type {
DownloadSource,
DownloadSourceSOAttributes,
DownloadSourceBase,
PolicySecretReference,
} from '../types';
import {
DownloadSourceError,
FleetEncryptedSavedObjectEncryptionKeyRequired,
FleetError,
} from '../errors';
import { SO_SEARCH_LIMIT } from '../../common';
import { deleteDownloadSourceSecrets, deleteSecrets, isSecretStorageEnabled } from './secrets';
import { agentPolicyService } from './agent_policy';
import { appContextService } from './app_context';
import { escapeSearchQueryPhrase } from './saved_object';
import { getFleetProxy } from './fleet_proxies';
import {
extractAndWriteDownloadSourcesSecrets,
extractAndUpdateDownloadSourceSecrets,
} from './secrets';
function savedObjectToDownloadSource(so: SavedObject<DownloadSourceSOAttributes>) {
const { source_id: sourceId, ...attributes } = so.attributes;
const { ssl, source_id: sourceId, ...attributes } = so.attributes;
return {
id: sourceId ?? so.id,
name: attributes.name,
host: attributes.host,
is_default: attributes.is_default,
proxy_id: attributes.proxy_id,
...attributes,
...(ssl ? { ssl: JSON.parse(ssl as string) } : {}),
};
}
const fakeRequest = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
} as unknown as KibanaRequest;
class DownloadSourceService {
private get encryptedSoClient() {
return appContextService.getInternalUserSOClient(fakeRequest);
}
public async get(soClient: SavedObjectsClientContract, id: string): Promise<DownloadSource> {
const soResponse = await soClient.get<DownloadSourceSOAttributes>(
const soResponse = await this.encryptedSoClient.get<DownloadSourceSOAttributes>(
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
id
);
@ -51,7 +86,7 @@ class DownloadSourceService {
}
public async list(soClient: SavedObjectsClientContract) {
const downloadSources = await soClient.find<DownloadSourceSOAttributes>({
const downloadSources = await this.encryptedSoClient.find<DownloadSourceSOAttributes>({
type: DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
page: 1,
perPage: SO_SEARCH_LIMIT,
@ -69,13 +104,20 @@ class DownloadSourceService {
public async create(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
downloadSource: DownloadSourceBase,
options?: { id?: string; overwrite?: boolean }
): Promise<DownloadSource> {
const logger = appContextService.getLogger();
logger.debug(`Creating new download source`);
const data: DownloadSourceSOAttributes = downloadSource;
const data: DownloadSourceSOAttributes = { ...omit(downloadSource, ['ssl', 'secrets']) };
if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) {
throw new FleetEncryptedSavedObjectEncryptionKeyRequired(
`Agent binary source needs encrypted saved object api key to be set`
);
}
await this.requireUniqueName(soClient, {
name: downloadSource.name,
@ -91,14 +133,32 @@ class DownloadSourceService {
const defaultDownloadSourceId = await this.getDefaultDownloadSourceId(soClient);
if (defaultDownloadSourceId) {
await this.update(soClient, defaultDownloadSourceId, { is_default: false });
await this.update(soClient, esClient, defaultDownloadSourceId, { is_default: false });
}
}
if (options?.id) {
data.source_id = options?.id;
}
if (downloadSource.ssl) {
data.ssl = JSON.stringify(downloadSource.ssl);
}
// Store secret values if enabled; if not, store plain text values
if (await isSecretStorageEnabled(esClient, soClient)) {
const { downloadSource: downloadSourceWithSecrets } =
await extractAndWriteDownloadSourcesSecrets({
downloadSource,
esClient,
});
const newSo = await soClient.create<DownloadSourceSOAttributes>(
if (downloadSourceWithSecrets.secrets)
data.secrets = downloadSourceWithSecrets.secrets as DownloadSourceSOAttributes['secrets'];
} else {
if (!downloadSource.ssl?.key && downloadSource.secrets?.ssl?.key) {
data.ssl = JSON.stringify({ ...downloadSource.ssl, ...downloadSource.secrets.ssl });
}
}
const newSo = await this.encryptedSoClient.create<DownloadSourceSOAttributes>(
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
data,
{
@ -112,12 +172,19 @@ class DownloadSourceService {
public async update(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
newData: Partial<DownloadSource>
) {
let secretsToDelete: PolicySecretReference[] = [];
const logger = appContextService.getLogger();
logger.debug(`Updating download source ${id} with ${newData}`);
const updateData: Partial<DownloadSourceSOAttributes> = newData;
const originalItem = await this.get(soClient, id);
const updateData: Partial<DownloadSourceSOAttributes> = {
...omit(newData, ['ssl', 'secrets']),
};
if (updateData.proxy_id) {
await this.throwIfProxyNotFound(soClient, updateData.proxy_id);
@ -129,15 +196,46 @@ class DownloadSourceService {
id,
});
}
if (newData.ssl) {
updateData.ssl = JSON.stringify(newData.ssl);
} else if (newData.ssl === null) {
// Explicitly set to null to allow to delete the field
updateData.ssl = null;
}
if (updateData.is_default) {
const defaultDownloadSourceId = await this.getDefaultDownloadSourceId(soClient);
if (defaultDownloadSourceId && defaultDownloadSourceId !== id) {
await this.update(soClient, defaultDownloadSourceId, { is_default: false });
await this.update(soClient, esClient, defaultDownloadSourceId, { is_default: false });
}
}
const soResponse = await soClient.update<DownloadSourceSOAttributes>(
// Store secret values if enabled; if not, store plain text values
if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateDownloadSourceSecrets({
oldDownloadSource: originalItem,
downloadSourceUpdate: newData,
esClient,
});
updateData.secrets = secretsRes.downloadSourceUpdate
.secrets as DownloadSourceSOAttributes['secrets'];
secretsToDelete = secretsRes.secretsToDelete;
} else {
if (!newData.ssl?.key && newData.secrets?.ssl?.key) {
updateData.ssl = JSON.stringify({ ...newData.ssl, ...newData.secrets.ssl });
}
}
if (secretsToDelete.length) {
try {
await deleteSecrets({ esClient, ids: secretsToDelete.map((s) => s.id) });
} catch (err) {
logger.warn(`Error cleaning up secrets for output ${id}: ${err.message}`);
}
}
const soResponse = await this.encryptedSoClient.update<DownloadSourceSOAttributes>(
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
id,
updateData
@ -162,8 +260,13 @@ class DownloadSourceService {
appContextService.getInternalUserESClient(),
id
);
logger.debug(`Deleted download source ${id}`);
return soClient.delete(DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, id);
await deleteDownloadSourceSecrets({
esClient: appContextService.getInternalUserESClient(),
downloadSource: targetDS,
});
logger.debug(`Deleting download source ${id}`);
return this.encryptedSoClient.delete(DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, id);
}
public async getDefaultDownloadSourceId(soClient: SavedObjectsClientContract) {
@ -176,7 +279,7 @@ class DownloadSourceService {
return savedObjectToDownloadSource(results.saved_objects[0]).id;
}
public async ensureDefault(soClient: SavedObjectsClientContract) {
public async ensureDefault(soClient: SavedObjectsClientContract, esClient: ElasticsearchClient) {
const downloadSources = await this.list(soClient);
const defaultDS = downloadSources.items.find((o) => o.is_default);
@ -188,7 +291,7 @@ class DownloadSourceService {
host: DEFAULT_DOWNLOAD_SOURCE_URI,
};
return await this.create(soClient, newDefaultDS, {
return await this.create(soClient, esClient, newDefaultDS, {
id: DEFAULT_DOWNLOAD_SOURCE_ID,
overwrite: true,
});
@ -201,7 +304,7 @@ class DownloadSourceService {
soClient: SavedObjectsClientContract,
downloadSource: { name: string; id?: string }
) {
const results = await soClient.find<DownloadSourceSOAttributes>({
const results = await this.encryptedSoClient.find<DownloadSourceSOAttributes>({
type: DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
searchFields: ['name'],
search: escapeSearchQueryPhrase(downloadSource.name),
@ -223,7 +326,7 @@ class DownloadSourceService {
}
public async listAllForProxyId(soClient: SavedObjectsClientContract, proxyId: string) {
const downloadSources = await soClient.find<DownloadSourceSOAttributes>({
const downloadSources = await this.encryptedSoClient.find<DownloadSourceSOAttributes>({
type: DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
searchFields: ['proxy_id'],
search: proxyId,
@ -248,7 +351,7 @@ class DownloadSourceService {
}
private async _getDefaultDownloadSourceSO(soClient: SavedObjectsClientContract) {
return await soClient.find<DownloadSourceSOAttributes>({
return await this.encryptedSoClient.find<DownloadSourceSOAttributes>({
type: DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
searchFields: ['is_default'],
search: 'true',

View file

@ -224,7 +224,7 @@ async function updateRelatedSavedObject(
);
await pMap(downloadSources, (downloadSource) =>
downloadSourceService.update(soClient, downloadSource.id, {
downloadSourceService.update(soClient, esClient, downloadSource.id, {
...omit(downloadSource, 'id'),
proxy_id: null,
})

View file

@ -17,6 +17,8 @@ import type {
NewFleetServerHost,
NewRemoteElasticsearchOutput,
Output,
DownloadSource,
DownloadSourceBase,
} from '../../common/types';
import { packageHasNoPolicyTemplates } from '../../common/services/policy_template';
@ -475,9 +477,7 @@ export async function isOutputSecretStorageEnabled(
return true;
}
logger.info(
'Output secrets storage is disabled as minimum fleet server version has not been met'
);
logger.info('Secrets storage is disabled as minimum fleet server version has not been met');
return false;
}
@ -952,9 +952,8 @@ export async function extractAndWriteFleetServerHostsSecrets(opts: {
}): Promise<{ fleetServerHost: NewFleetServerHost; secretReferences: PolicySecretReference[] }> {
const { fleetServerHost, esClient, secretHashes = {} } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost).filter(
(path) => typeof path.value === 'string'
);
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost);
const secretRes = await extractAndWriteSOSecrets<NewFleetServerHost>({
soObject: fleetServerHost,
secretPaths,
@ -1000,10 +999,7 @@ export async function deleteFleetServerHostsSecrets(opts: {
}): Promise<void> {
const { fleetServerHost, esClient } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost).filter(
(path) => typeof path.value === 'string'
);
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost);
await deleteSOSecrets(esClient, secretPaths);
}
@ -1025,3 +1021,91 @@ export function getFleetServerHostsSecretReferences(
return secretPaths;
}
// Download sources functions
function getDownloadSourcesSecretPaths(
downloadSource: DownloadSource | Partial<DownloadSource>
): SOSecretPath[] {
const secretPaths: SOSecretPath[] = [];
if (downloadSource?.secrets?.ssl?.key) {
secretPaths.push({
path: 'secrets.ssl.key',
value: downloadSource.secrets.ssl.key,
});
}
return secretPaths;
}
export async function extractAndWriteDownloadSourcesSecrets(opts: {
downloadSource: DownloadSourceBase;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ downloadSource: DownloadSourceBase; secretReferences: PolicySecretReference[] }> {
const { downloadSource, esClient, secretHashes = {} } = opts;
const secretPaths = getFleetServerHostsSecretPaths(downloadSource).filter(
(path) => typeof path.value === 'string'
);
const secretRes = await extractAndWriteSOSecrets<DownloadSourceBase>({
soObject: downloadSource,
secretPaths,
esClient,
secretHashes,
});
return {
downloadSource: secretRes.soObjectWithSecrets,
secretReferences: secretRes.secretReferences,
};
}
export async function extractAndUpdateDownloadSourceSecrets(opts: {
oldDownloadSource: DownloadSourceBase;
downloadSourceUpdate: Partial<DownloadSourceBase>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
downloadSourceUpdate: Partial<DownloadSourceBase>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldDownloadSource, downloadSourceUpdate, esClient, secretHashes } = opts;
const oldSecretPaths = getDownloadSourcesSecretPaths(oldDownloadSource);
const updatedSecretPaths = getDownloadSourcesSecretPaths(downloadSourceUpdate);
const secretsRes = await extractAndUpdateSOSecrets<DownloadSourceBase>({
updatedSoObject: downloadSourceUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes,
});
return {
downloadSourceUpdate: secretsRes.updatedSoObject,
secretReferences: secretsRes.secretReferences,
secretsToDelete: secretsRes.secretsToDelete,
};
}
export async function deleteDownloadSourceSecrets(opts: {
downloadSource: DownloadSourceBase;
esClient: ElasticsearchClient;
}): Promise<void> {
const { downloadSource, esClient } = opts;
const secretPaths = getDownloadSourcesSecretPaths(downloadSource);
await deleteSOSecrets(esClient, secretPaths);
}
export function getDownloadSourceSecretReferences(
downloadSource: DownloadSource
): PolicySecretReference[] {
const secretPaths: PolicySecretReference[] = [];
if (typeof downloadSource.secrets?.ssl?.key === 'object') {
secretPaths.push({
id: downloadSource.secrets.ssl.key.id,
});
}
return secretPaths;
}

View file

@ -188,7 +188,7 @@ async function createSetupSideEffects(
logger.debug('Setting Fleet server config');
await migrateSettingsToFleetServerHost(soClient, esClient);
logger.debug('Setting up Fleet download source');
const defaultDownloadSource = await downloadSourceService.ensureDefault(soClient);
const defaultDownloadSource = await downloadSourceService.ensureDefault(soClient, esClient);
// Need to be done before outputs and fleet server hosts as these object can reference a proxy
logger.debug('Setting up Proxy');
await ensurePreconfiguredFleetProxies(

View file

@ -245,6 +245,24 @@ function validateGlobalDataTagInput(tags: GlobalDataTag[]): string | undefined {
}
}
const BaseSSLSchema = schema.object({
verification_mode: schema.maybe(schema.string()),
certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
renegotiation: schema.maybe(schema.string()),
});
const BaseSecretsSchema = schema.object({
ssl: schema.maybe(
schema.object({
key: schema.object({
id: schema.maybe(schema.string()),
}),
})
),
});
export const NewAgentPolicySchema = schema.object({
...AgentPolicyBaseSchema,
force: schema.maybe(schema.boolean()),
@ -332,26 +350,8 @@ export const FullAgentPolicyResponseSchema = schema.object({
hosts: schema.arrayOf(schema.string()),
proxy_url: schema.maybe(schema.string()),
proxy_headers: schema.maybe(schema.any()),
ssl: schema.maybe(
schema.object({
verification_mode: schema.maybe(schema.string()),
certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
renegotiation: schema.maybe(schema.string()),
})
),
secrets: schema.maybe(
schema.object({
ssl: schema.maybe(
schema.object({
key: schema.object({
id: schema.maybe(schema.string()),
}),
})
),
})
),
ssl: schema.maybe(BaseSSLSchema),
secrets: schema.maybe(BaseSecretsSchema),
}),
schema.object({
kibana: schema.object({
@ -435,6 +435,8 @@ export const FullAgentPolicyResponseSchema = schema.object({
}),
download: schema.object({
sourceURI: schema.string(),
ssl: schema.maybe(BaseSSLSchema),
secrets: schema.maybe(BaseSecretsSchema),
}),
features: schema.recordOf(
schema.string(),

View file

@ -7,6 +7,13 @@
import { schema } from '@kbn/config-schema';
const secretRefSchema = schema.oneOf([
schema.object({
id: schema.string(),
}),
schema.string(),
]);
const DownloadSourceBaseSchema = {
id: schema.maybe(schema.string()),
name: schema.string(),
@ -23,6 +30,18 @@ const DownloadSourceBaseSchema = {
}),
])
),
ssl: schema.maybe(
schema.object({
certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
})
),
secrets: schema.maybe(
schema.object({
ssl: schema.maybe(schema.object({ key: schema.maybe(secretRefSchema) })),
})
),
};
export const DownloadSourceSchema = schema.object({ ...DownloadSourceBaseSchema });

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import { isDiffPathProtocol } from '../../../common/services';
import { OutputSchema } from '../models';
import { DownloadSourceResponseSchema, OutputSchema } from '../models';
import { FleetProxySchema } from './fleet_proxies';
import { FleetServerHostSchema } from './fleet_server_policy_config';
@ -119,24 +119,6 @@ export const GetEnrollmentSettingsResponseSchema = schema.object({
es_output: schema.maybe(OutputSchema),
es_output_proxy: schema.maybe(FleetProxySchema),
}),
download_source: schema.maybe(
schema.object({
id: schema.string(),
name: schema.string(),
host: schema.string(),
is_default: schema.boolean(),
proxy_id: schema.maybe(
schema.oneOf([
schema.literal(null),
schema.string({
meta: {
description:
'The ID of the proxy to use for this download source. See the proxies API for more information.',
},
}),
])
),
})
),
download_source: DownloadSourceResponseSchema,
download_source_proxy: schema.maybe(FleetProxySchema),
});

View file

@ -275,5 +275,11 @@ export interface DownloadSourceSOAttributes {
is_default: boolean;
source_id?: string;
proxy_id?: string | null;
ssl?: string | null; // encrypted ssl field
secrets?: {
ssl?: {
key?: { id: string };
};
};
}
export type SimpleSOAssetAttributes = SimpleSOAssetType['attributes'];

View file

@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { testUsers } from '../test_users';
@ -18,15 +19,106 @@ export default function (providerContext: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const fleetAndAgents = getService('fleetAndAgents');
const es = getService('es');
const clearAgents = async () => {
try {
await es.deleteByQuery({
index: '.fleet-agents',
refresh: true,
query: {
match_all: {},
},
});
} catch (err) {
// index doesn't exist
}
};
const createFleetServerPolicy = async (id: string) => {
await kibanaServer.savedObjects.create({
id: `package-policy-test`,
type: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
overwrite: true,
attributes: {
policy_ids: [id],
name: 'Fleet Server',
package: {
name: 'fleet_server',
},
},
});
};
const createFleetServerAgent = async (
agentPolicyId: string,
hostname: string,
agentVersion: string
) => {
const agentResponse = await es.index({
index: '.fleet-agents',
refresh: true,
body: {
access_api_key_id: 'api-key-3',
active: true,
policy_id: agentPolicyId,
type: 'PERMANENT',
local_metadata: {
host: { hostname },
elastic: { agent: { version: agentVersion } },
},
user_provided_metadata: {},
enrolled_at: new Date().toISOString(),
last_checkin: new Date().toISOString(),
tags: ['tag1'],
},
});
return agentResponse._id;
};
const getSecretById = (id: string) => {
return es.get({
index: '.fleet-secrets',
id,
});
};
const deleteAllSecrets = async () => {
try {
await es.deleteByQuery({
index: '.fleet-secrets',
query: {
match_all: {},
},
});
} catch (err) {
// index doesn't exist
}
};
describe('fleet_download_sources_crud', function () {
let defaultDownloadSourceId: string;
let fleetServerPolicyId: string;
skipIfNoDockerRegistry(providerContext);
before(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
await fleetAndAgents.setup();
const { body: apiResponse } = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'kibana')
.send({
name: 'Default Fleet Server policy',
namespace: 'default',
has_fleet_server: true,
is_default: true,
})
.expect(200);
const fleetServerPolicy = apiResponse.item;
fleetServerPolicyId = fleetServerPolicy.id;
await createFleetServerPolicy(fleetServerPolicyId);
const { body: response } = await supertest
.get(`/api/fleet/agent_download_sources`)
.expect(200);
@ -36,6 +128,7 @@ export default function (providerContext: FtrProviderContext) {
throw new Error('default download source not set');
}
defaultDownloadSourceId = defaultDownloadSource.id;
await deleteAllSecrets();
});
after(async () => {
@ -109,66 +202,6 @@ export default function (providerContext: FtrProviderContext) {
});
});
describe('PUT /agent_download_sources/{sourceId}', () => {
it('should allow to update an existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'https://test.co:403',
is_default: false,
})
.expect(200);
const {
body: { item: downloadSource },
} = await supertest
.get(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.expect(200);
expect(downloadSource.host).to.eql('https://test.co:403');
});
it('should allow to update is_default for existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new default host',
host: 'https://test.co',
is_default: true,
})
.expect(200);
await supertest.get(`/api/fleet/agent_download_sources`).expect(200);
});
it('should return a 404 when updating a non existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/idonotexists`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'https://test.co',
is_default: true,
})
.expect(404);
});
it('should return a 400 when passing a host that is not a valid uri', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'not a valid uri',
is_default: true,
})
.expect(400);
});
});
describe('POST /agent_download_sources', () => {
it('should allow to create a new download source host', async function () {
const { body: postResponse } = await supertest
@ -232,16 +265,310 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(400);
});
it('should allow to create a new download source host with ssl fields', async function () {
const { body: postResponse } = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'My download source with ssl',
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
})
.expect(200);
const { id: _, ...itemWithoutId } = postResponse.item;
expect(itemWithoutId).to.eql({
name: 'My download source with ssl',
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate: 'cert',
certificate_authorities: ['ca'],
key: 'KEY1',
},
});
});
it('should not allow ssl.key and secrets.ssl.key to be set at the same time', async function () {
const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
key: 'KEY',
},
secrets: { ssl: { key: 'KEY' } },
})
.expect(400);
expect(res.body.message).to.equal('Cannot specify both ssl.key and secrets.ssl.key');
});
it('should not store secrets if fleet server does not meet minimum version', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '7.0.0');
const { body: res } = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
secrets: {
ssl: {
key: 'KEY1',
},
},
})
.expect(200);
expect(Object.keys(res.item)).not.to.contain('secrets');
expect(Object.keys(res.item)).to.contain('ssl');
expect(Object.keys(res.item.ssl)).to.contain('key');
expect(res.item.ssl.key).to.equal('KEY1');
});
it('should store secrets if fleet server meets minimum version', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'KEY1' } },
})
.expect(200);
expect(Object.keys(res.body.item)).to.contain('secrets');
const secretId1 = res.body.item.secrets.ssl.key.id;
const secret1 = await getSecretById(secretId1);
// @ts-ignore _source unknown type
expect(secret1._source.value).to.equal('KEY1');
});
});
describe('PUT /agent_download_sources/{sourceId}', () => {
it('should allow to update an existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'https://test.co:403',
is_default: false,
})
.expect(200);
const {
body: { item: downloadSource },
} = await supertest
.get(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.expect(200);
expect(downloadSource.host).to.eql('https://test.co:403');
});
it('should allow to update is_default for existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new default host',
host: 'https://test.co',
is_default: true,
})
.expect(200);
await supertest.get(`/api/fleet/agent_download_sources`).expect(200);
});
it('should store secrets if fleet server meets minimum version', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new default host',
host: 'https://test.co',
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'KEY1' } },
})
.expect(200);
expect(Object.keys(res.body.item)).to.contain('secrets');
const secretId1 = res.body.item.secrets.ssl.key.id;
const secret1 = await getSecretById(secretId1);
// @ts-ignore _source unknown type
expect(secret1._source.value).to.equal('KEY1');
});
it('should allow secrets to be updated + delete unused secret', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'My download source with secrets',
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'KEY1' } },
})
.expect(200);
const dsId = res.body.item.id;
const secretId = res.body.item.secrets.ssl.key.id;
const secret = await getSecretById(secretId);
// @ts-ignore _source unknown type
expect(secret._source.value).to.equal('KEY1');
const updatedRes = await supertest
.put(`/api/fleet/agent_download_sources/${dsId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'My download source with secrets',
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'NEW_KEY' } },
})
.expect(200);
const updatedSecretId = updatedRes.body.item.secrets.ssl.key.id;
expect(updatedSecretId).not.to.equal(secretId);
const updatedSecret = await getSecretById(updatedSecretId);
// @ts-ignore _source unknown type
expect(updatedSecret._source.value).to.equal('NEW_KEY');
try {
await getSecretById(secretId);
expect().fail('Secret should have been deleted');
} catch (e) {
// not found
}
});
it('should allow to resave ssl.key as secret if already existing', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
key: 'KEY1',
},
})
.expect(200);
const dsId = res.body.item.id;
const updatedRes = await supertest
.put(`/api/fleet/agent_download_sources/${dsId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'NEW_KEY' } },
})
.expect(200);
const updatedSecretId = updatedRes.body.item.secrets.ssl.key.id;
const updatedSecret = await getSecretById(updatedSecretId);
// @ts-ignore _source unknown type
expect(updatedSecret._source.value).to.equal('NEW_KEY');
});
it('should return a 404 when updating a non existing download source', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/idonotexists`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'https://test.co',
is_default: true,
})
.expect(404);
});
it('should return a 400 when passing a host that is not a valid uri', async function () {
await supertest
.put(`/api/fleet/agent_download_sources/${defaultDownloadSourceId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
host: 'not a valid uri',
is_default: true,
})
.expect(400);
});
});
describe('proxy_id behaviour', () => {
const PROXY_ID = 'download-source-proxy-id';
before(async () => {
await supertest.post(`/api/fleet/proxies`).set('kbn-xsrf', 'xxxx').send({
id: PROXY_ID,
name: 'Download source proxy test',
url: 'https://some.source.proxy:3232',
});
await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080'],
is_default: true,
})
.expect(200);
await supertest
.post(`/api/fleet/proxies`)
.set('kbn-xsrf', 'xxxx')
.send({
id: PROXY_ID,
name: 'Download source proxy test',
url: 'https://some.source.proxy:3232',
})
.expect(200);
});
it('should allow creating a new download source host with a proxy_id ', async function () {
@ -288,14 +615,39 @@ export default function (providerContext: FtrProviderContext) {
description: '',
is_default: false,
download_source_id: downloadSourceId,
});
})
.expect(200);
const { id: agentPolicyId } = postAgentPolicyResponse.item;
await supertest
.post(`/api/fleet/package_policies`)
.set('kbn-xsrf', 'xxx')
.send({
force: true,
package: {
name: 'fleet_server',
version: '1.3.1',
},
name: `Fleet Server 1`,
namespace: 'default',
policy_ids: [agentPolicyId],
vars: {},
inputs: {
'fleet_server-fleet-server': {
enabled: true,
vars: {
custom: '',
},
streams: {},
},
},
})
.expect(200);
const { body: getAgentPolicyResponse } = await supertest
.get(`/api/fleet/agent_policies/${agentPolicyId}/full`)
.set('kbn-xsrf', 'xxxx')
.send();
.send()
.expect(200);
expect(getAgentPolicyResponse.item.agent.download.proxy_url).to.eql(
'https://some.source.proxy:3232'
@ -307,7 +659,7 @@ export default function (providerContext: FtrProviderContext) {
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'My download source',
name: 'My download source with invalid proxy',
host: 'http://test.fr:443',
proxy_id: 'this-proxy-id-does-not-exist',
is_default: false,
@ -436,6 +788,37 @@ export default function (providerContext: FtrProviderContext) {
defaultDSIdToDelete = defaultDSPostResponse.item.id;
});
it('should delete secrets when deleting a download source object', async function () {
await clearAgents();
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/agent_download_sources`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `My download source ${Date.now()}`,
host: 'http://test.fr:443',
is_default: false,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
secrets: { ssl: { key: 'KEY1' } },
})
.expect(200);
const dsId = res.body.item.id;
const secretId = res.body.item.secrets.ssl.key.id;
await supertest
.delete(`/api/fleet/agent_download_sources/${dsId}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
try {
await getSecretById(secretId);
expect().fail('Secret should have been deleted');
} catch (e) {
// not found
}
});
it('should return a 400 when trying to delete a default download source host ', async function () {
await supertest
.delete(`/api/fleet/agent_download_sources/${defaultDSIdToDelete}`)