Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cumulative set of the preboot stage adjustments #108514

Merged
merged 8 commits into from
Aug 23, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
};
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [HttpServicePreboot](./kibana-plugin-core-server.httpservicepreboot.md) &gt; [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;
```
Original file line number Diff line number Diff line change
Expand Up @@ -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>() =&gt; HttpServerInfo</code> | Provides common [information](./kibana-plugin-core-server.httpserverinfo.md) about the running preboot http server. |

## Methods

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/core_app/core_app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ describe('CoreApp', () => {
mockResponseFactory
);

expect(mockResponseFactory.renderAnonymousCoreApp).toHaveBeenCalled();
expect(mockResponseFactory.renderCoreApp).toHaveBeenCalled();
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/core/server/core_app/core_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class CoreApp {
httpResources: corePreboot.httpResources.createRegistrar(router),
router,
uiPlugins,
onResourceNotFound: (res) => res.renderAnonymousCoreApp(),
onResourceNotFound: (res) => res.renderCoreApp(),
});
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/core/server/elasticsearch/client/client_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/core/server/elasticsearch/client/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ElasticsearchClientConfig = Pick<
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
ssl?: Partial<ElasticsearchConfig['ssl']>;
keepAlive?: boolean;
caFingerprint?: ClientOptions['caFingerprint'];
};

/**
Expand Down Expand Up @@ -96,6 +97,10 @@ export function parseClientOptions(
);
}

if (config.caFingerprint != null) {
clientOptions.caFingerprint = config.caFingerprint;
}

azasypkin marked this conversation as resolved.
Show resolved Hide resolved
return clientOptions;
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/server/http/http_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const createInternalPrebootContractMock = () => {
csp: CspConfig.DEFAULT,
externalUrl: ExternalUrlConfig.DEFAULT,
auth: createAuthMock(),
getServerInfo: jest.fn(),
};
return mock;
};
Expand All @@ -98,6 +99,7 @@ const createPrebootContractMock = () => {
const mock: HttpServicePrebootMock = {
registerRoutes: internalMock.registerRoutes,
basePath: createBasePathMock(),
getServerInfo: jest.fn(),
};

return mock;
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/http/http_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand All @@ -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),
});
});

Expand Down
1 change: 1 addition & 0 deletions src/core/server/http/http_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export class HttpService

prebootSetup.registerRouterAfterListening(router);
},
getServerInfo: prebootSetup.getServerInfo,
};

return this.internalPreboot;
Expand Down
6 changes: 6 additions & 0 deletions src/core/server/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -155,6 +160,7 @@ export interface InternalHttpServicePreboot
| 'registerStaticDir'
| 'registerRouteHandlerContext'
| 'server'
| 'getServerInfo'
> {
registerRoutes(path: string, callback: (router: IRouter) => void): void;
}
Expand Down
1 change: 1 addition & 0 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1003,6 +1004,7 @@ export interface HttpServerInfo {
// @public
export interface HttpServicePreboot {
basePath: IBasePath;
getServerInfo: () => HttpServerInfo;
registerRoutes(path: string, callback: (router: IRouter) => void): void;
}

Expand Down
19 changes: 15 additions & 4 deletions src/plugins/interactive_setup/server/elasticsearch_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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',
Expand All @@ -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] },
Expand Down
15 changes: 10 additions & 5 deletions src/plugins/interactive_setup/server/elasticsearch_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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> = {
Expand All @@ -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;
Expand Down Expand Up @@ -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] },
Expand Down
14 changes: 7 additions & 7 deletions src/plugins/interactive_setup/server/routes/enroll.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion src/plugins/interactive_setup/server/routes/enroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down