Using search applications with untrusted clients

edit

Using search applications with untrusted clients

edit

When building a frontend application for search use cases, there are two main approaches to returning search results:

  1. The client (user’s browser) makes API requests to the application backend, which in turn makes a request to Elasticsearch. The Elasticsearch cluster is not exposed to the end user.
  2. The client (user’s browser) makes API requests directly to the search service - in this case the Elasticsearch cluster is reachable to the client.

This guide describes best practices when taking the second approach. Specifically, we will explain how to use search applications with frontend apps that make direct requests to the Search Application Search API.

This approach has a few advantages:

  • No need to maintain a passthrough query system between frontend applications and Elasticsearch
  • Direct requests to Elasticsearch result in faster response times
  • Query configuration is managed in one place: your search application configuration in Elasticsearch

We will cover:

Using Elasticsearch API keys with role restrictions

edit

When frontend applications can make direct API requests to Elasticsearch, it’s important to limit the operations they can perform. In our case, frontend applications should only be able to call the Search Application Search API. To ensure this, we will create Elasticsearch API keys with role restrictions. A role restriction is used to specify under what conditions a role should be effective.

The following Elasticsearch API key has access to the website-product-search search application, only through the Search Application Search API:

POST /_security/api_key
{
  "name": "my-restricted-api-key",
  "expiration": "7d",
  "role_descriptors": {
    "my-restricted-role-descriptor": {
      "indices": [
        {
          "names": ["website-product-search"], 
          "privileges": ["read"]
        }
      ],
      "restriction":  {
        "workflows": ["search_application_query"] 
      }
    }
  }
}

indices.name must be the name(s) of the Search Application(s), not the underlying Elasticsearch indices.

restriction.workflows must be set to the concrete value search_application_query.

It is crucial to specify the workflow restriction. Without this the Elasticsearch API key can directly call _search and issue arbitrary Elasticsearch queries. This is insecure when dealing with untrusted clients.

The response will look like this:

{
  "id": "v1CCJYkBvb5Pg9T-_JgO",
  "name": "my-restricted-api-key",
  "expiration": 1689156288526,
  "api_key": "ztVI-1Q4RjS8qFDxAVet5w",
  "encoded": "djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw"
}

The encoded value can then be directly used in the Authorization header. Here’s an example using cURL:

curl -XPOST "http://localhost:9200/_application/search_application/website-product-search/_search" \
 -H "Content-Type: application/json" \
 -H "Authorization: ApiKey djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw" \
 -d '{
  "params": {
    "field_name": "color",
    "field_value": "red",
    "agg_size": 5
  }
}'

If expiration is not present, by default Elasticsearch API keys never expire. The API key can be invalidated using the Invalidate API Key API.

Elasticsearch API keys with role restrictions can also use field and document level security. This further limits how frontend applications query a search application.

Parameter validation with search applications

edit

Your search applications use search templates to render queries. The template parameters are passed to the Search Application Search API. In the case of APIs used by frontend applications or untrusted clients, we need to have strict parameter validation. Search applications define a JSON schema that describes which parameters the Search Application Search API allows.

The following example defines a search application with strict parameter validation:

PUT _application/search_application/website-product-search
{
  "indices": [
    "website-products"
  ],
  "template": {
    "script": {
      "source": {
        "query": {
          "term": {
            "{{field_name}}": "{{field_value}}"
          }
        },
        "aggs": {
          "color_facet": {
            "terms": {
              "field": "color",
              "size": "{{agg_size}}"
            }
          }
        }
      },
      "params": {
        "field_name": "product_name",
        "field_value": "hello world",
        "agg_size": 5
      }
    },
    "dictionary": {
      "properties": {
        "field_name": {
          "type": "string",
          "enum": ["name", "color", "description"]
        },
        "field_value": {
          "type": "string"
        },
        "agg_size": {
          "type": "integer",
          "minimum": 1,
          "maximum": 10
        }
      },
      "required": [
        "field_name"
      ],
      "additionalProperties": false
    }
  }
}

Using that definition, the Search Application Search API performs the following parameter validation:

  • It only accepts the field_name, field_value and aggs_size parameters
  • field_name is restricted to only take the values "name", "color" and "description"
  • agg_size defines the size of the term aggregation and it can only take values between 1 and 10

Working with CORS

edit

Using this approach means that your user’s browser will make requests to the Elasticsearch API directly. Elasticsearch supports Cross-Origin Resource Sharing (CORS), but this feature is disabled by default. Therefore the browser will block these requests.

There are two workarounds for this:

Enable CORS on Elasticsearch

edit

This is the simplest option. Enable CORS on Elasticsearch by adding the following to your elasticsearch.yml file:

http.cors.allow-origin: "*" # Only use unrestricted value for local development
# Use a specific origin value in production, like `http.cors.allow-origin: "https://<my-website-domain.example>"`
http.cors.enabled: true
http.cors.allow-credentials: true
http.cors.allow-methods: OPTIONS, POST
http.cors.allow-headers: X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept

On Elastic Cloud, you can do this by editing your Elasticsearch user settings.

  1. From your deployment menu, go to the Edit page.
  2. In the Elasticsearch section, select Manage user settings and extensions.
  3. Update the user settings with the configuration above.
  4. Select Save changes.

Proxy the request through a server that supports CORS

edit

If you are unable to enable CORS on Elasticsearch, you can proxy the request through a server that supports CORS. This is more complicated, but is a viable option.

Learn more

edit