Conversation
Adds a new API endpoint that allows an appropriately privileged user to clone an existing API Key, provided that user has the API Key credential. This is roughly equivalent to the Grant API Key endpoint, which creates API Keys on behalf of a user (given the user's credential). This new endpoint is designed for cases where the Grant API Key endpoint would normally be used, except that the authenticating credential is an API Key. Since we don't support derived API Keys (that is, API Keys creating API Keys) we instead allow the client (e.g. Kibana) to clone the provided API Key for its own internal use.
|
Hi @tvernum, I've created a changelog YAML for you. |
🔍 Preview links for changed docs |
ℹ️ Important: Docs version tagging👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version. We use applies_to tags to mark version-specific features and changes. Expand for a quick overviewWhen to use applies_to tags:✅ At the page level to indicate which products/deployments the content applies to (mandatory) What NOT to do:❌ Don't remove or replace information that applies to an older version 🤔 Need help?
|
PurposeThis PR adds a Clone API Key REST API so that a user with the right privileges can create a new API key that has the same role descriptors and owner as an existing API Key. The source API Key is identified by supplying its encoded credential in the request. Use case: When the caller would normally use Grant API Key (creating an API key on behalf of a user), but the authenticating credential is itself an API key. REST API
Request body
Examples:Minimal required fields {
"api_key": "VnVhQ2ZHY0RvZGZWekVXTmJPRVp3YUdwe...",
"name": "my-cloned-key"
}All supported fields {
"api_key": "VnVhQ2ZHY0RvZGZWekVXTmJPRVp3YUdwe...",
"name": "my-cloned-key",
"expiration": "30d",
"metadata": {
"environment": "staging",
"purpose": "CI pipeline"
}
}ResponseSame as Create API Key / Grant API Key: Example:{
"id": "A1b2C3d4E5f6G7h8I9j0K",
"name": "my-cloned-key",
"api_key": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"expiration": 1740268800000,
"encoded": "QTFiOEMzZDRFNWY2RzdoOEo5ajBLOmExYjJjM2Q0ZTVmNmc3aDhpOWowazFsMm0zbjRvNXA2"
}3. Behaviour
4. Security model
5. Implementation details (for reviewers)
|
This adds a "create_apikey" security config change event when an API Key is cloned. It also adds tests for auditing of these events and the general audit event when the action is executed (including request filtering). In doing so, we moved `ApiKeyCredentials` into x-pack core so that the `CloneApiKeyRequest` could parse the credentials in the request and the source API Key Id could be included in the `security_config_change` audit record
|
Pinging @elastic/es-security (Team:Security) |
| "type": "enum", | ||
| "options": ["true", "false", "wait_for"], | ||
| "default": "false", | ||
| "description": "If `true` (the default) then refresh the affected shards to make this operation visible to search, if `wait_for` then wait for a refresh to make this operation visible to search, if `false` then do nothing with refreshes." |
There was a problem hiding this comment.
The wait_for seem to be default value in stateful and true (immediate) in serverless:
We can express this difference in the description, but "default": "false" is wrong in both cases. Probably best omitting default altogether?
...gin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/AuthenticationResult.java
Show resolved
Hide resolved
| if (result.isAuthenticated()) { | ||
| return result.getValue().v2(); | ||
| } else { | ||
| throw new ElasticsearchSecurityException(result.getMessage(), result.getException()); |
There was a problem hiding this comment.
If a user provides an API key that does not exist, this validation will return internal server error (500). This is because there is no root cause and ElasticsearchSecurityException defaults to 500 status. I think we need to handle unsuccessful cases similar as we do in tryAuthenticate where we wrap terminating authentication results with Exceptions.authenticationError(result.getMessage())
{
"error": {
"root_cause": [
{
"type": "security_exception",
"reason": "unable to find apikey with id A1b8C3d4E5f6G7h8J9j0K"
}
],
"type": "security_exception",
"reason": "unable to find apikey with id A1b8C3d4E5f6G7h8J9j0K"
},
"status": 500
}
Logged suppressed warning:
[2026-03-02T10:13:51,336][WARN ][r.suppressed ] [runTask-0] path: /_security/api_key/clone, params: {}, status: 500 org.elasticsearch.ElasticsearchSecurityException: unable to find apikey with id A1b8C3d4E5f6G7h8J9j0K
at org.elasticsearch.security@9.4.0-SNAPSHOT/org.elasticsearch.xpack.security.authc.ApiKeyService.lambda$validateCloneableApiKeyAndGetDoc$39(ApiKeyService.java:1542)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$MappedActionListener.onResponse(ActionListenerImplementations.java:105)
at org.elasticsearch.security@9.4.0-SNAPSHOT/org.elasticsearch.xpack.security.authc.ApiKeyService.lambda$loadApiKeyAndValidateCredentials$31(ApiKeyService.java:1322)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$DelegatingFailureActionListener.onResponse(ActionListenerImplementations.java:233)
at org.elasticsearch.security@9.4.0-SNAPSHOT/org.elasticsearch.xpack.security.authc.ApiKeyService.lambda$loadApiKeyDoc$25(ApiKeyService.java:1279)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListener$2.onResponse(ActionListener.java:258)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.support.ContextPreservingActionListener.onResponse(ContextPreservingActionListener.java:33)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListener$3.onResponse(ActionListener.java:416)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.tasks.TaskManager$1.onResponse(TaskManager.java:223)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.tasks.TaskManager$1.onResponse(TaskManager.java:217)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$RunBeforeActionListener.onResponse(ActionListenerImplementations.java:350)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListener$3.onResponse(ActionListener.java:416)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.support.ContextPreservingActionListener.onResponse(ContextPreservingActionListener.java:33)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListenerImplementations$MappedActionListener.onResponse(ActionListenerImplementations.java:111)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.action.ActionListenerResponseHandler.handleResponse(ActionListenerResponseHandler.java:49)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.transport.TransportService$ContextRestoreResponseHandler.handleResponse(TransportService.java:1514)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.transport.TransportService$DirectResponseChannel.processResponse(TransportService.java:1612)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.transport.TransportService$DirectResponseChannel$1.doRun(TransportService.java:1592)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:1114)
at org.elasticsearch.server@9.4.0-SNAPSHOT/org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:27)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
at java.base/java.lang.Thread.run(Thread.java:1474)
There was a problem hiding this comment.
The API also returns 500 when attempting to clone cross-cluster API keys:
{
"error": {
"root_cause": [
{
"type": "security_exception",
"reason": "authentication expected API key type of [rest], but API key [Ba3RrZwB7lWGJlxHOUCx] has type [cross_cluster]"
}
],
"type": "security_exception",
"reason": "authentication expected API key type of [rest], but API key [Ba3RrZwB7lWGJlxHOUCx] has type [cross_cluster]"
},
"status": 500
}
| ResponseException.class, | ||
| () -> cloneApiKey(CLONE_API_KEY_USER, invalidEncoded, randomAlphaOfLength(12), Map.of()) | ||
| ); | ||
| assertThat(e.getResponse().getStatusLine().getStatusCode(), greaterThanOrEqualTo(400)); |
There was a problem hiding this comment.
I suggest checking actual expected status code (same as done in other places) rather than greaterThanOrEqualTo(400). This test would have caught the issue mentioned in #142633 (comment)
Vale Linting ResultsSummary: 1 suggestion found 💡 Suggestions (1)
The Vale linter checks documentation changes against the Elastic Docs style guide. To use Vale locally or report issues, refer to Elastic style guide for Vale. |
jeramysoucy
left a comment
There was a problem hiding this comment.
No issues from Kibana security perspective. I see in the slack thread there was already discussion regarding the bypassing of expiration/long-lived vs. short lived credentials, but I suppose that is part of the trade off to solve the issue.
Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com>
Adds a new API endpoint that allows an appropriately privileged user
to clone an existing API Key, provided that user has the API Key
credential.
This is roughly equivalent to the Grant API Key endpoint, which
creates API Keys on behalf of a user (given the user's credential).
This new endpoint is designed for cases where the Grant API Key
endpoint would normally be used, except that the authenticating
credential is an API Key. Since we don't support derived API Keys
(that is, API Keys creating API Keys) we instead allow the client
(e.g. Kibana) to clone the provided API Key for its own internal use.
Resolves: #59304