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

Add config properties for HTTP security headers #97158

Merged
merged 15 commits into from
Apr 19, 2021

Conversation

jportner
Copy link
Contributor

@jportner jportner commented Apr 14, 2021

Resolves #52809 and #97153.

Overview

This PR adds the following config properties (each prefixed by server.securityResponseHeaders):

Config property Default value Notes
.strictTransportSecurity null (Header is not enabled) This controls the Strict-Transport-Security header, which is an excellent feature to support on your site and strengthens your implementation of TLS by getting the User Agent to enforce the use of HTTPS.
Can be set to null to disable this header.
.xContentTypeOptions 'nosniff' (Header is enabled) This controls the X-Content-Type-Options header, which stops a browser from trying to MIME-sniff the content type and forces it to stick with the declared content-type. The only valid value for this header is "X-Content-Type-Options: nosniff".
Can be set to null to disable this header.
.referrerPolicy 'no-referrer-when-downgrade' (Header is enabled) This controls the Referrer-Policy header, which is a new header that allows a site to control how much information the browser includes with navigations away from a document and should be set by all sites.
Can be set to null to disable this header.
.permissionsPolicy null (Header is not enabled) This controls the Permissions-Policy header, which is a new header that allows a site to control which features and APIs can be used in the browser.
Can be set to null to disable this header.
.disableEmbedding false (Header is not enabled, embedding is allowed) This influences the default Content-Security-Policy header and controls the X-Frame-Options header, which tells the browser whether you want to allow your site to be framed or not. By preventing a browser from framing your site you can defend against attacks like clickjacking.

New scan result from securityheaders.com with default settings ("C" rating):

image

Docs

Docs preview: link (diff)

Note concerning the docs: the asciidoc hacks that I needed to make for the settings table in bb8e2aa are kind of awful, but it seems to be the only solution that will work. The key names for the new config options are too long, and they stretch the left column of the table out to the maximum size as a result. Regular columns in AsciiDoc will not preserve leading whitespace characters, so I had to utilize an AsciiDoc block element column style with the a operator.

Screenshots before and after bb8e2aa

Before:

image

After:

image

@jportner jportner force-pushed the add-security-headers-config branch 3 times, most recently from a9feda4 to 0d90a4f Compare April 15, 2021 00:49
This was recently renamed to `server.maxPayload`, I just updated the
docs and other references accordingly. Unrelated to the other work in
this PR besides the fact that I am mucking around in the
`src/core/server/http/http_config.ts` file.
@jportner jportner force-pushed the add-security-headers-config branch from 0d90a4f to 62864b1 Compare April 15, 2021 01:18
@jportner jportner force-pushed the add-security-headers-config branch from 62864b1 to 5745057 Compare April 15, 2021 02:43
Copy link
Contributor Author

@jportner jportner left a comment

Choose a reason for hiding this comment

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

Author's notes for reviewers

Comment on lines 68 to 72
const additionalHeaders = {
...customHeaders,
...securityResponseHeaders,
...customResponseHeaders,
[KIBANA_NAME_HEADER]: serverName,
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We explicitly allow customResponseHeaders to override any values in securityResponseHeaders. This is so the new config properties do not break existing configurations that already set the same headers using customResponseHeaders.

We could detect this situation when the config is set and throw an error, but we'd have to make that change in the 8..0 release, I think.

Comment on lines 11 to 16
const strictTransportSecuritySchema = schema.object({
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
maxAge: schema.duration({ defaultValue: '1Y' }),
includeSubDomains: schema.boolean({ defaultValue: false }),
preload: schema.boolean({ defaultValue: false }),
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By default, Strict-Transport-Security is enabled with a max age of 1 year. This is recommended by MDN and most other authoritative sources. We could decrease this but I'm not sure we should.

I omitted includeSubDomains by default because of #52809 (comment).

Note: if Kibana is served over HTTP, this header is still sent, but browsers ignore it. It only affects browsers once they see this header in an HTTPS response.

Comment on lines +22 to +25
xContentTypeOptions: schema.oneOf([schema.literal('nosniff'), schema.literal(null)], {
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
defaultValue: 'nosniff',
}),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By default, X-Content-Type-Options is enabled. It only has one valid value. This shouldn't have any negative impact on users.

Comment on lines +26 to +40
referrerPolicy: schema.oneOf(
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
[
schema.literal('no-referrer'),
schema.literal('no-referrer-when-downgrade'),
schema.literal('origin'),
schema.literal('origin-when-cross-origin'),
schema.literal('same-origin'),
schema.literal('strict-origin'),
schema.literal('strict-origin-when-cross-origin'),
schema.literal('unsafe-url'),
schema.literal(null),
],
{ defaultValue: 'no-referrer-when-downgrade' }
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By default, Referrer-Policy is enabled with a value of "no-referrer-when-downgrade". This is not very strict, it does the bare minimum to prevent the Referer header from being sent to unsafe (HTTP) destinations when Kibana is served over HTTPS. See also #52809 (comment)

Comment on lines 41 to 43
permissionsPolicy: schema.oneOf([schema.string(), schema.literal(null)], {
defaultValue: 'camera=(), microphone=()', // disables camera and microphone access in Kibana and any embedded pages
}),
Copy link
Contributor Author

@jportner jportner Apr 15, 2021

Choose a reason for hiding this comment

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

By default, Permissions-Policy is enabled with a value of "camera=(), microphone=()". There is not a lot of authoritative info on this header yet (the header was previously Feature-Policy but has been slightly reworked and renamed). I figured we can set it with some safe feature policies as a starting point.

Note that Permissions-Policy is subtractive, so if we do not specify a directive, that is not affected. Kibana shouldn't ever need access to a user's camera or microphone, so what we are saying here is that Kibana (and any embedded pages) are not allowed to access these features.

See also: https://github.com/w3c/webappsec-permissions-policy/blob/main/permissions-policy-explainer.md

Edit: I believe another, more clear way of writing this permissions policy is: 'camera=(none), microphone=(none)'. While the W3C draft mentions the none keyword, the explainer document above does not.

Copy link
Contributor

Choose a reason for hiding this comment

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

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy#example leads me to believe that we should be using Permissions-Policy: camera 'none'; microphone 'none'. It also looks like this isn't very widely supported per https://caniuse.com/permissions-policy. Perhaps we should just hold off on this one until it matures? Does the lack of this header get flagged by scanners right now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The page is a bit misleading / outdated. That's the format for the old Feature-Policy header, it hasn't been updated to the Permissions-Policy format.

At any rate based on #97158 (comment) below, I think we will leave this header disabled by default and mark the config prop as experimental, if that sounds good to you.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good to me! Thanks.

permissionsPolicy: schema.oneOf([schema.string(), schema.literal(null)], {
defaultValue: 'camera=(), microphone=()', // disables camera and microphone access in Kibana and any embedded pages
}),
disableEmbedding: schema.boolean({ defaultValue: false }), // is used to control X-Frame-Options and CSP headers
Copy link
Contributor Author

Choose a reason for hiding this comment

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

When enabled, this changes the default Content-Security-Policy header (by adding the frame-ancestors directive) and adds the X-Frame-Options header.

Even though it appears that all of Kibana's target browsers support frame-ancestors, we still include X-Frame-Options for a couple of reasons:

  1. If the user has customized the Content-Security-Policy for some reason, we won't add the frame-ancestors directive -- so including X-Frame-Options as a backup is a good idea
  2. Naive security scanning tools can see a missing X-Frame-Options as a problem, even if there is a Content-Security-Policy present with a frame-ancestors directive

In the future (8.0 release?) I would love to change this to be enabled by default.

Copy link
Member

Choose a reason for hiding this comment

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

I came looking for answers to these questions - thanks for taking the time to explain!

@@ -116,12 +116,20 @@ kibana_vars=(
server.compression.referrerWhitelist
server.cors
server.cors.origin
server.customResponseHeaders
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a duplicate value so I removed it (it still exists below on line 127)

Comment on lines 131 to +132
server.maxPayloadBytes
server.maxPayload
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added server.maxPayload but left the deprecated server.maxPayloadBytes for now (see 8cc60bf)

Comment on lines +75 to +77
if (!rawCspConfig.rules?.length && source.disableEmbedding) {
this.rules.push(FRAME_ANCESTORS_RULE);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might be a good idea to log a warning if disableEmbedding is enabled but custom CSP rules are used that do not include the frame-ancestors directive. But I think we can leave that for a future enhancement, as we don't have the ability to log when config is ingested at the moment.

@jportner jportner marked this pull request as ready for review April 15, 2021 14:52
@jportner jportner requested review from a team as code owners April 15, 2021 14:52
Copy link
Member

@jbudz jbudz left a comment

Choose a reason for hiding this comment

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

kibana-docker LGTM!

@jportner jportner requested a review from a team April 15, 2021 14:58
Copy link
Contributor

@KOTungseth KOTungseth left a comment

Choose a reason for hiding this comment

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

I have a handful of nits, but otherwise, LGTM!

docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
Copy link
Contributor

@thomheymann thomheymann left a comment

Choose a reason for hiding this comment

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

Looks great! Tested locally and can confirm that Kibana responds with correct security headers.

Couple minor comments below.

docs/setup/settings.asciidoc Outdated Show resolved Hide resolved
a|
----
server.securityResponseHeaders:
strictTransportSecurity.preload:
Copy link
Contributor

Choose a reason for hiding this comment

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

@elastic/kibana-docs What's the best way of printing long settings like this? The table is too narrow to fit both the name and description. However, breaking it up like this makes it a bit confusing for users too.

Copy link
Contributor

Choose a reason for hiding this comment

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

@thomheymann the table capabilities in Asciidoc are limited, and I agree that breaking up the setting names is confusing. These would be best displayed in a definition list instead of a table. I propose that we merge these changes, then I'll fix the formatting before 7.13 releases.

Previously, this property contained three sub-properties that each
controlled individual directives in the header. This is really more
complexity than is necessary, we can simply allow users to enter in free
text for this header.
permissionsPolicy: schema.oneOf([schema.string(), schema.literal(null)], {
defaultValue: 'camera=(), microphone=()', // disables camera and microphone access in Kibana and any embedded pages
}),
disableEmbedding: schema.boolean({ defaultValue: false }), // is used to control X-Frame-Options and CSP headers
Copy link
Member

Choose a reason for hiding this comment

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

I came looking for answers to these questions - thanks for taking the time to explain!

],
{ defaultValue: 'no-referrer-when-downgrade' }
),
permissionsPolicy: schema.oneOf([schema.string(), schema.literal(null)], {
Copy link
Member

Choose a reason for hiding this comment

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

I'm wary of supporting production deployments of an experimental web technology, with very mixed browser support:

image


I'd be more comfortable if we omitted this option for the time being. Users who want to control this have a way to do so via custom response headers, and they can do so at their own risk.

If we have a compelling reason to keep this (aside from the score card), then I'd like to see this setting marked as experimental so that we aren't committing to support of this specific header.

I briefly looked around to see if there were high profile sites using this header, and I haven't found any usages of it yet. I know they exist somewhere, I just haven't found them 😉 . I was trying to get an idea of how comfortable other companies were using this experimental header in production.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I was on the fence about this as well. I just wanted to do as much as possible to get ahead of scanning tools, but I think the only one that checks for this atm is securityheaders.com, and I think if you don't use this header it just caps your score at "A" (instead of "A+").

Perhaps we can leave the config property in, but change it to null/disabled by default?

Copy link
Member

Choose a reason for hiding this comment

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

I'd still like to mark the property as experimental -- we don't know what the future holds for deprecating/removing features after the 8.0 release, so this would give us some more flexibility

Copy link
Member

@legrego legrego left a comment

Choose a reason for hiding this comment

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

One followup thought -- we should update core telemetry to indicate if these settings have been altered. It'd be great to track this over time in case we need data about future changes we want to make.

export const securityResponseHeadersSchema = schema.object({
strictTransportSecurity: schema.oneOf([schema.string(), schema.literal(null)], {
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
defaultValue: 'max-age=31536000', // 1 year in seconds
Copy link
Contributor

Choose a reason for hiding this comment

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

How does Strict-Transport-Security: max-age=31536000 affect Kibana when it's hosted on a base-path? For example, if Kibana is hosted at https://foo.com/kibana/ will it apply to websites at https://foo.com/bar? If it's in effect for the entire domain, at a minimum, we won't want to set this in 7.13.

Copy link
Contributor Author

@jportner jportner Apr 16, 2021

Choose a reason for hiding this comment

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

http://gph.is/1lX4Z3B

I'll make an issue to change the defaults for strictTransportSecurity and disableEmbedding in the 8.0 release.

Edit: added in #97348

@jportner jportner requested a review from a team as a code owner April 16, 2021 01:59
@jportner
Copy link
Contributor Author

Removed defaults for strictTransportSecurity and permissionsPolicy, and added usage data collection.

New securityheaders.com scan result:

image

@jportner
Copy link
Contributor Author

jportner commented Apr 16, 2021

One API integration test is failing with the following message:

     │1)    apis
     │       Telemetry
     │         /api/telemetry/v2/clusters/_stats
     │           validate data types
     │             should pass the schema validation:
     │
     │      Error: The telemetry schemas in 'src/plugins/telemetry/schema/' are out-of-date, please update it as required: [stack_stats.kibana.plugins.core.config.http.securityResponseHeaders.strictTransportSecurity]: expected value of type [string] but got [null]
     │       at ObjectType.validate (packages/kbn-config-schema/src/types/type.ts:70:13)
     │       at assertTelemetryPayload (test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts:137:34)
     │       at Context.apply (test/api_integration/apis/telemetry/telemetry_local.ts:58:11)
     │       at Object.apply (node_modules/@kbn/test/src/functional_test_runner/lib/mocha/wrap_function.js:73:30)

I set the type for this and some other security headers in the usage data config as string | null (reference), and I set the ES field mapping to "keyword" (reference). It seems that the existing schema validation in the API integration tests does not support nullable values, though.

I can think of two obvious solutions:

  1. Allow keyword and test values to be nullable in the API integration tests:
    diff --git a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts
    index b45930682e3..84409cfe1f7 100644
    --- a/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts
    +++ b/test/api_integration/apis/telemetry/utils/schema_to_config_schema.ts
    @@ -59,7 +59,7 @@ function valueSchemaToConfigSchema(value: TelemetrySchemaValue): Type<unknown> {
           case 'keyword':
           case 'text':
           case 'date':
    -        return schema.string();
    +        return schema.nullable(schema.string());
           case 'byte':
           case 'double':
           case 'float':
  2. Convert null values to "NULL" strings in the usage collector, and change the string | null type to string

@elastic/kibana-telemetry what do you think?

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Technically, LGTM

src/core/server/csp/csp_config.ts Outdated Show resolved Hide resolved
@jportner
Copy link
Contributor Author

jportner commented Apr 19, 2021

I can think of two obvious solutions:

  1. Allow keyword and test values to be nullable in the API integration tests:
     ...
  2. Convert null values to "NULL" strings in the usage collector, and change the string | null type to string

@elastic/kibana-telemetry what do you think?

I spoke to @Bamieh and @afharo offline and this point was raised:

my only concern with allowing null to types is that it'll bring more unexpected scenarios that it'll do good. it is also mean that we'd need to treat every null we get in the ES indexing since ES does not index null and it cannot be searched.

So we agreed that option (2) above is the best path forward. I updated this accordingly in a501b8c.

@jportner jportner enabled auto-merge (squash) April 19, 2021 14:40
Copy link
Member

@afharo afharo left a comment

Choose a reason for hiding this comment

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

Just one note around the descriptions :)

"strictTransportSecurity": {
"type": "keyword",
"_meta": {
"description": "The strictTransportSecurity response header, if it is configured."
Copy link
Member

Choose a reason for hiding this comment

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

if it is configured

I don't think this is true. We are always reporting it. Either the configured value or "NULL". Maybe I misread the code though. Can you confirm @jportner?

Copy link
Contributor Author

@jportner jportner Apr 19, 2021

Choose a reason for hiding this comment

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

Oh, yes sorry about that, I forgot to update the descriptions when I added the null coalescing. I'll update those now.

Edit: done in fd78745.

Edit 2: s/coalescing/coercing/

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Unknown metric groups

API count

id before after diff
core 2172 2174 +2

API count missing comments

id before after diff
core 991 992 +1

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@jportner jportner merged commit 978f5ec into elastic:master Apr 19, 2021
@jportner jportner added the auto-backport Deprecated - use backport:version if exact versions are needed label Apr 19, 2021
@kibanamachine
Copy link
Contributor

💔 Backport failed

Status Branch Result
7.x Commit could not be cherrypicked due to conflicts

To backport manually run:
node scripts/backport --pr 97158

jportner added a commit that referenced this pull request Apr 19, 2021
# Conflicts:
#	src/core/server/csp/csp_config.test.ts
@jportner jportner deleted the add-security-headers-config branch April 20, 2021 13:10
@timroes timroes added the Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! label Apr 26, 2021
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security (Team:Security)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-backport Deprecated - use backport:version if exact versions are needed release_note:enhancement Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.13.0 v8.0.0
Projects
None yet