信頼できないクライアントとの検索アプリケーションの使用

検索ユースケースのためのフロントエンドアプリケーションを構築する際、検索結果を返すための主なアプローチは2つあります:

  • 1. クライアント(ユーザーのブラウザ)がアプリケーションのバックエンドにAPIリクエストを行い、バックエンドがElasticsearchにリクエストを行います。Elasticsearchクラスターはエンドユーザーに公開されません。
  • 2. クライアント(ユーザーのブラウザ)が検索サービスに直接APIリクエストを行います。この場合、Elasticsearchクラスターはクライアントに到達可能です。

このガイドでは、2番目のアプローチを取る際のベストプラクティスについて説明します。具体的には、Search Application Search APIに直接リクエストを行うフロントエンドアプリと検索アプリケーションの使用方法を説明します。

このアプローチにはいくつかの利点があります:

  • フロントエンドアプリケーションとElasticsearchの間にパススルークエリシステムを維持する必要がありません
  • Elasticsearchへの直接リクエストは、応答時間を短縮します
  • クエリ設定は1か所で管理されます:Elasticsearch内の検索アプリケーション設定

以下の内容をカバーします:

役割制限を持つElasticsearch APIキーの使用

フロントエンドアプリケーションがElasticsearchに直接APIリクエストを行うことができる場合、実行できる操作を制限することが重要です。この場合、フロントエンドアプリケーションはSearch Application Search APIのみを呼び出すことができる必要があります。これを確実にするために、役割制限を持つElasticsearch APIキーを作成します。役割制限は、役割が有効であるべき条件を指定するために使用されます。

以下のElasticsearch APIキーは、Search Application Search APIを介してのみwebsite-product-search検索アプリケーションにアクセスできます:

Python

  1. resp = client.security.create_api_key(
  2. name="my-restricted-api-key",
  3. expiration="7d",
  4. role_descriptors={
  5. "my-restricted-role-descriptor": {
  6. "indices": [
  7. {
  8. "names": [
  9. "website-product-search"
  10. ],
  11. "privileges": [
  12. "read"
  13. ]
  14. }
  15. ],
  16. "restriction": {
  17. "workflows": [
  18. "search_application_query"
  19. ]
  20. }
  21. }
  22. },
  23. )
  24. print(resp)

Js

  1. const response = await client.security.createApiKey({
  2. name: "my-restricted-api-key",
  3. expiration: "7d",
  4. role_descriptors: {
  5. "my-restricted-role-descriptor": {
  6. indices: [
  7. {
  8. names: ["website-product-search"],
  9. privileges: ["read"],
  10. },
  11. ],
  12. restriction: {
  13. workflows: ["search_application_query"],
  14. },
  15. },
  16. },
  17. });
  18. console.log(response);

コンソール

  1. POST /_security/api_key
  2. {
  3. "name": "my-restricted-api-key",
  4. "expiration": "7d",
  5. "role_descriptors": {
  6. "my-restricted-role-descriptor": {
  7. "indices": [
  8. {
  9. "names": ["website-product-search"],
  10. "privileges": ["read"]
  11. }
  12. ],
  13. "restriction": {
  14. "workflows": ["search_application_query"]
  15. }
  16. }
  17. }
  18. }
indices.nameは検索アプリケーションの名前であり、基盤となるElasticsearchインデックスではありません。
restriction.workflowsは具体的な値search_application_queryに設定する必要があります。

ワークフローレストリクションを指定することが重要です。これがないと、Elasticsearch APIキーは_searchを直接呼び出し、任意のElasticsearchクエリを発行できます。これは信頼できないクライアントを扱う際には安全ではありません。

応答は次のようになります:

コンソール-結果

  1. {
  2. "id": "v1CCJYkBvb5Pg9T-_JgO",
  3. "name": "my-restricted-api-key",
  4. "expiration": 1689156288526,
  5. "api_key": "ztVI-1Q4RjS8qFDxAVet5w",
  6. "encoded": "djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw"
  7. }

エンコードされた値は、Authorizationヘッダーで直接使用できます。以下はcURLを使用した例です:

シェル

  1. curl -XPOST "http://localhost:9200/_application/search_application/website-product-search/_search" \
  2. -H "Content-Type: application/json" \
  3. -H "Authorization: ApiKey djFDQ0pZa0J2YjVQZzlULV9KZ086enRWSS0xUTRSalM4cUZEeEFWZXQ1dw" \
  4. -d '{
  5. "params": {
  6. "field_name": "color",
  7. "field_value": "red",
  8. "agg_size": 5
  9. }
  10. }'

expirationが存在しない場合、デフォルトでElasticsearch APIキーは期限切れになりません。APIキーは、APIキー無効化APIを使用して無効にできます。

役割制限を持つElasticsearch APIキーは、フィールドおよびドキュメントレベルのセキュリティも使用できます。これにより、フロントエンドアプリケーションが検索アプリケーションをクエリする方法がさらに制限されます。

検索アプリケーションでのパラメータ検証

検索アプリケーションは、検索テンプレートを使用してクエリをレンダリングします。テンプレートパラメータはSearch Application Search APIに渡されます。フロントエンドアプリケーションや信頼できないクライアントが使用するAPIの場合、厳格なパラメータ検証が必要です。検索アプリケーションは、Search Application Search APIが許可するパラメータを説明するJSONスキーマを定義します。

以下の例は、厳格なパラメータ検証を持つ検索アプリケーションを定義します:

Python

  1. resp = client.search_application.put(
  2. name="website-product-search",
  3. search_application={
  4. "indices": [
  5. "website-products"
  6. ],
  7. "template": {
  8. "script": {
  9. "source": {
  10. "query": {
  11. "term": {
  12. "{{field_name}}": "{{field_value}}"
  13. }
  14. },
  15. "aggs": {
  16. "color_facet": {
  17. "terms": {
  18. "field": "color",
  19. "size": "{{agg_size}}"
  20. }
  21. }
  22. }
  23. },
  24. "params": {
  25. "field_name": "product_name",
  26. "field_value": "hello world",
  27. "agg_size": 5
  28. }
  29. },
  30. "dictionary": {
  31. "properties": {
  32. "field_name": {
  33. "type": "string",
  34. "enum": [
  35. "name",
  36. "color",
  37. "description"
  38. ]
  39. },
  40. "field_value": {
  41. "type": "string"
  42. },
  43. "agg_size": {
  44. "type": "integer",
  45. "minimum": 1,
  46. "maximum": 10
  47. }
  48. },
  49. "required": [
  50. "field_name"
  51. ],
  52. "additionalProperties": False
  53. }
  54. }
  55. },
  56. )
  57. print(resp)

Js

  1. const response = await client.searchApplication.put({
  2. name: "website-product-search",
  3. search_application: {
  4. indices: ["website-products"],
  5. template: {
  6. script: {
  7. source: {
  8. query: {
  9. term: {
  10. "{{field_name}}": "{{field_value}}",
  11. },
  12. },
  13. aggs: {
  14. color_facet: {
  15. terms: {
  16. field: "color",
  17. size: "{{agg_size}}",
  18. },
  19. },
  20. },
  21. },
  22. params: {
  23. field_name: "product_name",
  24. field_value: "hello world",
  25. agg_size: 5,
  26. },
  27. },
  28. dictionary: {
  29. properties: {
  30. field_name: {
  31. type: "string",
  32. enum: ["name", "color", "description"],
  33. },
  34. field_value: {
  35. type: "string",
  36. },
  37. agg_size: {
  38. type: "integer",
  39. minimum: 1,
  40. maximum: 10,
  41. },
  42. },
  43. required: ["field_name"],
  44. additionalProperties: false,
  45. },
  46. },
  47. },
  48. });
  49. console.log(response);

コンソール

  1. PUT _application/search_application/website-product-search
  2. {
  3. "indices": [
  4. "website-products"
  5. ],
  6. "template": {
  7. "script": {
  8. "source": {
  9. "query": {
  10. "term": {
  11. "{{field_name}}": "{{field_value}}"
  12. }
  13. },
  14. "aggs": {
  15. "color_facet": {
  16. "terms": {
  17. "field": "color",
  18. "size": "{{agg_size}}"
  19. }
  20. }
  21. }
  22. },
  23. "params": {
  24. "field_name": "product_name",
  25. "field_value": "hello world",
  26. "agg_size": 5
  27. }
  28. },
  29. "dictionary": {
  30. "properties": {
  31. "field_name": {
  32. "type": "string",
  33. "enum": ["name", "color", "description"]
  34. },
  35. "field_value": {
  36. "type": "string"
  37. },
  38. "agg_size": {
  39. "type": "integer",
  40. "minimum": 1,
  41. "maximum": 10
  42. }
  43. },
  44. "required": [
  45. "field_name"
  46. ],
  47. "additionalProperties": false
  48. }
  49. }
  50. }

その定義を使用して、Search Application Search APIは次のパラメータ検証を実行します:

  • field_namefield_valueaggs_sizeパラメータのみを受け入れます
  • field_nameは「name」、「color」、「description」の値のみを取ることが制限されています
  • agg_sizeは用語集約のサイズを定義し、110の間の値のみを取ることができます

CORSとの連携

このアプローチを使用すると、ユーザーのブラウザがElasticsearch APIに直接リクエストを行います。Elasticsearchはクロスオリジンリソースシェアリング(CORS)をサポートしていますが、この機能はデフォルトで無効になっています。したがって、ブラウザはこれらのリクエストをブロックします。

これに対する2つの回避策があります:

ElasticsearchでCORSを有効にする

これは最も簡単なオプションです。次の内容をelasticsearch.ymlファイルに追加してElasticsearchでCORSを有効にします:

Yaml

  1. http.cors.allow-origin: "*" # ローカル開発のためにのみ制限のない値を使用してください
  2. # 本番環境で特定のオリジン値を使用するには、`http.cors.allow-origin: "https://
  3. <my-website-domain.example>"`のようにします
  4. http.cors.enabled: true
  5. http.cors.allow-credentials: true
  6. http.cors.allow-methods: OPTIONS, POST
  7. http.cors.allow-headers: X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept

Elastic Cloudでは、Elasticsearchユーザー設定を編集することでこれを行うことができます

  • 1. デプロイメントメニューから、編集ページに移動します。
  • 2. Elasticsearchセクションで、ユーザー設定と拡張機能を管理を選択します。
  • 3. 上記の設定でユーザー設定を更新します。
  • 4. 変更を保存を選択します。

CORSをサポートするサーバーを介してリクエストをプロキシする

ElasticsearchでCORSを有効にできない場合、CORSをサポートするサーバーを介してリクエストをプロキシすることができます。これはより複雑ですが、実行可能なオプションです。

詳細を学ぶ