Skip to content

Commit

Permalink
Support authenticating to Elasticsearch via service account tokens (#…
Browse files Browse the repository at this point in the history
…102121)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
legrego and kibanamachine authored Jul 12, 2021
1 parent a5eadd0 commit 76f4956
Show file tree
Hide file tree
Showing 18 changed files with 327 additions and 20 deletions.
4 changes: 4 additions & 0 deletions config/kibana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
#elasticsearch.username: "kibana_system"
#elasticsearch.password: "pass"

# Kibana can also authenticate to Elasticsearch via "service account tokens".
# If may use this token instead of a username/password.
# elasticsearch.serviceAccountToken: "my_token"

# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively.
# These settings enable SSL for outgoing requests from the Kibana server to the browser.
#server.ssl.enabled: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Configuration options to be used to create a [cluster client](./kibana-plugin-co
<b>Signature:</b>

```typescript
export declare type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password'> & {
export declare type ElasticsearchClientConfig = Pick<ElasticsearchConfig, 'customHeaders' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'requestHeadersWhitelist' | 'sniffInterval' | 'hosts' | 'username' | 'password' | 'serviceAccountToken'> & {
pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout'];
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
ssl?: Partial<ElasticsearchConfig['ssl']>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ export declare class ElasticsearchConfig
| [pingTimeout](./kibana-plugin-core-server.elasticsearchconfig.pingtimeout.md) | | <code>Duration</code> | Timeout after which PING HTTP request will be aborted and retried. |
| [requestHeadersWhitelist](./kibana-plugin-core-server.elasticsearchconfig.requestheaderswhitelist.md) | | <code>string[]</code> | List of Kibana client-side headers to send to Elasticsearch when request scoped cluster client is used. If this is an empty array then \*no\* client-side will be sent. |
| [requestTimeout](./kibana-plugin-core-server.elasticsearchconfig.requesttimeout.md) | | <code>Duration</code> | Timeout after which HTTP request will be aborted and retried. |
| [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md) | | <code>string</code> | If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.<!-- -->This is an alternative to specifying a username and password. |
| [shardTimeout](./kibana-plugin-core-server.elasticsearchconfig.shardtimeout.md) | | <code>Duration</code> | Timeout for Elasticsearch to wait for responses from shards. Set to 0 to disable. |
| [sniffInterval](./kibana-plugin-core-server.elasticsearchconfig.sniffinterval.md) | | <code>false &#124; Duration</code> | Interval to perform a sniff operation and make sure the list of nodes is complete. If <code>false</code> then sniffing is disabled. |
| [sniffOnConnectionFault](./kibana-plugin-core-server.elasticsearchconfig.sniffonconnectionfault.md) | | <code>boolean</code> | Specifies whether the client should immediately sniff for a more current list of nodes when a connection dies. |
| [sniffOnStart](./kibana-plugin-core-server.elasticsearchconfig.sniffonstart.md) | | <code>boolean</code> | Specifies whether the client should attempt to detect the rest of the cluster when it is first instantiated. |
| [ssl](./kibana-plugin-core-server.elasticsearchconfig.ssl.md) | | <code>Pick&lt;SslConfigSchema, Exclude&lt;keyof SslConfigSchema, 'certificateAuthorities' &#124; 'keystore' &#124; 'truststore'&gt;&gt; &amp; {</code><br/><code> certificateAuthorities?: string[];</code><br/><code> }</code> | Set of settings configure SSL connection between Kibana and Elasticsearch that are required when <code>xpack.ssl.verification_mode</code> in Elasticsearch is set to either <code>certificate</code> or <code>full</code>. |
| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | <code>string</code> | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. |
| [username](./kibana-plugin-core-server.elasticsearchconfig.username.md) | | <code>string</code> | If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [ElasticsearchConfig](./kibana-plugin-core-server.elasticsearchconfig.md) &gt; [serviceAccountToken](./kibana-plugin-core-server.elasticsearchconfig.serviceaccounttoken.md)

## ElasticsearchConfig.serviceAccountToken property

If Elasticsearch security features are enabled, this setting provides the service account token that the Kibana server users to perform its administrative functions.

This is an alternative to specifying a username and password.

<b>Signature:</b>

```typescript
readonly serviceAccountToken?: string;
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## ElasticsearchConfig.username property

If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions.
If Elasticsearch is protected with basic authentication, this setting provides the username that the Kibana server uses to perform its administrative functions. Cannot be used in conjunction with serviceAccountToken.

<b>Signature:</b>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<b>Signature:</b>

```typescript
export declare type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password'> & {
export declare type LegacyElasticsearchClientConfig = Pick<ConfigOptions, 'keepAlive' | 'log' | 'plugins'> & Pick<ElasticsearchConfig, 'apiVersion' | 'customHeaders' | 'requestHeadersWhitelist' | 'sniffOnStart' | 'sniffOnConnectionFault' | 'hosts' | 'username' | 'password' | 'serviceAccountToken'> & {
pingTimeout?: ElasticsearchConfig['pingTimeout'] | ConfigOptions['pingTimeout'];
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ConfigOptions['requestTimeout'];
sniffInterval?: ElasticsearchConfig['sniffInterval'] | ConfigOptions['sniffInterval'];
Expand Down
5 changes: 5 additions & 0 deletions docs/setup/settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ the username and password that the {kib} server uses to perform maintenance
on the {kib} index at startup. {kib} users still need to authenticate with
{es}, which is proxied through the {kib} server.

|[[elasticsearch-service-account-token]] `elasticsearch.serviceAccountToken:`
| beta[]. If your {es} is protected with basic authentication, this token provides the credentials
that the {kib} server uses to perform maintenance on the {kib} index at startup. This setting
is an alternative to `elasticsearch.username` and `elasticsearch.password`.

| `enterpriseSearch.host`
| The URL of your Enterprise Search instance

Expand Down
12 changes: 7 additions & 5 deletions src/cli/serve/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,14 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
delete extraCliOptions.env;

if (opts.dev) {
if (!has('elasticsearch.username')) {
set('elasticsearch.username', 'kibana_system');
}
if (!has('elasticsearch.serviceAccountToken')) {
if (!has('elasticsearch.username')) {
set('elasticsearch.username', 'kibana_system');
}

if (!has('elasticsearch.password')) {
set('elasticsearch.password', 'changeme');
if (!has('elasticsearch.password')) {
set('elasticsearch.password', 'changeme');
}
}

if (opts.ssl) {
Expand Down
48 changes: 46 additions & 2 deletions src/core/server/elasticsearch/client/client_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,27 @@ describe('parseClientOptions', () => {
);
});

it('adds an authorization header if `serviceAccountToken` is set', () => {
expect(
parseClientOptions(
createConfig({
serviceAccountToken: 'ABC123',
}),
false
)
).toEqual(
expect.objectContaining({
headers: expect.objectContaining({
authorization: `Bearer ABC123`,
}),
})
);
});

it('does not add auth to the nodes', () => {
const options = parseClientOptions(
createConfig({
username: 'user',
password: 'pass',
serviceAccountToken: 'ABC123',
hosts: ['http://node-A:9200'],
}),
true
Expand Down Expand Up @@ -252,6 +268,34 @@ describe('parseClientOptions', () => {
]
`);
});

it('does not add the authorization header even if `serviceAccountToken` is set', () => {
expect(
parseClientOptions(
createConfig({
serviceAccountToken: 'ABC123',
}),
true
).headers
).not.toHaveProperty('authorization');
});

it('does not add auth to the nodes even if `serviceAccountToken` is set', () => {
const options = parseClientOptions(
createConfig({
serviceAccountToken: 'ABC123',
hosts: ['http://node-A:9200'],
}),
true
);
expect(options.nodes).toMatchInlineSnapshot(`
Array [
Object {
"url": "http://node-a:9200/",
},
]
`);
});
});
});

Expand Down
16 changes: 11 additions & 5 deletions src/core/server/elasticsearch/client/client_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type ElasticsearchClientConfig = Pick<
| 'hosts'
| 'username'
| 'password'
| 'serviceAccountToken'
> & {
pingTimeout?: ElasticsearchConfig['pingTimeout'] | ClientOptions['pingTimeout'];
requestTimeout?: ElasticsearchConfig['requestTimeout'] | ClientOptions['requestTimeout'];
Expand Down Expand Up @@ -74,11 +75,16 @@ export function parseClientOptions(
};
}

if (config.username && config.password && !scoped) {
clientOptions.auth = {
username: config.username,
password: config.password,
};
if (!scoped) {
if (config.username && config.password) {
clientOptions.auth = {
username: config.username,
password: config.password,
};
} else if (config.serviceAccountToken) {
// TODO: change once ES client has native support for service account tokens: https://github.com/elastic/elasticsearch-js/issues/1477
clientOptions.headers!.authorization = `Bearer ${config.serviceAccountToken}`;
}
}

clientOptions.nodes = config.hosts.map((host) => convertHost(host));
Expand Down
20 changes: 20 additions & 0 deletions src/core/server/elasticsearch/elasticsearch_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ test('set correct defaults', () => {
"authorization",
],
"requestTimeout": "PT30S",
"serviceAccountToken": undefined,
"shardTimeout": "PT30S",
"sniffInterval": false,
"sniffOnConnectionFault": false,
Expand Down Expand Up @@ -377,3 +378,22 @@ test('#username throws if equal to "elastic", only while running from source', (
);
expect(() => config.schema.validate(obj, { dist: true })).not.toThrow();
});

test('serviceAccountToken throws if username is also set', () => {
const obj = {
username: 'elastic',
serviceAccountToken: 'abc123',
};

expect(() => config.schema.validate(obj)).toThrowErrorMatchingInlineSnapshot(
`"[serviceAccountToken]: serviceAccountToken cannot be specified when \\"username\\" is also set."`
);
});

test('serviceAccountToken does not throw if username is not set', () => {
const obj = {
serviceAccountToken: 'abc123',
};

expect(() => config.schema.validate(obj)).not.toThrow();
});
22 changes: 22 additions & 0 deletions src/core/server/elasticsearch/elasticsearch_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ export const configSchema = schema.object({
)
),
password: schema.maybe(schema.string()),
serviceAccountToken: schema.maybe(
schema.conditional(
schema.siblingRef('username'),
schema.never(),
schema.string(),
schema.string({
validate: () => {
return `serviceAccountToken cannot be specified when "username" is also set.`;
},
})
)
),
requestHeadersWhitelist: schema.oneOf(
[
schema.string({
Expand Down Expand Up @@ -272,6 +284,7 @@ export class ElasticsearchConfig {
/**
* If Elasticsearch is protected with basic authentication, this setting provides
* the username that the Kibana server uses to perform its administrative functions.
* Cannot be used in conjunction with serviceAccountToken.
*/
public readonly username?: string;

Expand All @@ -281,6 +294,14 @@ export class ElasticsearchConfig {
*/
public readonly password?: string;

/**
* If Elasticsearch security features are enabled, this setting provides the service account
* token that the Kibana server users to perform its administrative functions.
*
* This is an alternative to specifying a username and password.
*/
public readonly serviceAccountToken?: string;

/**
* Set of settings configure SSL connection between Kibana and Elasticsearch that
* are required when `xpack.ssl.verification_mode` in Elasticsearch is set to
Expand Down Expand Up @@ -314,6 +335,7 @@ export class ElasticsearchConfig {
this.healthCheckDelay = rawConfig.healthCheck.delay;
this.username = rawConfig.username;
this.password = rawConfig.password;
this.serviceAccountToken = rawConfig.serviceAccountToken;
this.customHeaders = rawConfig.customHeaders;

const { alwaysPresentCertificate, verificationMode } = rawConfig.ssl;
Expand Down
49 changes: 49 additions & 0 deletions src/core/server/elasticsearch/legacy/cluster_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,30 @@ describe('#callAsInternalUser', () => {
expect(mockEsClientInstance.ping).toHaveBeenLastCalledWith(mockParams);
});

test('sets the authorization header when a service account token is configured', async () => {
clusterClient = new LegacyClusterClient(
{ apiVersion: 'es-version', serviceAccountToken: 'ABC123' } as any,
logger.get(),
'custom-type'
);

const mockResponse = { data: 'ping' };
const mockParams = { param: 'ping' };
mockEsClientInstance.ping.mockImplementation(function mockCall(this: any) {
return Promise.resolve({
context: this,
response: mockResponse,
});
});

await clusterClient.callAsInternalUser('ping', mockParams);

expect(mockEsClientInstance.ping).toHaveBeenCalledWith({
headers: { authorization: 'Bearer ABC123' },
param: 'ping',
});
});

test('correctly deals with nested endpoint', async () => {
const mockResponse = { data: 'authenticate' };
const mockParams = { param: 'authenticate' };
Expand Down Expand Up @@ -355,6 +379,31 @@ describe('#asScoped', () => {
);
});

test('does not set the authorization header when a service account token is configured', async () => {
clusterClient = new LegacyClusterClient(
{
apiVersion: 'es-version',
requestHeadersWhitelist: ['zero'],
serviceAccountToken: 'ABC123',
} as any,
logger.get(),
'custom-type'
);

clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
);

const expectedHeaders = { zero: '0' };

expect(MockScopedClusterClient).toHaveBeenCalledTimes(1);
expect(MockScopedClusterClient).toHaveBeenCalledWith(
expect.any(Function),
expect.any(Function),
expectedHeaders
);
});

test('both scoped and internal API caller fail if cluster client is closed', async () => {
clusterClient.asScoped(
httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } })
Expand Down
7 changes: 7 additions & 0 deletions src/core/server/elasticsearch/legacy/cluster_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ export class LegacyClusterClient implements ILegacyClusterClient {
) => {
this.assertIsNotClosed();

if (this.config.serviceAccountToken) {
clientParams.headers = {
...clientParams.headers,
authorization: `Bearer ${this.config.serviceAccountToken}`,
};
}

return await (callAPI.bind(null, this.client) as LegacyAPICaller)(
endpoint,
clientParams,
Expand Down
Loading

0 comments on commit 76f4956

Please sign in to comment.