mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[7.x] Add TLS client authentication support. (#43317)
This commit is contained in:
parent
c63218ce5d
commit
d106a4b3a9
12 changed files with 289 additions and 7 deletions
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md)
|
||||
|
||||
## IKibanaSocket.authorizationError property
|
||||
|
||||
The reason why the peer's certificate has not been verified. This property becomes available only when `authorized` is `false`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly authorizationError?: Error;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) > [authorized](./kibana-plugin-server.ikibanasocket.authorized.md)
|
||||
|
||||
## IKibanaSocket.authorized property
|
||||
|
||||
Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is `undefined`<!-- -->.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly authorized?: boolean;
|
||||
```
|
|
@ -12,6 +12,13 @@ A tiny abstraction for TCP socket.
|
|||
export interface IKibanaSocket
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [authorizationError](./kibana-plugin-server.ikibanasocket.authorizationerror.md) | <code>Error</code> | The reason why the peer's certificate has not been verified. This property becomes available only when <code>authorized</code> is <code>false</code>. |
|
||||
| [authorized](./kibana-plugin-server.ikibanasocket.authorized.md) | <code>boolean</code> | Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS isn't used the value is <code>undefined</code>. |
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Description |
|
||||
|
|
|
@ -296,6 +296,10 @@ files that should be trusted.
|
|||
Details on the format, and the valid options, are available via the
|
||||
https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation].
|
||||
|
||||
`server.ssl.clientAuthentication:`:: *Default: none* Controls the server’s behavior in regard to requesting a certificate from client
|
||||
connections. Valid values are `required`, `optional`, and `none`. `required` forces a client to present a certificate, while `optional`
|
||||
requests a client certificate but the client is not required to present one.
|
||||
|
||||
`server.ssl.enabled:`:: *Default: "false"* Enables SSL for outgoing requests
|
||||
from the Kibana server to the browser. When set to `true`,
|
||||
`server.ssl.certificate` and `server.ssl.key` are required.
|
||||
|
|
|
@ -45,6 +45,7 @@ Object {
|
|||
"!SRP",
|
||||
"!CAMELLIA",
|
||||
],
|
||||
"clientAuthentication": "none",
|
||||
"enabled": false,
|
||||
"supportedProtocols": Array [
|
||||
"TLSv1.1",
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { config } from '.';
|
||||
import { config, HttpConfig } from '.';
|
||||
import { Env } from '../config';
|
||||
import { getEnvOptions } from '../config/__mocks__/env';
|
||||
|
||||
test('has defaults for config', () => {
|
||||
const httpSchema = config.schema;
|
||||
|
@ -111,6 +113,46 @@ describe('with TLS', () => {
|
|||
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('throws if TLS is not enabled but `clientAuthentication` is `optional`', () => {
|
||||
const httpSchema = config.schema;
|
||||
const obj = {
|
||||
port: 1234,
|
||||
ssl: {
|
||||
enabled: false,
|
||||
clientAuthentication: 'optional',
|
||||
},
|
||||
};
|
||||
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[ssl]: must enable ssl to use [clientAuthentication]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if TLS is not enabled but `clientAuthentication` is `required`', () => {
|
||||
const httpSchema = config.schema;
|
||||
const obj = {
|
||||
port: 1234,
|
||||
ssl: {
|
||||
enabled: false,
|
||||
clientAuthentication: 'required',
|
||||
},
|
||||
};
|
||||
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[ssl]: must enable ssl to use [clientAuthentication]"`
|
||||
);
|
||||
});
|
||||
|
||||
test('can specify `none` for [clientAuthentication] if ssl is not enabled', () => {
|
||||
const obj = {
|
||||
ssl: {
|
||||
enabled: false,
|
||||
clientAuthentication: 'none',
|
||||
},
|
||||
};
|
||||
|
||||
const configValue = config.schema.validate(obj);
|
||||
expect(configValue.ssl.clientAuthentication).toBe('none');
|
||||
});
|
||||
|
||||
test('can specify single `certificateAuthority` as a string', () => {
|
||||
const obj = {
|
||||
ssl: {
|
||||
|
@ -202,4 +244,55 @@ describe('with TLS', () => {
|
|||
httpSchema.validate(allKnownWithOneUnknownProtocols)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('HttpConfig instance should properly interpret `none` client authentication', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
clientAuthentication: 'none',
|
||||
},
|
||||
}),
|
||||
Env.createDefault(getEnvOptions())
|
||||
);
|
||||
|
||||
expect(httpConfig.ssl.requestCert).toBe(false);
|
||||
expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
|
||||
});
|
||||
|
||||
test('HttpConfig instance should properly interpret `optional` client authentication', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
clientAuthentication: 'optional',
|
||||
},
|
||||
}),
|
||||
Env.createDefault(getEnvOptions())
|
||||
);
|
||||
|
||||
expect(httpConfig.ssl.requestCert).toBe(true);
|
||||
expect(httpConfig.ssl.rejectUnauthorized).toBe(false);
|
||||
});
|
||||
|
||||
test('HttpConfig instance should properly interpret `required` client authentication', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
clientAuthentication: 'required',
|
||||
},
|
||||
}),
|
||||
Env.createDefault(getEnvOptions())
|
||||
);
|
||||
|
||||
expect(httpConfig.ssl.requestCert).toBe(true);
|
||||
expect(httpConfig.ssl.rejectUnauthorized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
readFileSync: jest.fn(),
|
||||
}));
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import Joi from 'joi';
|
||||
|
||||
import { defaultValidationErrorHandler, HapiValidationError } from './http_tools';
|
||||
import { defaultValidationErrorHandler, HapiValidationError, getServerOptions } from './http_tools';
|
||||
import { HttpServer } from './http_server';
|
||||
import { HttpConfig } from './http_config';
|
||||
import { HttpConfig, config } from './http_config';
|
||||
import { Router } from './router';
|
||||
import { loggingServiceMock } from '../logging/logging_service.mock';
|
||||
import { ByteSizeValue } from '@kbn/config-schema';
|
||||
import { Env } from '../config';
|
||||
import { getEnvOptions } from '../config/__mocks__/env';
|
||||
|
||||
const emptyOutput = {
|
||||
statusCode: 400,
|
||||
|
@ -41,6 +47,8 @@ const emptyOutput = {
|
|||
},
|
||||
};
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
describe('defaultValidationErrorHandler', () => {
|
||||
it('formats value validation errors correctly', () => {
|
||||
expect.assertions(1);
|
||||
|
@ -97,3 +105,68 @@ describe('timeouts', () => {
|
|||
await server.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getServerOptions', () => {
|
||||
beforeEach(() =>
|
||||
jest.requireMock('fs').readFileSync.mockImplementation((path: string) => `content-${path}`)
|
||||
);
|
||||
|
||||
it('properly configures TLS with default options', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
},
|
||||
}),
|
||||
Env.createDefault(getEnvOptions())
|
||||
);
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": undefined,
|
||||
"cert": "content-some-certificate-path",
|
||||
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
"honorCipherOrder": true,
|
||||
"key": "content-some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": false,
|
||||
"requestCert": false,
|
||||
"secureOptions": 67108864,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('properly configures TLS with client authentication', () => {
|
||||
const httpConfig = new HttpConfig(
|
||||
config.schema.validate({
|
||||
ssl: {
|
||||
enabled: true,
|
||||
key: 'some-key-path',
|
||||
certificate: 'some-certificate-path',
|
||||
certificateAuthorities: ['ca-1', 'ca-2'],
|
||||
clientAuthentication: 'required',
|
||||
},
|
||||
}),
|
||||
Env.createDefault(getEnvOptions())
|
||||
);
|
||||
|
||||
expect(getServerOptions(httpConfig).tls).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"ca": Array [
|
||||
"content-ca-1",
|
||||
"content-ca-2",
|
||||
],
|
||||
"cert": "content-some-certificate-path",
|
||||
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
"honorCipherOrder": true,
|
||||
"key": "content-some-key-path",
|
||||
"passphrase": undefined,
|
||||
"rejectUnauthorized": true,
|
||||
"requestCert": true,
|
||||
"secureOptions": 67108864,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -71,6 +71,7 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = {
|
|||
passphrase: ssl.keyPassphrase,
|
||||
secureOptions: ssl.getSecureOptions(),
|
||||
requestCert: ssl.requestCert,
|
||||
rejectUnauthorized: ssl.rejectUnauthorized,
|
||||
};
|
||||
|
||||
options.tls = tlsOptions;
|
||||
|
|
|
@ -56,4 +56,50 @@ describe('KibanaSocket', () => {
|
|||
expect(socket.getPeerCertificate()).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorized', () => {
|
||||
it('returns `undefined` for net.Socket instance', () => {
|
||||
const socket = new KibanaSocket(new Socket());
|
||||
|
||||
expect(socket.authorized).toBeUndefined();
|
||||
});
|
||||
|
||||
it('mirrors the value of tls.Socket.authorized', () => {
|
||||
const tlsSocket = new TLSSocket(new Socket());
|
||||
|
||||
tlsSocket.authorized = true;
|
||||
let socket = new KibanaSocket(tlsSocket);
|
||||
expect(tlsSocket.authorized).toBe(true);
|
||||
expect(socket.authorized).toBe(true);
|
||||
|
||||
tlsSocket.authorized = false;
|
||||
socket = new KibanaSocket(tlsSocket);
|
||||
expect(tlsSocket.authorized).toBe(false);
|
||||
expect(socket.authorized).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorizationError', () => {
|
||||
it('returns `undefined` for net.Socket instance', () => {
|
||||
const socket = new KibanaSocket(new Socket());
|
||||
|
||||
expect(socket.authorizationError).toBeUndefined();
|
||||
});
|
||||
|
||||
it('mirrors the value of tls.Socket.authorizationError', () => {
|
||||
const tlsSocket = new TLSSocket(new Socket());
|
||||
tlsSocket.authorizationError = undefined as any;
|
||||
|
||||
let socket = new KibanaSocket(tlsSocket);
|
||||
expect(tlsSocket.authorizationError).toBeUndefined();
|
||||
expect(socket.authorizationError).toBeUndefined();
|
||||
|
||||
const authorizationError = new Error('some error');
|
||||
tlsSocket.authorizationError = authorizationError;
|
||||
socket = new KibanaSocket(tlsSocket);
|
||||
|
||||
expect(tlsSocket.authorizationError).toBe(authorizationError);
|
||||
expect(socket.authorizationError).toBe(authorizationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,10 +37,30 @@ export interface IKibanaSocket {
|
|||
* @returns An object representing the peer's certificate.
|
||||
*/
|
||||
getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS
|
||||
* isn't used the value is `undefined`.
|
||||
*/
|
||||
readonly authorized?: boolean;
|
||||
|
||||
/**
|
||||
* The reason why the peer's certificate has not been verified. This property becomes available
|
||||
* only when `authorized` is `false`.
|
||||
*/
|
||||
readonly authorizationError?: Error;
|
||||
}
|
||||
|
||||
export class KibanaSocket implements IKibanaSocket {
|
||||
constructor(private readonly socket: Socket) {}
|
||||
readonly authorized?: boolean;
|
||||
readonly authorizationError?: Error;
|
||||
|
||||
constructor(private readonly socket: Socket) {
|
||||
if (this.socket instanceof TLSSocket) {
|
||||
this.authorized = this.socket.authorized;
|
||||
this.authorizationError = this.socket.authorizationError;
|
||||
}
|
||||
}
|
||||
|
||||
getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
|
||||
getPeerCertificate(detailed: false): PeerCertificate | null;
|
||||
|
|
|
@ -49,13 +49,20 @@ export const sslSchema = schema.object(
|
|||
schema.oneOf([schema.literal('TLSv1'), schema.literal('TLSv1.1'), schema.literal('TLSv1.2')]),
|
||||
{ defaultValue: ['TLSv1.1', 'TLSv1.2'], minSize: 1 }
|
||||
),
|
||||
requestCert: schema.maybe(schema.boolean({ defaultValue: false })),
|
||||
clientAuthentication: schema.oneOf(
|
||||
[schema.literal('none'), schema.literal('optional'), schema.literal('required')],
|
||||
{ defaultValue: 'none' }
|
||||
),
|
||||
},
|
||||
{
|
||||
validate: ssl => {
|
||||
if (ssl.enabled && (!ssl.key || !ssl.certificate)) {
|
||||
return 'must specify [certificate] and [key] when ssl is enabled';
|
||||
}
|
||||
|
||||
if (!ssl.enabled && ssl.clientAuthentication !== 'none') {
|
||||
return 'must enable ssl to use [clientAuthentication]';
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -69,7 +76,8 @@ export class SslConfig {
|
|||
public certificate: string | undefined;
|
||||
public certificateAuthorities: string[] | undefined;
|
||||
public keyPassphrase: string | undefined;
|
||||
public requestCert: boolean | undefined;
|
||||
public requestCert: boolean;
|
||||
public rejectUnauthorized: boolean;
|
||||
|
||||
public cipherSuites: string[];
|
||||
public supportedProtocols: string[];
|
||||
|
@ -86,7 +94,8 @@ export class SslConfig {
|
|||
this.keyPassphrase = config.keyPassphrase;
|
||||
this.cipherSuites = config.cipherSuites;
|
||||
this.supportedProtocols = config.supportedProtocols;
|
||||
this.requestCert = config.requestCert;
|
||||
this.requestCert = config.clientAuthentication !== 'none';
|
||||
this.rejectUnauthorized = config.clientAuthentication === 'required';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -261,6 +261,8 @@ export type IContextProvider<TContext extends Record<string, any>, TContextName
|
|||
|
||||
// @public
|
||||
export interface IKibanaSocket {
|
||||
readonly authorizationError?: Error;
|
||||
readonly authorized?: boolean;
|
||||
// (undocumented)
|
||||
getPeerCertificate(detailed: true): DetailedPeerCertificate | null;
|
||||
// (undocumented)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue