From 5aee5da843fc1e7747b66bb45e8ebf7a808c144d Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Wed, 23 Aug 2023 14:57:32 +0200 Subject: [PATCH] Allow Kibana to restrict the usage of JWT for a predefined set of routes only. (#163806) ## Summary Allow Kibana to restrict the usage of JWT for a predefined set of routes only in Serverless environment by default. This capability is not available in non-Serverless environment. Any route that needs to be accessed in Serverless environemnt using JWT as a means of authentication should include `security:acceptJWT` tag. ## How to test If you'd like to generate your own JWT to test the PR, please follow the steps outlined in https://github.com/elastic/kibana/pull/159117#issue-1743796706 or just run functional test server and use static JWT from the Serverless test. This PR also generated a Serverless Docker image that you can use in your Dev/QA MKI cluster. - [x] Implementation functionality and add unit tests - [x] Update metrics/status routes to include new `security:acceptJWT` tag - [x] Update serverless test suite to include a test for `security:acceptJWT` __Fixes: https://github.com/elastic/kibana/issues/162632__ --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- .../src/routes/status.ts | 5 +- .../server/routes/stats/stats.ts | 5 +- .../authentication/authenticator.test.ts | 27 ++++- .../server/authentication/authenticator.ts | 8 +- .../authentication/providers/http.test.ts | 108 ++++++++++++++++++ .../server/authentication/providers/http.ts | 33 ++++++ x-pack/plugins/security/server/config.test.ts | 90 +++++++++++++++ x-pack/plugins/security/server/config.ts | 5 + x-pack/plugins/security/server/routes/tags.ts | 6 + .../background_task_utilization.test.ts | 2 + .../routes/background_task_utilization.ts | 3 + .../server/routes/metrics.test.ts | 1 + .../task_manager/server/routes/metrics.ts | 3 + .../packages/helpers/kibana.jsonc | 2 +- .../test_suites/common/index.ts | 1 + .../common/security/authentication_http.ts | 51 +++++++++ x-pack/test_serverless/shared/config.base.ts | 21 +++- 18 files changed, 363 insertions(+), 10 deletions(-) create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b245e10f3ee86..824b5b4fdca11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -595,7 +595,7 @@ packages/kbn-search-api-panels @elastic/enterprise-search-frontend examples/search_examples @elastic/kibana-data-discovery packages/kbn-search-response-warnings @elastic/kibana-data-discovery x-pack/plugins/searchprofiler @elastic/platform-deployment-management -x-pack/test/security_api_integration/packages/helpers @elastic/kibana-core +x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security x-pack/plugins/security @elastic/kibana-security x-pack/plugins/security_solution_ess @elastic/security-solution x-pack/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts index 403686bdf2688..e06d667b4c78b 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status.ts @@ -82,7 +82,10 @@ export const registerStatusRoute = ({ path: '/api/status', options: { authRequired: 'optional', - tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + // The `api` tag ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page. + // The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to + // ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly. + tags: ['api', 'security:acceptJWT'], access: 'public', // needs to be public to allow access from "system" users like k8s readiness probes. }, validate: { diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index 8c32003f38098..6e4a606216035 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -55,7 +55,10 @@ export function registerStatsRoute({ path: '/api/stats', options: { authRequired: !config.allowAnonymous, - tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + // The `api` tag ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page. + // The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to + // ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly. + tags: ['api', 'security:acceptJWT'], access: 'public', // needs to be public to allow access from "system" users like metricbeat. }, validate: { diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 2de2fdbf4df2a..fbc31e588dc51 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -62,12 +62,14 @@ function getMockOptions({ selector, accessAgreementMessage, customLogoutURL, + configContext = {}, }: { providers?: Record | string[]; http?: Partial; selector?: AuthenticatorOptions['config']['authc']['selector']; accessAgreementMessage?: string; customLogoutURL?: string; + configContext?: Record; } = {}) { const auditService = auditServiceMock.create(); auditLogger = auditLoggerMock.create(); @@ -86,10 +88,10 @@ function getMockOptions({ loggers: loggingSystemMock.create(), getServerBaseURL: jest.fn(), config: createConfig( - ConfigSchema.validate({ - authc: { selector, providers, http }, - ...accessAgreementObj, - }), + ConfigSchema.validate( + { authc: { selector, providers, http }, ...accessAgreementObj }, + configContext + ), loggingSystemMock.create().get(), { isTLSEnabled: false } ), @@ -317,6 +319,23 @@ describe('Authenticator', () => { }); }); + it('includes JWT options if specified', () => { + new Authenticator( + getMockOptions({ + providers: { basic: { basic1: { order: 0 } } }, + http: { jwt: { taggedRoutesOnly: true } }, + configContext: { serverless: true }, + }) + ); + + expect( + jest.requireMock('./providers/http').HTTPAuthenticationProvider + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'bearer', 'basic']), + jwt: { taggedRoutesOnly: true }, + }); + }); + it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => { new Authenticator( getMockOptions({ diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 24329c0e7575f..6fa811397a81b 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -648,7 +648,13 @@ export class Authenticator { throw new Error(`Provider name "${options.name}" is reserved.`); } - this.providers.set(options.name, new HTTPAuthenticationProvider(options, { supportedSchemes })); + this.providers.set( + options.name, + new HTTPAuthenticationProvider(options, { + supportedSchemes, + jwt: this.options.config.authc.http.jwt, + }) + ); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/http.test.ts b/x-pack/plugins/security/server/authentication/providers/http.test.ts index c1e7ba662c513..90ff62294ff3f 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.test.ts @@ -15,6 +15,7 @@ import { mockAuthenticationProviderOptions } from './base.mock'; import { HTTPAuthenticationProvider } from './http'; import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; import { securityMock } from '../../mocks'; +import { ROUTE_TAG_ACCEPT_JWT } from '../../routes/tags'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; @@ -144,6 +145,113 @@ describe('HTTPAuthenticationProvider', () => { } }); + it('succeeds for JWT authentication if not restricted to tagged routes.', async () => { + const header = 'Bearer header.body.signature'; + const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } }); + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['bearer']), + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ + ...user, + authentication_provider: { type: 'http', name: 'http' }, + }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + }); + + it('succeeds for non-JWT authentication if JWT restricted to tagged routes.', async () => { + const header = 'Basic xxx'; + const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['bearer', 'basic']), + jwt: { taggedRoutesOnly: true }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ + ...user, + authentication_provider: { type: 'http', name: 'http' }, + }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + }); + + it('succeeds for JWT authentication if restricted to tagged routes and route is tagged.', async () => { + const header = 'Bearer header.body.signature'; + const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } }); + const request = httpServerMock.createKibanaRequest({ + headers: { authorization: header }, + routeTags: [ROUTE_TAG_ACCEPT_JWT], + }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['bearer']), + jwt: { taggedRoutesOnly: true }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded({ + ...user, + authentication_provider: { type: 'http', name: 'http' }, + }) + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + }); + + it('fails for JWT authentication if restricted to tagged routes and route is NOT tagged.', async () => { + const header = 'Bearer header.body.signature'; + const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } }); + const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } }); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + mockOptions.client.asScoped.mockClear(); + + const provider = new HTTPAuthenticationProvider(mockOptions, { + supportedSchemes: new Set(['bearer']), + jwt: { taggedRoutesOnly: true }, + }); + + await expect(provider.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + + expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } }); + + expect(request.headers.authorization).toBe(header); + }); + it('fails if authentication via `authorization` header with supported scheme fails.', async () => { const failureReason = new errors.ResponseError(securityMock.createApiResponse({ body: {} })); for (const { schemes, header } of [ diff --git a/x-pack/plugins/security/server/authentication/providers/http.ts b/x-pack/plugins/security/server/authentication/providers/http.ts index 21c2b25d3be8a..dd4bf8c40a435 100644 --- a/x-pack/plugins/security/server/authentication/providers/http.ts +++ b/x-pack/plugins/security/server/authentication/providers/http.ts @@ -9,12 +9,22 @@ import type { KibanaRequest } from '@kbn/core/server'; import type { AuthenticationProviderOptions } from './base'; import { BaseAuthenticationProvider } from './base'; +import { ROUTE_TAG_ACCEPT_JWT } from '../../routes/tags'; import { AuthenticationResult } from '../authentication_result'; import { DeauthenticationResult } from '../deauthentication_result'; import { HTTPAuthorizationHeader } from '../http_authentication'; +/** + * A type-string of the Elasticsearch JWT realm. + */ +const JWT_REALM_TYPE = 'jwt'; + interface HTTPAuthenticationProviderOptions { supportedSchemes: Set; + jwt?: { + // When set, only routes marked with `ROUTE_TAG_ACCEPT_JWT` tag will accept JWT as a means of authentication. + taggedRoutesOnly: boolean; + }; } /** @@ -32,6 +42,11 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { */ private readonly supportedSchemes: Set; + /** + * Options relevant to the JWT authentication. + */ + private readonly jwt: HTTPAuthenticationProviderOptions['jwt']; + constructor( protected readonly options: Readonly, httpOptions: Readonly @@ -44,6 +59,7 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { this.supportedSchemes = new Set( [...httpOptions.supportedSchemes].map((scheme) => scheme.toLowerCase()) ); + this.jwt = httpOptions.jwt; } /** @@ -79,6 +95,23 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug( `Request to ${request.url.pathname}${request.url.search} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.` ); + + // If Kibana is configured to restrict JWT authentication only to selected routes, ensure that the route is marked + // with the `ROUTE_TAG_ACCEPT_JWT` tag to bypass that restriction. + if ( + user.authentication_realm.type === JWT_REALM_TYPE && + this.jwt?.taggedRoutesOnly && + !request.route.options.tags.includes(ROUTE_TAG_ACCEPT_JWT) + ) { + // Log a portion of the JWT signature to make debugging easier. + const jwtExcerpt = authorizationHeader.credentials.slice(-10); + this.logger.error( + `Attempted to authenticate with JWT credentials (…${jwtExcerpt}) against ${request.url.pathname}${request.url.search}, but it's not allowed. ` + + `Ensure that the route is defined with the "${ROUTE_TAG_ACCEPT_JWT}" tag.` + ); + return AuthenticationResult.notHandled(); + } + return AuthenticationResult.succeeded(user); } catch (err) { this.logger.debug( diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 47b16b5752794..04b16aebab9cc 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -183,6 +183,68 @@ describe('config schema', () => { "showNavLinks": true, } `); + + expect(ConfigSchema.validate({}, { serverless: true, dist: true })).toMatchInlineSnapshot(` + Object { + "audit": Object { + "enabled": false, + }, + "authc": Object { + "http": Object { + "autoSchemesEnabled": true, + "enabled": true, + "jwt": Object { + "taggedRoutesOnly": true, + }, + "schemes": Array [ + "apikey", + "bearer", + ], + }, + "providers": Object { + "anonymous": undefined, + "basic": Object { + "basic": Object { + "accessAgreement": undefined, + "description": undefined, + "enabled": true, + "hint": undefined, + "icon": undefined, + "order": 0, + "session": Object { + "idleTimeout": undefined, + "lifespan": undefined, + }, + "showInSelector": true, + }, + }, + "kerberos": undefined, + "oidc": undefined, + "pki": undefined, + "saml": undefined, + "token": undefined, + }, + "selector": Object {}, + }, + "cookieName": "sid", + "enabled": true, + "loginAssistanceMessage": "", + "public": Object {}, + "secureCookies": false, + "session": Object { + "cleanupInterval": "PT1H", + "idleTimeout": "P3D", + "lifespan": "P30D", + }, + "showInsecureClusterWarning": true, + "showNavLinks": true, + "ui": Object { + "roleManagementEnabled": true, + "roleMappingManagementEnabled": true, + "userManagementEnabled": true, + }, + } + `); }); it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => { @@ -1412,6 +1474,34 @@ describe('config schema', () => { }); }); + describe('authc.http', () => { + it('should not allow xpack.security.authc.http.jwt.* to be configured outside of the serverless context', () => { + expect(() => + ConfigSchema.validate( + { authc: { http: { jwt: { taggedRoutesOnly: false } } } }, + { serverless: false } + ) + ).toThrowErrorMatchingInlineSnapshot( + `"[authc.http.jwt]: a value wasn't expected to be present"` + ); + }); + + it('should allow xpack.security.authc.http.jwt.* to be configured inside of the serverless context', () => { + expect( + ConfigSchema.validate( + { authc: { http: { jwt: { taggedRoutesOnly: false } } } }, + { serverless: true } + ).ui + ).toMatchInlineSnapshot(` + Object { + "roleManagementEnabled": true, + "roleMappingManagementEnabled": true, + "userManagementEnabled": true, + } + `); + }); + }); + describe('ui', () => { it('should not allow xpack.security.ui.* to be configured outside of the serverless context', () => { expect(() => diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 15cb6461b97b5..a5483b4e70ba2 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -279,6 +279,11 @@ export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), autoSchemesEnabled: schema.boolean({ defaultValue: true }), schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey', 'bearer'] }), + jwt: offeringBasedSchema({ + serverless: schema.object({ + taggedRoutesOnly: schema.boolean({ defaultValue: true }), + }), + }), }), }), audit: schema.object({ diff --git a/x-pack/plugins/security/server/routes/tags.ts b/x-pack/plugins/security/server/routes/tags.ts index 090c04d29757f..a6ffd49d53a52 100644 --- a/x-pack/plugins/security/server/routes/tags.ts +++ b/x-pack/plugins/security/server/routes/tags.ts @@ -25,3 +25,9 @@ export const ROUTE_TAG_CAN_REDIRECT = 'security:canRedirect'; * parties, require special handling. */ export const ROUTE_TAG_AUTH_FLOW = 'security:authFlow'; + +/** + * If `xpack.security.authc.http.jwt.taggedRoutesOnly` flag is set, then only routes marked with this tag will accept + * JWT as a means of authentication. + */ +export const ROUTE_TAG_ACCEPT_JWT = 'security:acceptJWT'; diff --git a/x-pack/plugins/task_manager/server/routes/background_task_utilization.test.ts b/x-pack/plugins/task_manager/server/routes/background_task_utilization.test.ts index e70c78b8e2120..322060b4f9b61 100644 --- a/x-pack/plugins/task_manager/server/routes/background_task_utilization.test.ts +++ b/x-pack/plugins/task_manager/server/routes/background_task_utilization.test.ts @@ -57,11 +57,13 @@ describe('backgroundTaskUtilizationRoute', () => { `"/internal/task_manager/_background_task_utilization"` ); expect(config1.options?.authRequired).toEqual(true); + expect(config1.options?.tags).toEqual(['security:acceptJWT']); const [config2] = router.get.mock.calls[1]; expect(config2.path).toMatchInlineSnapshot(`"/api/task_manager/_background_task_utilization"`); expect(config2.options?.authRequired).toEqual(true); + expect(config2.options?.tags).toEqual(['security:acceptJWT']); }); it(`sets "authRequired" to false when config.unsafe.authenticate_background_task_utilization is set to false`, async () => { diff --git a/x-pack/plugins/task_manager/server/routes/background_task_utilization.ts b/x-pack/plugins/task_manager/server/routes/background_task_utilization.ts index 38b1ce9966f33..b72b8ad5a7043 100644 --- a/x-pack/plugins/task_manager/server/routes/background_task_utilization.ts +++ b/x-pack/plugins/task_manager/server/routes/background_task_utilization.ts @@ -117,6 +117,9 @@ export function backgroundTaskUtilizationRoute( options: { access: 'public', // access must be public to allow "system" users, like metrics collectors, to access these routes authRequired: routeOption.isAuthenticated ?? true, + // The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to + // ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly. + tags: ['security:acceptJWT'], }, }, async function ( diff --git a/x-pack/plugins/task_manager/server/routes/metrics.test.ts b/x-pack/plugins/task_manager/server/routes/metrics.test.ts index a9703aa7548dd..172e29d61f110 100644 --- a/x-pack/plugins/task_manager/server/routes/metrics.test.ts +++ b/x-pack/plugins/task_manager/server/routes/metrics.test.ts @@ -28,6 +28,7 @@ describe('metricsRoute', () => { const [config] = router.get.mock.calls[0]; expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/metrics"`); + expect(config.options?.tags).toEqual(['security:acceptJWT']); }); it('emits resetMetric$ event when route is accessed and reset query param is true', async () => { diff --git a/x-pack/plugins/task_manager/server/routes/metrics.ts b/x-pack/plugins/task_manager/server/routes/metrics.ts index 737f2b44fd79e..692227899979b 100644 --- a/x-pack/plugins/task_manager/server/routes/metrics.ts +++ b/x-pack/plugins/task_manager/server/routes/metrics.ts @@ -48,6 +48,9 @@ export function metricsRoute(params: MetricsRouteParams) { path: `/api/task_manager/metrics`, options: { access: 'public', + // The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to + // ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly. + tags: ['security:acceptJWT'], }, // Uncomment when we determine that we can restrict API usage to Global admins based on telemetry // options: { tags: ['access:taskManager'] }, diff --git a/x-pack/test/security_api_integration/packages/helpers/kibana.jsonc b/x-pack/test/security_api_integration/packages/helpers/kibana.jsonc index 6eeec0c14c5cd..accbad4620166 100644 --- a/x-pack/test/security_api_integration/packages/helpers/kibana.jsonc +++ b/x-pack/test/security_api_integration/packages/helpers/kibana.jsonc @@ -1,6 +1,6 @@ { "type": "shared-common", "id": "@kbn/security-api-integration-helpers", - "owner": "@elastic/kibana-core", + "owner": "@elastic/kibana-security", "devOnly": true } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index 3f6751b8e4d02..4ea6b50bb8f2f 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./security/anonymous')); loadTestFile(require.resolve('./security/api_keys')); loadTestFile(require.resolve('./security/authentication')); + loadTestFile(require.resolve('./security/authentication_http')); loadTestFile(require.resolve('./security/authorization')); loadTestFile(require.resolve('./security/misc')); loadTestFile(require.resolve('./security/response_headers')); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts new file mode 100644 index 0000000000000..1555ebe352df9 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication_http.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertestWithoutAuth'); + + describe('security/authentication/http', function () { + it('allows JWT HTTP authentication only for selected routes', async () => { + const jsonWebToken = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tpYmFuYS5lbGFzdGljLmNvL2p3dC8iLCJzdWIiOiJlbGFzdGljLWFnZW50IiwiYXVkIjoiZWxhc3RpY3NlYXJjaCIsIm5hbWUiOiJFbGFzdGljIEFnZW50IiwiaWF0Ijo5NDY2ODQ4MDAsImV4cCI6NDA3MDkwODgwMH0.P7RHKZlLskS5DfVRqoVO4ivoIq9rXl2-GW6hhC9NvTSkwphYivcjpTVcyENZvxTTvJJNqcyx6rF3T-7otTTIHBOZIMhZauc5dob-sqcN_mT2htqm3BpSdlJlz60TBq6diOtlNhV212gQCEJMPZj0MNj7kZRj_GsECrTaU7FU0A3HAzkbdx15vQJMKZiFbbQCVI7-X2J0bZzQKIWfMHD-VgHFwOe6nomT-jbYIXtCBDd6fNj1zTKRl-_uzjVqNK-h8YW1h6tE4xvZmXyHQ1-9yNKZIWC7iEaPkBLaBKQulLU5MvW3AtVDUhzm6--5H1J85JH5QhRrnKYRon7ZW5q1AQ'; + + // Check 5 routes that are currently known to accept JWT as a means of authentication. + for (const allowedPath of [ + '/api/status', + '/api/stats', + '/api/task_manager/_background_task_utilization', + '/internal/task_manager/_background_task_utilization', + '/api/task_manager/metrics', + ]) { + await supertest + .get(allowedPath) + .set('Authorization', `Bearer ${jsonWebToken}`) + .set('ES-Client-Authentication', 'SharedSecret my_super_secret') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(200); + } + + // Make sure it's not possible to use JWT to have interactive sessions. + await supertest + .get('/') + .set('Authorization', `Bearer ${jsonWebToken}`) + .set('ES-Client-Authentication', 'SharedSecret my_super_secret') + .expect(401); + + // Make sure it's not possible to use JWT to access any other APIs. + await supertest + .get('/internal/security/me') + .set('Authorization', `Bearer ${jsonWebToken}`) + .set('ES-Client-Authentication', 'SharedSecret my_super_secret') + .set(svlCommonApi.getInternalRequestHeader()) + .expect(401); + }); + }); +} diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index d9d34a1e14198..4a61bf164be2c 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -28,6 +28,8 @@ export default async () => { '../../test/security_api_integration/plugins/saml_provider' ); + const jwksPath = require.resolve('@kbn/security-api-integration-helpers/oidc/jwks.json'); + return { servers, @@ -35,8 +37,25 @@ export default async () => { license: 'trial', from: 'snapshot', serverArgs: [ + 'xpack.security.authc.realms.file.file1.order=-100', + + 'xpack.security.authc.realms.jwt.jwt1.order=-98', + `xpack.security.authc.realms.jwt.jwt1.token_type=access_token`, + 'xpack.security.authc.realms.jwt.jwt1.client_authentication.type=shared_secret', + `xpack.security.authc.realms.jwt.jwt1.client_authentication.shared_secret=my_super_secret`, + `xpack.security.authc.realms.jwt.jwt1.allowed_issuer=https://kibana.elastic.co/jwt/`, + `xpack.security.authc.realms.jwt.jwt1.allowed_subjects=elastic-agent`, + 'xpack.security.authc.realms.jwt.jwt1.allowed_audiences=elasticsearch', + `xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms=[RS256]`, + `xpack.security.authc.realms.jwt.jwt1.claims.principal=sub`, + `xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${jwksPath}`, + + // TODO: We should set this flag to `false` as soon as we fully migrate tests to SAML and file realms. + `xpack.security.authc.realms.native.native1.enabled=true`, + `xpack.security.authc.realms.native.native1.order=-97`, + 'xpack.security.authc.token.enabled=true', - 'xpack.security.authc.realms.saml.cloud-saml-kibana.order=0', + 'xpack.security.authc.realms.saml.cloud-saml-kibana.order=101', `xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${idpPath}`, 'xpack.security.authc.realms.saml.cloud-saml-kibana.idp.entity_id=http://www.elastic.co/saml1', `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://localhost:${servers.kibana.port}`,