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

Move CSP options to new platform #52698

Merged
merged 13 commits into from
Dec 13, 2019
Merged
3 changes: 1 addition & 2 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,19 @@
/x-pack/plugins/licensing/ @elastic/kibana-platform
/packages/kbn-config-schema/ @elastic/kibana-platform
/src/legacy/server/config/ @elastic/kibana-platform
/src/legacy/server/csp/ @elastic/kibana-platform
/src/legacy/server/http/ @elastic/kibana-platform
/src/legacy/server/i18n/ @elastic/kibana-platform
/src/legacy/server/logging/ @elastic/kibana-platform
/src/legacy/server/saved_objects/ @elastic/kibana-platform
/src/legacy/server/status/ @elastic/kibana-platform

# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform
/x-pack/legacy/plugins/security/ @elastic/kibana-security
/x-pack/legacy/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/src/legacy/server/csp/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security

Expand Down
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-server](./kibana-plugin-server.md) &gt; [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) &gt; [csp](./kibana-plugin-server.httpservicesetup.csp.md)

## HttpServiceSetup.csp property

The CSP config used for Kibana.

<b>Signature:</b>

```typescript
csp: ICspConfig;
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface HttpServiceSetup
| [basePath](./kibana-plugin-server.httpservicesetup.basepath.md) | <code>IBasePath</code> | Access or manipulate the Kibana base path See [IBasePath](./kibana-plugin-server.ibasepath.md)<!-- -->. |
| [createCookieSessionStorageFactory](./kibana-plugin-server.httpservicesetup.createcookiesessionstoragefactory.md) | <code>&lt;T&gt;(cookieOptions: SessionStorageCookieOptions&lt;T&gt;) =&gt; Promise&lt;SessionStorageFactory&lt;T&gt;&gt;</code> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) |
| [createRouter](./kibana-plugin-server.httpservicesetup.createrouter.md) | <code>() =&gt; IRouter</code> | Provides ability to declare a handler function for a particular path and HTTP request method. |
| [csp](./kibana-plugin-server.httpservicesetup.csp.md) | <code>ICspConfig</code> | The CSP config used for Kibana. |
| [isTlsEnabled](./kibana-plugin-server.httpservicesetup.istlsenabled.md) | <code>boolean</code> | Flag showing whether a server was configured to use TLS connection. |
| [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | <code>(handler: AuthenticationHandler) =&gt; void</code> | To define custom authentication and/or authorization mechanism for incoming requests. |
| [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | <code>(handler: OnPostAuthHandler) =&gt; void</code> | To define custom logic to perform for incoming requests. |
Expand Down
13 changes: 13 additions & 0 deletions docs/development/core/server/kibana-plugin-server.icspconfig.md
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-server](./kibana-plugin-server.md) &gt; [ICspConfig](./kibana-plugin-server.icspconfig.md)

## ICspConfig type

CSP configuration for use in Kibana.

<b>Signature:</b>

```typescript
export declare type ICspConfig = Pick<CspConfig, keyof CspConfig>;
```
1 change: 1 addition & 0 deletions docs/development/core/server/kibana-plugin-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) |
| [IClusterClient](./kibana-plugin-server.iclusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>).<!-- -->See [ClusterClient](./kibana-plugin-server.clusterclient.md)<!-- -->. |
| [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [ICspConfig](./kibana-plugin-server.icspconfig.md) | CSP configuration for use in Kibana. |
| [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. |
| [ISavedObjectsRepository](./kibana-plugin-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) |
| [IScopedClusterClient](./kibana-plugin-server.iscopedclusterclient.md) | Serves the same purpose as "normal" <code>ClusterClient</code> but exposes additional <code>callAsCurrentUser</code> method that doesn't use credentials of the Kibana internal user (as <code>callAsInternalUser</code> does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.<!-- -->See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md)<!-- -->. |
Expand Down
39 changes: 39 additions & 0 deletions src/core/server/csp/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { TypeOf, schema } from '@kbn/config-schema';

export type CspConfigType = TypeOf<typeof config.schema>;

export const config = {
// TODO: Move this to server.csp using config deprecations
// ? https://github.com/elastic/kibana/pull/52251
path: 'csp',
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding renaming the csp config key, I will handle this in another PR and make sure we reach consensus between the Platform and Security teams =)

schema: schema.object({
rules: schema.arrayOf(schema.string(), {
defaultValue: [
`script-src 'unsafe-eval' 'self'`,
`worker-src blob: 'self'`,
`style-src 'unsafe-inline' 'self'`,
],
}),
strict: schema.boolean({ defaultValue: true }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to be careful when backporting this, as the default in 7.x is false

kobelb marked this conversation as resolved.
Show resolved Hide resolved
warnLegacyBrowsers: schema.boolean({ defaultValue: true }),
}),
};
97 changes: 97 additions & 0 deletions src/core/server/csp/csp_config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CspConfig } from '.';

// CSP rules aren't strictly additive, so any change can potentially expand or
// restrict the policy in a way we consider a breaking change. For that reason,
// we test the default rules exactly so any change to those rules gets flagged
// for manual review. In other words, this test is intentionally fragile to draw
// extra attention if defaults are modified in any way.
//
// A test failure here does not necessarily mean this change cannot be made,
// but any change here should undergo sufficient scrutiny by the Kibana
// security team.
//
// The tests use inline snapshots to make it as easy as possible to identify
// the nature of a change in defaults during a PR review.

describe('CspConfig', () => {
test('DEFAULT', () => {
expect(CspConfig.DEFAULT).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
"rules": Array [
"script-src 'unsafe-eval' 'self'",
"worker-src blob: 'self'",
"style-src 'unsafe-inline' 'self'",
],
"strict": true,
"warnLegacyBrowsers": true,
}
`);
});

test('defaults from config', () => {
expect(new CspConfig()).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
"rules": Array [
"script-src 'unsafe-eval' 'self'",
"worker-src blob: 'self'",
"style-src 'unsafe-inline' 'self'",
],
"strict": true,
"warnLegacyBrowsers": true,
}
`);
});

test('creates from partial config', () => {
expect(new CspConfig({ strict: false, warnLegacyBrowsers: false })).toMatchInlineSnapshot(`
CspConfig {
"header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'",
"rules": Array [
"script-src 'unsafe-eval' 'self'",
"worker-src blob: 'self'",
"style-src 'unsafe-inline' 'self'",
],
"strict": false,
"warnLegacyBrowsers": false,
}
`);
});

test('computes header from rules', () => {
const cspConfig = new CspConfig({ rules: ['alpha', 'beta', 'gamma'] });

expect(cspConfig).toMatchInlineSnapshot(`
CspConfig {
"header": "alpha; beta; gamma",
"rules": Array [
"alpha",
"beta",
"gamma",
],
"strict": true,
"warnLegacyBrowsers": true,
}
`);
});
});
71 changes: 71 additions & 0 deletions src/core/server/csp/csp_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CspConfigType, config } from './config';

const DEFAULT_CONFIG = Object.freeze(config.schema.validate({}));
eliperelman marked this conversation as resolved.
Show resolved Hide resolved

/**
* CSP configuration for use in Kibana.
* @public
*/
export type ICspConfig = Pick<CspConfig, keyof CspConfig>;

/**
* CSP configuration for use in Kibana.
* @internal
*/
export class CspConfig {
static readonly DEFAULT = new CspConfig();

/**
* The CSP rules used for Kibana.
*/
public readonly rules: string[];

/**
* Specify whether browsers that do not support CSP should be
* able to use Kibana. Use `true` to block and `false` to allow.
*/
public readonly strict: boolean;

/**
* Specify whether users with legacy browsers should be warned
* about their lack of Kibana security compliance.
*/
public readonly warnLegacyBrowsers: boolean;

/**
* The CSP rules in a formatted directives string for use
* in a `Content-Security-Policy` header.
*/
public readonly header: string;

/**
* Returns the default CSP configuration when passed with no config
*/
constructor(rawCspConfig: Partial<CspConfigType> = {}) {
const source = { ...DEFAULT_CONFIG, ...rawCspConfig };

this.rules = source.rules;
this.strict = source.strict;
this.warnLegacyBrowsers = source.warnLegacyBrowsers;
this.header = source.rules.join('; ');
}
}
15 changes: 3 additions & 12 deletions src/legacy/server/csp/index.ts → src/core/server/csp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@
* under the License.
*/

export const DEFAULT_CSP_RULES = Object.freeze([
`script-src 'unsafe-eval' 'self'`,
`worker-src blob: 'self'`,
`style-src 'unsafe-inline' 'self'`,
]);
import { CspConfig, ICspConfig } from './csp_config';
import { CspConfigType, config } from './config';

export const DEFAULT_CSP_STRICT = true;

export const DEFAULT_CSP_WARN_LEGACY_BROWSERS = true;

export function createCSPRuleString(rules: string[]) {
return rules.join('; ');
}
export { CspConfig, CspConfigType, config, ICspConfig };
3 changes: 3 additions & 0 deletions src/core/server/http/http_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ describe('with TLS', () => {
clientAuthentication: 'none',
},
}),
{} as any,
Env.createDefault(getEnvOptions())
);

Expand All @@ -273,6 +274,7 @@ describe('with TLS', () => {
clientAuthentication: 'optional',
},
}),
{} as any,
Env.createDefault(getEnvOptions())
);

Expand All @@ -290,6 +292,7 @@ describe('with TLS', () => {
clientAuthentication: 'required',
},
}),
{} as any,
Env.createDefault(getEnvOptions())
);

Expand Down
29 changes: 16 additions & 13 deletions src/core/server/http/http_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
import { Env } from '../config';
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
import { SslConfig, sslSchema } from './ssl_config';

const validBasePathRegex = /(^$|^\/.*[^\/]$)/;
Expand Down Expand Up @@ -132,23 +133,25 @@ export class HttpConfig {
public defaultRoute?: string;
public ssl: SslConfig;
public compression: { enabled: boolean; referrerWhitelist?: string[] };
public csp: ICspConfig;

/**
* @internal
*/
constructor(rawConfig: HttpConfigType, env: Env) {
this.autoListen = rawConfig.autoListen;
this.host = rawConfig.host;
this.port = rawConfig.port;
this.cors = rawConfig.cors;
this.maxPayload = rawConfig.maxPayload;
this.basePath = rawConfig.basePath;
this.keepaliveTimeout = rawConfig.keepaliveTimeout;
this.socketTimeout = rawConfig.socketTimeout;
this.rewriteBasePath = rawConfig.rewriteBasePath;
constructor(rawHttpConfig: HttpConfigType, rawCspConfig: CspConfigType, env: Env) {
this.autoListen = rawHttpConfig.autoListen;
this.host = rawHttpConfig.host;
this.port = rawHttpConfig.port;
this.cors = rawHttpConfig.cors;
this.maxPayload = rawHttpConfig.maxPayload;
this.basePath = rawHttpConfig.basePath;
this.keepaliveTimeout = rawHttpConfig.keepaliveTimeout;
this.socketTimeout = rawHttpConfig.socketTimeout;
this.rewriteBasePath = rawHttpConfig.rewriteBasePath;
this.publicDir = env.staticFilesDir;
this.ssl = new SslConfig(rawConfig.ssl || {});
this.defaultRoute = rawConfig.defaultRoute;
this.compression = rawConfig.compression;
this.ssl = new SslConfig(rawHttpConfig.ssl || {});
this.defaultRoute = rawHttpConfig.defaultRoute;
this.compression = rawHttpConfig.compression;
this.csp = new CspConfig(rawCspConfig);
}
}
2 changes: 2 additions & 0 deletions src/core/server/http/http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface HttpServerSetup {
*/
registerRouter: (router: IRouter) => void;
basePath: HttpServiceSetup['basePath'];
csp: HttpServiceSetup['csp'];
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
Expand Down Expand Up @@ -109,6 +110,7 @@ export class HttpServer {
this.createCookieSessionStorageFactory(cookieOptions, config.basePath),
registerAuth: this.registerAuth.bind(this),
basePath: basePathService,
csp: config.csp,
auth: {
get: this.authState.get,
isAuthenticated: this.authState.isAuthenticated,
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 @@ -18,6 +18,7 @@
*/

import { Server } from 'hapi';
import { CspConfig } from '../csp';
import { mockRouter } from './router/router.mock';
import { InternalHttpServiceSetup } from './types';
import { HttpService } from './http_service';
Expand Down Expand Up @@ -55,6 +56,7 @@ const createSetupContractMock = () => {
registerOnPreResponse: jest.fn(),
createRouter: jest.fn().mockImplementation(() => mockRouter.create({})),
basePath: createBasePathMock(),
csp: CspConfig.DEFAULT,
auth: {
get: jest.fn(),
isAuthenticated: jest.fn(),
Expand Down
Loading