mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Cumulative set of the preboot stage adjustments (#108514)
This commit is contained in:
parent
8deaa573de
commit
3a0f209bde
17 changed files with 83 additions and 19 deletions
|
@ -14,5 +14,6 @@ export declare type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'custo
|
|||
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
|
||||
ssl?: Partial<ElasticsearchConfig['ssl']>;
|
||||
keepAlive?: boolean;
|
||||
caFingerprint?: ClientOptions['caFingerprint'];
|
||||
};
|
||||
```
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) > [getServerInfo](./kibana-plugin-core-server.httpservicepreboot.getserverinfo.md)
|
||||
|
||||
## HttpServicePreboot.getServerInfo property
|
||||
|
||||
Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running preboot http server.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
```
|
|
@ -73,6 +73,7 @@ httpPreboot.registerRoutes('my-plugin', (router) => {
|
|||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [basePath](./kibana-plugin-core-server.httpservicepreboot.basepath.md) | <code>IBasePath</code> | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-core-server.ibasepath.md)<!-- -->. |
|
||||
| [getServerInfo](./kibana-plugin-core-server.httpservicepreboot.getserverinfo.md) | <code>() => HttpServerInfo</code> | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running preboot http server. |
|
||||
|
||||
## Methods
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ describe('CoreApp', () => {
|
|||
mockResponseFactory
|
||||
);
|
||||
|
||||
expect(mockResponseFactory.renderAnonymousCoreApp).toHaveBeenCalled();
|
||||
expect(mockResponseFactory.renderCoreApp).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ export class CoreApp {
|
|||
httpResources: corePreboot.httpResources.createRegistrar(router),
|
||||
router,
|
||||
uiPlugins,
|
||||
onResourceNotFound: (res) => res.renderAnonymousCoreApp(),
|
||||
onResourceNotFound: (res) => res.renderCoreApp(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -163,6 +163,12 @@ describe('parseClientOptions', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('`caFingerprint` option', () => {
|
||||
const options = parseClientOptions(createConfig({ caFingerprint: 'ab:cd:ef' }), false);
|
||||
|
||||
expect(options.caFingerprint).toBe('ab:cd:ef');
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
|
|
@ -35,6 +35,7 @@ export type ElasticsearchClientConfig = Pick<
|
|||
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
|
||||
ssl?: Partial<ElasticsearchConfig['ssl']>;
|
||||
keepAlive?: boolean;
|
||||
caFingerprint?: ClientOptions['caFingerprint'];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -96,6 +97,10 @@ export function parseClientOptions(
|
|||
);
|
||||
}
|
||||
|
||||
if (config.caFingerprint != null) {
|
||||
clientOptions.caFingerprint = config.caFingerprint;
|
||||
}
|
||||
|
||||
return clientOptions;
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,7 @@ const createInternalPrebootContractMock = () => {
|
|||
csp: CspConfig.DEFAULT,
|
||||
externalUrl: ExternalUrlConfig.DEFAULT,
|
||||
auth: createAuthMock(),
|
||||
getServerInfo: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
@ -98,6 +99,7 @@ const createPrebootContractMock = () => {
|
|||
const mock: HttpServicePrebootMock = {
|
||||
registerRoutes: internalMock.registerRoutes,
|
||||
basePath: createBasePathMock(),
|
||||
getServerInfo: jest.fn(),
|
||||
};
|
||||
|
||||
return mock;
|
||||
|
|
|
@ -379,6 +379,7 @@ test('returns `preboot` http server contract on preboot', async () => {
|
|||
auth: Symbol('auth'),
|
||||
basePath: Symbol('basePath'),
|
||||
csp: Symbol('csp'),
|
||||
getServerInfo: jest.fn(),
|
||||
};
|
||||
|
||||
mockHttpServer.mockImplementation(() => ({
|
||||
|
@ -397,6 +398,7 @@ test('returns `preboot` http server contract on preboot', async () => {
|
|||
registerRouteHandlerContext: expect.any(Function),
|
||||
registerRoutes: expect.any(Function),
|
||||
registerStaticDir: expect.any(Function),
|
||||
getServerInfo: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -128,6 +128,7 @@ export class HttpService
|
|||
|
||||
prebootSetup.registerRouterAfterListening(router);
|
||||
},
|
||||
getServerInfo: prebootSetup.getServerInfo,
|
||||
};
|
||||
|
||||
return this.internalPreboot;
|
||||
|
|
|
@ -142,6 +142,11 @@ export interface HttpServicePreboot {
|
|||
* See {@link IBasePath}.
|
||||
*/
|
||||
basePath: IBasePath;
|
||||
|
||||
/**
|
||||
* Provides common {@link HttpServerInfo | information} about the running preboot http server.
|
||||
*/
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -155,6 +160,7 @@ export interface InternalHttpServicePreboot
|
|||
| 'registerStaticDir'
|
||||
| 'registerRouteHandlerContext'
|
||||
| 'server'
|
||||
| 'getServerInfo'
|
||||
> {
|
||||
registerRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
}
|
||||
|
|
|
@ -115,6 +115,7 @@ export function createPluginPrebootSetupContext(
|
|||
http: {
|
||||
registerRoutes: deps.http.registerRoutes,
|
||||
basePath: deps.http.basePath,
|
||||
getServerInfo: deps.http.getServerInfo,
|
||||
},
|
||||
preboot: {
|
||||
isSetupOnHold: deps.preboot.isSetupOnHold,
|
||||
|
|
|
@ -807,6 +807,7 @@ export type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders
|
|||
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
|
||||
ssl?: Partial<ElasticsearchConfig['ssl']>;
|
||||
keepAlive?: boolean;
|
||||
caFingerprint?: ClientOptions['caFingerprint'];
|
||||
};
|
||||
|
||||
// @public
|
||||
|
@ -1003,6 +1004,7 @@ export interface HttpServerInfo {
|
|||
// @public
|
||||
export interface HttpServicePreboot {
|
||||
basePath: IBasePath;
|
||||
getServerInfo: () => HttpServerInfo;
|
||||
registerRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -308,7 +308,7 @@ describe('ElasticsearchService', () => {
|
|||
mockEnrollClient.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
|
||||
await expect(
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'] })
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'], caFingerprint: 'DE:AD:BE:EF' })
|
||||
).rejects.toMatchInlineSnapshot(`[ResponseError: {"message":"oh no"}]`);
|
||||
|
||||
expect(mockEnrollClient.asScoped).toHaveBeenCalledTimes(1);
|
||||
|
@ -327,7 +327,11 @@ describe('ElasticsearchService', () => {
|
|||
mockEnrollClient.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
|
||||
await expect(
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1', 'host2'] })
|
||||
setupContract.enroll({
|
||||
apiKey: 'apiKey',
|
||||
hosts: ['host1', 'host2'],
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(`[Error: Unable to connect to any of the provided hosts.]`);
|
||||
|
||||
expect(mockEnrollClient.close).toHaveBeenCalledTimes(2);
|
||||
|
@ -351,7 +355,7 @@ describe('ElasticsearchService', () => {
|
|||
);
|
||||
|
||||
await expect(
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'] })
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'], caFingerprint: 'DE:AD:BE:EF' })
|
||||
).rejects.toMatchInlineSnapshot(`[ResponseError: {"message":"oh no"}]`);
|
||||
|
||||
expect(mockEnrollClient.asScoped).toHaveBeenCalledTimes(1);
|
||||
|
@ -404,7 +408,11 @@ some weird+ca/with
|
|||
`;
|
||||
|
||||
await expect(
|
||||
setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1', 'host2'] })
|
||||
setupContract.enroll({
|
||||
apiKey: 'apiKey',
|
||||
hosts: ['host1', 'host2'],
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
})
|
||||
).resolves.toEqual({
|
||||
ca: expectedCa,
|
||||
host: 'host2',
|
||||
|
@ -417,14 +425,17 @@ some weird+ca/with
|
|||
// Check that we created clients with the right parameters
|
||||
expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledTimes(3);
|
||||
expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('enroll', {
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
hosts: ['host1'],
|
||||
ssl: { verificationMode: 'none' },
|
||||
});
|
||||
expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('enroll', {
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
hosts: ['host2'],
|
||||
ssl: { verificationMode: 'none' },
|
||||
});
|
||||
expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('authenticate', {
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
hosts: ['host2'],
|
||||
serviceAccountToken: 'some-value',
|
||||
ssl: { certificateAuthorities: [expectedCa] },
|
||||
|
|
|
@ -34,9 +34,7 @@ import { getDetailedErrorMessage } from './errors';
|
|||
interface EnrollParameters {
|
||||
apiKey: string;
|
||||
hosts: string[];
|
||||
// TODO: Integrate fingerprint check as soon core supports this new option:
|
||||
// https://github.com/elastic/kibana/pull/108514
|
||||
caFingerprint?: string;
|
||||
caFingerprint: string;
|
||||
}
|
||||
|
||||
export interface ElasticsearchServiceSetupDeps {
|
||||
|
@ -141,10 +139,12 @@ export class ElasticsearchService {
|
|||
* @param apiKey The ApiKey to use to authenticate Kibana enrollment request.
|
||||
* @param hosts The list of Elasticsearch node addresses to enroll with. The addresses are supposed
|
||||
* to point to exactly same Elasticsearch node, potentially available via different network interfaces.
|
||||
* @param caFingerprint The fingerprint of the root CA certificate that is supposed to sign certificate presented by
|
||||
* the Elasticsearch node we're enrolling with. Should be in a form of a hex colon-delimited string in upper case.
|
||||
*/
|
||||
private async enroll(
|
||||
elasticsearch: ElasticsearchServicePreboot,
|
||||
{ apiKey, hosts }: EnrollParameters
|
||||
{ apiKey, hosts, caFingerprint }: EnrollParameters
|
||||
): Promise<EnrollResult> {
|
||||
const scopeableRequest: ScopeableRequest = { headers: { authorization: `ApiKey ${apiKey}` } };
|
||||
const elasticsearchConfig: Partial<ElasticsearchClientConfig> = {
|
||||
|
@ -153,10 +153,14 @@ export class ElasticsearchService {
|
|||
|
||||
// We should iterate through all provided hosts until we find an accessible one.
|
||||
for (const host of hosts) {
|
||||
this.logger.debug(`Trying to enroll with "${host}" host`);
|
||||
this.logger.debug(
|
||||
`Trying to enroll with "${host}" host using "${caFingerprint}" CA fingerprint.`
|
||||
);
|
||||
|
||||
const enrollClient = elasticsearch.createClient('enroll', {
|
||||
...elasticsearchConfig,
|
||||
hosts: [host],
|
||||
caFingerprint,
|
||||
});
|
||||
|
||||
let enrollmentResponse;
|
||||
|
@ -197,6 +201,7 @@ export class ElasticsearchService {
|
|||
|
||||
// Now try to use retrieved password and CA certificate to authenticate to this host.
|
||||
const authenticateClient = elasticsearch.createClient('authenticate', {
|
||||
caFingerprint,
|
||||
hosts: [host],
|
||||
serviceAccountToken: enrollResult.serviceAccountToken.value,
|
||||
ssl: { certificateAuthorities: [enrollResult.ca] },
|
||||
|
|
|
@ -113,7 +113,7 @@ describe('Enroll routes', () => {
|
|||
mockRouteParams.preboot.isSetupOnHold.mockReturnValue(false);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -134,7 +134,7 @@ describe('Enroll routes', () => {
|
|||
);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -164,7 +164,7 @@ describe('Enroll routes', () => {
|
|||
mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(false);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -203,7 +203,7 @@ describe('Enroll routes', () => {
|
|||
);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -236,7 +236,7 @@ describe('Enroll routes', () => {
|
|||
);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -273,7 +273,7 @@ describe('Enroll routes', () => {
|
|||
mockRouteParams.kibanaConfigWriter.writeConfig.mockResolvedValue();
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest({
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' },
|
||||
body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'deadbeef' },
|
||||
});
|
||||
|
||||
await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({
|
||||
|
@ -286,7 +286,7 @@ describe('Enroll routes', () => {
|
|||
expect(mockRouteParams.elasticsearch.enroll).toHaveBeenCalledWith({
|
||||
apiKey: 'some-key',
|
||||
hosts: ['host1', 'host2'],
|
||||
caFingerprint: 'ab:cd:ef',
|
||||
caFingerprint: 'DE:AD:BE:EF',
|
||||
});
|
||||
|
||||
expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledTimes(1);
|
||||
|
|
|
@ -73,12 +73,20 @@ export function defineEnrollRoutes({
|
|||
});
|
||||
}
|
||||
|
||||
// Convert a plain hex string returned in the enrollment token to a format that ES client
|
||||
// expects, i.e. to a colon delimited hex string in upper case: deadbeef -> DE:AD:BE:EF.
|
||||
const colonFormattedCaFingerprint =
|
||||
request.body.caFingerprint
|
||||
.toUpperCase()
|
||||
.match(/.{1,2}/g)
|
||||
?.join(':') ?? '';
|
||||
|
||||
let enrollResult: EnrollResult;
|
||||
try {
|
||||
enrollResult = await elasticsearch.enroll({
|
||||
apiKey: request.body.apiKey,
|
||||
hosts: request.body.hosts,
|
||||
caFingerprint: request.body.caFingerprint,
|
||||
caFingerprint: colonFormattedCaFingerprint,
|
||||
});
|
||||
} catch {
|
||||
// For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue