[Fleet] Add SSL options to fleet server hosts settings (#208091)

Fixes https://github.com/elastic/kibana/issues/207322

## Summary
Show SSL options for fleet server host in Fleet server settings section
and in add fleet server host flyout
- Registered fleet server host as a encrypted save object and the new
mappings added under `ssl` property, mirroring what's already existing
for `logstash` and `kafka` outputs
- The new options are displayed in the UI, both when adding a new fleet
server host from the flyout and when editing an existing one.
- The values are then added to the full agent policy
- The values for `ssh.key` and `ssh.es_key` can additionally be saved as
secrets but for now this option is not enabled until [fleet server
supports it](https://github.com/elastic/fleet-server/issues/4470) - I
used the feature flag `enableSSLSecrets`

<details>
  <summary>Screenshots</summary>
<img width="803" alt="Screenshot 2025-02-14 at 10 23 41"
src="https://github.com/user-attachments/assets/e1bf8c93-e8c0-4351-b86b-a7f8a8b0ec72"
/>
<img width="801" alt="Screenshot 2025-02-14 at 10 23 36"
src="https://github.com/user-attachments/assets/f96d2a5c-0285-41d1-953b-e662ccdcd514"
/>
<img width="780" alt="Screenshot 2025-02-04 at 14 34 52"
src="https://github.com/user-attachments/assets/e854fc28-d4aa-4b01-8634-e1f37f70419b"
/>
<img width="804" alt="Screenshot 2025-02-04 at 14 35 00"
src="https://github.com/user-attachments/assets/f507c34a-774e-4aa1-94b2-b912539d6143"
/>
<img width="791" alt="Screenshot 2025-02-04 at 09 25 28"
src="https://github.com/user-attachments/assets/82c1f761-7ee5-42d0-8b8f-23848cfc0391"
/>

Generated policy:
<img width="795" alt="Screenshot 2025-02-24 at 16 43 58"
src="https://github.com/user-attachments/assets/5ef4e34f-5850-4449-8a70-7de10750bb84"
/>
<img width="796" alt="Screenshot 2025-02-24 at 16 44 15"
src="https://github.com/user-attachments/assets/bdcf70fe-72f0-4df0-9a9e-40346407a1df"
/>




</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-03 13:23:00 +01:00 committed by GitHub
parent 3fcd11ce4d
commit 151fa26a5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 5052 additions and 947 deletions

View file

@ -25956,12 +25956,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
},
@ -26069,6 +26158,95 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -26117,12 +26295,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}
@ -26295,12 +26562,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}
@ -26394,6 +26750,95 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -26441,12 +26886,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}

View file

@ -25956,12 +25956,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
},
@ -26069,6 +26158,95 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -26117,12 +26295,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}
@ -26295,12 +26562,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}
@ -26394,6 +26750,95 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
@ -26441,12 +26886,101 @@
"proxy_id": {
"nullable": true,
"type": "string"
},
"secrets": {
"additionalProperties": false,
"properties": {
"ssl": {
"additionalProperties": false,
"properties": {
"es_key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
},
"key": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
{
"type": "string"
}
]
}
},
"type": "object"
}
},
"type": "object"
},
"ssl": {
"additionalProperties": false,
"nullable": true,
"properties": {
"certificate": {
"type": "string"
},
"certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"client_auth": {
"enum": [
"optional",
"required",
"none"
],
"type": "string"
},
"es_certificate": {
"type": "string"
},
"es_certificate_authorities": {
"items": {
"type": "string"
},
"type": "array"
},
"es_key": {
"type": "string"
},
"key": {
"type": "string"
}
},
"type": "object"
}
},
"required": [
"id",
"name",
"host_urls"
"host_urls",
"id"
],
"type": "object"
}

View file

@ -24602,10 +24602,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
type: array
page:
type: number
@ -24678,6 +24733,61 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- name
- host_urls
@ -24713,10 +24823,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':
@ -24833,10 +24998,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':
@ -24898,6 +25118,61 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- proxy_id
responses:
@ -24932,10 +25207,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':

View file

@ -26675,10 +26675,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
type: array
page:
type: number
@ -26750,6 +26805,61 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- name
- host_urls
@ -26785,10 +26895,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':
@ -26903,10 +27068,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':
@ -26967,6 +27187,61 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- proxy_id
responses:
@ -27001,10 +27276,65 @@ paths:
proxy_id:
nullable: true
type: string
secrets:
additionalProperties: false
type: object
properties:
ssl:
additionalProperties: false
type: object
properties:
es_key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
key:
anyOf:
- additionalProperties: false
type: object
properties:
id:
type: string
required:
- id
- type: string
ssl:
additionalProperties: false
nullable: true
type: object
properties:
certificate:
type: string
certificate_authorities:
items:
type: string
type: array
client_auth:
enum:
- optional
- required
- none
type: string
es_certificate:
type: string
es_certificate_authorities:
items:
type: string
type: array
es_key:
type: string
key:
type: string
required:
- id
- name
- host_urls
- id
required:
- item
'400':

View file

@ -28,6 +28,7 @@ module.exports = {
/x-pack[\/\\]platform[\/\\]packages[\/\\]shared[\/\\]kbn-elastic-assistant[\/\\]impl[\/\\]data_anonymization_editor[\/\\]context_editor[\/\\]get_columns[\/\\]index.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]fleet_server_instructions[\/\\]components[\/\\]fleet_server_hosts_form.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]fleet_server_instructions[\/\\]index.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]fleet_server_instructions[\/\\]steps[\/\\]add_fleet_server_host.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]fleet_server_instructions[\/\\]steps[\/\\]create_service_token.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]generate_service_token.tsx/,
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]components[\/\\]search_bar.tsx/,

View file

@ -1713,6 +1713,7 @@
}
},
"fleet-fleet-server-host": {
"dynamic": false,
"properties": {
"host_urls": {
"index": false,

View file

@ -109,7 +109,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"file-upload-usage-collection-telemetry": "06e0a8c04f991e744e09d03ab2bd7f86b2088200",
"fileShare": "5be52de1747d249a221b5241af2838264e19aaa1",
"fleet-agent-policies": "f69f7c5639f4cf9e85077c904e161f3574ac3ca2",
"fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417",
"fleet-fleet-server-host": "232d98738d5321b86edc426e21a9ca2f607da999",
"fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f",
"fleet-package-policies": "b1ded996118af658bc420a737ff3c4d784641fc7",
"fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e",

View file

@ -10,8 +10,10 @@ import type { SecurityRoleDescriptor } from '@elastic/elasticsearch/lib/api/type
import type { agentPolicyStatuses } from '../../constants';
import type { MonitoringType, PolicySecretReference, ValueOf } from '..';
import type { SOSecret } from '..';
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
import type { Output, OutputSecret } from './output';
import type { Output } from './output';
export type AgentPolicyStatus = typeof agentPolicyStatuses;
@ -126,6 +128,7 @@ export interface FullAgentPolicyInput {
};
streams?: FullAgentPolicyInputStream[];
processors?: FullAgentPolicyAddFields[];
ssl?: BaseSSLConfig;
[key: string]: any;
}
@ -145,6 +148,7 @@ export type FullAgentPolicyOutputPermissions = Record<string, SecurityRoleDescri
export type FullAgentPolicyOutput = Pick<Output, 'type' | 'hosts' | 'ca_sha256'> & {
proxy_url?: string;
proxy_headers?: any;
ssl?: BaseSSLConfig;
[key: string]: any;
};
@ -221,19 +225,22 @@ export interface FullAgentPolicy {
};
}
export interface BaseSSLConfig {
verification_mode?: string;
certificate_authorities?: string[];
renegotiation?: string;
certificate?: string;
key?: string;
client_authentication?: string;
}
export interface FullAgentPolicyFleetConfig {
hosts: string[];
proxy_url?: string;
proxy_headers?: any;
ssl?: {
verification_mode?: string;
certificate_authorities?: string[];
renegotiation?: string;
certificate?: string;
key?: OutputSecret;
};
ssl?: BaseSSLConfig;
secrets?: {
ssl?: { key?: OutputSecret };
ssl?: { key?: SOSecret };
};
}

View file

@ -4,8 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ValueOf } from '..';
// SO definition for this type is declared in server/types/interfaces
import type { SOSecret } from './secret';
export const clientAuth = {
Optional: 'optional',
Required: 'required',
None: 'none',
} as const;
export type ClientAuth = typeof clientAuth;
export interface NewFleetServerHost {
name: string;
host_urls: string[];
@ -13,6 +22,21 @@ export interface NewFleetServerHost {
is_preconfigured: boolean;
is_internal?: boolean;
proxy_id?: string | null;
ssl?: {
certificate_authorities?: string[];
certificate?: string;
key?: string;
es_certificate_authorities?: string[];
es_certificate?: string;
es_key?: string;
client_auth?: ValueOf<ClientAuth>;
} | null;
secrets?: {
ssl?: {
key?: SOSecret;
es_key?: SOSecret;
};
};
}
export interface FleetServerHost extends NewFleetServerHost {

View file

@ -13,6 +13,7 @@ import type { kafkaTopicWhenType } from '../../constants';
import type { kafkaAcknowledgeReliabilityLevel } from '../../constants';
import type { kafkaVerificationModes } from '../../constants';
import type { kafkaConnectionType } from '../../constants';
import type { SOSecret } from '..';
export type OutputType = typeof outputType;
export type KafkaCompressionType = typeof kafkaCompressionType;
@ -23,12 +24,6 @@ export type KafkaPartitionType = typeof kafkaPartitionType;
export type KafkaTopicWhenType = typeof kafkaTopicWhenType;
export type KafkaAcknowledgeReliabilityLevel = typeof kafkaAcknowledgeReliabilityLevel;
export type KafkaVerificationMode = typeof kafkaVerificationModes;
export type OutputSecret =
| string
| {
id: string;
hash?: string;
};
export type OutputPreset = 'custom' | 'balanced' | 'throughput' | 'scale' | 'latency';
@ -54,7 +49,7 @@ interface NewBaseOutput {
allow_edit?: string[];
secrets?: {
ssl?: {
key?: OutputSecret;
key?: SOSecret;
};
};
preset?: OutputPreset;
@ -68,10 +63,10 @@ export interface NewRemoteElasticsearchOutput extends NewBaseOutput {
type: OutputType['RemoteElasticsearch'];
service_token?: string | null;
secrets?: {
service_token?: OutputSecret;
kibana_api_key?: OutputSecret;
service_token?: SOSecret;
kibana_api_key?: SOSecret;
ssl?: {
key?: OutputSecret;
key?: SOSecret;
};
};
sync_integrations?: boolean;
@ -81,6 +76,11 @@ export interface NewRemoteElasticsearchOutput extends NewBaseOutput {
export interface NewLogstashOutput extends NewBaseOutput {
type: OutputType['Logstash'];
secrets?: {
ssl?: {
key?: SOSecret;
};
};
}
export type NewOutput =
@ -141,9 +141,9 @@ export interface KafkaOutput extends NewBaseOutput {
broker_timeout?: number;
required_acks?: ValueOf<KafkaAcknowledgeReliabilityLevel>;
secrets?: {
password?: OutputSecret;
password?: SOSecret;
ssl?: {
key?: OutputSecret;
key?: SOSecret;
};
};
}

View file

@ -21,10 +21,18 @@ export interface SecretPath {
path: string[];
value: PackagePolicyConfigRecordEntry;
}
export interface OutputSecretPath {
export interface SOSecretPath {
path: string;
value: string | { id: string };
}
export type SOSecret =
| string
| {
id: string;
hash?: string;
};
// this is used in the top level secret_refs array on package and agent policies
export interface PolicySecretReference {
id: string;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { FleetServerHost } from '../models';
import type { FleetServerHost, NewFleetServerHost } from '../models';
import type { ListResult } from './common';
@ -15,24 +15,11 @@ export interface PutFleetServerHostsRequest {
params: {
itemId: string;
};
body: {
name?: string;
host_urls?: string[];
is_default?: boolean;
is_internal?: boolean;
proxy_id?: string | null;
};
body: Partial<NewFleetServerHost>;
}
export interface PostFleetServerHostsRequest {
body: {
id?: string;
name?: string;
host_urls?: string[];
is_default?: boolean;
is_internal?: boolean;
proxy_id?: string | null;
};
body: Partial<FleetServerHost>;
}
export interface PostFleetServerHostsResponse {

View file

@ -73,7 +73,7 @@ export const AdvancedTab: React.FunctionComponent<AdvancedTabProps> = ({
getInstallFleetServerStep({
isFleetServerReady,
serviceToken,
fleetServerHost: fleetServerHostForm.fleetServerHost?.host_urls[0],
fleetServerHost: fleetServerHostForm.fleetServerHost,
fleetServerPolicyId: fleetServerPolicyId || selectedPolicyId,
deploymentMode,
disabled: !Boolean(serviceToken),

View file

@ -13,13 +13,18 @@ import {
useInput,
useSwitchInput,
validateInputs,
useSecretInput,
useRadioInput,
} from '../../../hooks';
import type { FleetServerHost } from '../../../types';
import type { FleetServerHostSSLInputsType } from '../../../sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form';
import {
validateName,
validateFleetServerHosts,
} from '../../../sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form';
import type { ClientAuth, NewFleetServerHost, ValueOf } from '../../../../../../common/types';
import { clientAuth } from '../../../../../../common/types';
export interface FleetServerHostForm {
fleetServerHosts: FleetServerHost[];
@ -28,11 +33,7 @@ export interface FleetServerHostForm {
fleetServerHost?: FleetServerHost | null;
setFleetServerHost: React.Dispatch<React.SetStateAction<FleetServerHost | undefined | null>>;
error?: string;
inputs: {
hostUrlsInput: ReturnType<typeof useComboInput>;
nameInput: ReturnType<typeof useInput>;
isDefaultInput: ReturnType<typeof useSwitchInput>;
};
inputs: FleetServerHostSSLInputsType;
}
export const useFleetServerHost = (): FleetServerHostForm => {
@ -44,16 +45,82 @@ export const useFleetServerHost = (): FleetServerHostForm => {
const isDefaultInput = useSwitchInput(false, isPreconfigured || fleetServerHost?.is_default);
const hostUrlsInput = useComboInput('hostUrls', [], validateFleetServerHosts, isPreconfigured);
const inputs = useMemo(
const sslCertificateAuthoritiesInput = useComboInput(
'sslCertificateAuthoritiesComboxBox',
fleetServerHost?.ssl?.certificate_authorities ?? [],
undefined,
undefined
);
const sslCertificateInput = useInput(
fleetServerHost?.ssl?.certificate ?? '',
() => undefined,
undefined
);
const sslEsCertificateAuthoritiesInput = useComboInput(
'sslEsCertificateAuthoritiesComboxBox',
fleetServerHost?.ssl?.es_certificate_authorities ?? [],
undefined,
undefined
);
const sslEsCertificateInput = useInput(
fleetServerHost?.ssl?.es_certificate ?? '',
() => undefined,
undefined
);
const sslClientAuthInput = useRadioInput(
fleetServerHost?.ssl?.client_auth ?? clientAuth.None,
undefined
);
const sslKeyInput = useInput(fleetServerHost?.ssl?.key ?? '', undefined, undefined);
const sslESKeyInput = useInput(fleetServerHost?.ssl?.es_key ?? '', undefined, undefined);
const sslKeySecretInput = useSecretInput(
(fleetServerHost as FleetServerHost)?.secrets?.ssl?.key,
undefined,
undefined
);
const sslESKeySecretInput = useSecretInput(
(fleetServerHost as FleetServerHost)?.secrets?.ssl?.es_key,
undefined,
undefined
);
const inputs: FleetServerHostSSLInputsType = useMemo(
() => ({
nameInput,
isDefaultInput,
hostUrlsInput,
sslCertificateAuthoritiesInput,
sslCertificateInput,
sslEsCertificateAuthoritiesInput,
sslEsCertificateInput,
sslKeyInput,
sslESKeyInput,
sslKeySecretInput,
sslESKeySecretInput,
sslClientAuthInput,
}),
[nameInput, isDefaultInput, hostUrlsInput]
[
nameInput,
isDefaultInput,
hostUrlsInput,
sslCertificateAuthoritiesInput,
sslCertificateInput,
sslEsCertificateAuthoritiesInput,
sslEsCertificateInput,
sslKeyInput,
sslESKeyInput,
sslKeySecretInput,
sslESKeySecretInput,
sslClientAuthInput,
]
);
const validate = useCallback(() => validateInputs(inputs), [inputs]);
const validate = useCallback(() => validateInputs({ ...inputs }), [inputs]);
const { data, resendRequest: refreshGetFleetServerHosts } = useGetFleetServerHosts();
@ -76,12 +143,33 @@ export const useFleetServerHost = (): FleetServerHostForm => {
return;
}
setIsFleetServerHostSubmitted(false);
const newFleetServerHost = {
name: inputs.nameInput.value,
host_urls: inputs.hostUrlsInput.value,
is_default: inputs.isDefaultInput.value,
const newFleetServerHost: Partial<NewFleetServerHost> = {
name: nameInput.value,
host_urls: hostUrlsInput.value,
is_default: isDefaultInput.value,
ssl: {
certificate: sslCertificateInput.value,
key: sslKeyInput.value || undefined,
certificate_authorities: sslCertificateAuthoritiesInput.value.filter((val) => val !== ''),
es_certificate: sslEsCertificateInput.value,
es_key: sslESKeyInput.value || undefined,
es_certificate_authorities: sslEsCertificateAuthoritiesInput.value.filter(
(val) => val !== ''
),
...(sslClientAuthInput.value !== clientAuth.None && {
client_auth: sslClientAuthInput.value as ValueOf<ClientAuth>,
}),
},
...(((!sslKeyInput.value && sslKeySecretInput.value) ||
(!sslESKeyInput.value && sslESKeySecretInput.value)) && {
secrets: {
ssl: {
key: sslKeySecretInput.value || undefined,
es_key: sslESKeySecretInput.value || undefined,
},
},
}),
};
const res = await sendPostFleetServerHost(newFleetServerHost);
if (res.error) {
throw res.error;
@ -97,10 +185,19 @@ export const useFleetServerHost = (): FleetServerHostForm => {
return res.data.item;
}, [
validate,
nameInput.value,
hostUrlsInput.value,
isDefaultInput.value,
sslCertificateInput.value,
sslKeyInput.value,
sslCertificateAuthoritiesInput.value,
sslEsCertificateInput.value,
sslESKeyInput.value,
sslEsCertificateAuthoritiesInput.value,
sslClientAuthInput.value,
sslKeySecretInput.value,
sslESKeySecretInput.value,
refreshGetFleetServerHosts,
inputs.nameInput.value,
inputs.hostUrlsInput.value,
inputs.isDefaultInput.value,
]);
return {

View file

@ -53,7 +53,7 @@ export const QuickStartTab: React.FunctionComponent<Props> = ({ onClose }) => {
}),
getInstallFleetServerStep({
isFleetServerReady,
fleetServerHost: fleetServerHost?.host_urls[0],
fleetServerHost,
fleetServerPolicyId,
serviceToken,
deploymentMode: 'quickstart',

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import type { EuiStepProps } from '@elastic/eui';
import { EuiIconTip } from '@elastic/eui';
import { EuiAccordion, EuiIconTip } from '@elastic/eui';
import {
EuiSwitch,
EuiButton,
@ -24,12 +24,22 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
import type { FleetServerHost } from '../../../types';
import { useStartServices, useLink } from '../../../hooks';
import { useStartServices, useLink, useFleetStatus } from '../../../hooks';
import type { FleetServerHostForm } from '../hooks';
import { MultiRowInput } from '../../../sections/settings/components/multi_row_input';
import { FleetServerHostSelect } from '../components';
import { SSLFormSection } from '../../../sections/settings/components/fleet_server_hosts_flyout/ssl_form_section';
import { ExperimentalFeaturesService } from '../../../services';
const StyledEuiAccordion = styled(EuiAccordion)`
.ingest-active-button {
color: ${(props) => props.theme.eui.euiColorPrimary};
}
`;
export const getAddFleetServerHostStep = ({
fleetServerHostForm,
@ -70,6 +80,69 @@ export const AddFleetServerHostStepContent = ({
const [submittedFleetServerHost, setSubmittedFleetServerHost] = useState<FleetServerHost>();
const { notifications } = useStartServices();
const { getHref } = useLink();
const { enableSSLSecrets } = ExperimentalFeaturesService.get();
const [isFirstLoad, setIsFirstLoad] = React.useState(true);
const [isConvertedToSecret, setIsConvertedToSecret] = React.useState({
sslKey: false,
sslESKey: false,
});
const [secretsToggleState, setSecretsToggleState] = useState<'disabled' | true | false>(true);
const useSecretsStorage = secretsToggleState === true;
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 key 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 });
}
if (inputs.sslESKeyInput.value && !inputs.sslESKeySecretInput.value) {
inputs.sslESKeySecretInput.setValue(inputs.sslESKeyInput.value);
inputs.sslESKeyInput.clear();
setIsConvertedToSecret({ ...isConvertedToSecret, sslESKey: true });
}
}
}, [
inputs.sslKeyInput,
inputs.sslKeySecretInput,
isFirstLoad,
setIsFirstLoad,
isConvertedToSecret,
inputs.sslESKeyInput,
inputs.sslESKeySecretInput,
secretsToggleState,
useSecretsStorage,
enableSSLSecrets,
]);
const onToggleSecretAndClearValue = (secretEnabled: boolean) => {
if (secretEnabled) {
inputs.sslKeyInput.clear();
inputs.sslESKeyInput.clear();
} else {
inputs.sslKeySecretInput.setValue('');
inputs.sslESKeySecretInput.setValue('');
}
setIsConvertedToSecret({ ...isConvertedToSecret, sslKey: false, sslESKey: false });
onToggleSecretStorage(secretEnabled);
};
const onSubmit = useCallback(async () => {
try {
@ -170,6 +243,27 @@ export const AddFleetServerHostStepContent = ({
{error && <EuiFormErrorText>{error}</EuiFormErrorText>}
</>
</EuiFormRow>
<EuiSpacer size="m" />
<StyledEuiAccordion
id="advancedSSLOptions"
data-test-subj="advancedSSLOptionsButton"
buttonContent={
<FormattedMessage
id="xpack.fleet.fleetServerSetup.SSLOptionsToggleLabel"
defaultMessage="SSL options"
/>
}
buttonClassName="ingest-active-button"
>
<EuiSpacer size="s" />
<SSLFormSection
inputs={inputs}
useSecretsStorage={enableSSLSecrets && useSecretsStorage}
onToggleSecretAndClearValue={onToggleSecretAndClearValue}
isConvertedToSecret={isConvertedToSecret}
/>
</StyledEuiAccordion>
<EuiSpacer size="m" />
{fleetServerHosts.length > 0 ? (
<EuiFormRow fullWidth {...inputs.isDefaultInput.formRowProps}>
<EuiSwitch

View file

@ -20,6 +20,8 @@ import { PlatformSelector } from '../..';
import { getInstallCommandForPlatform } from '../utils';
import type { FleetServerHost } from '../../../types';
import type { DeploymentMode } from './set_deployment_mode';
export function getInstallFleetServerStep({
@ -33,7 +35,7 @@ export function getInstallFleetServerStep({
isFleetServerReady: boolean;
disabled: boolean;
serviceToken?: string;
fleetServerHost?: string;
fleetServerHost?: FleetServerHost | null;
fleetServerPolicyId?: string;
deploymentMode: DeploymentMode;
}): EuiStepProps {
@ -55,7 +57,7 @@ export function getInstallFleetServerStep({
const InstallFleetServerStepContent: React.FunctionComponent<{
serviceToken?: string;
fleetServerHost?: string;
fleetServerHost?: FleetServerHost | null;
fleetServerPolicyId?: string;
deploymentMode: DeploymentMode;
}> = ({ serviceToken, fleetServerHost, fleetServerPolicyId, deploymentMode }) => {

View file

@ -8,6 +8,14 @@
import { getInstallCommandForPlatform } from './install_command_utils';
describe('getInstallCommandForPlatform', () => {
const fleetServerHost = {
id: 'host-id1',
name: 'host',
host_urls: ['http://fleetserver:8220'],
is_default: false,
is_preconfigured: false,
};
describe('without policy id', () => {
it('should return the correct command if the the policyId is not set for linux', () => {
expect(
@ -441,7 +449,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -467,7 +475,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -495,7 +503,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -521,7 +529,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -547,7 +555,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
});
@ -577,7 +585,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -604,7 +612,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -633,7 +641,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -660,7 +668,7 @@ describe('getInstallCommandForPlatform', () => {
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
isProductionDeployment: true,
})
).toMatchInlineSnapshot(`
@ -680,6 +688,78 @@ describe('getInstallCommandForPlatform', () => {
--install-servers"
`);
});
describe('with full fleet server hosts settings', () => {
it('should return the command with correct SSL options', () => {
const fullFleetServerHost = {
...fleetServerHost,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
},
};
const res = getInstallCommandForPlatform({
platform: 'deb_x86_64',
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: fullFleetServerHost,
isProductionDeployment: true,
});
expect(res).toMatchInlineSnapshot(`
"curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb
sudo dpkg -i elastic-agent--amd64.deb
sudo systemctl enable elastic-agent
sudo systemctl start elastic-agent
sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\
--fleet-server-es=http://elasticsearch:9200 \\\\
--fleet-server-service-token=service-token-1 \\\\
--fleet-server-policy=policy-1 \\\\
--certificate-authorities='cert authorities' \\\\
--fleet-server-es-ca='path/to/EScert' \\\\
--fleet-server-cert='path/to/cert' \\\\
--fleet-server-cert-key=<PATH_TO_FLEET_SERVER_CERT_KEY> \\\\
--fleet-server-port=8220 \\\\
--install-servers"
`);
});
it('should return the command with SSL options and placeholders', () => {
const fullFleetServerHost = {
...fleetServerHost,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
},
};
const res = getInstallCommandForPlatform({
platform: 'deb_x86_64',
esOutputHost: 'http://elasticsearch:9200',
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: fullFleetServerHost,
isProductionDeployment: true,
});
expect(res).toMatchInlineSnapshot(`
"curl -L -O https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent--amd64.deb
sudo dpkg -i elastic-agent--amd64.deb
sudo systemctl enable elastic-agent
sudo systemctl start elastic-agent
sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\
--fleet-server-es=http://elasticsearch:9200 \\\\
--fleet-server-service-token=service-token-1 \\\\
--fleet-server-policy=policy-1 \\\\
--certificate-authorities='cert authorities' \\\\
--fleet-server-es-ca=<PATH_TO_ES_CERT> \\\\
--fleet-server-cert='path/to/cert' \\\\
--fleet-server-cert-key=<PATH_TO_FLEET_SERVER_CERT_KEY> \\\\
--fleet-server-port=8220 \\\\
--install-servers"
`);
});
});
});
describe('with simple proxy settings', () => {
@ -697,7 +777,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -738,7 +818,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -781,7 +861,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -822,7 +902,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -863,7 +943,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -908,7 +988,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -950,7 +1030,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -994,7 +1074,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1036,7 +1116,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1083,7 +1163,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1161,7 +1241,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1240,7 +1320,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1292,7 +1372,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',
@ -1372,7 +1452,7 @@ describe('getInstallCommandForPlatform', () => {
},
serviceToken: 'service-token-1',
policyId: 'policy-1',
fleetServerHost: 'http://fleetserver:8220',
fleetServerHost,
downloadSource: {
id: 'download-src',
name: 'download-src',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { DownloadSource, FleetProxy } from '../../../../../../common/types';
import type { DownloadSource, FleetProxy, FleetServerHost } from '../../../../../../common/types';
import {
getDownloadBaseUrl,
getDownloadSourceProxyArgs,
@ -120,7 +120,7 @@ export function getInstallCommandForPlatform({
esOutputProxy?: FleetProxy | undefined;
serviceToken: string;
policyId?: string;
fleetServerHost?: string;
fleetServerHost?: FleetServerHost | null;
isProductionDeployment?: boolean;
sslCATrustedFingerprint?: string;
kibanaVersion?: string;
@ -134,7 +134,7 @@ export function getInstallCommandForPlatform({
const commandArguments = [];
if (isProductionDeployment && fleetServerHost) {
commandArguments.push(['url', fleetServerHost]);
commandArguments.push(['url', fleetServerHost?.host_urls[0]]);
}
commandArguments.push(['fleet-server-es', esOutputHost]);
@ -148,11 +148,22 @@ export function getInstallCommandForPlatform({
}
if (isProductionDeployment) {
commandArguments.push(['certificate-authorities', '<PATH_TO_CA>']);
const certificateAuthorities = fleetServerHost?.ssl?.certificate_authorities
? `'${fleetServerHost?.ssl?.certificate_authorities}'`
: '<PATH_TO_CA>';
const fleetServerCert = fleetServerHost?.ssl?.certificate
? `'${fleetServerHost?.ssl?.certificate}'`
: '<PATH_TO_FLEET_SERVER_CERT>';
commandArguments.push(['certificate-authorities', certificateAuthorities]);
if (!sslCATrustedFingerprint) {
commandArguments.push(['fleet-server-es-ca', '<PATH_TO_ES_CERT>']);
const esCert = fleetServerHost?.ssl?.es_certificate
? `'${fleetServerHost?.ssl?.es_certificate}'`
: '<PATH_TO_ES_CERT>';
commandArguments.push(['fleet-server-es-ca', esCert]);
}
commandArguments.push(['fleet-server-cert', '<PATH_TO_FLEET_SERVER_CERT>']);
commandArguments.push(['fleet-server-cert', fleetServerCert]);
commandArguments.push(['fleet-server-cert-key', '<PATH_TO_FLEET_SERVER_CERT_KEY>']);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
@ -30,11 +30,14 @@ import {
import { MultiRowInput } from '../multi_row_input';
import { MAX_FLYOUT_WIDTH } from '../../../../constants';
import { useStartServices } from '../../../../hooks';
import { useFleetStatus, useStartServices } from '../../../../hooks';
import type { FleetServerHost, FleetProxy } from '../../../../types';
import { TextInput } from '../form';
import { ProxyWarning } from '../fleet_proxies_table/proxy_warning';
import { ExperimentalFeaturesService } from '../../../../services';
import { SSLFormSection } from './ssl_form_section';
import { useFleetServerHostsForm } from './use_fleet_server_host_form';
export interface FleetServerHostsFlyoutProps {
@ -60,6 +63,70 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
[proxies]
);
const { enableSSLSecrets } = ExperimentalFeaturesService.get();
const [isFirstLoad, setIsFirstLoad] = React.useState(true);
const [isConvertedToSecret, setIsConvertedToSecret] = React.useState({
sslKey: false,
sslESKey: false,
});
const [secretsToggleState, setSecretsToggleState] = useState<'disabled' | true | false>(true);
const useSecretsStorage = secretsToggleState === true;
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 key 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 });
}
if (inputs.sslESKeyInput.value && !inputs.sslESKeySecretInput.value) {
inputs.sslESKeySecretInput.setValue(inputs.sslESKeyInput.value);
inputs.sslESKeyInput.clear();
setIsConvertedToSecret({ ...isConvertedToSecret, sslESKey: true });
}
}
}, [
inputs.sslKeyInput,
inputs.sslKeySecretInput,
isFirstLoad,
setIsFirstLoad,
isConvertedToSecret,
inputs.sslESKeyInput,
inputs.sslESKeySecretInput,
secretsToggleState,
useSecretsStorage,
enableSSLSecrets,
]);
const onToggleSecretAndClearValue = (secretEnabled: boolean) => {
if (secretEnabled) {
inputs.sslKeyInput.clear();
inputs.sslESKeyInput.clear();
} else {
inputs.sslKeySecretInput.setValue('');
inputs.sslESKeySecretInput.setValue('');
}
setIsConvertedToSecret({ ...isConvertedToSecret, sslKey: false, sslESKey: false });
onToggleSecretStorage(secretEnabled);
};
return (
<EuiFlyout onClose={onClose} maxWidth={MAX_FLYOUT_WIDTH}>
<EuiFlyoutHeader hasBorder={true}>
@ -196,16 +263,16 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
<EuiComboBox
fullWidth
data-test-subj="fleetServerHostsFlyout.proxyIdInput"
{...inputs.proxyIdInput.props}
onChange={(options) => inputs.proxyIdInput.setValue(options?.[0]?.value ?? '')}
{...inputs.proxyIdInput?.props}
onChange={(options) => inputs?.proxyIdInput?.setValue(options?.[0]?.value ?? '')}
selectedOptions={
inputs.proxyIdInput.value !== ''
? proxiesOptions.filter((option) => option.value === inputs.proxyIdInput.value)
inputs?.proxyIdInput?.value !== ''
? proxiesOptions.filter((option) => option.value === inputs.proxyIdInput?.value)
: []
}
options={proxiesOptions}
singleSelection={{ asPlainText: true }}
isDisabled={inputs.proxyIdInput.props.disabled}
isDisabled={inputs.proxyIdInput?.props.disabled}
isClearable={true}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHostsFlyout.proxyIdPlaceholder',
@ -230,6 +297,13 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
}
/>
</EuiFormRow>
<EuiSpacer size="l" />
<SSLFormSection
inputs={inputs}
useSecretsStorage={enableSSLSecrets && useSecretsStorage}
onToggleSecretAndClearValue={onToggleSecretAndClearValue}
isConvertedToSecret={isConvertedToSecret}
/>
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
@ -249,6 +323,7 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
isDisabled={form.isDisabled}
onClick={form.submit}
data-test-subj="saveApplySettingsBtn"
aria-label="Save and apply settings"
>
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.saveButton"

View file

@ -0,0 +1,256 @@
/*
* 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, EuiRadioGroup, EuiSpacer } 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 { clientAuth } from '../../../../../../../common/types';
import type { FleetServerHostSSLInputsType } from './use_fleet_server_host_form';
interface Props {
inputs: FleetServerHostSSLInputsType;
useSecretsStorage: boolean;
isConvertedToSecret: {
sslKey: boolean;
sslESKey: boolean;
};
onToggleSecretAndClearValue: (secretEnabled: boolean) => void;
}
export const SSLFormSection: React.FunctionComponent<Props> = (props) => {
const { inputs, useSecretsStorage, isConvertedToSecret, onToggleSecretAndClearValue } = props;
const clientAuthenticationsOptions = [
{
id: clientAuth.None,
label: 'None',
'data-test-subj': 'clientAuthNoneRadioButton',
},
{
id: clientAuth.Required,
label: 'Required',
'data-test-subj': 'clientAuthUsernamePasswordRadioButton',
},
{
id: clientAuth.Optional,
label: 'Optional',
'data-test-subj': 'clientAuthSSLRadioButton',
},
];
return (
<>
<MultiRowInput
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslCertificateAuthoritiesInputPlaceholder',
{
defaultMessage: 'Specify certificate authority',
}
)}
label={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslCertificateAuthoritiesInputLabel',
{
defaultMessage: 'Server SSL certificate authorities',
}
)}
multiline={true}
sortable={false}
{...inputs.sslCertificateAuthoritiesInput.props}
/>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHosts.sslCertificateInputLabel"
defaultMessage="Client SSL certificate"
/>
}
{...inputs.sslCertificateInput.formRowProps}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslCertificateInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslCertificateInputPlaceholder',
{
defaultMessage: 'Specify SSL certificate',
}
)}
/>
</EuiFormRow>
{!useSecretsStorage ? (
<SecretFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHosts.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.fleetServerHosts.sslKeyInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
) : (
<SecretFormRow
fullWidth
title={i18n.translate('xpack.fleet.settings.fleetServerHosts.sslKeySecretInputTitle', {
defaultMessage: 'Client SSL certificate key',
})}
{...inputs.sslKeySecretInput.formRowProps}
useSecretsStorage={useSecretsStorage}
isConvertedToSecret={isConvertedToSecret.sslKey}
onToggleSecretStorage={onToggleSecretAndClearValue}
cancelEdit={inputs.sslKeySecretInput.cancelEdit}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslKeySecretInput.props}
data-test-subj="sslKeySecretInput"
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslKeySecretInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
)}
<MultiRowInput
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslEsCertificateAuthoritiesInputPlaceholder',
{
defaultMessage: 'Specify Elasticsearch certificate authority',
}
)}
label={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslEsCertificateAuthoritiesInputLabel',
{
defaultMessage: 'Elasticsearch certificate authorities',
}
)}
multiline={true}
sortable={false}
{...inputs.sslEsCertificateAuthoritiesInput.props}
/>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHosts.sslEsCertificateInputLabel"
defaultMessage="SSL certificate for Elasticsearch"
/>
}
{...inputs.sslEsCertificateInput.formRowProps}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslEsCertificateInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslEsCertificateInputPlaceholder',
{
defaultMessage: 'Specify Elasticsearch SSL certificate',
}
)}
/>
</EuiFormRow>
{!useSecretsStorage ? (
<SecretFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHosts.sslEsKeyInputLabel"
defaultMessage="SSL certificate key for Elasticsearch"
/>
}
{...inputs.sslESKeyInput.formRowProps}
useSecretsStorage={useSecretsStorage}
onToggleSecretStorage={onToggleSecretAndClearValue}
disabled={!useSecretsStorage}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslESKeyInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslKeyInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
) : (
<SecretFormRow
fullWidth
title={i18n.translate('xpack.fleet.settings.fleetServerHosts.sslEsKeySecretInputTitle', {
defaultMessage: 'SSL certificate key for Elasticsearch',
})}
{...inputs.sslESKeySecretInput.formRowProps}
useSecretsStorage={useSecretsStorage}
isConvertedToSecret={isConvertedToSecret.sslKey}
onToggleSecretStorage={onToggleSecretAndClearValue}
cancelEdit={inputs.sslESKeySecretInput.cancelEdit}
>
<EuiTextArea
fullWidth
rows={5}
{...inputs.sslESKeySecretInput.props}
data-test-subj="sslESKeySecretInput"
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHosts.sslESKeySecretInputPlaceholder',
{
defaultMessage: 'Specify certificate key',
}
)}
/>
</SecretFormRow>
)}
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHosts.clientAuthenticationInputLabel"
defaultMessage="Client auth"
/>
}
>
<EuiRadioGroup
style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 30 }}
data-test-subj={'fleetServerHosts.clientAuthenticationRadioInput'}
options={clientAuthenticationsOptions}
compressed
{...inputs.sslClientAuthInput.props}
/>
</EuiFormRow>
</>
);
};

View file

@ -70,7 +70,37 @@ describe('useFleetServerHostsForm', () => {
await testRenderer.waitFor(() => expect(onSuccess).toBeCalled());
});
it('should allow the user to correct and submit a invalid form', async () => {
it('should submit a valid form with SSL options', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
testRenderer.startServices.http.post.mockResolvedValue({});
const { result } = testRenderer.renderHook(() =>
useFleetServerHostsForm(
{
id: 'id1',
name: 'fleet server 1',
host_urls: [],
is_default: false,
is_preconfigured: false,
ssl: {
certificate_authorities: ['cert authorities'],
es_certificate_authorities: ['ES cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
},
},
onSuccess
)
);
act(() => result.current.inputs.hostUrlsInput.props.onChange(['https://test.fr']));
await act(() => result.current.submit());
await testRenderer.waitFor(() => expect(onSuccess).toBeCalled());
});
it('should allow the user to correct and submit an invalid form', async () => {
const testRenderer = createFleetTestRendererMock();
const onSuccess = jest.fn();
testRenderer.startServices.http.post.mockResolvedValue({});

View file

@ -4,12 +4,12 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// copy this one
import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useRadioInput } from '../../../../hooks';
import {
sendPostFleetServerHost,
sendPutFleetServerHost,
@ -19,13 +19,32 @@ import {
useStartServices,
useSwitchInput,
validateInputs,
useSecretInput,
} from '../../../../hooks';
import { isDiffPathProtocol } from '../../../../../../../common/services';
import { useConfirmModal } from '../../hooks/use_confirm_modal';
import type { FleetServerHost } from '../../../../types';
import type { ClientAuth, NewFleetServerHost, ValueOf } from '../../../../../../../common/types';
import { clientAuth } from '../../../../../../../common/types';
const URL_REGEX = /^(https):\/\/[^\s$.?#].[^\s]*$/gm;
export interface FleetServerHostSSLInputsType {
nameInput: ReturnType<typeof useInput>;
hostUrlsInput: ReturnType<typeof useComboInput>;
isDefaultInput: ReturnType<typeof useSwitchInput>;
proxyIdInput?: ReturnType<typeof useInput>;
sslCertificateInput: ReturnType<typeof useInput>;
sslKeyInput: ReturnType<typeof useInput>;
sslKeySecretInput: ReturnType<typeof useSecretInput>;
sslCertificateAuthoritiesInput: ReturnType<typeof useComboInput>;
sslEsCertificateInput: ReturnType<typeof useInput>;
sslESKeyInput: ReturnType<typeof useInput>;
sslESKeySecretInput: ReturnType<typeof useSecretInput>;
sslEsCertificateAuthoritiesInput: ReturnType<typeof useComboInput>;
sslClientAuthInput: ReturnType<typeof useRadioInput>;
}
const ConfirmTitle = () => (
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle"
@ -146,17 +165,82 @@ export function useFleetServerHostsForm(
);
const proxyIdInput = useInput(fleetServerHost?.proxy_id ?? '', () => undefined, isEditDisabled);
const inputs = useMemo(
const sslCertificateAuthoritiesInput = useComboInput(
'sslCertificateAuthoritiesComboxBox',
fleetServerHost?.ssl?.certificate_authorities ?? [],
undefined,
isEditDisabled
);
const sslCertificateInput = useInput(
fleetServerHost?.ssl?.certificate ?? '',
() => undefined,
isEditDisabled
);
const sslEsCertificateAuthoritiesInput = useComboInput(
'sslEsCertificateAuthoritiesComboxBox',
fleetServerHost?.ssl?.es_certificate_authorities ?? [],
undefined,
isEditDisabled
);
const sslEsCertificateInput = useInput(
fleetServerHost?.ssl?.es_certificate ?? '',
() => undefined,
isEditDisabled
);
const sslKeyInput = useInput(fleetServerHost?.ssl?.key ?? '', undefined, isEditDisabled);
const sslESKeyInput = useInput(fleetServerHost?.ssl?.es_key ?? '', undefined, isEditDisabled);
const sslKeySecretInput = useSecretInput(
(fleetServerHost as FleetServerHost)?.secrets?.ssl?.key,
undefined,
isEditDisabled
);
const sslESKeySecretInput = useSecretInput(
(fleetServerHost as FleetServerHost)?.secrets?.ssl?.es_key,
undefined,
isEditDisabled
);
const sslClientAuthInput = useRadioInput(
fleetServerHost?.ssl?.client_auth ?? clientAuth.None,
isEditDisabled
);
const inputs: FleetServerHostSSLInputsType = useMemo(
() => ({
nameInput,
isDefaultInput,
hostUrlsInput,
proxyIdInput,
sslCertificateAuthoritiesInput,
sslCertificateInput,
sslEsCertificateAuthoritiesInput,
sslEsCertificateInput,
sslKeyInput,
sslESKeyInput,
sslKeySecretInput,
sslESKeySecretInput,
sslClientAuthInput,
}),
[nameInput, isDefaultInput, hostUrlsInput, proxyIdInput]
[
nameInput,
isDefaultInput,
hostUrlsInput,
proxyIdInput,
sslCertificateAuthoritiesInput,
sslCertificateInput,
sslEsCertificateAuthoritiesInput,
sslEsCertificateInput,
sslKeyInput,
sslESKeyInput,
sslKeySecretInput,
sslESKeySecretInput,
sslClientAuthInput,
]
);
const validate = useCallback(() => validateInputs(inputs), [inputs]);
const validate = useCallback(() => validateInputs({ ...inputs }), [inputs]);
const submit = useCallback(async () => {
try {
@ -167,12 +251,35 @@ export function useFleetServerHostsForm(
return;
}
setIsLoading(true);
const data = {
const data: Partial<NewFleetServerHost> = {
name: nameInput.value,
host_urls: hostUrlsInput.value,
is_default: isDefaultInput.value,
proxy_id: proxyIdInput.value !== '' ? proxyIdInput.value : null,
ssl: {
certificate: sslCertificateInput.value,
key: sslKeyInput.value || undefined,
certificate_authorities: sslCertificateAuthoritiesInput.value.filter((val) => val !== ''),
es_certificate: sslEsCertificateInput.value,
es_key: sslESKeyInput.value || undefined,
es_certificate_authorities: sslEsCertificateAuthoritiesInput.value.filter(
(val) => val !== ''
),
...(sslClientAuthInput.value !== clientAuth.None && {
client_auth: sslClientAuthInput.value as ValueOf<ClientAuth>,
}),
},
...(((!sslKeyInput.value && sslKeySecretInput.value) ||
(!sslESKeyInput.value && sslESKeySecretInput.value)) && {
secrets: {
ssl: {
key: sslKeySecretInput.value || undefined,
es_key: sslESKeySecretInput.value || undefined,
},
},
}),
};
if (fleetServerHost) {
const res = await sendPutFleetServerHost(fleetServerHost.id, data);
if (res.error) {
@ -200,24 +307,32 @@ export function useFleetServerHostsForm(
});
}
}, [
fleetServerHost,
validate,
confirm,
nameInput.value,
hostUrlsInput.value,
isDefaultInput.value,
proxyIdInput.value,
validate,
notifications,
confirm,
sslCertificateInput.value,
sslKeyInput.value,
sslCertificateAuthoritiesInput.value,
sslEsCertificateInput.value,
sslESKeyInput.value,
sslEsCertificateAuthoritiesInput.value,
sslClientAuthInput.value,
sslKeySecretInput.value,
sslESKeySecretInput.value,
fleetServerHost,
notifications.toasts,
onSuccess,
]);
const hasChanged = Object.values(inputs).some((input) => input.hasChanged);
const isDisabled =
isEditDisabled ||
isLoading ||
(!hostUrlsInput.hasChanged &&
!isDefaultInput.hasChanged &&
!nameInput.hasChanged &&
!proxyIdInput.hasChanged) ||
!hasChanged ||
hostUrlsInput.props.isInvalid ||
nameInput.props.isInvalid;

View file

@ -173,6 +173,7 @@ export function useRadioInput(defaultValue: string, disabled = false) {
setValue,
value,
hasChanged,
validate: () => true,
};
}

View file

@ -11,7 +11,7 @@ import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../constants'
import { packagePolicyService } from '../services';
import { getAgentStatusForAgentPolicy } from '../services/agents';
import { listFleetServerHosts } from '../services/fleet_server_host';
import { fleetServerHostService } from '../services/fleet_server_host';
const DEFAULT_USAGE = {
total_all_statuses: 0,
@ -45,7 +45,7 @@ export const getFleetServerUsage = async (
return DEFAULT_USAGE;
}
const fleetServerHosts = await listFleetServerHosts(soClient);
const fleetServerHosts = await fleetServerHostService.list(soClient);
const numHostsUrls = fleetServerHosts.items.flatMap((host) => host.host_urls).length;
// Find all policies with Fleet server than query agent status

View file

@ -7,7 +7,7 @@
import { SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants';
import { agentPolicyService, appContextService } from '../../services';
import * as fleetServerService from '../../services/fleet_server_host';
import { fleetServerHostService } from '../../services/fleet_server_host';
import { withDefaultErrorHandler } from '../../services/security/fleet_router';
import { postFleetServerHost, putFleetServerHostHandler } from './handler';
@ -32,13 +32,9 @@ describe('fleet server hosts handler', () => {
beforeEach(() => {
jest.spyOn(appContextService, 'getLogger').mockReturnValue({ error: jest.fn() } as any);
jest
.spyOn(fleetServerService, 'createFleetServerHost')
.mockResolvedValue({ id: 'host1' } as any);
jest
.spyOn(fleetServerService, 'updateFleetServerHost')
.mockResolvedValue({ id: 'host1' } as any);
jest.spyOn(fleetServerService, 'getFleetServerHost').mockResolvedValue({
jest.spyOn(fleetServerHostService, 'create').mockResolvedValue({ id: 'host1' } as any);
jest.spyOn(fleetServerHostService, 'update').mockResolvedValue({ id: 'host1' } as any);
jest.spyOn(fleetServerHostService, 'get').mockResolvedValue({
id: SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID,
host_urls: ['http://elasticsearch:9200'],
} as any);
@ -90,6 +86,54 @@ describe('fleet server hosts handler', () => {
expect(res).toEqual({ body: { item: { id: 'host1' } } });
});
it('should return error if both ssl.key and secrets.ssl.key are provided', async () => {
jest
.spyOn(appContextService, 'getCloud')
.mockReturnValue({ isServerlessEnabled: false } as any);
const res = await postFleetServerHostWithErrorHandler(
mockContext,
{
body: {
id: 'host1',
host_urls: ['http://localhost:8080'],
ssl: { key: 'token1' },
secrets: { ssl: { key: 'token1' } },
},
} as any,
mockResponse as any
);
expect(res).toEqual({
body: { message: 'Cannot specify both ssl.key and secrets.ssl.key' },
statusCode: 400,
});
});
it('should return error if both ssl.es_key and secrets.ssl.es_key are provided', async () => {
jest
.spyOn(appContextService, 'getCloud')
.mockReturnValue({ isServerlessEnabled: false } as any);
const res = await postFleetServerHostWithErrorHandler(
mockContext,
{
body: {
id: 'host1',
host_urls: ['http://localhost:8080'],
ssl: { es_key: 'token1' },
secrets: { ssl: { es_key: 'token1' } },
},
} as any,
mockResponse as any
);
expect(res).toEqual({
body: { message: 'Cannot specify both ssl.es_key and secrets.ssl.es_key' },
statusCode: 400,
});
});
it('should return error on put in serverless if host url is different from default', async () => {
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any);

View file

@ -10,24 +10,29 @@ import type { RequestHandler, SavedObjectsClientContract } from '@kbn/core/serve
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import { isEqual } from 'lodash';
import Boom from '@hapi/boom';
import { SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants';
import { FleetServerHostUnauthorizedError } from '../../errors';
import { agentPolicyService, appContextService } from '../../services';
import { agentPolicyService, appContextService, fleetServerHostService } from '../../services';
import {
createFleetServerHost,
deleteFleetServerHost,
getFleetServerHost,
listFleetServerHosts,
updateFleetServerHost,
} from '../../services/fleet_server_host';
import type {
FleetServerHost,
GetOneFleetServerHostRequestSchema,
PostFleetServerHostRequestSchema,
PutFleetServerHostRequestSchema,
} from '../../types';
function ensureNoDuplicateSecrets(fleetServerHost: Partial<FleetServerHost>) {
if (fleetServerHost.ssl?.key && fleetServerHost.secrets?.ssl?.key) {
throw Boom.badRequest('Cannot specify both ssl.key and secrets.ssl.key');
}
if (fleetServerHost.ssl?.es_key && fleetServerHost.secrets?.ssl?.es_key) {
throw Boom.badRequest('Cannot specify both ssl.es_key and secrets.ssl.es_key');
}
}
async function checkFleetServerHostsWriteAPIsAllowed(
soClient: SavedObjectsClientContract,
hostUrls: string[]
@ -38,7 +43,7 @@ async function checkFleetServerHostsWriteAPIsAllowed(
}
// Fleet Server hosts must have the default host URL in serverless.
const serverlessDefaultFleetServerHost = await getFleetServerHost(
const serverlessDefaultFleetServerHost = await fleetServerHostService.get(
soClient,
SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID
);
@ -62,8 +67,11 @@ export const postFleetServerHost: RequestHandler<
await checkFleetServerHostsWriteAPIsAllowed(soClient, request.body.host_urls);
const { id, ...data } = request.body;
const FleetServerHost = await createFleetServerHost(
ensureNoDuplicateSecrets(data);
const FleetServerHost = await fleetServerHostService.create(
soClient,
esClient,
{ ...data, is_preconfigured: false },
{ id }
);
@ -83,7 +91,7 @@ export const getFleetServerHostHandler: RequestHandler<
> = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
try {
const item = await getFleetServerHost(soClient, request.params.itemId);
const item = await fleetServerHostService.get(soClient, request.params.itemId);
const body = {
item,
};
@ -108,7 +116,7 @@ export const deleteFleetServerHostHandler: RequestHandler<
const soClient = coreContext.savedObjects.client;
const esClient = coreContext.elasticsearch.client.asInternalUser;
await deleteFleetServerHost(soClient, esClient, request.params.itemId);
await fleetServerHostService.delete(soClient, esClient, request.params.itemId);
const body = {
id: request.params.itemId,
};
@ -139,8 +147,14 @@ export const putFleetServerHostHandler: RequestHandler<
if (request.body.host_urls) {
await checkFleetServerHostsWriteAPIsAllowed(soClient, request.body.host_urls);
}
ensureNoDuplicateSecrets(request.body);
const item = await updateFleetServerHost(soClient, request.params.itemId, request.body);
const item = await fleetServerHostService.update(
soClient,
esClient,
request.params.itemId,
request.body
);
const body = {
item,
};
@ -165,7 +179,7 @@ export const putFleetServerHostHandler: RequestHandler<
export const getAllFleetServerHostsHandler: RequestHandler = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
const res = await listFleetServerHosts(soClient);
const res = await fleetServerHostService.list(soClient);
const body = {
items: res.items,
page: res.page,

View file

@ -11,12 +11,7 @@ import type { FleetRequestHandlerContext } from '../..';
import { xpackMocks } from '../../mocks';
import { ListResponseSchema } from '../schema/utils';
import { FleetServerHostSchema, FleetServerHostResponseSchema } from '../../types';
import {
createFleetServerHost,
getFleetServerHost,
listFleetServerHosts,
updateFleetServerHost,
} from '../../services/fleet_server_host';
import { fleetServerHostService } from '../../services';
import {
getAllFleetServerHostsHandler,
@ -33,13 +28,13 @@ jest.mock('../../services', () => ({
agentPolicyService: {
bumpAllAgentPolicies: jest.fn().mockResolvedValue({}),
},
}));
jest.mock('../../services/fleet_server_host', () => ({
listFleetServerHosts: jest.fn(),
createFleetServerHost: jest.fn(),
updateFleetServerHost: jest.fn(),
getFleetServerHost: jest.fn(),
fleetServerHostService: {
list: jest.fn(),
get: jest.fn(),
create: jest.fn(),
update: jest.fn().mockResolvedValue({}),
delete: jest.fn(),
},
}));
describe('schema validation', () => {
@ -68,7 +63,7 @@ describe('schema validation', () => {
page: 1,
perPage: 20,
};
(listFleetServerHosts as jest.Mock).mockResolvedValue(expectedResponse);
(fleetServerHostService.list as jest.Mock).mockResolvedValue(expectedResponse);
await getAllFleetServerHostsHandler(context, {} as any, response);
expect(response.ok).toHaveBeenCalledWith({
@ -89,7 +84,7 @@ describe('schema validation', () => {
proxy_id: 'proxy1',
},
};
(createFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item);
(fleetServerHostService.create as jest.Mock).mockResolvedValue(expectedResponse.item);
await postFleetServerHost(
context,
{
@ -119,7 +114,7 @@ describe('schema validation', () => {
proxy_id: null,
},
};
(updateFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item);
(fleetServerHostService.update as jest.Mock).mockResolvedValue(expectedResponse.item);
await putFleetServerHostHandler(
context,
{
@ -150,7 +145,7 @@ describe('schema validation', () => {
proxy_id: null,
},
};
(getFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item);
(fleetServerHostService.get as jest.Mock).mockResolvedValue(expectedResponse.item);
await getFleetServerHostHandler(
context,
{ body: {}, params: { itemId: 'host1' } } as any,

View file

@ -6,7 +6,7 @@
*/
import fetch from 'node-fetch';
import * as fleetServerService from '../../services/fleet_server_host';
import { fleetServerHostService } from '../../services/fleet_server_host';
import { PostHealthCheckResponseSchema } from '../../types';
@ -42,7 +42,7 @@ describe('Fleet server health_check handler', () => {
});
it('should return a bad request error if the requested fleet server host has no host_urls', async () => {
jest.spyOn(fleetServerService, 'getFleetServerHost').mockResolvedValue({
jest.spyOn(fleetServerHostService, 'get').mockResolvedValue({
id: 'default-fleet-server',
name: 'Default',
is_default: true,
@ -70,7 +70,7 @@ describe('Fleet server health_check handler', () => {
name: 'Default',
};
jest.spyOn(fleetServerService, 'getFleetServerHost').mockResolvedValue({
jest.spyOn(fleetServerHostService, 'get').mockResolvedValue({
id: 'default-fleet-server',
name: 'Default',
is_default: true,
@ -105,7 +105,7 @@ describe('Fleet server health_check handler', () => {
it('should return an error when host id is not found', async () => {
jest
.spyOn(fleetServerService, 'getFleetServerHost')
.spyOn(fleetServerHostService, 'get')
.mockRejectedValue({ output: { statusCode: 404 }, isBoom: true });
const res = await postHealthCheckHandler(
@ -123,7 +123,7 @@ describe('Fleet server health_check handler', () => {
});
it('should return status `offline` when fetch request gets aborted', async () => {
jest.spyOn(fleetServerService, 'getFleetServerHost').mockResolvedValue({
jest.spyOn(fleetServerHostService, 'get').mockResolvedValue({
id: 'default-fleet-server',
name: 'Default',
is_default: true,

View file

@ -8,7 +8,7 @@
import type { TypeOf } from '@kbn/config-schema';
import fetch from 'node-fetch';
import { getFleetServerHost } from '../../services/fleet_server_host';
import { fleetServerHostService } from '../../services/fleet_server_host';
import type { FleetRequestHandler, PostHealthCheckRequestSchema } from '../../types';
@ -23,7 +23,7 @@ export const postHealthCheckHandler: FleetRequestHandler<
const soClient = coreContext.savedObjects.client;
try {
const fleetServerHost = await getFleetServerHost(soClient, id);
const fleetServerHost = await fleetServerHostService.get(soClient, id);
if (
!fleetServerHost ||

View file

@ -1147,6 +1147,7 @@ export const getSavedObjectTypes = (
importableAndExportable: false,
},
mappings: {
dynamic: false,
properties: {
name: { type: 'keyword' },
is_default: { type: 'boolean' },
@ -1167,6 +1168,14 @@ export const getSavedObjectTypes = (
},
],
},
'2': {
changes: [
{
type: 'mappings_addition',
addedMappings: {},
},
],
},
},
},
[FLEET_PROXY_SAVED_OBJECT_TYPE]: {
@ -1253,11 +1262,16 @@ export const OUTPUT_INCLUDE_AAD_FIELDS = new Set([
'channel_buffer_size',
]);
// dangerouslyExposeValue added to allow the user with access to the SO to see and edit these values through the UI
export const OUTPUT_ENCRYPTED_FIELDS = new Set([
{ key: 'ssl', dangerouslyExposeValue: true },
{ key: 'password', dangerouslyExposeValue: true },
]);
export const FLEET_SERVER_HOST_ENCRYPTED_FIELDS = new Set([
{ key: 'ssl', dangerouslyExposeValue: true },
]);
export function registerEncryptedSavedObjects(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
) {
@ -1277,4 +1291,10 @@ export function registerEncryptedSavedObjects(
attributesToEncrypt: new Set(['token']),
attributesToIncludeInAAD: new Set(['policy_id', 'token_plain']),
});
encryptedSavedObjects.registerType({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
attributesToEncrypt: FLEET_SERVER_HOST_ENCRYPTED_FIELDS,
// enforceRandomId allows to create an SO with an arbitrary id
enforceRandomId: false,
});
}

View file

@ -16,6 +16,7 @@ import { agentPolicyService } from '../agent_policy';
import { agentPolicyUpdateEventHandler } from '../agent_policy_update';
import { appContextService } from '../app_context';
import { getPackageInfo } from '../epm/packages';
import { getFleetServerHostsForAgentPolicy } from '../fleet_server_host';
import {
generateFleetConfig,
@ -26,6 +27,7 @@ import {
import { getMonitoringPermissions } from './monitoring_permissions';
jest.mock('../epm/packages');
jest.mock('../fleet_server_host');
const mockedGetElasticAgentMonitoringPermissions = getMonitoringPermissions as jest.Mock<
ReturnType<typeof getMonitoringPermissions>
@ -34,6 +36,9 @@ const mockedAgentPolicyService = agentPolicyService as jest.Mocked<typeof agentP
const soClientMock = savedObjectsClientMock.create();
const mockedGetPackageInfo = getPackageInfo as jest.Mock<ReturnType<typeof getPackageInfo>>;
const mockedGetFleetServerHostsForAgentPolicy = getFleetServerHostsForAgentPolicy as jest.Mock<
ReturnType<typeof getFleetServerHostsForAgentPolicy>
>;
function mockAgentPolicy(data: Partial<AgentPolicy>) {
mockedAgentPolicyService.get.mockResolvedValue({
@ -51,18 +56,6 @@ function mockAgentPolicy(data: Partial<AgentPolicy>) {
});
}
jest.mock('../fleet_server_host', () => {
return {
getFleetServerHostsForAgentPolicy: async () => {
return {
id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c',
is_default: true,
host_urls: ['http://fleetserver:8220'],
};
},
};
});
jest.mock('../agent_policy');
jest.mock('../output', () => {
@ -154,6 +147,14 @@ function getAgentPolicyUpdateMock() {
describe('getFullAgentPolicy', () => {
beforeEach(() => {
mockedGetFleetServerHostsForAgentPolicy.mockResolvedValue({
name: 'default Fleet Server',
id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c',
is_default: true,
host_urls: ['http://fleetserver:8220'],
is_preconfigured: false,
});
getAgentPolicyUpdateMock().mockClear();
mockedAgentPolicyService.get.mockReset();
mockedGetElasticAgentMonitoringPermissions.mockReset();
@ -1028,6 +1029,42 @@ describe('getFullAgentPolicy', () => {
},
});
});
it('should have ssl options in outputs when fleet server host has es ssl options', async () => {
mockedGetFleetServerHostsForAgentPolicy.mockResolvedValue({
name: 'default Fleet Server',
id: '93f74c0-e876-11ea-b7d3-8b2acec6f75c',
is_default: true,
host_urls: ['http://fleetserver:8220'],
is_preconfigured: false,
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',
},
});
mockAgentPolicy({});
const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy');
expect(agentPolicy?.outputs).toMatchObject({
default: {
hosts: ['http://127.0.0.1:9201'],
preset: 'balanced',
type: 'elasticsearch',
},
'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',
},
});
});
});
describe('getFullMonitoringSettings', () => {
@ -1309,7 +1346,7 @@ ssl.test: 123
type: 'logstash',
},
undefined,
true
false
);
expect(policyOutput).toMatchInlineSnapshot(`

View file

@ -28,6 +28,7 @@ import type {
AgentPolicy,
} from '../../types';
import type {
FullAgentPolicyInput,
FullAgentPolicyMonitoring,
FullAgentPolicyOutputPermissions,
PackageInfo,
@ -44,7 +45,7 @@ import { getPackageInfo } from '../epm/packages';
import { pkgToPkgKey, splitPkgKey } from '../epm/registry';
import { appContextService } from '../app_context';
import { getOutputSecretReferences } from '../secrets';
import { getFleetServerHostsSecretReferences, getOutputSecretReferences } from '../secrets';
import { getMonitoringPermissions } from './monitoring_permissions';
import { storedPackagePoliciesToAgentInputs } from '.';
@ -87,7 +88,7 @@ export async function getFullAgentPolicy(
outputs,
proxies,
dataOutput,
fleetServerHosts,
fleetServerHost,
monitoringOutput,
downloadSourceUri,
downloadSourceProxyUri,
@ -119,6 +120,7 @@ export async function getFullAgentPolicy(
packageInfoCache.set(pkgKey, packageInfo);
})
);
const bootstrapOutputConfig = generateFleetServerOutputSSLConfig(fleetServerHost);
const inputs = (
await storedPackagePoliciesToAgentInputs(
@ -134,7 +136,18 @@ export async function getFullAgentPolicy(
if (output) {
input.use_output = getOutputIdForAgentPolicy(output);
}
if (input.type === 'fleet-server' && fleetServerHost) {
const sslInputConfig = generateSSLConfigForFleetServerInput(fleetServerHost);
if (sslInputConfig) {
input = {
...input,
...sslInputConfig,
...(bootstrapOutputConfig
? { use_output: `fleetserver-output-${fleetServerHost.id}` }
: {}),
};
}
}
return input;
});
const features = (agentPolicy.agent_features || []).reduce((acc, { name, ...featureConfig }) => {
@ -143,6 +156,10 @@ export async function getFullAgentPolicy(
}, {} as NonNullable<FullAgentPolicy['agent']>['features']);
const outputSecretReferences = outputs.flatMap((output) => getOutputSecretReferences(output));
const fleetserverHostSecretReferences = fleetServerHost
? getFleetServerHostsSecretReferences(fleetServerHost)
: [];
const packagePolicySecretReferences = (agentPolicy?.package_policies || []).flatMap(
(policy) => policy.secret_references || []
);
@ -150,18 +167,22 @@ export async function getFullAgentPolicy(
const fullAgentPolicy: FullAgentPolicy = {
id: agentPolicy.id,
outputs: {
...(bootstrapOutputConfig ? bootstrapOutputConfig : {}),
...outputs.reduce<FullAgentPolicy['outputs']>((acc, output) => {
acc[getOutputIdForAgentPolicy(output)] = transformOutputToFullPolicyOutput(
output,
output.proxy_id ? proxies.find((proxy) => output.proxy_id === proxy.id) : undefined,
standalone
);
return acc;
}, {}),
},
inputs,
secret_references: [...outputSecretReferences, ...packagePolicySecretReferences],
secret_references: [
...outputSecretReferences,
...fleetserverHostSecretReferences,
...packagePolicySecretReferences,
],
revision: agentPolicy.revision,
agent: {
download: {
@ -267,8 +288,8 @@ export async function getFullAgentPolicy(
}, {});
// only add fleet server hosts if not in standalone
if (!standalone && fleetServerHosts) {
fullAgentPolicy.fleet = generateFleetConfig(agentPolicy, fleetServerHosts, proxies, outputs);
if (!standalone && fleetServerHost) {
fullAgentPolicy.fleet = generateFleetConfig(agentPolicy, fleetServerHost, proxies, outputs);
}
const settingsValues = getSettingsValuesForAgentPolicy(
@ -318,18 +339,17 @@ export async function getFullAgentPolicy(
if (agentPolicy.overrides) {
return deepMerge<FullAgentPolicy>(fullAgentPolicy, agentPolicy.overrides);
}
return fullAgentPolicy;
}
export function generateFleetConfig(
agentPolicy: AgentPolicy,
fleetServerHosts: FleetServerHost,
fleetServerHost: FleetServerHost,
proxies: FleetProxy[],
outputs: Output[]
): FullAgentPolicy['fleet'] {
const config: FullAgentPolicy['fleet'] = {
hosts: fleetServerHosts.host_urls,
hosts: fleetServerHost.host_urls,
};
// generating the ssl configs for checking into Fleet
@ -370,8 +390,8 @@ export function generateFleetConfig(
}
}
const fleetServerHostproxy = fleetServerHosts.proxy_id
? proxies.find((proxy) => proxy.id === fleetServerHosts.proxy_id)
const fleetServerHostproxy = fleetServerHost.proxy_id
? proxies.find((proxy) => proxy.id === fleetServerHost.proxy_id)
: null;
if (fleetServerHostproxy) {
config.proxy_url = fleetServerHostproxy.url;
@ -398,6 +418,40 @@ export function generateFleetConfig(
return config;
}
// Generate the SSL configs for fleet-server type input
// Corresponding to --fleet-server-cert, --fleet-server-cert-key, --certificate-authorites cli options
function generateSSLConfigForFleetServerInput(fleetServerHost: FleetServerHost) {
const inputConfig: Partial<FullAgentPolicyInput> = {};
if (fleetServerHost?.ssl) {
inputConfig.ssl = {
...(fleetServerHost.ssl.certificate_authorities && {
certificate_authorities: fleetServerHost.ssl.certificate_authorities,
}),
...(fleetServerHost.ssl.certificate && {
certificate: fleetServerHost.ssl.certificate,
}),
...(fleetServerHost.ssl.key &&
!fleetServerHost?.secrets?.ssl?.key && {
key: fleetServerHost.ssl.key,
}),
...(fleetServerHost.ssl.client_auth && {
client_authentication: fleetServerHost.ssl.client_auth,
}),
};
}
if (fleetServerHost?.secrets) {
inputConfig.secrets = {
...(fleetServerHost?.secrets?.ssl?.key &&
!fleetServerHost?.ssl?.key && {
ssl: { key: fleetServerHost.secrets?.ssl?.key },
}),
};
}
return inputConfig;
}
export function transformOutputToFullPolicyOutput(
output: Output,
proxy?: FleetProxy,
@ -576,6 +630,45 @@ export function transformOutputToFullPolicyOutput(
return newOutput;
}
// 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):
| {
[key: string]: FullAgentPolicyOutput;
}
| undefined {
if (!fleetServerHost || (!fleetServerHost?.ssl && !fleetServerHost.secrets)) return undefined;
const outputConfig: FullAgentPolicyOutput = { type: 'elasticsearch' };
if (fleetServerHost?.ssl) {
outputConfig.ssl = {
...(fleetServerHost.ssl.es_certificate_authorities && {
certificate_authorities: fleetServerHost.ssl.es_certificate_authorities,
}),
...(fleetServerHost.ssl.es_certificate && {
certificate: fleetServerHost.ssl.es_certificate,
}),
...(fleetServerHost.ssl.es_key &&
!fleetServerHost?.secrets?.ssl?.es_key && {
key: fleetServerHost.ssl.es_key,
}),
};
}
if (fleetServerHost?.secrets) {
outputConfig.secrets = {
...(fleetServerHost?.secrets?.ssl?.es_key &&
!fleetServerHost?.ssl?.es_key && {
ssl: { key: fleetServerHost.secrets?.ssl?.es_key },
}),
};
}
return {
[`fleetserver-output-${fleetServerHost?.id}`]: outputConfig,
};
}
export function getFullMonitoringSettings(
agentPolicy: Pick<
AgentPolicy,

View file

@ -55,7 +55,7 @@ export async function fetchRelatedSavedObjects(
.getLogger()
?.warn(`Unable to get fleet server hosts for policy ${agentPolicy?.id}: ${err.message}`);
return;
return undefined;
}),
]);
@ -94,6 +94,6 @@ export async function fetchRelatedSavedObjects(
monitoringOutput,
downloadSourceUri,
downloadSourceProxyUri,
fleetServerHosts,
fleetServerHost: fleetServerHosts,
};
}

View file

@ -17,7 +17,7 @@ import type { AgentPolicy, NewAgentPolicy } from '../../types';
import { appContextService } from '../app_context';
import { listEnrollmentApiKeys } from '../api_keys';
import { listFleetServerHosts } from '../fleet_server_host';
import { fleetServerHostService } from '../fleet_server_host';
import { agentlessAgentService } from './agentless_agent';
@ -48,8 +48,8 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
const mockedListEnrollmentApiKeys = listEnrollmentApiKeys as jest.Mock<
ReturnType<typeof listEnrollmentApiKeys>
>;
const mockedListFleetServerHosts = listFleetServerHosts as jest.Mock<
ReturnType<typeof listFleetServerHosts>
const mockedFleetServerHostService = fleetServerHostService as jest.Mocked<
typeof fleetServerHostService
>;
function getAgentPolicyCreateMock() {
@ -115,7 +115,7 @@ describe('Agentless Agent service', () => {
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -215,7 +215,7 @@ describe('Agentless Agent service', () => {
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -314,7 +314,7 @@ describe('Agentless Agent service', () => {
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -425,7 +425,7 @@ describe('Agentless Agent service', () => {
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -620,7 +620,7 @@ describe('Agentless Agent service', () => {
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -681,7 +681,7 @@ describe('Agentless Agent service', () => {
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -737,7 +737,7 @@ describe('Agentless Agent service', () => {
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -806,7 +806,7 @@ describe('Agentless Agent service', () => {
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -946,7 +946,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({ items: [] } as any);
mockedFleetServerHostService.list.mockResolvedValue({ items: [] } as any);
mockedListEnrollmentApiKeys.mockResolvedValue({
items: [
{
@ -984,7 +984,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked',
@ -1025,7 +1025,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1085,7 +1085,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1145,7 +1145,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1205,7 +1205,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1265,7 +1265,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1325,7 +1325,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1385,7 +1385,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
@ -1445,7 +1445,7 @@ describe('Agentless Agent service', () => {
},
} as any);
jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isCloudEnabled: true } as any);
mockedListFleetServerHosts.mockResolvedValue({
mockedFleetServerHostService.list.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',

View file

@ -34,7 +34,7 @@ import {
import { appContextService } from '../app_context';
import { listEnrollmentApiKeys } from '../api_keys';
import { listFleetServerHosts } from '../fleet_server_host';
import { fleetServerHostService } from '../fleet_server_host';
import type { AgentlessConfig } from '../utils/agentless';
import { prependAgentlessApiBasePathToEndpoint, isAgentlessEnabled } from '../utils/agentless';
import {
@ -336,7 +336,7 @@ class AgentlessAgentService {
kuery: `policy_id:"${policyId}"`,
});
const { items: fleetHosts } = await listFleetServerHosts(soClient);
const { items: fleetHosts } = await fleetServerHostService.list(soClient);
// Tech Debt: change this when we add the internal fleet server config to use the internal fleet server host
// https://github.com/elastic/security-team/issues/9695
const defaultFleetHost =

View file

@ -15,7 +15,7 @@ import { FLEET_PROXY_SAVED_OBJECT_TYPE } from '../constants';
import { appContextService } from './app_context';
import { deleteFleetProxy } from './fleet_proxies';
import { listFleetServerHostsForProxyId, updateFleetServerHost } from './fleet_server_host';
import { fleetServerHostService } from './fleet_server_host';
import { outputService } from './output';
import { downloadSourceService } from './download_source';
@ -24,14 +24,9 @@ jest.mock('./download_source');
jest.mock('./fleet_server_host');
jest.mock('./app_context');
const mockedListFleetServerHostsForProxyId = listFleetServerHostsForProxyId as jest.MockedFunction<
typeof listFleetServerHostsForProxyId
const mockedFleetServerHostService = fleetServerHostService as jest.Mocked<
typeof fleetServerHostService
>;
const mockedUpdateFleetServerHost = updateFleetServerHost as jest.MockedFunction<
typeof updateFleetServerHost
>;
const mockedOutputService = outputService as jest.Mocked<typeof outputService>;
const mockedDownloadSourceService = downloadSourceService as jest.Mocked<
typeof downloadSourceService
@ -61,7 +56,8 @@ describe('Fleet proxies service', () => {
mockedDownloadSourceService.listAllForProxyId.mockReset();
mockedOutputService.update.mockReset();
soClientMock.delete.mockReset();
mockedUpdateFleetServerHost.mockReset();
mockedFleetServerHostService.update.mockReset();
mockedFleetServerHostService.listAllForProxyId.mockReset();
mockedDownloadSourceService.listAllForProxyId.mockImplementation(async () => ({
items: [],
total: 0,
@ -93,7 +89,7 @@ describe('Fleet proxies service', () => {
perPage: 10,
};
});
mockedListFleetServerHostsForProxyId.mockImplementation(async (_, proxyId) => {
mockedFleetServerHostService.listAllForProxyId.mockImplementation(async (_, proxyId) => {
if (proxyId === PROXY_IDS.RELATED_PRECONFIGURED) {
return {
items: [
@ -178,7 +174,7 @@ describe('Fleet proxies service', () => {
fromPreconfiguration: true,
});
expect(mockedOutputService.update).toBeCalled();
expect(mockedUpdateFleetServerHost).toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalled();
expect(soClientMock.delete).toBeCalled();
});
});

View file

@ -30,7 +30,7 @@ import type {
import { appContextService } from './app_context';
import { listFleetServerHostsForProxyId, updateFleetServerHost } from './fleet_server_host';
import { fleetServerHostService } from './fleet_server_host';
import { outputService } from './output';
import { downloadSourceService } from './download_source';
@ -206,7 +206,7 @@ async function updateRelatedSavedObject(
await pMap(
fleetServerHosts,
(fleetServerHost) =>
updateFleetServerHost(soClient, fleetServerHost.id, {
fleetServerHostService.update(soClient, esClient, fleetServerHost.id, {
...omit(fleetServerHost, 'id'),
proxy_id: null,
}),
@ -237,7 +237,7 @@ export async function getFleetProxyRelatedSavedObjects(
) {
const [{ items: fleetServerHosts }, { items: outputs }, { items: downloadSources }] =
await Promise.all([
listFleetServerHostsForProxyId(soClient, proxyId),
fleetServerHostService.listAllForProxyId(soClient, proxyId),
outputService.listAllForProxyId(soClient, proxyId),
downloadSourceService.listAllForProxyId(soClient, proxyId),
]);

View file

@ -15,15 +15,18 @@ import {
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
DEFAULT_FLEET_SERVER_HOST_ID,
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
} from '../constants';
import { appContextService } from './app_context';
import { deleteFleetServerHost, migrateSettingsToFleetServerHost } from './fleet_server_host';
import { fleetServerHostService, migrateSettingsToFleetServerHost } from './fleet_server_host';
import { agentPolicyService } from './agent_policy';
import { getAgentsByKuery } from './agents';
jest.mock('./app_context');
jest.mock('./agent_policy');
jest.mock('./agents');
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
@ -31,63 +34,46 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
}));
mockedAppContextService.getExperimentalFeatures.mockReturnValue({} as any);
let mockedLogger: jest.Mocked<Logger>;
const mockedGetAgentsByKuery = getAgentsByKuery as jest.MockedFunction<typeof getAgentsByKuery>;
describe('migrateSettingsToFleetServerHost', () => {
beforeEach(() => {
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
function getMockedSoClient(options?: { id?: string; findHosts?: boolean; findSettings?: boolean }) {
const soClient = savedObjectsClientMock.create();
mockedAppContextService.getInternalUserSOClient.mockReturnValue(soClient);
soClient.get.mockImplementation(async (t: string, id: string) => {
return {
id: 'test1',
attributes: {},
} as any;
});
it('should not migrate settings if a default fleet server policy config exists', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockImplementation(({ type }) => {
if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) {
return { saved_objects: [{ id: 'test123' }] } as any;
}
throw new Error('Not mocked');
});
await migrateSettingsToFleetServerHost(soClient);
expect(soClient.create).not.toBeCalled();
soClient.create.mockImplementation(async (type, data, createOptions) => {
return {
id: createOptions?.id || 'generated-id',
type,
attributes: {},
references: [],
};
});
it('should not migrate settings if there is not old settings', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockImplementation(({ type }) => {
if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) {
return { saved_objects: [] } as any;
}
if (type === GLOBAL_SETTINGS_SAVED_OBJECT_TYPE) {
soClient.find.mockImplementation(({ type }) => {
if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) {
if (options?.findHosts) {
return {
saved_objects: [],
saved_objects: [
{
id: 'test123',
attributes: { name: 'fleetServerHost', host_urls: [], is_default: true },
},
],
} as any;
}
return { saved_objects: [] } as any;
}
throw new Error('Not mocked');
});
soClient.create.mockResolvedValue({
id: DEFAULT_FLEET_SERVER_HOST_ID,
attributes: {},
} as any);
await migrateSettingsToFleetServerHost(soClient);
expect(soClient.create).not.toBeCalled();
});
it('should migrate settings to new saved object', async () => {
const soClient = savedObjectsClientMock.create();
soClient.find.mockImplementation(({ type }) => {
if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) {
return { saved_objects: [] } as any;
}
if (type === GLOBAL_SETTINGS_SAVED_OBJECT_TYPE) {
if (type === GLOBAL_SETTINGS_SAVED_OBJECT_TYPE) {
if (options?.findSettings) {
return {
saved_objects: [
{
@ -99,15 +85,99 @@ describe('migrateSettingsToFleetServerHost', () => {
} as any;
}
throw new Error('Not mocked');
});
return {
saved_objects: [],
} as any;
}
soClient.create.mockResolvedValue({
id: DEFAULT_FLEET_SERVER_HOST_ID,
attributes: {},
if (type === PACKAGE_POLICY_SAVED_OBJECT_TYPE) {
return {
saved_objects: [
{
id: 'existing-package-policy',
type: 'ingest-package-policies',
score: 1,
references: [],
version: '1.0.0',
attributes: {
name: 'fleet-server',
description: '',
namespace: 'default',
enabled: true,
policy_id: 'fleet-server-id-1',
policy_ids: ['fleet-server-id-1'],
package: {
name: 'fleet-server',
title: 'Fleet Server',
version: '0.9.0',
},
inputs: [],
},
},
],
} as any;
}
throw new Error('Not mocked');
});
return soClient;
}
describe('migrateSettingsToFleetServerHost', () => {
beforeEach(() => {
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: true,
} as any);
});
const esMock = elasticsearchServiceMock.createInternalClient();
it('should not migrate settings if a default fleet server policy config exists', async () => {
const soClient = getMockedSoClient({ id: DEFAULT_FLEET_SERVER_HOST_ID, findHosts: true });
await migrateSettingsToFleetServerHost(soClient, esMock);
expect(soClient.create).not.toBeCalled();
});
it('should not migrate settings if there is no old settings', async () => {
const soClient = getMockedSoClient({ id: DEFAULT_FLEET_SERVER_HOST_ID });
mockedGetAgentsByKuery.mockResolvedValueOnce({ agents: [] } as any);
await migrateSettingsToFleetServerHost(soClient, esMock);
expect(soClient.create).not.toBeCalled();
});
it('should migrate settings to new saved object', async () => {
const soClient = getMockedSoClient({ findSettings: true });
mockedGetAgentsByKuery.mockResolvedValueOnce({
agents: [
{
id: '1',
local_metadata: {
elastic: {
agent: {
version: '10.0.0',
},
},
},
},
{
id: '2',
local_metadata: {
elastic: {
agent: {
version: '10.0.0',
},
},
},
},
],
} as any);
await migrateSettingsToFleetServerHost(soClient);
await migrateSettingsToFleetServerHost(soClient, esMock);
expect(soClient.create).toBeCalledWith(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
expect.objectContaining({
@ -119,18 +189,67 @@ describe('migrateSettingsToFleetServerHost', () => {
})
);
});
it('should not work if getEncryptedSavedObjectsSetup is not set', async () => {
const soClient = getMockedSoClient({ findSettings: true });
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: false,
} as any);
await expect(() => migrateSettingsToFleetServerHost(soClient, esMock)).rejects.toThrow(
'Fleet server host needs encrypted saved object api key to be set'
);
});
});
describe('deleteFleetServerHost', () => {
it('should removeFleetServerHostFromAll agent policies without force if not deleted from preconfiguration', async () => {
const soMock = savedObjectsClientMock.create();
soMock.get.mockResolvedValue({
id: 'test1',
attributes: {},
describe('create', () => {
beforeEach(() => {
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: true,
} as any);
});
afterEach(() => {
jest.resetAllMocks();
});
it('should throw if encryptedSavedObject is not configured', async () => {
const soMock = getMockedSoClient();
const esMock = elasticsearchServiceMock.createInternalClient();
await deleteFleetServerHost(soMock, esMock, 'test1', {});
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: false,
} as any);
await expect(
fleetServerHostService.create(
soMock,
esMock,
{
name: 'Test',
host_urls: [],
is_default: false,
is_preconfigured: false,
},
{ id: 'output-test' }
)
).rejects.toThrow(`Fleet server host needs encrypted saved object api key to be set`);
});
});
describe('delete fleetServerHost', () => {
beforeEach(() => {
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
});
afterEach(() => {
jest.resetAllMocks();
});
it('should removeFleetServerHostFromAll agent policies without force if not deleted from preconfiguration', async () => {
const soMock = getMockedSoClient();
const esMock = elasticsearchServiceMock.createInternalClient();
await fleetServerHostService.delete(soMock, esMock, 'test1', {});
expect(jest.mocked(agentPolicyService.removeFleetServerHostFromAll)).toBeCalledWith(
esMock,
@ -141,14 +260,10 @@ describe('deleteFleetServerHost', () => {
);
});
it('should removeFleetServerHostFromAll agent policies with force if deleted from preconfiguration', async () => {
const soMock = savedObjectsClientMock.create();
const soMock = getMockedSoClient();
soMock.get.mockResolvedValue({
id: 'test1',
attributes: {},
} as any);
const esMock = elasticsearchServiceMock.createInternalClient();
await deleteFleetServerHost(soMock, esMock, 'test1', {
await (fleetServerHostService.delete as jest.Mock)(soMock, esMock, 'test1', {
fromPreconfiguration: true,
});

View file

@ -5,12 +5,17 @@
* 2.0.
*/
import { omit } from 'lodash';
import type {
ElasticsearchClient,
SavedObjectsClientContract,
SavedObject,
KibanaRequest,
} from '@kbn/core/server';
import type { Nullable } from 'tough-cookie';
import { normalizeHostsForAgents } from '../../common/services';
import {
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
@ -25,215 +30,360 @@ import type {
FleetServerHost,
NewFleetServerHost,
AgentPolicy,
PolicySecretReference,
} from '../types';
import { FleetServerHostUnauthorizedError, FleetServerHostNotFoundError } from '../errors';
import {
FleetServerHostUnauthorizedError,
FleetServerHostNotFoundError,
FleetEncryptedSavedObjectEncryptionKeyRequired,
} from '../errors';
import { appContextService } from './app_context';
import { agentPolicyService } from './agent_policy';
import { escapeSearchQueryPhrase } from './saved_object';
import {
deleteFleetServerHostsSecrets,
deleteSecrets,
extractAndUpdateFleetServerHostsSecrets,
extractAndWriteFleetServerHostsSecrets,
isSecretStorageEnabled,
} from './secrets';
function savedObjectToFleetServerHost(so: SavedObject<FleetServerHostSOAttributes>) {
const data = { ...so.attributes };
function savedObjectToFleetServerHost(
so: SavedObject<FleetServerHostSOAttributes>
): FleetServerHost {
const { ssl, proxy_id: proxyId, ...attributes } = so.attributes;
if (data.proxy_id === null) {
delete data.proxy_id;
}
return { id: so.id, ...data };
return {
id: so.id,
...attributes,
...(ssl ? { ssl: JSON.parse(ssl as string) } : {}),
...(proxyId ? { proxy_id: proxyId } : {}),
};
}
export async function createFleetServerHost(
soClient: SavedObjectsClientContract,
data: NewFleetServerHost,
options?: { id?: string; overwrite?: boolean; fromPreconfiguration?: boolean }
): Promise<FleetServerHost> {
const logger = appContextService.getLogger();
if (data.is_default) {
const defaultItem = await getDefaultFleetServerHost(soClient);
if (defaultItem && defaultItem.id !== options?.id) {
await updateFleetServerHost(
soClient,
defaultItem.id,
{ is_default: false },
{ fromPreconfiguration: options?.fromPreconfiguration }
const fakeRequest = {
headers: {},
getBasePath: () => '',
path: '/',
route: { settings: {} },
url: {
href: '/',
},
raw: {
req: {
url: '/',
},
},
} as unknown as KibanaRequest;
class FleetServerHostService {
private get encryptedSoClient() {
return appContextService.getInternalUserSOClient(fakeRequest);
}
public async create(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
fleetServerHost: NewFleetServerHost,
options?: {
id?: string;
overwrite?: boolean;
fromPreconfiguration?: boolean;
secretHashes?: Record<string, any>;
}
): Promise<FleetServerHost> {
const logger = appContextService.getLogger();
const data: FleetServerHostSOAttributes = { ...omit(fleetServerHost, ['ssl', 'secrets']) };
if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) {
throw new FleetEncryptedSavedObjectEncryptionKeyRequired(
`Fleet server host needs encrypted saved object api key to be set`
);
}
}
if (data.host_urls) {
data.host_urls = data.host_urls.map(normalizeHostsForAgents);
}
logger.debug(`Creating fleet server host with ${data}`);
const res = await soClient.create<FleetServerHostSOAttributes>(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
data,
{ id: options?.id, overwrite: options?.overwrite }
);
logger.debug(`Created fleet server host ${options?.id}`);
return savedObjectToFleetServerHost(res);
}
export async function getFleetServerHost(
soClient: SavedObjectsClientContract,
id: string
): Promise<FleetServerHost> {
const res = await soClient.get<FleetServerHostSOAttributes>(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
id
);
return savedObjectToFleetServerHost(res);
}
export async function listFleetServerHosts(soClient: SavedObjectsClientContract) {
const res = await soClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
});
return {
items: res.saved_objects.map<FleetServerHost>(savedObjectToFleetServerHost),
total: res.total,
page: res.page,
perPage: res.per_page,
};
}
export async function listFleetServerHostsForProxyId(
soClient: SavedObjectsClientContract,
proxyId: string
) {
const res = await soClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
searchFields: ['proxy_id'],
search: escapeSearchQueryPhrase(proxyId),
});
return {
items: res.saved_objects.map<FleetServerHost>(savedObjectToFleetServerHost),
total: res.total,
page: res.page,
perPage: res.per_page,
};
}
export async function deleteFleetServerHost(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
options?: { fromPreconfiguration?: boolean }
) {
const logger = appContextService.getLogger();
logger.debug(`Deleting fleet server host ${id}`);
const fleetServerHost = await getFleetServerHost(soClient, id);
if (fleetServerHost.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetServerHostUnauthorizedError(
`Cannot delete ${id} preconfigured fleet server host`
);
}
if (fleetServerHost.is_default) {
throw new FleetServerHostUnauthorizedError(
`Default Fleet Server hosts ${id} cannot be deleted.`
);
}
await agentPolicyService.removeFleetServerHostFromAll(esClient, id, {
force: options?.fromPreconfiguration,
});
return await soClient.delete(FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, id);
}
export async function updateFleetServerHost(
soClient: SavedObjectsClientContract,
id: string,
data: Partial<FleetServerHost>,
options?: { fromPreconfiguration?: boolean }
) {
const logger = appContextService.getLogger();
logger.debug(`Updating fleet server host ${id}`);
const originalItem = await getFleetServerHost(soClient, id);
if (data.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetServerHostUnauthorizedError(
`Cannot update ${id} preconfigured fleet server host`
);
}
if (data.is_default) {
const defaultItem = await getDefaultFleetServerHost(soClient);
if (defaultItem && defaultItem.id !== id) {
await updateFleetServerHost(
soClient,
defaultItem.id,
{
is_default: false,
},
{ fromPreconfiguration: options?.fromPreconfiguration }
);
}
}
if (data.host_urls) {
data.host_urls = data.host_urls.map(normalizeHostsForAgents);
}
await soClient.update<FleetServerHostSOAttributes>(FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, id, data);
logger.debug(`Updated fleet server host ${id}`);
return {
...originalItem,
...data,
};
}
export async function bulkGetFleetServerHosts(
soClient: SavedObjectsClientContract,
ids: string[],
{ ignoreNotFound = false } = { ignoreNotFound: true }
) {
if (ids.length === 0) {
return [];
}
const res = await soClient.bulkGet<FleetServerHostSOAttributes>(
ids.map((id) => ({
id,
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
}))
);
return res.saved_objects
.map((so) => {
if (so.error) {
if (!ignoreNotFound || so.error.statusCode !== 404) {
throw so.error;
}
return undefined;
if (fleetServerHost.is_default) {
const defaultItem = await this.getDefaultFleetServerHost(soClient);
if (defaultItem && defaultItem.id !== options?.id) {
await this.update(
soClient,
esClient,
defaultItem.id,
{ is_default: false },
{ fromPreconfiguration: options?.fromPreconfiguration }
);
}
}
return savedObjectToFleetServerHost(so);
})
.filter(
(fleetServerHostOrUndefined): fleetServerHostOrUndefined is FleetServerHost =>
typeof fleetServerHostOrUndefined !== 'undefined'
if (fleetServerHost.host_urls) {
data.host_urls = fleetServerHost.host_urls.map(normalizeHostsForAgents);
}
if (fleetServerHost.ssl) {
data.ssl = JSON.stringify(fleetServerHost.ssl);
}
// Store secret values if enabled; if not, store plain text values
if (await isSecretStorageEnabled(esClient, soClient)) {
const { fleetServerHost: fleetServerHostWithSecrets } =
await extractAndWriteFleetServerHostsSecrets({
fleetServerHost,
esClient,
secretHashes: fleetServerHost.is_preconfigured ? options?.secretHashes : undefined,
});
if (fleetServerHostWithSecrets.secrets)
data.secrets = fleetServerHostWithSecrets.secrets as FleetServerHostSOAttributes['secrets'];
} else {
if (
(!fleetServerHost.ssl?.key && fleetServerHost.secrets?.ssl?.key) ||
(!fleetServerHost.ssl?.es_key && fleetServerHost.secrets?.ssl?.es_key)
) {
data.ssl = JSON.stringify({ ...fleetServerHost.ssl, ...fleetServerHost.secrets.ssl });
}
}
const res = await this.encryptedSoClient.create<FleetServerHostSOAttributes>(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
data,
{ id: options?.id, overwrite: options?.overwrite }
);
logger.debug(`Created fleet server host ${options?.id}`);
return savedObjectToFleetServerHost(res);
}
public async get(soClient: SavedObjectsClientContract, id: string): Promise<FleetServerHost> {
// add code to retrieve encrypted fields
const res = await this.encryptedSoClient.get<FleetServerHostSOAttributes>(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
id
);
return savedObjectToFleetServerHost(res);
}
public async list(soClient: SavedObjectsClientContract) {
const res = await this.encryptedSoClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
});
return {
items: res.saved_objects.map<FleetServerHost>(savedObjectToFleetServerHost),
total: res.total,
page: res.page,
perPage: res.per_page,
};
}
public async listAllForProxyId(soClient: SavedObjectsClientContract, proxyId: string) {
const res = await this.encryptedSoClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
searchFields: ['proxy_id'],
search: escapeSearchQueryPhrase(proxyId),
});
return {
items: res.saved_objects.map<FleetServerHost>(savedObjectToFleetServerHost),
total: res.total,
page: res.page,
perPage: res.per_page,
};
}
// export async function deleteFleetServerHost(
public async delete(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
options?: { fromPreconfiguration?: boolean }
) {
const logger = appContextService.getLogger();
logger.debug(`Deleting fleet server host ${id}`);
const fleetServerHost = await this.get(soClient, id);
if (fleetServerHost.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetServerHostUnauthorizedError(
`Cannot delete ${id} preconfigured fleet server host`
);
}
if (fleetServerHost.is_default) {
throw new FleetServerHostUnauthorizedError(
`Default Fleet Server hosts ${id} cannot be deleted.`
);
}
await agentPolicyService.removeFleetServerHostFromAll(esClient, id, {
force: options?.fromPreconfiguration,
});
const soDeleteResult = await this.encryptedSoClient.delete(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
id
);
await deleteFleetServerHostsSecrets({
fleetServerHost,
esClient: appContextService.getInternalUserESClient(),
});
return soDeleteResult;
}
public async update(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
id: string,
data: Partial<FleetServerHost>,
options?: { fromPreconfiguration?: boolean; secretHashes?: Record<string, any> }
) {
let secretsToDelete: PolicySecretReference[] = [];
const logger = appContextService.getLogger();
logger.debug(`Updating fleet server host ${id}`);
const originalItem = await this.get(soClient, id);
const updateData: Nullable<Partial<FleetServerHostSOAttributes>> = {
...omit(data, ['ssl', 'secrets']),
};
if (data.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetServerHostUnauthorizedError(
`Cannot update ${id} preconfigured fleet server host`
);
}
if (data.is_default) {
const defaultItem = await this.getDefaultFleetServerHost(soClient);
if (defaultItem && defaultItem.id !== id) {
await this.update(
soClient,
esClient,
defaultItem.id,
{
is_default: false,
},
{ fromPreconfiguration: options?.fromPreconfiguration }
);
}
}
if (data.host_urls) {
updateData.host_urls = data.host_urls.map(normalizeHostsForAgents);
}
if (data.ssl) {
updateData.ssl = JSON.stringify(data.ssl);
} else if (data.ssl === null) {
// Explicitly set to null to allow to delete the field
updateData.ssl = null;
}
// Store secret values if enabled; if not, store plain text values
if (await isSecretStorageEnabled(esClient, soClient)) {
const secretsRes = await extractAndUpdateFleetServerHostsSecrets({
oldFleetServerHost: originalItem,
fleetServerHostUpdate: data,
esClient,
secretHashes: data.is_preconfigured ? options?.secretHashes : undefined,
});
updateData.secrets = secretsRes.fleetServerHostUpdate
.secrets as FleetServerHostSOAttributes['secrets'];
secretsToDelete = secretsRes.secretsToDelete;
} else {
if (
(!data.ssl?.key && data.secrets?.ssl?.key) ||
(!data.ssl?.es_key && data.secrets?.ssl?.es_key)
) {
updateData.ssl = JSON.stringify({ ...data.ssl, ...data.secrets.ssl });
}
}
await this.encryptedSoClient.update<FleetServerHostSOAttributes>(
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
id,
updateData
);
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}`);
}
}
logger.debug(`Updated fleet server host ${id}`);
return {
...originalItem,
...updateData,
};
}
public async bulkGet(
soClient: SavedObjectsClientContract,
ids: string[],
{ ignoreNotFound = false } = { ignoreNotFound: true }
) {
if (ids.length === 0) {
return [];
}
const res = await this.encryptedSoClient.bulkGet<FleetServerHostSOAttributes>(
ids.map((id) => ({
id,
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
}))
);
return res.saved_objects
.map((so) => {
if (so.error) {
if (!ignoreNotFound || so.error.statusCode !== 404) {
throw so.error;
}
return undefined;
}
return savedObjectToFleetServerHost(so);
})
.filter(
(fleetServerHostOrUndefined): fleetServerHostOrUndefined is FleetServerHost =>
typeof fleetServerHostOrUndefined !== 'undefined'
);
}
// /**
// * Get the default Fleet server policy hosts or throw if it does not exists
// */
public async getDefaultFleetServerHost(
soClient: SavedObjectsClientContract
): Promise<FleetServerHost | null> {
const res = await this.encryptedSoClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
filter: `${FLEET_SERVER_HOST_SAVED_OBJECT_TYPE}.attributes.is_default:true`,
});
if (res.saved_objects.length === 0) {
return null;
}
return savedObjectToFleetServerHost(res.saved_objects[0]);
}
}
export const fleetServerHostService = new FleetServerHostService();
export async function getFleetServerHostsForAgentPolicy(
soClient: SavedObjectsClientContract,
agentPolicy: Pick<AgentPolicy, 'fleet_server_host_id'>
) {
if (agentPolicy.fleet_server_host_id) {
return getFleetServerHost(soClient, agentPolicy.fleet_server_host_id);
return fleetServerHostService.get(soClient, agentPolicy.fleet_server_host_id);
}
const defaultFleetServerHost = await getDefaultFleetServerHost(soClient);
const defaultFleetServerHost = await fleetServerHostService.getDefaultFleetServerHost(soClient);
if (!defaultFleetServerHost) {
throw new FleetServerHostNotFoundError('Default Fleet Server host is not setup');
}
@ -241,29 +391,14 @@ export async function getFleetServerHostsForAgentPolicy(
return defaultFleetServerHost;
}
/**
* Get the default Fleet server policy hosts or throw if it does not exists
*/
export async function getDefaultFleetServerHost(
soClient: SavedObjectsClientContract
): Promise<FleetServerHost | null> {
const res = await soClient.find<FleetServerHostSOAttributes>({
type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
filter: `${FLEET_SERVER_HOST_SAVED_OBJECT_TYPE}.attributes.is_default:true`,
});
if (res.saved_objects.length === 0) {
return null;
}
return savedObjectToFleetServerHost(res.saved_objects[0]);
}
/**
* Migrate Global setting fleet server hosts to their own saved object
*/
export async function migrateSettingsToFleetServerHost(soClient: SavedObjectsClientContract) {
const defaultFleetServerHost = await getDefaultFleetServerHost(soClient);
export async function migrateSettingsToFleetServerHost(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient
) {
const defaultFleetServerHost = await fleetServerHostService.getDefaultFleetServerHost(soClient);
if (defaultFleetServerHost) {
return;
}
@ -282,8 +417,9 @@ export async function migrateSettingsToFleetServerHost(soClient: SavedObjectsCli
}
// Migrate
await createFleetServerHost(
await fleetServerHostService.create(
soClient,
esClient,
{
name: 'Default',
host_urls: oldSettings.attributes.fleet_server_hosts,

View file

@ -38,6 +38,7 @@ export { outputService } from './output';
export { downloadSourceService } from './download_source';
export { settingsService };
export { dataStreamService } from './data_streams';
export { fleetServerHostService } from './fleet_server_host';
// Plugin services
export { appContextService } from './app_context';

View file

@ -18,7 +18,7 @@ import {
listFleetProxies,
updateFleetProxy,
} from '../fleet_proxies';
import { listFleetServerHostsForProxyId } from '../fleet_server_host';
import { fleetServerHostService } from '../fleet_server_host';
import { agentPolicyService } from '../agent_policy';
import { outputService } from '../output';
@ -96,7 +96,7 @@ async function createOrUpdatePreconfiguredFleetProxies(
);
// Bump all the agent policy that use that proxy
const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([
listFleetServerHostsForProxyId(soClient, id),
fleetServerHostService.listAllForProxyId(soClient, id),
outputService.listAllForProxyId(soClient, id),
]);
if (
@ -146,7 +146,7 @@ async function cleanPreconfiguredFleetProxies(
}
const [{ items: fleetServerHosts }, { items: outputs }] = await Promise.all([
listFleetServerHostsForProxyId(soClient, existingFleetProxy.id),
fleetServerHostService.listAllForProxyId(soClient, existingFleetProxy.id),
outputService.listAllForProxyId(soClient, existingFleetProxy.id),
]);
const isUsed = fleetServerHosts.length > 0 || outputs.length > 0;

View file

@ -8,12 +8,9 @@ import { savedObjectsClientMock, elasticsearchServiceMock } from '@kbn/core/serv
import { securityMock } from '@kbn/security-plugin/server/mocks';
import { appContextService } from '../app_context';
import {
getDefaultFleetServerHost,
createFleetServerHost,
bulkGetFleetServerHosts,
updateFleetServerHost,
} from '../fleet_server_host';
import { fleetServerHostService } from '../fleet_server_host';
import type { FleetServerHost } from '../../../common/types';
import {
createCloudFleetServerHostIfNeeded,
@ -21,8 +18,7 @@ import {
getPreconfiguredFleetServerHostFromConfig,
createOrUpdatePreconfiguredFleetServerHosts,
} from './fleet_server_host';
import type { FleetServerHost } from '../../../common/types';
import { hashSecret } from './outputs';
jest.mock('../fleet_server_host');
jest.mock('../app_context');
@ -33,17 +29,8 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
...securityMock.createSetup(),
}));
const mockedGetDefaultFleetServerHost = getDefaultFleetServerHost as jest.MockedFunction<
typeof getDefaultFleetServerHost
>;
const mockedCreateFleetServerHost = createFleetServerHost as jest.MockedFunction<
typeof createFleetServerHost
>;
const mockedUpdateFleetServerHost = updateFleetServerHost as jest.MockedFunction<
typeof updateFleetServerHost
>;
const mockedBulkGetFleetServerHosts = bulkGetFleetServerHosts as jest.MockedFunction<
typeof bulkGetFleetServerHosts
const mockedFleetServerHostService = fleetServerHostService as jest.Mocked<
typeof fleetServerHostService
>;
describe('getPreconfiguredFleetServerHostFromConfig', () => {
@ -64,6 +51,30 @@ describe('getPreconfiguredFleetServerHostFromConfig', () => {
expect(res).toEqual(config.fleetServerHosts);
});
it('should work with preconfigured fleetServerHosts that have SSL options', () => {
const config = {
fleetServerHosts: [
{
id: 'id1',
name: 'fleet server 1',
host_urls: [],
is_default: false,
is_preconfigured: false,
ssl: {
certificate_authorities: ['cert authorities'],
es_certificate_authorities: ['es cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
},
},
],
};
const res = getPreconfiguredFleetServerHostFromConfig(config);
expect(res).toEqual(config.fleetServerHosts);
});
it('should work with agents.fleet_server.hosts', () => {
const config = {
agents: { fleet_server: { hosts: ['http://test.fr'] } },
@ -217,19 +228,22 @@ describe('getCloudFleetServersHosts', () => {
describe('createCloudFleetServerHostIfNeeded', () => {
afterEach(() => {
mockedCreateFleetServerHost.mockReset();
mockedFleetServerHostService.create.mockReset();
mockedAppContextService.getCloud.mockReset();
});
it('should do nothing if there is no cloud fleet server hosts', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createCloudFleetServerHostIfNeeded(soClient);
await createCloudFleetServerHostIfNeeded(soClient, esClient);
expect(mockedCreateFleetServerHost).not.toBeCalled();
expect(mockedFleetServerHostService.create).not.toBeCalled();
});
it('should do nothing if there is already an host configured', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
mockedAppContextService.getCloud.mockReturnValue({
cloudId:
'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==',
@ -242,17 +256,19 @@ describe('createCloudFleetServerHostIfNeeded', () => {
projectId: undefined,
},
});
mockedGetDefaultFleetServerHost.mockResolvedValue({
mockedFleetServerHostService.get.mockResolvedValue({
id: 'test',
} as any);
await createCloudFleetServerHostIfNeeded(soClient);
await createCloudFleetServerHostIfNeeded(soClient, esClient);
expect(mockedCreateFleetServerHost).not.toBeCalled();
expect(mockedFleetServerHostService.create).not.toBeCalled();
});
it('should create a new fleet server hosts if there is no host configured', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
mockedAppContextService.getCloud.mockReturnValue({
cloudId:
'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==',
@ -266,16 +282,17 @@ describe('createCloudFleetServerHostIfNeeded', () => {
projectId: undefined,
},
});
mockedGetDefaultFleetServerHost.mockResolvedValue(null);
mockedFleetServerHostService.get.mockResolvedValue(null as any);
soClient.create.mockResolvedValue({
id: 'test-id',
attributes: {},
} as any);
await createCloudFleetServerHostIfNeeded(soClient);
await createCloudFleetServerHostIfNeeded(soClient, esClient);
expect(mockedCreateFleetServerHost).toBeCalledTimes(1);
expect(mockedCreateFleetServerHost).toBeCalledWith(
expect(mockedFleetServerHostService.create).toBeCalledTimes(1);
expect(mockedFleetServerHostService.create).toBeCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
host_urls: ['https://deployment-id-1.fleet.us-east-1.aws.found.io'],
@ -287,8 +304,10 @@ describe('createCloudFleetServerHostIfNeeded', () => {
});
describe('createOrUpdatePreconfiguredFleetServerHosts', () => {
beforeEach(() => {
mockedBulkGetFleetServerHosts.mockResolvedValue([
let secretHash: string;
beforeEach(async () => {
secretHash = await hashSecret('secretKey');
mockedFleetServerHostService.bulkGet.mockResolvedValue([
{
id: 'fleet-123',
name: 'TEST',
@ -301,11 +320,88 @@ describe('createOrUpdatePreconfiguredFleetServerHosts', () => {
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
},
{
id: 'fleet-with-secrets',
name: 'TEST_SECRETS',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
secrets: {
ssl: {
key: { id: 'test123', hash: secretHash },
},
},
},
] as FleetServerHost[]);
});
afterEach(() => {
mockedBulkGetFleetServerHosts.mockReset();
mockedFleetServerHostService.bulkGet.mockReset();
jest.resetAllMocks();
});
it('should create a preconfigured fleet server host that does not exist', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'new-fleet-server-host',
name: 'TEST_1',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
},
]);
expect(mockedFleetServerHostService.create).toBeCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
name: 'TEST_1',
}),
expect.anything()
);
expect(mockedFleetServerHostService.update).not.toBeCalled();
});
it('should create a preconfigured fleet server host with secrets that does not exist', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'new-fleet-server-host',
name: 'TEST_1',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
},
},
]);
expect(mockedFleetServerHostService.create).toBeCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({
name: 'TEST_1',
secrets: {
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
},
}),
expect.anything()
);
expect(mockedFleetServerHostService.update).not.toBeCalled();
});
it('should update preconfigured fleet server hosts if is_internal flag changes', async () => {
@ -319,11 +415,224 @@ describe('createOrUpdatePreconfiguredFleetServerHosts', () => {
is_default: false,
is_internal: true,
host_urls: ['http://test-internal.fr'],
is_preconfigured: false,
is_preconfigured: true,
},
]);
expect(mockedCreateFleetServerHost).not.toBeCalled();
expect(mockedUpdateFleetServerHost).toBeCalled();
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-internal',
expect.objectContaining({
is_internal: true,
}),
{ fromPreconfiguration: true, secretHashes: {} }
);
});
it('should update preconfigured fleet server hosts if host_urls change', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-internal',
name: 'TEST_INTERNAL',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr', 'http://test.fr'],
is_preconfigured: true,
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-internal',
expect.objectContaining({
host_urls: ['http://test-internal.fr', 'http://test.fr'],
}),
{ fromPreconfiguration: true, secretHashes: {} }
);
});
it('should update preconfigured fleet server hosts if proxy_id change', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-internal',
name: 'TEST_INTERNAL',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
proxy_id: 'proxy-test',
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-internal',
expect.objectContaining({
proxy_id: 'proxy-test',
}),
{ fromPreconfiguration: true, secretHashes: {} }
);
});
it('should update preconfigured fleet server hosts if preconfigured host exists and changed to have ssl', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-internal',
name: 'TEST_INTERNAL',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-internal',
expect.objectContaining({
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
}),
expect.anything()
);
});
it('should update preconfigured fleet server hosts if preconfigured host exists and changed to have secrets', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-internal',
name: 'TEST_INTERNAL',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
},
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-internal',
expect.objectContaining({
secrets: {
ssl: {
key: 'unsecureKey1',
es_key: 'unsecureKey2',
},
},
}),
expect.anything()
);
});
it('should update preconfigured fleet server hosts if preconfigured host with secrets exists and changes', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-with-secrets',
name: 'TEST_SECRETS',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'secretKey',
es_key: 'secretKey2',
},
},
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).toBeCalledWith(
expect.anything(),
expect.anything(),
'fleet-with-secrets',
expect.objectContaining({
secrets: {
ssl: {
key: 'secretKey',
es_key: 'secretKey2',
},
},
}),
expect.anything()
);
});
it('should not update preconfigured fleet server hosts if no fields changed', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-internal',
name: 'TEST_INTERNAL',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).not.toBeCalled();
});
it('should not update preconfigured fleet server hosts with secrets if no fields changed', async () => {
const soClient = savedObjectsClientMock.create();
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
await createOrUpdatePreconfiguredFleetServerHosts(soClient, esClient, [
{
id: 'fleet-with-secrets',
name: 'TEST_SECRETS',
is_default: false,
is_internal: false,
host_urls: ['http://test-internal.fr'],
is_preconfigured: true,
secrets: {
ssl: {
key: 'secretKey',
},
},
},
]);
expect(mockedFleetServerHostService.create).not.toBeCalled();
expect(mockedFleetServerHostService.update).not.toBeCalled();
});
});

View file

@ -15,17 +15,12 @@ import { FleetError } from '../../errors';
import type { FleetServerHost } from '../../types';
import { appContextService } from '../app_context';
import {
bulkGetFleetServerHosts,
createFleetServerHost,
deleteFleetServerHost,
listFleetServerHosts,
updateFleetServerHost,
getDefaultFleetServerHost,
} from '../fleet_server_host';
import { fleetServerHostService } from '../fleet_server_host';
import { agentPolicyService } from '../agent_policy';
import { isDifferent } from './utils';
import { hashSecret, isSecretDifferent } from './outputs';
export function getCloudFleetServersHosts() {
const cloudSetup = appContextService.getCloud();
@ -81,7 +76,7 @@ export async function ensurePreconfiguredFleetServerHosts(
esClient,
preconfiguredFleetServerHosts
);
await createCloudFleetServerHostIfNeeded(soClient);
await createCloudFleetServerHostIfNeeded(soClient, esClient);
await cleanPreconfiguredFleetServerHosts(soClient, esClient, preconfiguredFleetServerHosts);
}
@ -90,7 +85,7 @@ export async function createOrUpdatePreconfiguredFleetServerHosts(
esClient: ElasticsearchClient,
preconfiguredFleetServerHosts: FleetServerHost[]
) {
const existingFleetServerHosts = await bulkGetFleetServerHosts(
const existingFleetServerHosts = await fleetServerHostService.bulkGet(
soClient,
preconfiguredFleetServerHosts.map(({ id }) => id),
{ ignoreNotFound: true }
@ -105,36 +100,37 @@ export async function createOrUpdatePreconfiguredFleetServerHosts(
const { id, ...data } = preconfiguredFleetServerHost;
const isCreate = !existingHost;
const isUpdateWithNewData =
(existingHost &&
(!existingHost.is_preconfigured ||
existingHost.is_default !== preconfiguredFleetServerHost.is_default ||
existingHost.name !== preconfiguredFleetServerHost.name ||
isDifferent(existingHost.is_internal, preconfiguredFleetServerHost.is_internal) ||
isDifferent(
existingHost.host_urls.map(normalizeHostsForAgents),
preconfiguredFleetServerHost.host_urls.map(normalizeHostsForAgents)
))) ||
isDifferent(existingHost?.proxy_id, preconfiguredFleetServerHost.proxy_id);
existingHost &&
(!existingHost.is_preconfigured ||
(await isPreconfiguredFleetServerHostDifferentFromCurrent(
existingHost,
preconfiguredFleetServerHost
)));
const secretHashes = await hashSecrets(preconfiguredFleetServerHost);
if (isCreate) {
await createFleetServerHost(
await fleetServerHostService.create(
soClient,
esClient,
{
...data,
is_preconfigured: true,
},
{ id, overwrite: true, fromPreconfiguration: true }
{ id, overwrite: true, fromPreconfiguration: true, secretHashes }
);
} else if (isUpdateWithNewData) {
await updateFleetServerHost(
await fleetServerHostService.update(
soClient,
esClient,
id,
{
...data,
is_preconfigured: true,
},
{ fromPreconfiguration: true }
{ fromPreconfiguration: true, secretHashes }
);
if (data.is_default) {
await agentPolicyService.bumpAllAgentPolicies(esClient);
@ -146,16 +142,20 @@ export async function createOrUpdatePreconfiguredFleetServerHosts(
);
}
export async function createCloudFleetServerHostIfNeeded(soClient: SavedObjectsClientContract) {
export async function createCloudFleetServerHostIfNeeded(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient
) {
const cloudServerHosts = getCloudFleetServersHosts();
if (!cloudServerHosts || cloudServerHosts.length === 0) {
return;
}
const defaultFleetServerHost = await getDefaultFleetServerHost(soClient);
const defaultFleetServerHost = await fleetServerHostService.getDefaultFleetServerHost(soClient);
if (!defaultFleetServerHost) {
await createFleetServerHost(
await fleetServerHostService.create(
soClient,
esClient,
{
name: 'Default',
is_default: true,
@ -172,7 +172,7 @@ export async function cleanPreconfiguredFleetServerHosts(
esClient: ElasticsearchClient,
preconfiguredFleetServerHosts: FleetServerHost[]
) {
const existingFleetServerHosts = await listFleetServerHosts(soClient);
const existingFleetServerHosts = await fleetServerHostService.list(soClient);
const existingPreconfiguredHosts = existingFleetServerHosts.items.filter(
(o) => o.is_preconfigured === true
);
@ -186,8 +186,9 @@ export async function cleanPreconfiguredFleetServerHosts(
}
if (existingFleetServerHost.is_default) {
await updateFleetServerHost(
await fleetServerHostService.update(
soClient,
esClient,
existingFleetServerHost.id,
{ is_preconfigured: false },
{
@ -195,7 +196,7 @@ export async function cleanPreconfiguredFleetServerHosts(
}
);
} else {
await deleteFleetServerHost(soClient, esClient, existingFleetServerHost.id, {
await fleetServerHostService.delete(soClient, esClient, existingFleetServerHost.id, {
fromPreconfiguration: true,
});
}
@ -207,3 +208,53 @@ function getConfigFleetServerHosts(config?: FleetConfigType) {
? config?.agents?.fleet_server?.hosts
: undefined;
}
async function hashSecrets(preconfiguredFleetServerHost: FleetServerHost) {
let secrets: Record<string, any> = {};
if (typeof preconfiguredFleetServerHost.secrets?.ssl?.key === 'string') {
const key = await hashSecret(preconfiguredFleetServerHost.secrets?.ssl?.key);
secrets = {
ssl: {
key,
},
};
}
if (typeof preconfiguredFleetServerHost.secrets?.ssl?.key === 'string') {
const esKey = await hashSecret(preconfiguredFleetServerHost.secrets?.ssl?.key);
secrets = {
...(secrets ? secrets : {}),
ssl: { es_key: esKey },
};
}
return secrets;
}
async function isPreconfiguredFleetServerHostDifferentFromCurrent(
existingFleetServerHost: FleetServerHost,
preconfiguredFleetServerHost: Partial<FleetServerHost>
): Promise<boolean> {
const secretFieldsAreDifferent = async (): Promise<boolean> => {
const sslKeyHashIsDifferent = await isSecretDifferent(
preconfiguredFleetServerHost.secrets?.ssl?.key,
existingFleetServerHost.secrets?.ssl?.key
);
const sslESKeyHashIsDifferent = await isSecretDifferent(
preconfiguredFleetServerHost.secrets?.ssl?.es_key,
existingFleetServerHost.secrets?.ssl?.es_key
);
return sslKeyHashIsDifferent || sslESKeyHashIsDifferent;
};
return (
existingFleetServerHost.is_default !== preconfiguredFleetServerHost.is_default ||
existingFleetServerHost.name !== preconfiguredFleetServerHost.name ||
isDifferent(existingFleetServerHost.is_internal, preconfiguredFleetServerHost.is_internal) ||
isDifferent(
existingFleetServerHost.host_urls.map(normalizeHostsForAgents),
preconfiguredFleetServerHost?.host_urls?.map(normalizeHostsForAgents)
) ||
isDifferent(existingFleetServerHost?.proxy_id, preconfiguredFleetServerHost.proxy_id) ||
isDifferent(existingFleetServerHost?.ssl, preconfiguredFleetServerHost?.ssl) ||
secretFieldsAreDifferent()
);
}

View file

@ -19,7 +19,7 @@ import type {
PreconfiguredOutput,
Output,
NewOutput,
OutputSecret,
SOSecret,
KafkaOutput,
NewRemoteElasticsearchOutput,
} from '../../../common/types';
@ -255,13 +255,13 @@ export async function cleanPreconfiguredOutputs(
}
}
const hasHash = (secret?: OutputSecret): secret is { id: string; hash: string } => {
const hasHash = (secret?: SOSecret): secret is { id: string; hash: string } => {
return !!secret && typeof secret !== 'string' && !!secret.hash;
};
async function isSecretDifferent(
preconfiguredValue: OutputSecret | undefined,
existingSecret: OutputSecret | undefined
export async function isSecretDifferent(
preconfiguredValue: SOSecret | undefined,
existingSecret: SOSecret | undefined
): Promise<boolean> {
if (!existingSecret && preconfiguredValue) {
return true;

View file

@ -24,10 +24,12 @@ import { appContextService } from './app_context';
import {
getPolicySecretPaths,
diffSecretPaths,
diffOutputSecretPaths,
diffSOSecretPaths,
extractAndWriteSecrets,
extractAndUpdateSecrets,
extractAndUpdateOutputSecrets,
extractAndWriteFleetServerHostsSecrets,
extractAndUpdateFleetServerHostsSecrets,
} from './secrets';
describe('secrets', () => {
@ -1440,11 +1442,152 @@ describe('secrets', () => {
expect(result.secretsToDelete).toEqual([{ id: 'token' }]);
});
});
describe('extractAndWriteFleetServerHostsSecrets', () => {
const esClientMock = elasticsearchServiceMock.createInternalClient();
esClientMock.transport.request.mockImplementation(async (req) => {
return {
id: uuidv4(),
};
});
beforeEach(() => {
esClientMock.transport.request.mockClear();
});
it('should create new secrets', async () => {
const fleetServerHost = {
id: 'id1',
name: 'fleet server 1',
host_urls: [],
is_default: false,
is_preconfigured: false,
ssl: {
certificate_authorities: ['cert authorities'],
es_certificate_authorities: ['es cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
},
secrets: {
ssl: {
key: 'key1',
es_key: 'key2',
},
},
};
const res = await extractAndWriteFleetServerHostsSecrets({
fleetServerHost,
esClient: esClientMock,
});
expect(res.fleetServerHost).toEqual({
...fleetServerHost,
secrets: {
ssl: {
es_key: {
id: expect.any(String),
},
key: {
id: expect.any(String),
},
},
},
});
expect(res.secretReferences).toEqual([{ id: expect.anything() }, { id: expect.anything() }]);
});
});
describe('extractAndUpdateFleetServerHostsSecrets', () => {
const esClientMock = elasticsearchServiceMock.createInternalClient();
esClientMock.transport.request.mockImplementation(async (req) => {
return {
id: uuidv4(),
};
});
beforeEach(() => {
esClientMock.transport.request.mockClear();
});
it('should update existing secrets', async () => {
const fleetServerHost = {
id: 'id1',
name: 'fleet server 1',
host_urls: [],
is_default: false,
is_preconfigured: false,
ssl: {
certificate_authorities: ['cert authorities'],
es_certificate_authorities: ['es cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
},
secrets: {
ssl: {
key: 'key1',
es_key: 'key2',
},
},
};
const updatedFleetServerHost = {
...fleetServerHost,
secrets: {
ssl: {
key: 'newkey1',
es_key: 'newkey2',
},
},
};
const res = await extractAndUpdateFleetServerHostsSecrets({
oldFleetServerHost: fleetServerHost,
fleetServerHostUpdate: updatedFleetServerHost,
esClient: esClientMock,
});
expect(res.fleetServerHostUpdate).toEqual({
...fleetServerHost,
secrets: {
ssl: {
es_key: {
id: expect.any(String),
},
key: {
id: expect.any(String),
},
},
},
});
expect(res.secretReferences).toEqual([{ id: expect.anything() }, { id: expect.anything() }]);
expect(res.secretsToDelete).toEqual([{ id: undefined }, { id: undefined }]);
});
});
});
describe('diffOutputSecretPaths', () => {
describe('diffSOSecretPaths', () => {
const paths1 = [
{
path: 'somepath1',
value: {
id: 'secret-1',
},
},
{
path: 'somepath2',
value: {
id: 'secret-2',
},
},
];
const paths2 = [
paths1[0],
{
path: 'somepath2',
value: 'newvalue',
},
];
it('should return empty array if no secrets', () => {
expect(diffOutputSecretPaths([], [])).toEqual({
expect(diffSOSecretPaths([], [])).toEqual({
toCreate: [],
toDelete: [],
noChange: [],
@ -1459,7 +1602,7 @@ describe('diffOutputSecretPaths', () => {
},
},
];
expect(diffOutputSecretPaths(paths, paths)).toEqual({
expect(diffSOSecretPaths(paths, paths)).toEqual({
toCreate: [],
toDelete: [],
noChange: paths,
@ -1487,37 +1630,14 @@ describe('diffOutputSecretPaths', () => {
},
];
expect(diffOutputSecretPaths(paths, paths.slice().reverse())).toEqual({
expect(diffSOSecretPaths(paths, paths.slice().reverse())).toEqual({
toCreate: [],
toDelete: [],
noChange: paths,
});
});
it('single secret modified', () => {
const paths1 = [
{
path: 'somepath1',
value: {
id: 'secret-1',
},
},
{
path: 'somepath2',
value: {
id: 'secret-2',
},
},
];
const paths2 = [
paths1[0],
{
path: 'somepath2',
value: 'newvalue',
},
];
expect(diffOutputSecretPaths(paths1, paths2)).toEqual({
expect(diffSOSecretPaths(paths1, paths2)).toEqual({
toCreate: [
{
path: 'somepath2',
@ -1536,7 +1656,7 @@ describe('diffOutputSecretPaths', () => {
});
});
it('double secret modified', () => {
const paths1 = [
const pathsDouble1 = [
{
path: 'somepath1',
value: {
@ -1551,7 +1671,7 @@ describe('diffOutputSecretPaths', () => {
},
];
const paths2 = [
const pathsDouble2 = [
{
path: 'somepath1',
value: 'newvalue1',
@ -1562,7 +1682,7 @@ describe('diffOutputSecretPaths', () => {
},
];
expect(diffOutputSecretPaths(paths1, paths2)).toEqual({
expect(diffSOSecretPaths(pathsDouble1, pathsDouble2)).toEqual({
toCreate: [
{
path: 'somepath1',
@ -1590,9 +1710,8 @@ describe('diffOutputSecretPaths', () => {
noChange: [],
});
});
it('single secret added', () => {
const paths1 = [
const pathsSingle1 = [
{
path: 'somepath1',
value: {
@ -1601,7 +1720,7 @@ describe('diffOutputSecretPaths', () => {
},
];
const paths2 = [
const pathsSingle2 = [
paths1[0],
{
path: 'somepath2',
@ -1609,7 +1728,7 @@ describe('diffOutputSecretPaths', () => {
},
];
expect(diffOutputSecretPaths(paths1, paths2)).toEqual({
expect(diffSOSecretPaths(pathsSingle1, pathsSingle2)).toEqual({
toCreate: [
{
path: 'somepath2',

View file

@ -11,10 +11,12 @@ import { get, keyBy } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import type {
FleetServerHost,
SOSecretPath,
KafkaOutput,
NewFleetServerHost,
NewRemoteElasticsearchOutput,
Output,
OutputSecretPath,
} from '../../common/types';
import { packageHasNoPolicyTemplates } from '../../common/services/policy_template';
@ -256,138 +258,6 @@ export async function extractAndWriteSecrets(opts: {
};
}
export async function extractAndWriteOutputSecrets(opts: {
output: NewOutput;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ output: NewOutput; secretReferences: PolicySecretReference[] }> {
const { output, esClient, secretHashes = {} } = opts;
const secretPaths = getOutputSecretPaths(output.type, output).filter(
(path) => typeof path.value === 'string'
);
if (secretPaths.length === 0) {
return { output, secretReferences: [] };
}
const secrets = await createSecrets({
esClient,
values: secretPaths.map(({ value }) => value as string),
});
const outputWithSecretRefs = JSON.parse(JSON.stringify(output));
secretPaths.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
set(outputWithSecretRefs, secretPath.path, {
id: secrets[i].id,
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
return {
output: outputWithSecretRefs,
secretReferences: secrets.map(({ id }) => ({ id })),
};
}
function getOutputSecretPaths(
outputType: NewOutput['type'],
output: NewOutput | Partial<Output>
): OutputSecretPath[] {
const outputSecretPaths: OutputSecretPath[] = [];
if (outputType === 'kafka') {
const kafkaOutput = output as KafkaOutput;
if (kafkaOutput?.secrets?.password) {
outputSecretPaths.push({
path: 'secrets.password',
value: kafkaOutput.secrets.password,
});
}
}
if (outputType === 'remote_elasticsearch') {
const remoteESOutput = output as NewRemoteElasticsearchOutput;
if (remoteESOutput.secrets?.service_token) {
outputSecretPaths.push({
path: 'secrets.service_token',
value: remoteESOutput.secrets.service_token,
});
}
if (remoteESOutput.secrets?.kibana_api_key) {
outputSecretPaths.push({
path: 'secrets.kibana_api_key',
value: remoteESOutput.secrets.kibana_api_key,
});
}
}
// common to all outputs
if (output?.secrets?.ssl?.key) {
outputSecretPaths.push({
path: 'secrets.ssl.key',
value: output.secrets.ssl.key,
});
}
return outputSecretPaths;
}
export async function deleteOutputSecrets(opts: {
output: Output;
esClient: ElasticsearchClient;
}): Promise<void> {
const { output, esClient } = opts;
const outputType = output.type;
const outputSecretPaths = getOutputSecretPaths(outputType, output);
if (outputSecretPaths.length === 0) {
return Promise.resolve();
}
const secretIds = outputSecretPaths.map(({ value }) => (value as { id: string }).id);
try {
return deleteSecrets({ esClient, ids: secretIds });
} catch (err) {
appContextService.getLogger().warn(`Error deleting secrets: ${err}`);
}
}
export function getOutputSecretReferences(output: Output): PolicySecretReference[] {
const outputSecretPaths: PolicySecretReference[] = [];
if (typeof output.secrets?.ssl?.key === 'object') {
outputSecretPaths.push({
id: output.secrets.ssl.key.id,
});
}
if (output.type === 'kafka' && typeof output?.secrets?.password === 'object') {
outputSecretPaths.push({
id: output.secrets.password.id,
});
}
if (output.type === 'remote_elasticsearch') {
if (typeof output?.secrets?.service_token === 'object') {
outputSecretPaths.push({
id: output.secrets.service_token.id,
});
}
if (typeof output?.secrets?.kibana_api_key === 'object') {
outputSecretPaths.push({
id: output.secrets.kibana_api_key.id,
});
}
}
return outputSecretPaths;
}
export async function extractAndUpdateSecrets(opts: {
oldPackagePolicy: PackagePolicy;
packagePolicyUpdate: UpdatePackagePolicy;
@ -443,59 +313,6 @@ export async function extractAndUpdateSecrets(opts: {
secretsToDelete,
};
}
export async function extractAndUpdateOutputSecrets(opts: {
oldOutput: Output;
outputUpdate: Partial<Output>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
outputUpdate: Partial<Output>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldOutput, outputUpdate, esClient, secretHashes } = opts;
const outputType = outputUpdate.type || oldOutput.type;
const oldSecretPaths = getOutputSecretPaths(oldOutput.type, oldOutput);
const updatedSecretPaths = getOutputSecretPaths(outputType, outputUpdate);
if (!oldSecretPaths.length && !updatedSecretPaths.length) {
return { outputUpdate, secretReferences: [], secretsToDelete: [] };
}
const { toCreate, toDelete, noChange } = diffOutputSecretPaths(
oldSecretPaths,
updatedSecretPaths
);
const createdSecrets = await createSecrets({
esClient,
values: toCreate.map((secretPath) => secretPath.value as string),
});
const outputWithSecretRefs = JSON.parse(JSON.stringify(outputUpdate));
toCreate.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
set(outputWithSecretRefs, secretPath.path, {
id: createdSecrets[i].id,
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
const secretReferences = [
...noChange.map((secretPath) => ({ id: (secretPath.value as { id: string }).id })),
...createdSecrets.map(({ id }) => ({ id })),
];
return {
outputUpdate: outputWithSecretRefs,
secretReferences,
secretsToDelete: toDelete.map((secretPath) => ({
id: (secretPath.value as { id: string }).id,
})),
};
}
function isSecretVar(varDef: RegistryVarsEntry) {
return varDef.secret === true;
@ -547,38 +364,6 @@ export function diffSecretPaths(
return { toCreate: [...toCreate, ...remainingNewPaths], toDelete, noChange };
}
export function diffOutputSecretPaths(
oldPaths: OutputSecretPath[],
newPaths: OutputSecretPath[]
): { toCreate: OutputSecretPath[]; toDelete: OutputSecretPath[]; noChange: OutputSecretPath[] } {
const toCreate: OutputSecretPath[] = [];
const toDelete: OutputSecretPath[] = [];
const noChange: OutputSecretPath[] = [];
const newPathsByPath = keyBy(newPaths, 'path');
for (const oldPath of oldPaths) {
if (!newPathsByPath[oldPath.path]) {
toDelete.push(oldPath);
}
const newPath = newPathsByPath[oldPath.path];
if (newPath && newPath.value) {
const newValue = newPath.value;
if (typeof newValue === 'string') {
toCreate.push(newPath);
toDelete.push(oldPath);
} else {
noChange.push(newPath);
}
}
delete newPathsByPath[oldPath.path];
}
const remainingNewPaths = Object.values(newPathsByPath);
return { toCreate: [...toCreate, ...remainingNewPaths], toDelete, noChange };
}
// Given a package policy and a package,
// returns an array of lodash style paths to all secrets and their current values
export function getPolicySecretPaths(
@ -850,3 +635,393 @@ function getPolicyWithSecretReferences(
return result;
}
/**
* Common functions for SO objects
* Currently used for outputs and fleet server hosts
*/
/**
* diffSOSecretPaths
* Makes the diff betwwen old and new secrets paths
*/
export function diffSOSecretPaths(
oldPaths: SOSecretPath[],
newPaths: SOSecretPath[]
): { toCreate: SOSecretPath[]; toDelete: SOSecretPath[]; noChange: SOSecretPath[] } {
const toCreate: SOSecretPath[] = [];
const toDelete: SOSecretPath[] = [];
const noChange: SOSecretPath[] = [];
const newPathsByPath = keyBy(newPaths, 'path');
for (const oldPath of oldPaths) {
if (!newPathsByPath[oldPath.path]) {
toDelete.push(oldPath);
}
const newPath = newPathsByPath[oldPath.path];
if (newPath && newPath.value) {
const newValue = newPath.value;
if (typeof newValue === 'string') {
toCreate.push(newPath);
toDelete.push(oldPath);
} else {
noChange.push(newPath);
}
}
delete newPathsByPath[oldPath.path];
}
const remainingNewPaths = Object.values(newPathsByPath);
return { toCreate: [...toCreate, ...remainingNewPaths], toDelete, noChange };
}
/**
* deleteSOSecrets
* Given an array of secret paths, deletes the corresponding secrets
*/
export async function deleteSOSecrets(
esClient: ElasticsearchClient,
secretPaths: SOSecretPath[]
): Promise<void> {
if (secretPaths.length === 0) {
return Promise.resolve();
}
const secretIds = secretPaths.map(({ value }) => (value as { id: string }).id);
try {
return deleteSecrets({ esClient, ids: secretIds });
} catch (err) {
appContextService.getLogger().warn(`Error deleting secrets: ${err}`);
}
}
/**
* extractAndWriteSOSecrets
* Takes a generic object T and its secret paths
* Creates new secrets and returns the references
*/
async function extractAndWriteSOSecrets<T>(opts: {
soObject: T;
esClient: ElasticsearchClient;
secretPaths: SOSecretPath[];
secretHashes?: Record<string, any>;
}): Promise<{ soObjectWithSecrets: T; secretReferences: PolicySecretReference[] }> {
const { soObject, esClient, secretPaths, secretHashes = {} } = opts;
if (secretPaths.length === 0) {
return { soObjectWithSecrets: soObject, secretReferences: [] };
}
const secrets = await createSecrets({
esClient,
values: secretPaths.map(({ value }) => value as string),
});
const objectWithSecretRefs = JSON.parse(JSON.stringify(soObject));
secretPaths.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
set(objectWithSecretRefs, secretPath.path, {
id: secrets[i].id,
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
return {
soObjectWithSecrets: objectWithSecretRefs,
secretReferences: secrets.map(({ id }) => ({ id })),
};
}
/**
* extractAndUpdateSOSecrets
* Takes a generic object T to update and its old and new secret paths
* Updates secrets and returns the references
*/
async function extractAndUpdateSOSecrets<T>(opts: {
updatedSoObject: Partial<T>;
oldSecretPaths: SOSecretPath[];
updatedSecretPaths: SOSecretPath[];
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
updatedSoObject: Partial<T>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { updatedSoObject, oldSecretPaths, updatedSecretPaths, esClient, secretHashes } = opts;
if (!oldSecretPaths.length && !updatedSecretPaths.length) {
return { updatedSoObject, secretReferences: [], secretsToDelete: [] };
}
const { toCreate, toDelete, noChange } = diffSOSecretPaths(oldSecretPaths, updatedSecretPaths);
const createdSecrets = await createSecrets({
esClient,
values: toCreate.map((secretPath) => secretPath.value as string),
});
const soObjectWithSecretRefs = JSON.parse(JSON.stringify(updatedSoObject));
toCreate.forEach((secretPath, i) => {
const pathWithoutPrefix = secretPath.path.replace('secrets.', '');
const maybeHash = get(secretHashes, pathWithoutPrefix);
set(soObjectWithSecretRefs, secretPath.path, {
id: createdSecrets[i].id,
...(typeof maybeHash === 'string' && { hash: maybeHash }),
});
});
const secretReferences = [
...noChange.map((secretPath) => ({ id: (secretPath.value as { id: string }).id })),
...createdSecrets.map(({ id }) => ({ id })),
];
return {
updatedSoObject: soObjectWithSecretRefs,
secretReferences,
secretsToDelete: toDelete.map((secretPath) => ({
id: (secretPath.value as { id: string }).id,
})),
};
}
// Outputs functions
export async function extractAndWriteOutputSecrets(opts: {
output: NewOutput;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ output: NewOutput; secretReferences: PolicySecretReference[] }> {
const { output, esClient, secretHashes = {} } = opts;
const secretPaths = getOutputSecretPaths(output.type, output).filter(
(path) => typeof path.value === 'string'
);
const secretRes = await extractAndWriteSOSecrets<NewOutput>({
soObject: output,
secretPaths,
esClient,
secretHashes,
});
return { output: secretRes.soObjectWithSecrets, secretReferences: secretRes.secretReferences };
}
export async function extractAndUpdateOutputSecrets(opts: {
oldOutput: Output;
outputUpdate: Partial<Output>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
outputUpdate: Partial<Output>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldOutput, outputUpdate, esClient, secretHashes } = opts;
const outputType = outputUpdate.type || oldOutput.type;
const oldSecretPaths = getOutputSecretPaths(oldOutput.type, oldOutput);
const updatedSecretPaths = getOutputSecretPaths(outputType, outputUpdate);
const secretRes = await extractAndUpdateSOSecrets<Output>({
updatedSoObject: outputUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes: outputUpdate.is_preconfigured ? secretHashes : undefined,
});
return {
outputUpdate: secretRes.updatedSoObject,
secretReferences: secretRes.secretReferences,
secretsToDelete: secretRes.secretsToDelete,
};
}
function getOutputSecretPaths(
outputType: NewOutput['type'],
output: NewOutput | Partial<Output>
): SOSecretPath[] {
const outputSecretPaths: SOSecretPath[] = [];
if (outputType === 'kafka') {
const kafkaOutput = output as KafkaOutput;
if (kafkaOutput?.secrets?.password) {
outputSecretPaths.push({
path: 'secrets.password',
value: kafkaOutput.secrets.password,
});
}
}
if (outputType === 'remote_elasticsearch') {
const remoteESOutput = output as NewRemoteElasticsearchOutput;
if (remoteESOutput.secrets?.service_token) {
outputSecretPaths.push({
path: 'secrets.service_token',
value: remoteESOutput.secrets.service_token,
});
}
if (remoteESOutput.secrets?.kibana_api_key) {
outputSecretPaths.push({
path: 'secrets.kibana_api_key',
value: remoteESOutput.secrets.kibana_api_key,
});
}
}
// common to all outputs
if (output?.secrets?.ssl?.key) {
outputSecretPaths.push({
path: 'secrets.ssl.key',
value: output.secrets.ssl.key,
});
}
return outputSecretPaths;
}
export async function deleteOutputSecrets(opts: {
output: Output;
esClient: ElasticsearchClient;
}): Promise<void> {
const { output, esClient } = opts;
const outputType = output.type;
const outputSecretPaths = getOutputSecretPaths(outputType, output);
await deleteSOSecrets(esClient, outputSecretPaths);
}
export function getOutputSecretReferences(output: Output): PolicySecretReference[] {
const outputSecretPaths: PolicySecretReference[] = [];
if (typeof output.secrets?.ssl?.key === 'object') {
outputSecretPaths.push({
id: output.secrets.ssl.key.id,
});
}
if (output.type === 'kafka' && typeof output?.secrets?.password === 'object') {
outputSecretPaths.push({
id: output.secrets.password.id,
});
}
if (output.type === 'remote_elasticsearch') {
if (typeof output?.secrets?.service_token === 'object') {
outputSecretPaths.push({
id: output.secrets.service_token.id,
});
}
if (typeof output?.secrets?.kibana_api_key === 'object') {
outputSecretPaths.push({
id: output.secrets.kibana_api_key.id,
});
}
}
return outputSecretPaths;
}
// Fleet server hosts functions
function getFleetServerHostsSecretPaths(
fleetServerHost: NewFleetServerHost | Partial<FleetServerHost>
): SOSecretPath[] {
const secretPaths: SOSecretPath[] = [];
if (fleetServerHost?.secrets?.ssl?.key) {
secretPaths.push({
path: 'secrets.ssl.key',
value: fleetServerHost.secrets.ssl.key,
});
}
if (fleetServerHost?.secrets?.ssl?.es_key) {
secretPaths.push({
path: 'secrets.ssl.es_key',
value: fleetServerHost.secrets.ssl.es_key,
});
}
return secretPaths;
}
export async function extractAndWriteFleetServerHostsSecrets(opts: {
fleetServerHost: NewFleetServerHost;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{ fleetServerHost: NewFleetServerHost; secretReferences: PolicySecretReference[] }> {
const { fleetServerHost, esClient, secretHashes = {} } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost).filter(
(path) => typeof path.value === 'string'
);
const secretRes = await extractAndWriteSOSecrets<NewFleetServerHost>({
soObject: fleetServerHost,
secretPaths,
esClient,
secretHashes,
});
return {
fleetServerHost: secretRes.soObjectWithSecrets,
secretReferences: secretRes.secretReferences,
};
}
export async function extractAndUpdateFleetServerHostsSecrets(opts: {
oldFleetServerHost: NewFleetServerHost;
fleetServerHostUpdate: Partial<NewFleetServerHost>;
esClient: ElasticsearchClient;
secretHashes?: Record<string, any>;
}): Promise<{
fleetServerHostUpdate: Partial<NewFleetServerHost>;
secretReferences: PolicySecretReference[];
secretsToDelete: PolicySecretReference[];
}> {
const { oldFleetServerHost, fleetServerHostUpdate, esClient, secretHashes } = opts;
const oldSecretPaths = getFleetServerHostsSecretPaths(oldFleetServerHost);
const updatedSecretPaths = getFleetServerHostsSecretPaths(fleetServerHostUpdate);
const secretsRes = await extractAndUpdateSOSecrets<FleetServerHost>({
updatedSoObject: fleetServerHostUpdate,
oldSecretPaths,
updatedSecretPaths,
esClient,
secretHashes,
});
return {
fleetServerHostUpdate: secretsRes.updatedSoObject,
secretReferences: secretsRes.secretReferences,
secretsToDelete: secretsRes.secretsToDelete,
};
}
export async function deleteFleetServerHostsSecrets(opts: {
fleetServerHost: NewFleetServerHost;
esClient: ElasticsearchClient;
}): Promise<void> {
const { fleetServerHost, esClient } = opts;
const secretPaths = getFleetServerHostsSecretPaths(fleetServerHost).filter(
(path) => typeof path.value === 'string'
);
await deleteSOSecrets(esClient, secretPaths);
}
export function getFleetServerHostsSecretReferences(
fleetServerHost: FleetServerHost
): PolicySecretReference[] {
const secretPaths: PolicySecretReference[] = [];
if (typeof fleetServerHost.secrets?.ssl?.key === 'object') {
secretPaths.push({
id: fleetServerHost.secrets.ssl.key.id,
});
}
if (typeof fleetServerHost.secrets?.ssl?.es_key === 'object') {
secretPaths.push({
id: fleetServerHost.secrets.ssl.es_key.id,
});
}
return secretPaths;
}

View file

@ -19,14 +19,14 @@ import { DeleteUnenrolledAgentsPreconfiguredError } from '../errors';
import { appContextService } from './app_context';
import { getSettings, saveSettings, settingsSetup } from './settings';
import { auditLoggingService } from './audit_logging';
import { listFleetServerHosts } from './fleet_server_host';
import { fleetServerHostService } from './fleet_server_host';
jest.mock('./app_context');
jest.mock('./audit_logging');
jest.mock('./fleet_server_host');
const mockListFleetServerHosts = listFleetServerHosts as jest.MockedFunction<
typeof listFleetServerHosts
const mockedFleetServerHostService = fleetServerHostService as jest.Mocked<
typeof fleetServerHostService
>;
const mockedAuditLoggingService = auditLoggingService as jest.Mocked<typeof auditLoggingService>;
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
@ -36,6 +36,7 @@ mockedAppContextService.getSecuritySetup.mockImplementation(() => ({
describe('settingsSetup', () => {
afterEach(() => {
jest.resetAllMocks();
mockedAppContextService.getCloud.mockReset();
mockedAppContextService.getConfig.mockReset();
});
@ -86,7 +87,7 @@ describe('settingsSetup', () => {
type: 'so_type',
});
mockListFleetServerHosts.mockResolvedValueOnce({
mockedFleetServerHostService.list.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
@ -126,7 +127,7 @@ describe('getSettings', () => {
total: 1,
});
mockListFleetServerHosts.mockResolvedValueOnce({
mockedFleetServerHostService.list.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
@ -182,6 +183,9 @@ describe('getSettings', () => {
});
describe('saveSettings', () => {
afterEach(() => {
mockedAuditLoggingService.writeCustomSoAuditLog.mockReset();
});
describe('when settings object exists', () => {
it('should call audit logger', async () => {
const soClient = savedObjectsClientMock.create();
@ -212,7 +216,7 @@ describe('saveSettings', () => {
type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
});
mockListFleetServerHosts.mockResolvedValueOnce({
mockedFleetServerHostService.list.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
@ -229,11 +233,7 @@ describe('saveSettings', () => {
await saveSettings(soClient, newData);
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalledWith({
action: 'create',
id: GLOBAL_SETTINGS_ID,
savedObjectType: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
});
expect(mockedAuditLoggingService.writeCustomSoAuditLog).toHaveBeenCalled();
});
describe('when settings object does not exist', () => {
@ -293,7 +293,7 @@ describe('saveSettings', () => {
per_page: 10,
total: 1,
});
mockListFleetServerHosts.mockResolvedValueOnce({
mockedFleetServerHostService.list.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',
@ -349,7 +349,7 @@ describe('saveSettings', () => {
per_page: 10,
total: 1,
});
mockListFleetServerHosts.mockResolvedValueOnce({
mockedFleetServerHostService.list.mockResolvedValueOnce({
items: [
{
id: 'fleet-server-host',

View file

@ -5,8 +5,11 @@
* 2.0.
*/
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import type { ElasticsearchClientMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import type { Logger } from '@kbn/core/server';
import { MessageSigningError } from '../../common/errors';
import { createAppContextStartContractMock, xpackMocks } from '../mocks';
@ -16,12 +19,19 @@ import { ensurePreconfiguredPackagesAndPolicies } from '.';
import { appContextService } from './app_context';
import { getInstallations } from './epm/packages';
import { setupUpgradeManagedPackagePolicies } from './setup/managed_package_policies';
import { getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig } from './preconfiguration/delete_unenrolled_agent_setting';
import { setupFleet } from './setup';
import { isPackageInstalled } from './epm/packages/install';
import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version';
import { createOrUpdateFleetSyncedIntegrationsIndex } from './setup/fleet_synced_integrations';
jest.mock('./app_context');
jest.mock('./preconfiguration');
jest.mock('./preconfiguration/outputs');
jest.mock('./preconfiguration/fleet_proxies');
jest.mock('./preconfiguration/space_settings');
jest.mock('./preconfiguration/fleet_server_host');
jest.mock('./preconfiguration/delete_unenrolled_agent_setting');
jest.mock('./settings');
jest.mock('./output');
jest.mock('./download_source');
@ -35,6 +45,13 @@ jest.mock('./epm/elasticsearch/template/install', () => {
};
});
jest.mock('./backfill_agentless');
jest.mock('./epm/packages/install');
jest.mock('./setup/upgrade_agent_policy_schema_version');
jest.mock('./setup/fleet_synced_integrations');
const mockedAppContextService = appContextService as jest.Mocked<typeof appContextService>;
let mockedLogger: jest.Mocked<Logger>;
const mockedMethodThrowsError = (mockFn: jest.Mock) =>
mockFn.mockImplementation(() => {
@ -47,17 +64,30 @@ const mockedMethodThrowsCustom = (mockFn: jest.Mock) =>
throw new CustomTestError('method mocked to throw');
});
function getMockedSoClient() {
const soClient = savedObjectsClientMock.create();
mockedAppContextService.getInternalUserSOClient.mockReturnValue(soClient);
soClient.get.mockResolvedValue({ attributes: {} } as any);
soClient.find.mockResolvedValue({ saved_objects: [] } as any);
soClient.bulkGet.mockResolvedValue({ saved_objects: [] } as any);
soClient.create.mockResolvedValue({ attributes: {} } as any);
soClient.delete.mockResolvedValue({});
return soClient;
}
describe('setupFleet', () => {
let context: ReturnType<typeof xpackMocks.createRequestHandlerContext>;
let soClient: jest.Mocked<SavedObjectsClientContract>;
let esClient: ElasticsearchClientMock;
beforeEach(async () => {
context = xpackMocks.createRequestHandlerContext();
// prevents `Logger not set.` and other appContext errors
appContextService.start(createAppContextStartContractMock());
soClient = context.core.savedObjects.client;
mockedAppContextService.start(createAppContextStartContractMock());
esClient = context.core.elasticsearch.client.asInternalUser;
mockedLogger = loggerMock.create();
mockedAppContextService.getLogger.mockReturnValue(mockedLogger);
(getInstallations as jest.Mock).mockResolvedValueOnce({
saved_objects: [],
@ -68,22 +98,21 @@ describe('setupFleet', () => {
});
(setupUpgradeManagedPackagePolicies as jest.Mock).mockResolvedValue([]);
soClient.get.mockResolvedValue({ attributes: {} } as any);
soClient.find.mockResolvedValue({ saved_objects: [] } as any);
soClient.bulkGet.mockResolvedValue({ saved_objects: [] } as any);
soClient.create.mockResolvedValue({ attributes: {} } as any);
soClient.delete.mockResolvedValue({});
(getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig as jest.Mock).mockResolvedValue([]);
(isPackageInstalled as jest.Mock).mockResolvedValue(true);
(upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined);
(createOrUpdateFleetSyncedIntegrationsIndex as jest.Mock).mockResolvedValue(undefined);
});
afterEach(async () => {
jest.clearAllMocks();
appContextService.stop();
mockedAppContextService.stop();
});
describe('should reject with any error thrown underneath', () => {
it('SO client throws plain Error', async () => {
mockedMethodThrowsError(setupUpgradeManagedPackagePolicies as jest.Mock);
const soClient = getMockedSoClient();
mockedMethodThrowsError(getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig as jest.Mock);
const setupPromise = setupFleet(soClient, esClient);
await expect(setupPromise).rejects.toThrow('SO method mocked to throw');
@ -91,6 +120,8 @@ describe('setupFleet', () => {
});
it('SO client throws other error', async () => {
const soClient = getMockedSoClient();
mockedMethodThrowsCustom(setupUpgradeManagedPackagePolicies as jest.Mock);
const setupPromise = setupFleet(soClient, esClient);
@ -100,6 +131,8 @@ describe('setupFleet', () => {
});
it('should not return non fatal errors when upgrade result has no errors', async () => {
const soClient = getMockedSoClient();
const result = await setupFleet(soClient, esClient);
expect(result).toEqual({
@ -108,25 +141,9 @@ describe('setupFleet', () => {
});
});
it('should return non fatal errors when generateKeyPair result has errors', async () => {
const messageSigninError = new MessageSigningError('test');
jest
.mocked(appContextService.getMessageSigningService()!.generateKeyPair)
.mockRejectedValue(messageSigninError);
const result = await setupFleet(soClient, esClient);
expect(result).toEqual({
isInitialized: true,
nonFatalErrors: [
{
error: messageSigninError,
},
],
});
});
it('should create and delete lock if not exists', async () => {
const soClient = getMockedSoClient();
soClient.get.mockRejectedValue({ isBoom: true, output: { statusCode: 404 } } as any);
const result = await setupFleet(soClient, esClient, { useLock: true });
@ -144,6 +161,8 @@ describe('setupFleet', () => {
});
it('should return not initialized if lock exists', async () => {
const soClient = getMockedSoClient();
const result = await setupFleet(soClient, esClient, { useLock: true });
expect(result).toEqual({
@ -155,6 +174,8 @@ describe('setupFleet', () => {
});
it('should return not initialized if lock could not be created', async () => {
const soClient = getMockedSoClient();
soClient.get.mockRejectedValue({ isBoom: true, output: { statusCode: 404 } } as any);
soClient.create.mockRejectedValue({ isBoom: true, output: { statusCode: 409 } } as any);
const result = await setupFleet(soClient, esClient, { useLock: true });
@ -167,6 +188,8 @@ describe('setupFleet', () => {
});
it('should delete previous lock if created more than 1 hour ago', async () => {
const soClient = getMockedSoClient();
soClient.get.mockResolvedValue({
attributes: { started_at: new Date(Date.now() - 60 * 60 * 1000 - 1000).toISOString() },
} as any);
@ -180,4 +203,28 @@ describe('setupFleet', () => {
expect(soClient.create).toHaveBeenCalled();
expect(soClient.delete).toHaveBeenCalledTimes(2);
});
it('should return non fatal errors when generateKeyPair result has errors', async () => {
const soClient = getMockedSoClient();
const messageSigningError = new MessageSigningError('test');
mockedAppContextService.getMessageSigningService.mockImplementation(() => ({
generateKeyPair: jest.fn().mockRejectedValueOnce(messageSigningError),
rotateKeyPair: jest.fn(),
isEncryptionAvailable: true,
sign: jest.fn(),
getPublicKey: jest.fn(),
}));
const result = await setupFleet(soClient, esClient);
expect(result).toEqual({
isInitialized: true,
nonFatalErrors: [
{
error: messageSigningError,
},
],
});
});
});

View file

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

View file

@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema';
import semverValid from 'semver/functions/valid';
import { PRECONFIGURATION_LATEST_KEYWORD } from '../../constants';
import type { PreconfiguredOutput } from '../../../common/types';
import { clientAuth, type PreconfiguredOutput } from '../../../common/types';
import {
ElasticSearchSchema,
@ -35,6 +35,13 @@ const varsSchema = schema.maybe(
)
);
const secretRefSchema = schema.oneOf([
schema.object({
id: schema.string(),
}),
schema.string(),
]);
export const PreconfiguredPackagesSchema = schema.arrayOf(
schema.object({
name: schema.string(),
@ -109,6 +116,31 @@ export const PreconfiguredFleetServerHostsSchema = schema.arrayOf(
is_internal: schema.maybe(schema.boolean()),
host_urls: schema.arrayOf(schema.string(), { minSize: 1 }),
proxy_id: schema.nullable(schema.string()),
secrets: schema.maybe(
schema.object({
ssl: schema.maybe(schema.object({ key: schema.maybe(secretRefSchema) })),
})
),
ssl: schema.maybe(
schema.oneOf([
schema.literal(null),
schema.object({
certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
es_certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
es_certificate: schema.maybe(schema.string()),
es_key: schema.maybe(schema.string()),
client_auth: schema.maybe(
schema.oneOf([
schema.literal(clientAuth.Optional),
schema.literal(clientAuth.Required),
schema.literal(clientAuth.None),
])
),
}),
])
),
}),
{ defaultValue: [] }
);

View file

@ -7,7 +7,51 @@
import { schema } from '@kbn/config-schema';
export const FleetServerHostSchema = schema.object({
import { clientAuth } from '../../../common/types';
const secretRefSchema = schema.oneOf([
schema.object({
id: schema.string(),
}),
schema.string(),
]);
export const FleetServerHostBaseSchema = schema.object({
name: schema.maybe(schema.string()),
host_urls: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
is_default: schema.maybe(schema.boolean({ defaultValue: false })),
is_internal: schema.maybe(schema.boolean()),
proxy_id: schema.nullable(schema.string()),
secrets: schema.maybe(
schema.object({
ssl: schema.maybe(
schema.object({ key: schema.maybe(secretRefSchema), es_key: schema.maybe(secretRefSchema) })
),
})
),
ssl: schema.maybe(
schema.oneOf([
schema.literal(null),
schema.object({
certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
certificate: schema.maybe(schema.string()),
key: schema.maybe(schema.string()),
es_certificate_authorities: schema.maybe(schema.arrayOf(schema.string())),
es_certificate: schema.maybe(schema.string()),
es_key: schema.maybe(schema.string()),
client_auth: schema.maybe(
schema.oneOf([
schema.literal(clientAuth.Optional),
schema.literal(clientAuth.Required),
schema.literal(clientAuth.None),
])
),
}),
])
),
});
export const FleetServerHostSchema = FleetServerHostBaseSchema.extends({
id: schema.string(),
name: schema.string(),
host_urls: schema.arrayOf(schema.string(), { minSize: 1 }),
@ -33,13 +77,7 @@ export const GetOneFleetServerHostRequestSchema = {
export const PutFleetServerHostRequestSchema = {
params: schema.object({ itemId: schema.string() }),
body: schema.object({
name: schema.maybe(schema.string()),
host_urls: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
is_default: schema.maybe(schema.boolean({ defaultValue: false })),
is_internal: schema.maybe(schema.boolean()),
proxy_id: schema.nullable(schema.string()),
}),
body: FleetServerHostBaseSchema,
};
export const GetAllFleetServerHostRequestSchema = {};

View file

@ -12,6 +12,7 @@ import { isDiffPathProtocol } from '../../../common/services';
import { OutputSchema } from '../models';
import { FleetProxySchema } from './fleet_proxies';
import { FleetServerHostSchema } from './fleet_server_policy_config';
export const GetSettingsRequestSchema = {};
@ -113,17 +114,7 @@ export const GetEnrollmentSettingsResponseSchema = schema.object({
})
),
has_active: schema.boolean(),
host: schema.maybe(
schema.object({
id: schema.string(),
name: schema.string(),
host_urls: schema.arrayOf(schema.string()),
is_default: schema.boolean(),
is_preconfigured: schema.boolean(),
is_internal: schema.maybe(schema.boolean()),
proxy_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])),
})
),
host: schema.maybe(FleetServerHostSchema),
host_proxy: schema.maybe(FleetProxySchema),
es_output: schema.maybe(OutputSchema),
es_output_proxy: schema.maybe(FleetProxySchema),

View file

@ -111,6 +111,13 @@ export interface FleetServerHostSOAttributes {
is_preconfigured: boolean;
is_internal?: boolean;
proxy_id?: string | null;
secrets?: {
ssl?: {
key?: { id: string };
es_key?: { id: string };
};
};
ssl?: string | null;
}
export interface PackagePolicySOAttributes {

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';
@ -15,9 +16,87 @@ export default function (providerContext: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const fleetAndAgents = getService('fleetAndAgents');
const es = getService('es');
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
}
};
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 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 clearAgents = async () => {
try {
await es.deleteByQuery({
index: '.fleet-agents',
refresh: true,
query: {
match_all: {},
},
});
} catch (err) {
// index doesn't exist
}
};
describe('fleet_fleet_server_hosts_crud', function () {
let defaultFleetServerHostId: string;
let fleetServerPolicyId: string;
skipIfNoDockerRegistry(providerContext);
@ -29,17 +108,16 @@ export default function (providerContext: FtrProviderContext) {
await kibanaServer.savedObjects.clean({
types: ['fleet-fleet-server-host'],
});
const { body: defaultRes } = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
id: 'test-default-123',
name: 'Default',
is_default: true,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
})
.expect(200);
await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
@ -48,8 +126,22 @@ export default function (providerContext: FtrProviderContext) {
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
})
.expect(200);
const { body: apiResponse } = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'kibana')
.send({
name: 'Fleet Server policy 1',
namespace: 'default',
has_fleet_server: true,
})
.expect(200);
const fleetServerPolicy = apiResponse.item;
fleetServerPolicyId = fleetServerPolicy.id;
defaultFleetServerHostId = defaultRes.item.id;
await createFleetServerPolicy(fleetServerPolicyId);
await deleteAllSecrets();
});
after(async () => {
@ -73,7 +165,7 @@ export default function (providerContext: FtrProviderContext) {
expect(fleetServerHost).to.eql({
item: {
id: 'test-default-123',
id: defaultFleetServerHostId,
name: 'Default',
is_default: true,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
@ -87,36 +179,6 @@ export default function (providerContext: FtrProviderContext) {
});
});
describe('PUT /fleet_server_hosts/{itemId}', () => {
it('should allow to update an existing fleet server host', async function () {
await supertest
.put(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Default updated',
})
.expect(200);
const {
body: { item: fleetServerHost },
} = await supertest
.get(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.expect(200);
expect(fleetServerHost.name).to.eql('Default updated');
});
it('should return a 404 when updating a non existing fleet server host', async function () {
await supertest
.put(`/api/fleet/fleet_server_hosts/idonotexists`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
})
.expect(404);
});
});
describe('POST /fleet_server_hosts', () => {
it('should allow to create a default fleet server host with id', async function () {
const id = `test-${Date.now()}`;
@ -139,6 +201,30 @@ export default function (providerContext: FtrProviderContext) {
expect(fleetServerHost.is_default).to.be(true);
});
it('should allow to create a default fleet server host with SSL options', async function () {
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
})
.expect(200);
const {
body: { item: fleetServerHost },
} = await supertest.get(`/api/fleet/fleet_server_hosts/${res.body.item.id}`).expect(200);
expect(fleetServerHost.is_default).to.be(true);
});
it('should not unset default fleet server host on id conflict', async function () {
const id = `test-${Date.now()}`;
@ -170,6 +256,288 @@ export default function (providerContext: FtrProviderContext) {
expect(fleetServerHost.is_default).to.be(true);
});
it('should not allow ssl.key and secrets.ssl.key to be set at the same time', async function () {
const id = `test-${Date.now()}`;
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
id,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
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 allow ssl.es_key and secrets.ssl.es_key to be set at the same time', async function () {
const id = `test-${Date.now()}`;
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
id,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
es_key: 'KEY',
},
secrets: { ssl: { es_key: 'KEY' } },
})
.expect(400);
expect(res.body.message).to.equal('Cannot specify both ssl.es_key and secrets.ssl.es_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 res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
secrets: { ssl: { key: 'KEY1', es_key: 'KEY2' } },
})
.expect(200);
expect(Object.keys(res.body.item)).not.to.contain('secrets');
expect(Object.keys(res.body.item)).to.contain('ssl');
expect(Object.keys(res.body.item.ssl)).to.contain('key');
expect(res.body.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/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
secrets: { ssl: { key: 'KEY1', es_key: 'KEY2' } },
})
.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');
const secretId2 = res.body.item.secrets.ssl.es_key.id;
const secret2 = await getSecretById(secretId2);
// @ts-ignore _source unknown type
expect(secret2._source.value).to.equal('KEY2');
});
});
describe('PUT /fleet_server_hosts/{itemId}', () => {
it('should allow to update an existing fleet server host', async function () {
await supertest
.put(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Default updated',
})
.expect(200);
const {
body: { item: fleetServerHost },
} = await supertest
.get(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.expect(200);
expect(fleetServerHost.name).to.eql('Default updated');
});
it('should allow to update an existing fleet server host with SSL options', async function () {
await supertest
.put(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Default',
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
})
.expect(200);
const {
body: { item: fleetServerHost },
} = await supertest
.get(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`)
.expect(200);
expect(fleetServerHost.ssl.certificate).to.eql('path/to/cert');
expect(fleetServerHost.ssl.certificate_authorities).to.eql(['cert authorities']);
expect(fleetServerHost.ssl.es_certificate).to.eql('path/to/EScert');
expect(fleetServerHost.ssl.es_certificate_authorities).to.eql(['ES cert authorities']);
});
it('should return a 404 when updating a non existing fleet server host', async function () {
await supertest
.put(`/api/fleet/fleet_server_hosts/idonotexists`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new host1',
})
.expect(404);
});
it('should allow secrets to be updated + delete unused secret', async function () {
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
secrets: { ssl: { key: 'KEY1', es_key: 'KEY2' } },
})
.expect(200);
const hostId = 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/fleet_server_hosts/${hostId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
is_default: true,
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
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
}
});
});
describe('DELETE /fleet_server_hosts/{itemId}', () => {
let hostId1: string;
before(async () => {
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Test',
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
})
.expect(200);
hostId1 = res.body.item.id;
});
it('should allow to delete an a fleet server host', async function () {
const { body: deleteResponse } = await supertest
.delete(`/api/fleet/fleet_server_hosts/${hostId1}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(deleteResponse.id).to.eql(hostId1);
});
it('should delete secrets when deleting a fleet server host', async function () {
await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0');
const res = await supertest
.post(`/api/fleet/fleet_server_hosts`)
.set('kbn-xsrf', 'xxxx')
.send({
name: `Default ${Date.now()}`,
host_urls: ['https://test.fr:8080', 'https://test.fr:8081'],
ssl: {
certificate_authorities: ['cert authorities'],
certificate: 'path/to/cert',
es_certificate: 'path/to/EScert',
es_certificate_authorities: ['ES cert authorities'],
},
secrets: { ssl: { es_key: 'KEY2' } },
})
.expect(200);
const hostWithSecretsId = res.body.item.id;
const secretId = res.body.item.secrets.ssl.es_key.id;
await supertest
.delete(`/api/fleet/fleet_server_hosts/${hostWithSecretsId}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
try {
await getSecretById(secretId);
expect().fail('Secret should have been deleted');
} catch (e) {
// not found
}
});
});
});
}