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

Enables preventing access to internal APIs #156935

Conversation

TinaHeiligers
Copy link
Contributor

@TinaHeiligers TinaHeiligers commented May 5, 2023

Part of #151940

fix #152282
fix #152283
fix #152293
partially address #152287

Summary

This PR follows on from the initial work done to more formally declare APIs as internal or public:

  • Allows internal API access to be configurable
  • Adapts Core's internal router to handle the access parameter
  • Adapts Core's browser-side http service to send an additional x-elastic-internal-origin: Kibana header to in all requests
  • Adapts some browser-side code not using Core's http service.

Details

Requests to internal Kibana APIs will be restricted for serverless. This allows us more flexibility in making breaking changes to internal APIs, without having to go through a lengthy deprecation period, as is required for changes to public APIs.

Why are we doing this?

  • The restriction is there to help mitigate external adoption of (what is intended to be) experimental/internal APIs. These APIs are subject to frequent changes and extensive adoption externally has historically forced us into corners of having to maintain some APIs as is to prevent breaking end-user integrations.

Explicitly enforcing a difference between internal and public APIs has a few other advantages:

  • We're only promising to maintain a small subset of Kibana's ~ 2500 APIs,
  • We can make breaking changes to internal routes
  • External documentation overhead reduced (please don't forget about your internal customers!)

There's always the catch that our code is public and end-users can still get around the restriction by adding the x-elastic-internal-origin header. However, if and when they do so and something 'breaks', we're not held liable.

What this means to plugin authors :

Core applies the restriction when enabled through HTTP config as server.restrictInternalApis: <boolean>.

What this means is that once we "flip the switch", any requests to internal APIs will throw if they don't send the x-elastic-internal-product header.

I've handled the known cases in this issue and opened #152287 asking teams to help out.

Checklist

  • Documentation was added for features that require explanation or tutorials (code comments and jsdocs. We explicitly don't intend to document the config option publically)
  • Unit or functional tests were updated or added to match the most common scenarios
  • If a plugin configuration key changed, check if it needs to be added to the docker list
  • If a plugin configuration key changed, check if it needs to be allowlisted in the cloud (not needed for cloud-current offerings)

Risk Matrix

Risk Probability Severity Mitigation/Notes
end-users add the x-elastic-internal-origin header to access internal APIs Low High All public documentation on internal APIs must have the experimental warning and clearly state that internal APIs are subject to frequent changes
Upstream services don't include the header and requests to internal APIs fail Medium Medium The Core team needs to ensure intra-stack components are updated too

For maintainers

@TinaHeiligers TinaHeiligers added Feature:http release_note:skip Skip the PR/issue when compiling release notes backport:skip This commit does not require backporting Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.9.0 labels May 5, 2023
@TinaHeiligers
Copy link
Contributor Author

@elasticmachine merge upstream

@@ -17,3 +17,6 @@ xpack.license_management.enabled: false
# Other disabled plugins
#xpack.canvas.enabled: false #only disabable in dev-mode
xpack.reporting.enabled: false

# Enforce restring access to internal APIs see https://github.com/elastic/kibana/issues/151940
# server.restrictInternalApis: true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added here for intended purpose. Once all intra-stack components and Kibana browser-side code not using Core's http service are updated, we can enable the restriction.

Copy link
Contributor Author

@TinaHeiligers TinaHeiligers left a comment

Choose a reason for hiding this comment

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

Self review

@@ -160,6 +160,7 @@ describe('Fetch', () => {
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'content-type': 'application/json',
'kbn-version': 'VERSION',
'x-elastic-internal-origin': 'Kibana',
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're not concerned about the content here, only that the header is present.

@@ -150,6 +150,7 @@ const configSchema = schema.object(
},
}
),
restrictInternalApis: schema.boolean({ defaultValue: false }), // allow access to internal routes by default to prevent breaking changes in current offerings
Copy link
Contributor Author

@TinaHeiligers TinaHeiligers May 8, 2023

Choose a reason for hiding this comment

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

Code comment to explain the intent.

@@ -39,6 +40,27 @@ export const createXsrfPostAuthHandler = (config: HttpConfig): OnPostAuthHandler
};
};

export const createRestrictInternalRoutesPostAuthHandler = (
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 takes a similar approach to createVersionCheckPostAuthHandler.

@@ -86,7 +106,12 @@ export const registerCoreHandlers = (
config: HttpConfig,
env: Env
) => {
// add headers based on config
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 don't often update these and I added comments to more easily follow where they're applied.

@@ -157,7 +157,9 @@ export interface BulkGetItem {
export function isKibanaRequest({ headers }: KibanaRequest) {
// The presence of these two request headers gives us a good indication that this is a first-party request from the Kibana client.
// We can't be 100% certain, but this is a reasonable attempt.
return headers && headers['kbn-version'] && headers.referer;
return (
headers && headers['kbn-version'] && headers.referer && headers['x-elastic-internal-origin']
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ATM, there are 4: core usage stats client, cases plugin, rule registry, and spaces usage stats client. I've added the header to core's implementation.

Are the concerns Joe raised still val a valid reason to have 4 implementations of isKibanaRequest or can we add the extra header to that check?

A while back, Joe raised concerns about adding a header to all Kibana requests:

  1. other integrations might attempt to use this header
  2. adding a new header might present problems for users with reverse proxies that may strip headers not in an allow-list; this would cause false negative for first-party request detection

If these are still valid, then having different implementations is warrented. @response-ops @kibana-security would a single method fullfil your needs or are concerns around false negatives be an issue for you?

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO (1) and (2) should be mitigated because we will not be restricting access to internal endpoints by default so everything should continue working as-is for on-prem.

// We can't be 100% certain, but this is a reasonable attempt.
return headers && headers['kbn-version'] && headers.referer;
return (
headers && headers['kbn-version'] && headers.referer && headers['x-elastic-internal-origin']
Copy link
Contributor Author

Choose a reason for hiding this comment

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

similar comment to above

Copy link
Contributor Author

@TinaHeiligers TinaHeiligers left a comment

Choose a reason for hiding this comment

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

More self review comments

src/plugins/bfetch/public/plugin.ts Show resolved Hide resolved

const isApiRoute =
route.options.tags.includes(ROUTE_TAG_API) ||
route.path.startsWith('/api/') ||
route.path.startsWith('/internal/');
const isAjaxRequest = hasVersionHeader || hasXsrfHeader;
const isAjaxRequest = hasVersionHeader || hasXsrfHeader || hasIternalOriginHeader;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@elastic/kibana-security I took the initiative and applied the implementation. Feel free to suggest changes, I'm not too familiar with your domains.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, @TinaHeiligers. Question for you: Under what conditions will requests be expected to have an internal origin header, but not one of the version or xsrf headers?

Copy link
Contributor Author

@TinaHeiligers TinaHeiligers May 9, 2023

Choose a reason for hiding this comment

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

@legrego good question! I can't think of one TBH. Can you?
Maybe it's better not to add the internal header, it could become confusing down the line. I'll make the change.

Copy link
Member

Choose a reason for hiding this comment

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

++ yeah I don't think this change is necessary at this point, but thanks for taking this function into consideration!

@TinaHeiligers TinaHeiligers marked this pull request as ready for review May 9, 2023 01:45
@TinaHeiligers TinaHeiligers requested review from a team as code owners May 9, 2023 01:45
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-core (Team:Core)

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

Good job @TinaHeiligers ! Thanks for responding to my questions.

@botelastic botelastic bot added the Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) label May 9, 2023
@@ -11,7 +11,7 @@ import { ROUTE_TAG_API, ROUTE_TAG_CAN_REDIRECT } from '../routes/tags';
import { canRedirectRequest } from './can_redirect_request';

describe('can_redirect_request', () => {
it('returns true if request does not have either a kbn-version or kbn-xsrf header', () => {
it('returns true if request does not have either a kbn-version or kbn-xsrf header or x-elastic-internal-origin', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
it('returns true if request does not have either a kbn-version or kbn-xsrf header or x-elastic-internal-origin', () => {
it('returns true if request does not have either a kbn-version or kbn-xsrf header', () => {

Copy link
Member

@lukasolson lukasolson left a comment

Choose a reason for hiding this comment

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

Bfetch changes LGTM

Copy link
Member

@tsullivan tsullivan left a comment

Choose a reason for hiding this comment

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

bfetch changes LGTM

Copy link
Member

@tsullivan tsullivan left a comment

Choose a reason for hiding this comment

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

bfetch changes LGTM

@TinaHeiligers
Copy link
Contributor Author

@elasticmachine merge upstream

@TinaHeiligers TinaHeiligers enabled auto-merge (squash) May 10, 2023 00:34
@TinaHeiligers
Copy link
Contributor Author

@elasticmachine merge upstream

@TinaHeiligers TinaHeiligers merged commit 7bbe92f into elastic:main May 10, 2023
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
bfetch 22 24 +2

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/core-http-common 0 1 +1
@kbn/core-http-server-internal 49 50 +1
@kbn/server-http-tools 50 51 +1
total +3

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
bfetch 6.9KB 6.9KB +37.0B
core 363.9KB 364.1KB +267.0B
total +304.0B
Unknown metric groups

API count

id before after diff
@kbn/core-http-common 5 6 +1
@kbn/core-http-server-internal 55 56 +1
@kbn/server-http-tools 53 54 +1
total +3

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
securitySolution 400 404 +4
total +6

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
securitySolution 480 484 +4
total +6

History

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

TinaHeiligers added a commit that referenced this pull request Oct 2, 2024
fix #163654

This PR enforces internal API restrictions in our standard offering.
Internal APIs are subject to rapid change and are intentionally not
public. By restricting their access, we protect consumers from these
rapid changes.

This PR does not change any public APIs and they remain available for
external consumption.

## Note to reviewers:
I chose the most practical way of resolving the failures (add the header
or disable the restriction).

## Details
Requests to internal Kibana APIs will be restricted globally. This
allows more flexibility in making breaking changes to internal APIs,
without a risk to external consumers.

## Why are we doing this?
The restriction is there to help mitigate the risk of breaking external
integrations consuming APIs. Internal APIs are subject to frequent
changes, necessary for feature development.

## What this means to plugin authors :
Kibana core applies the restriction when enabled through HTTP config.

## What this means to Kibana consumers:
Explicitly restricting access to internal APIs has advantages for
external consumers:
- Consumers can safely integrate with Kibana's stable, public APIs
- Consumers are protected from Internal route development, which may
involve breaking changes
- Relevant information in Kibana's external documentation that is
user-friendly and complete.

KB article explaining the change (tracked as part of
elastic/kibana-team#1044)

## Release note
Starting with this release, requests to internal Kibana APIs are
globally restricted by default. This change is designed to provide more
flexibility in making breaking changes to internal APIs while protecting
external consumers from unexpected disruptions.
**Key Changes**:
• _Internal API Access_: External consumers no longer have access to
Kibana’s internal APIs, which are now strictly reserved for internal
development and subject to frequent changes. This helps ensure that
external integrations only interact with stable, public APIs.
• _Error Handling_: When a request is made to an internal API without
the proper internal identifier (header or query parameter), Kibana will
respond with a 400 Bad Request error, indicating that the route exists
but is not allowed under the current Kibana configuration.

## How to test this
### Running kibana
1. Set `server.restrictInternalApis: true` in `kibana.yml`
2. Start es with any license
3. Start kibana
4. Make an external request to an internal API:
<details>
  <summary>curl request to get the config for 9.0:</summary>
  
  ```unset
curl --location
'localhost:5601/abc/api/kibana/management/saved_objects/_bulk_get' \
--header 'Content-Type: application/json' \
--header 'kbn-xsrf: kibana' \
--header 'x-elastic-internal-origin: kibana' \
--header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \
--data '[
    {
        "type": "config",
        "id": "9.0.0"
    }
]'
  ```
</details>

The request should be successful.

5. Make the same curl request without the internal origin header
<details>
  <summary>curl:</summary>
  
  ```unset
curl --location
'localhost:5601/abc/api/kibana/management/saved_objects/_bulk_get' \
--header 'Content-Type: application/json' \
--header 'kbn-xsrf: kibana' \
--header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \
--data '[
    {
        "type": "config",
        "id": "9.0.0"
    }
]'
  ```
</details>

The response should be an error similar to:
`{"statusCode":400,"error":"Bad Request","message":"uri
[/api/kibana/management/saved_objects/_bulk_get] with method [post]
exists but is not available with the current configuration"}`

6. Remove `server.restrictInternalApis` from `kibana.yml` or set it to
`false`.

7. Repeat both curl requests above. Both should respond without an
error.


### Checklist
Delete any items that are not applicable to this PR.
- [X] [Documentation] was added for features that require explanation or
tutorials (In PR #191943)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios (and PR
#192407)
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
(docker list updated in #156935,
cloud stack packs for 9.0 kibana to follow)


### Risk Matrix

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| The restriction is knowingly bypassed by end-users adding the required
header to use `internal` APIs | Low | High | Kibana's internal APIs are
not documented in the OAS specifications. External consumption will be
prevented unless explicitly bypassed. |
| Upstream services don't include the header and requests to `internal`
APIs fail | Medium | Medium | The Core team needs to ensure intra-stack
components are updated too |


### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:ExpressionLanguage Interpreter expression language (aka canvas pipeline) Feature:http release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc v8.9.0
Projects
None yet
10 participants