API key does or does not rely on permissions from user that created it

We had previously been creating API keys with our SSO user accounts. Then we found that after an SSO IdP provider change our users were effectively “different” such that we could no longer edit API keys (e.g. to add or remove permissions) and had to recreate a lot of them.

Because of that, we have created a shared user with very limited permissions, only to manage api keys, with the thought that even a future SSO change won't render our API keys orphaned (not editable). When I login with that user and create an API key - we get mixed results when then calling APIs with that key.

Index permissions seem to work ok. The code using the API key succeeds to post new documents to indices.

Cluster permissions are not working. Trying to call /_ingest/pipeline/my-pipeline and getting 403 response

action [cluster:admin/ingest/pipeline/get] is unauthorized for API key id [6w5rvZoByBvH2vsLbT-1] of user [fusion-apikeys-dev], this action is granted by the cluster privileges [read_pipeline,manage_ingest_pipelines,manage_pipeline,manage,all]"

API is created with this, which I understand with “role_desriptors” does NOT leverage anything about the user that created the API key.

POST /_security/api_key
{
  "name": "ingest-pipeline-apikey-dev-333",
  "role_descriptors": {
    "ingest_pipeline_apikey_role_333": {
      "cluster": ["manage_pipeline", "read_pipeline", "manage_ingest_pipelines"]
    }
  }
}

Can you help sort me out?
Are cluster permissions treated differently with API keys and the user that created them?

Hi @qd-danh

I am not sure I am following so lets define what privileges are applied to a created API Key

In short a user can not create an API Key with greater / more privileges than that user has at that point in time they create API Key.

The privileges that the API key results in in an intersection of the Current Users PIT privileges with the Roles defined in the API Key.

So the answer to your question in the title is ...Yes the API Key that is created does rely on the privileges of the user that creates it

As far as I know that applies to all privileges, cluster, indices, renote_indicies, applications etc

This is documented here in the Roles section

This approach keeps users from creating API Keys with more privileges than they currently have .. AKA does not allow privilege escalation which would be a security flaw.

Thank you @stephenb for the confirmation.

Since the actual privileges for the API key are computed based on current user PIT privileges, it makes it tough to know those are. When I look at the API key either in Kibana or from GET API, I only see what roles are in the role_descriptors from when the API was created.

Even if I look at the roles for the user that created it, I don't know what the roles were at the time when the API key was created.

Is there a way to see the actual computed privileges for the API key, or am I misunderstanding?

To explain our scenario a little more:
We understand that only the creator of an API key can edit it later. If an SSO user creates an API key then is either OOO or no longer around, and we need to adjust privileges on the key, then we have to create a completely new key. So our thinking was to use a lower privileged non-SSO account that we share - to create API keys. But it's becoming apparent now that this "api creator" account needs to have the superset of privileges needed for the various API keys created. Make sense?

Thanks again.

Lots to parse there ...

Yes to see what the PIT privileges from the user that created the key use the with_limited_by flag.

Use the GET API with

with_limited_by boolean Generally available; Added in 8.5.0

Return the snapshot of the owner user's role descriptors associated with the API key. An API key's actual permission is the intersection of its assigned role descriptors and the owner user's role descriptors.

With respect to how you want to manage the user / how users create API keys there are several approaches... and it depends if you are using automation, your overal maturity etc... plus your overall security architecture / posture / philosopy. Who can / can not create API keys with what privileges is very use case dependent.

With respect to SSO users creating keys, yes that is an issue if they are disabled etc. that key can not be updated.

Personally I am not a big "Update API Keys" fan (there are valid uses cases), I prefer (preference) is to create new and rotate the keys which may or may not work for you, (understood that could be painful)

I have sophisticated customer where end users submit a PR / Github action, and their key shows up in a corp Secrets Store... a user never logs in directly to create an API key.

Generally, I see users use a "tightly managed" Native Realm user for API key creation.

But yes in basic the User that creates the key needs to have a Superset of the privileges that the key needs.

Hope that helps a bit....

1 Like

Depending on how radical your changes were, you might be able to solve this with Security Domains.

But otherwise, yes, if you change your definition of user identity, then the user is no longer able to be identified as the owner of an API key (or other resources that depend on ownership).

Great thanks @stephenb for the tip about with_limited_by .
Initially I was feeling comfortable with your answer and understood it. But now today I have a scenario that is not following that "intersection between API key and user that created it" and so now I'm confused. Thought I would illustrate it here, I am probably missing something else.

We use a Serilog sink in .NET that sends logging documents to Elastic, and at startup it makes a call to GET / to retrieve the Elastic server version so that it can adjust it's functionality.

Recently created an API key with my limited user and see an error at runtime, calling GET / needs monitor privilege. So using the logic you explained above, the API key AND the user that creates the key need monitor privilege. That's what I did (added role to the user and updated the API key). All is well. GET / now works.

But ... long ago, created an API key (mentioned above that the user who created it was through different SSO, so effectively an orphaned user) and that is using the same Serilog library. Presumably it's calling GET / at startup as well. Using the with_limited_by feature, the user that created it definitely has monitor priv (has cluster: all) BUT the API key does NOT have that permission. So ... how is that API key succeeding?

Showing the details here. THANK YOU if you see anything that would clear up my understanding.

This one works. Has cluster monitor priv in the API key AND the user that created it.

{
  "api_keys": [
    {
      "id": "GZawLZoBmD8a3ZkIDdlN",
      "name": "(*REDACTED*)-logging-dev",
      "type": "rest",
      "creation": 1761703103827,
      "invalidated": false,
      "username": "(*REDACTED*)apikeys-dev",
      "realm": "native",
      "realm_type": "native",
      "metadata": {},
      "role_descriptors": {
        "document-writer": {
          "cluster": [
            "monitor"
          ],
          "indices": [
            {
              "names": [
                "(*REDACTED*)-logging-dev*"
              ],
              "privileges": [
                "create_index",
                "create",
                "create_doc",
                "write",
                "read",
                "index"
              ],
              "allow_restricted_indices": false
            }
          ],
          "applications": [],
          "run_as": [],
          "metadata": {},
          "transient_metadata": {
            "enabled": true
          }
        }
      },
      "limited_by": [
        {
          "manage-apikeys": {
            "cluster": [
              "manage_api_key",
              "manage_own_api_key",
              "monitor"
            ],
            "indices": [],
            "applications": [
              {
                "application": "kibana-.kibana",
                "privileges": [
                  "feature_dev_tools.all"
                ],
                "resources": [
                  "space:commandcenter",
                  "space:default"
                ]
              }
            ],
            "run_as": [],
            "metadata": {},
            "transient_metadata": {
              "enabled": true
            }
          }
        }
      ]
    }
  ]
}

This one ALSO WORKS. Only has the monitor privilege from the user that created it, not on the API key. But calling GET / with this API key succeeds.

{
  "api_keys": [
    {
      "id": "i3Iz94MBZMpVBpglS5nf",
      "name": "(*REDACTED*)-logging-dev",
      "type": "rest",
      "creation": 1666299677667,
      "invalidated": false,
      "username": "3094815541",
      "realm": "cloud-saml-kibana",
      "metadata": {},
      "role_descriptors": {
        "document-writer": {
          "cluster": [],
          "indices": [
            {
              "names": [
                "(*REDACTED*)-logging-dev*"
              ],
              "privileges": [
                "create_index",
                "create",
                "create_doc",
                "write",
                "read"
              ],
              "allow_restricted_indices": false
            }
          ],
          "applications": [],
          "run_as": [],
          "metadata": {},
          "transient_metadata": {
            "enabled": true
          }
        }
      },
      "limited_by": [
        {
          "superuser": {
            "cluster": [
              "all"
            ],
            "indices": [
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "all"
                ],
                "allow_restricted_indices": false
              },
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "monitor",
                  "read",
                  "view_index_metadata",
                  "read_cross_cluster"
                ],
                "allow_restricted_indices": true
              }
            ],
            "applications": [
              {
                "application": "*",
                "privileges": [
                  "*"
                ],
                "resources": [
                  "*"
                ]
              }
            ],
            "run_as": [
              "*"
            ],
            "metadata": {
              "_reserved": true
            },
            "transient_metadata": {
              "enabled": true
            }
          }
        }
      ]
    }
  ]
}

I will take a look when I can I notice the one that works unexpectedly is built on the "realm": "cloud-saml-kibana" realm. (not sure that has anything but just curious that the 2 "comparisions" are constructed different ways

Also can you also show the command you run + the output that is key to making debug easier. (I can guess but then .. just guessing)

Hi @qd-danh

Hmmmm I get the expected results

POST /_security/api_key
{
  "name": "logs-reader-api-key",
  "role_descriptors": {
    "logs_reader_role": {
      "cluster": [],
      "indices": [
        {
          "names": ["logs-*"],
          "privileges": ["read", "view_index_metadata"]
        }
      ]
    }
  }
}

# Don't Worry not valid values :) 
 {
   "id": "mwFEdfgdfgKolCVLzxclb-",
   "name": "logs-reader-api-key",
   "api_key": "SXovvEdfgdfgdZT-IJWpLAlOA",
   "encoded": "bXdGRUU1cdhvdnZFZjg4YlpULdfhgdfghUlKV3BfgdffgdfgMQWxPQQ=="
}

Then Check Key

GET _security/api_key/?id=mwFEE5sBKolCVLzxclb-&with_limited_by=true

{
  "api_keys": [
    {
      "id": "mwFEE5sBKolCVLzxclb-",
      "name": "logs-reader-api-key",
      "type": "rest",
      "creation": 1765554811648,
      "invalidated": false,
      "username": "4212746406",
      "realm": "cloud-saml-kibana",
      "realm_type": "saml",
      "metadata": {},
      "role_descriptors": {
        "logs_reader_role": {
          "cluster": [],
          "indices": [
            {
              "names": [
                "logs-*"
              ],
              "privileges": [
                "read",
                "view_index_metadata"
              ],
              "allow_restricted_indices": false
            }
          ],
          "applications": [],
          "run_as": [],
          "metadata": {},
          "transient_metadata": {
            "enabled": true
          }
        }
      },
      "limited_by": [
        {
          "superuser": {
            "cluster": [
              "all"
            ],
            "indices": [
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "all"
                ],
                "allow_restricted_indices": false
              },
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "monitor",
                  "read",
                  "view_index_metadata",
                  "read_cross_cluster"
                ],
                "allow_restricted_indices": true
              }
            ],
            "applications": [
              {
                "application": "*",
                "privileges": [
                  "*"
                ],
                "resources": [
                  "*"
                ]
              }
            ],
            "run_as": [
              "*"
            ],
            "metadata": {
              "_reserved": true
            },
            "transient_metadata": {
              "enabled": true
            },
            "remote_indices": [
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "all"
                ],
                "allow_restricted_indices": false,
                "clusters": [
                  "*"
                ]
              },
              {
                "names": [
                  "*"
                ],
                "privileges": [
                  "monitor",
                  "read",
                  "view_index_metadata",
                  "read_cross_cluster"
                ],
                "allow_restricted_indices": true,
                "clusters": [
                  "*"
                ]
              }
            ],
            "remote_cluster": [
              {
                "privileges": [
                  "monitor_enrich",
                  "monitor_stats"
                ],
                "clusters": [
                  "*"
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}

Looks Good

Then Try Key
GET / Does Not Work as expected : 403

curl -S -H "Authorization: ApiKey $API_KEY" -X GET "https://somerandomecluster.es.us-west1.gcp.cloud.es.io" | jq               

{
  "error": {
    "root_cause": [
      {
        "type": "security_exception",
        "reason": "action [cluster:monitor/main] is unauthorized for API key id [mwFEE5sBKolCVLzxclb-] of user [4212746406], this action is granted by the cluster privileges [monitor,manage,all]"
      }
    ],
    "type": "security_exception",
    "reason": "action [cluster:monitor/main] is unauthorized for API key id [mwFEE5sBKolCVLzxclb-] of user [4212746406], this action is granted by the cluster privileges [monitor,manage,all]"
  },
  "status": 403
}

Then try the search on the logs works

curl -S -H "Authorization: ApiKey $API_KEY" -X GET "https://somerandomecluster.es.us-west1.gcp.cloud.es.io/logs-*/_search?size=1" | jq

{
  "took": 83,
  "timed_out": false,
  "_shards": {
    "total": 176,
    "successful": 176,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 10000,
      "relation": "gte"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": ".ds-logs-azure.eventhub-default-2025.11.30-000020",
        "_id": "90I275oBKolCVLzx4ypk",
        "_score": 1.0,
        "_source": {
          "cloud": {
            "availability_zone": "",
            "instance": {
              "name": "stephenb-logginig-test",
              "id": "2fce0248-0dbf-4e17-9750-911c9ebaa3fa"
            },
            "provider": "azure",
            "machine": {
              "type": "Standard_D2ads_v5"
            },
            "service": {
              "name": "Virtual Machines"
            },
            "region": "westus",
            "account": {
              "id": "ef3a5cc0-6972-4eac-94b7-e76a54c6aee8"
            }
          },
          "input": {
            "type": "azure-eventhub"
          },
          "agent": {
            "name": "stephenb-logginig-test",
            "id": "9eaf0e8f-a570-401b-8b63-0e4adb8e70ce",
            "ephemeral_id": "6d9427ff-267e-471a-b0de-e9f519d8d2ee",
            "type": "filebeat",
            "version": "8.19.5"
          },
          "@timestamp": "2025-12-05T15:52:13.081Z",
          "ecs": {
            "version": "8.11.0"
          },
    ....
}

So I am not seeing what your are seeing...

So perhaps you have something else going on. Somehow you're not using the API key you think you are, but I think I pretty much duplicated what you did and it seems to be honoring the privileges