GDC エアギャップに Elastic Stack の生成 AI 機能をデプロイする

概要

このガイドでは、Google Distributed Cloud(GDC)エアギャップ共有クラスタに Elastic Stack(Elasticsearch と Kibana)をエンドツーエンドでデプロイする方法について説明します。また、大規模言語モデル(LLM)サービスと統合して、Elastic の生成 AI 機能を評価する方法についても説明します。

このデプロイでは、GDC エアギャップ Marketplace で入手できる公式の Elastic Cloud on Kubernetes(ECK)オペレーターを使用します。オペレーターは、共有 Kubernetes クラスタのプロジェクト Namespace にインストールされ、Elasticsearch リソースと Kibana リソースのプロビジョニングとライフサイクル管理を自動化します。デプロイされると、Kibana は LLM 推論エンドポイント(GDC のマネージド Gemini、Ollama や vLLM などのセルフマネージド OpenAI 互換サービスなど)に接続します。この統合により、Elastic AI Agent、Agent Builder で構築されたカスタム エージェント、Attack Discovery などの Elastic の生成 AI 機能が強化されます。

このガイドは、Elastic v9.4.0、Elastic Kubernetes Operator v3.4.0、GDC v1.16.2 を対象として作成されています。異なるバージョンを使用している場合、一部の手順が若干異なることがあります。

アーキテクチャ

このアーキテクチャは、GDC 共有ユーザー クラスタのプロジェクト名前空間内で ECK オペレーターによって管理される Elastic Stack デプロイで構成されています。Kibana は、内部 GDC ネットワークを介して LLM 推論エンドポイントと通信します。

Elastic Stack のアーキテクチャ図。

このアーキテクチャの主なコンポーネントは次のとおりです。

  • GDC 共有クラスタ: 複数のプロジェクトに接続された組織スコープの共有 Kubernetes クラスタ。GDC Marketplace ソフトウェア パッケージとアプリケーション ワークロードをホストします。
  • GDC プロジェクト: ECK オペレーター、Elasticsearch クラスタ、Kibana インスタンス、関連する Kubernetes Secret と NetworkPolicy を分離する共有クラスタ上のプロジェクト Namespace。
  • Elastic Cloud on Kubernetes(ECK)オペレーター: GDC Marketplace からプロジェクトの名前空間にインストールされたオペレーターは、Elasticsearch カスタム リソースと Kibana カスタム リソースを管理し、生成 AI 機能と外部 LLM コネクタに必要な Elastic ライセンスを調整します。
  • Elasticsearch: 運用データとセキュリティ データを保存してインデックスに登録し、Kibana の AI 機能とエージェントによって生成された検索クエリと ES|QL クエリを実行します。
  • Kibana: Elastic の生成 AI 機能をホストします。これには、Elastic AI Agent(自然言語によるログ調査と ES|QL 生成用)、Agent Builder(Elasticsearch データに対するカスタム会話エージェントとツールの構築用)、Attack Discovery(セキュリティ アラートの相関分析と攻撃チェーン分析用)が含まれます。Kibana は、ユーザー、Elasticsearch、LLM サービス間のマルチステップ ツール呼び出しをオーケストレートする LLM アクション コネクタも管理します。
  • LLM サービス: マネージド LLM サービス(GDC の Gemini など)またはセルフマネージド推論サーバー(Ollama や vLLM など)。内部 GDC ネットワークを介して Kibana にチャット補完機能とツール呼び出し機能を提供します。

始める前に

デプロイを開始する前に、GDC 環境が次の前提条件と前提を満たしていることを確認してください。

  • 共有 Kubernetes クラスタと GDC プロジェクト: 既存の GDC 共有クラスタと、そのクラスタに接続されている GDC プロジェクトにアクセスできます。このプロジェクトは、このガイドでデプロイされるすべての Kubernetes リソースの名前空間コンテナとして機能します。クラスタに、ECK オペレーター、Elasticsearch、Kibana のワークロードをホストするのに十分なコンピューティング リソースと永続ストレージ リソースがあることを確認します。
  • LLM サービス: LLM サービスへのネットワーク アクセスがある。マネージド Gemini サービス(GDC 環境で利用可能な場合)またはセルフマネージド OpenAI 互換 LLM サービス(GPU プロジェクトで Ollama または vLLM を介して提供される gemma4:31b など)。LLM インフラストラクチャ自体のデプロイは、このガイドの対象外ですが、特定のモデルと API の要件については、LLM サービスの要件で詳しく説明されています。
  • GDC 環境に付属する Elastic のバージョンを取得します。

    export ELASTIC_VERSION=$(curl -sk "https://harbor.${ORG}.${ZONE}.${GDCH_DOMAIN}/api/v2.0/projects/library/repositories/elasticsearch%252Felasticsearch/artifacts" | jq -r '[.[].tags[]?.name] | sort_by(split(".") | map(tonumber)) | last')
    
  • このガイド全体で参照される環境変数を構成します。これらの値は、特定の環境に合わせて必要に応じて変更してください。

    # GDC environment configuration
    export PROJECT_ID="your-project-id"
    export CLUSTER_NAME="your-shared-cluster-name"
    export ZONE="your-zone-name"
    export USER_NAME="your-gdc-user-email"
    
    # Marketplace package and Elastic Stack configuration
    export MARKETPLACE_SERVICE_NAME="elastic"
    
    # LLM service configuration
    export LLM_CONNECTOR_NAME="your-llm-connector-name"
    export LLM_API_URL="http://your-llm-service-host:11434/v1/chat/completions"
    export LLM_MODEL_NAME="your-llm-model-name"
    export LLM_API_KEY="your-llm-api-key"
    
  • gdcloud を構成します。

    gdcloud config set project ${PROJECT_ID}
    gdcloud config set core/zone ${ZONE}
    
  • ターゲット GDC プロジェクト(${PROJECT_ID})でユーザーに必要なプロジェクト レベルの IAM ロールをリクエストまたはバインドします。GDC Marketplace のライフサイクル ドキュメントと GDC ネットワーキングの要件に基づき、少なくとも次のプロジェクト ロールが必要です。

    • Namespace 管理者(namespace-admin): 共有クラスタのプロジェクト Namespace 内で Kubernetes ワークロード、Secret、Service、カスタム リソースを作成して管理するための完全な管理者権限。
    • プロジェクト閲覧者(project-viewer): GDC コンソールと CLI で既存のプロジェクトとそのリソースを表示するための読み取りアクセス権。
    • Marketplace 編集者(marketplace-editor): プロジェクトで GDC Marketplace ソフトウェア パッケージをインストール、アップグレード、アンインストールする権限。
    • プロジェクト NetworkPolicy 管理者(project-networkpolicy-admin): Kibana と Elasticsearch の LoadBalancer サービスにプロジェクト間トラフィックと外部トラフィックが到達できるように、グローバル ProjectNetworkPolicy ルールを構成する権限。

    これらのロールを付与するよう、組織の IAM 管理者またはプロジェクトの IAM 管理者に依頼します(IAM 管理者権限がある場合は、次のコマンドを実行します)。

    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=namespace-admin
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=project-viewer
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=marketplace-editor
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=project-networkpolicy-admin
    
  • 共有クラスタの kubeconfig を取得し、kubectl の ku シェル エイリアスを構成します。

    KUBECONFIG=kubeconfig-${CLUSTER_NAME}.yaml gdcloud clusters \
      get-credentials ${CLUSTER_NAME} \
      --zone ${ZONE}
    
    alias ku="kubectl --kubeconfig=kubeconfig-${CLUSTER_NAME}.yaml"
    

Elastic マーケットプレイス パッケージをインストールする

このセクションでは、GDC Marketplace ライフサイクル ガイドに沿って、GDC エアギャップ Marketplace からプロジェクトの Namespace に Elastic Cloud on Kubernetes(ECK)オペレーターをインストールします。

gdcloud CLI(または GDC コンソール)を使用してインストールする

  1. 利用可能な Marketplace ソフトウェア パッケージを一覧表示して、サービス名と利用可能なバージョンを確認します。

    gdcloud marketplace services list
    

    出力は次のようになります。

    SERVICE      LATEST VERSION
    confluent    <CONFLUENT_VERSION>
    dataproc     <DATAPROC_VERSION>
    elastic      <ECK_OPERATOR_VERSION>
    mariadb      <MARIADB_VERSION>
    mattermost   <MATTERMOST_VERSION>
    mongodb      <MONGODB_VERSION>
    neo4j        <NEO4J_VERSION>
    redis        <REDIS_VERSION>
    starboard    <STARBOARD_VERSION>
    starburst    <STARBURST_VERSION>
    tes          <TES_VERSION>
    vault        <VAULT_VERSION>
    
  2. ターゲットの共有クラスタに Elastic Marketplace パッケージをインストールします。

    gdcloud marketplace services install ${MARKETPLACE_SERVICE_NAME} \
      --cluster=${CLUSTER_NAME} \
      --zone=${ZONE} \
      --project ${PROJECT_ID}
    

    *(または、GDC コンソールでプロジェクト ${PROJECT_ID} を選択し、[Marketplace] に移動して、[Elastic Cloud on Kubernetes] リストを選択し、[インストール] をクリックして、クラスタ ${CLUSTER_NAME} を選択し、[インストール] をクリックします)。*

  3. elastic-operator StatefulSet と Pod がプロジェクトの名前空間(${PROJECT_ID})で実行されていることを確認します。

    ku get statefulset,pods -n ${PROJECT_ID}
    

    出力は次のようになります。

    NAME                                READY   AGE
    statefulset.apps/elastic-operator   1/1     3m
    
    NAME                   READY   STATUS    RESTARTS   AGE
    pod/elastic-operator-0 1/1     Running   0          3m
    

Elastic Stack をデプロイする

Elastic Stack マニフェストを作成する

GDC Marketplace パッケージは、ローカルのエアギャップ Harbor レジストリから Elasticsearch と Kibana のコンテナ イメージを pull するように事前構成された ECK オペレーターをデプロイします。

elastic-stack.yaml を作成して、1 ノードの Elasticsearch クラスタ、Kibana UI インスタンス、LoadBalancer HTTP サービス、allow-all NetworkPolicy をプロジェクトの名前空間(${PROJECT_ID})にデプロイします。このマニフェストには、ECK オペレーターによって調整されたライセンス Secret も含まれています。ここでは、独自のエンタープライズ ライセンスをまだお持ちでない場合に備えて、30 日間のエンタープライズ トライアル(eck-trial-license)として構成されています。

cat <<EOF > elastic-stack.yaml
apiVersion: v1
kind: Secret
metadata:
  name: eck-trial-license
  namespace: ${PROJECT_ID}
  labels:
    license.k8s.elastic.co/type: enterprise_trial
  annotations:
    elastic.co/eula: accepted
---
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: elastic-cluster
  namespace: ${PROJECT_ID}
spec:
  version: ${ELASTIC_VERSION}
  http:
    service:
      spec:
        type: LoadBalancer
  nodeSets:
    - name: default
      count: 1
      config:
        # Disable mmap to avoid requiring privileged host sysctl modifications
        node.store.allow_mmap: false
      podTemplate:
        spec:
          initContainers:
            - name: sysctl
              securityContext:
                privileged: false
                allowPrivilegeEscalation: false
                runAsNonRoot: true
                capabilities:
                  drop:
                    - ALL
              command: ["sh", "-c", "true"]
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 2Gi
                  cpu: 500m
                limits:
                  memory: 4Gi
                  cpu: 2
      volumeClaimTemplates:
        - metadata:
            name: elasticsearch-data
          spec:
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 10Gi
---
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: kibana-ui
  namespace: ${PROJECT_ID}
spec:
  version: ${ELASTIC_VERSION}
  count: 1
  elasticsearchRef:
    name: elastic-cluster
  http:
    service:
      spec:
        type: LoadBalancer
  podTemplate:
    spec:
      containers:
        - name: kibana
          resources:
            requests:
              memory: 1Gi
              cpu: 200m
            limits:
              memory: 2Gi
              cpu: 1
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
  namespace: ${PROJECT_ID}
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - ipBlock:
            cidr: 0.0.0.0/0
EOF

デプロイを適用して確認する

次のようにマニフェストを適用します。

ku apply -f elastic-stack.yaml

グローバル ProjectNetworkPolicy を適用して、GDC ファイアウォール ルールが LoadBalancer IP へのインバウンド トラフィックと、プロジェクト間のトラフィック(LLM サービスとの通信など)を許可するようにします。

kubectl --kubeconfig=kubeconfig-global.yaml apply -f - <<EOF
apiVersion: networking.global.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  namespace: ${PROJECT_ID}
  name: allow-inbound-traffic-from-external
spec:
  policyType: Ingress
  subject:
    subjectType: UserWorkload
  ingress:
    - from:
        - ipBlock:
            cidr: 0.0.0.0/0
---
apiVersion: networking.global.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  namespace: ${PROJECT_ID}
  name: allow-inbound-traffic-from-all-projects
spec:
  policyType: Ingress
  subject:
    subjectType: UserWorkload
  ingress:
    - from:
        - projects: {}
EOF

Elasticsearch と Kibana の両方が Running ステータスと Ready ステータスに達し、${CLUSTER_NAME} で LoadBalancer IP を取得するまで、Pod、サービス、カスタム リソースをモニタリングします。

ku get pods,svc,elasticsearch,kibana -n ${PROJECT_ID}

リソースの準備が整うと、出力は次のようになります。

NAME                                READY   STATUS    RESTARTS   AGE
pod/elastic-cluster-es-default-0    1/1     Running   0          3m
pod/elastic-operator-0              1/1     Running   0          15m
pod/kibana-ui-kb-869844478d-59ptw   1/1     Running   0          3m

NAME                                                                      TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
service/elastic-cluster-es-default                                        ClusterIP      None            <none>         9200/TCP         11m
service/elastic-cluster-es-http                                           LoadBalancer   10.101.18.181   <ELASTIC_EXTERNAL_IP>   9200:32159/TCP   11m
service/elastic-cluster-es-internal-http                                  ClusterIP      10.101.18.81    <none>         9200/TCP         11m
service/elastic-cluster-es-transport                                      ClusterIP      None            <none>         9300/TCP         11m
service/g-svc-g-fre-elastic-cluster-es-http-shared-clus-extern-56d5eb67   ClusterIP      None            <none>         9200/TCP         11m
service/g-svc-g-fre-kibana-ui-kb-http-shared-cluster-0b-extern-75a78b1f   ClusterIP      None            <none>         5601/TCP         11m
service/kibana-ui-kb-http                                                 LoadBalancer   10.101.19.86    <KIBANA_EXTERNAL_IP>   5601:32735/TCP   11m

NAME                                                         HEALTH   NODES   VERSION             PHASE   AGE
elasticsearch.elasticsearch.k8s.elastic.co/elastic-cluster   green    1       <ELASTIC_VERSION>   Ready   3m

NAME                                     HEALTH   NODES   VERSION             AGE
kibana.kibana.k8s.elastic.co/kibana-ui   green    1       <ELASTIC_VERSION>   3m

Elasticsearch と Kibana に割り当てられた外部 LoadBalancer IP をエクスポートして、以降の手順で使用します。

export ELASTICSEARCH_IP=$(ku get svc elastic-cluster-es-http \
  -n ${PROJECT_ID} \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

export KIBANA_IP=$(ku get svc kibana-ui-kb-http \
  -n ${PROJECT_ID} \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "Kibana External URL:        https://${KIBANA_IP}:5601"
echo "Elasticsearch External URL: https://${ELASTICSEARCH_IP}:9200"

認証情報を取得して Kibana UI にアクセスする

elastic スーパーユーザーのパスワードを取得する

ECK は、デフォルトの elastic スーパーユーザー パスワードを含む Kubernetes Secret(elastic-cluster-es-elastic-user)を自動的に生成します。次のコマンドで取得してデコードします。

export ELASTIC_PASSWORD=$(ku get secret elastic-cluster-es-elastic-user \
  -n ${PROJECT_ID} \
  -o jsonpath='{.data.elastic}' | base64 -d)
echo "${ELASTIC_PASSWORD}"

接続を確認してログインする

  1. ワークステーションから HTTPS 経由で Elasticsearch にアクセスできることを確認します。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" "https://${ELASTICSEARCH_IP}:9200"
    
  2. ブラウザで https://${KIBANA_IP}:5601 を開き、自己署名証明書の警告を受け入れて、次の認証情報でログインします。

    • ユーザー名: elastic
    • パスワード: <output from ELASTIC_PASSWORD above>

Elastic ライセンスを確認する

Elastic の生成 AI 機能(Elastic AI エージェント、Agent Builder、Attack Discovery など)と外部 LLM コネクタには、Enterprise ライセンス(または有効な 30 日間の Enterprise トライアル ライセンス)が必要です。

Elasticsearch クラスタが ECK オペレーターによって管理されている場合、ECK の license-controller は、オペレーターの名前空間(${PROJECT_ID})内の Kubernetes Secret とクラスタ ライセンスを継続的に調整します。本番環境の Enterprise ライセンスをまだお持ちでない場合、elastic-stack.yaml(license.k8s.elastic.co/type: enterprise_trial)に含まれる eck-trial-license Secret は、30 日間の Enterprise トライアル ライセンスを自動的にプロビジョニングして維持するよう ECK に指示します。(ECK ライセンス Secret なしで POST /_license/start_trial を介して手動でトライアルを開始すると、ECK は次の調整サイクルでクラスタを basic に戻します。)

CLI で有効なライセンスを確認するには:

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${ELASTICSEARCH_IP}:9200/_license" | jq .

*(または、Kibana でナビゲーション メニューを開き、[Management] > [Dev Tools] に移動して、[Console] タブで GET /_license を実行します)。*

出力は次のようになります。"status": "active" と "type": "enterprise"(試用ライセンスを使用している場合は "type": "trial")が表示されます。

{
  "license": {
    "status": "active",
    "uid": "d8c2b566-8c2b-436c-a1ea-5502d6790505",
    "type": "trial",
    "issue_date": "2026-09-17T23:19:25.053Z",
    "issue_date_in_millis": 1789687165053,
    "expiry_date": "2026-10-17T23:19:25.053Z",
    "expiry_date_in_millis": 1792279165053,
    "max_nodes": 1000,
    "max_resource_units": null,
    "issued_to": "elastic-cluster",
    "issuer": "elasticsearch",
    "start_date_in_millis": -1
  }
}

組み込みのサンプル データセットを読み込む

カスタムのデータ取り込みパイプラインを作成したり、外部インターネット アクセスを必要としたりせずに、Elastic の AI 機能を評価するには、Kibana の組み込みのサンプル ウェブログ データセット(kibana_sample_data_logs)を読み込みます。サンプルデータ アーカイブは Kibana コンテナ イメージ内にバンドルされているため、エアギャップ環境に数秒でインストールされます。このデータセットには、クライアント IP アドレス(clientip)、HTTP ステータス コード(response)、送信元と送信先の国(geo.src、geo.dest)、転送バイト数(bytes)、URL、ユーザー エージェント文字列を含む 14,074 件のウェブアクセス ログ ドキュメントが入力されます。

その他の組み込みデータセット(flights と ecommerce)については、Kibana でサンプルデータを追加すると elastic-datasets-builtin.md をご覧ください。

データセットを読み込む

次のコマンドを実行して、Kibana の内部サンプルデータ エンドポイントを介して kibana_sample_data_logs を読み込みます。

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  -X POST "https://${KIBANA_IP}:5601/api/sample_data/logs" \
  -H "kbn-xsrf: true" \
  -H "x-elastic-internal-origin: Kibana"

サンプル データセットを確認する

logs サンプル データセットが Kibana で installed とマークされていることを確認し、バッキング データ ストリームに Elasticsearch の 14,074 ドキュメントが含まれていることを確認します。

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${KIBANA_IP}:5601/api/sample_data" \
  -H "kbn-xsrf: true" \
  -H "x-elastic-internal-origin: Kibana" \
  | jq '.[] | select(.id == "logs") | {id, name, status}'

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${ELASTICSEARCH_IP}:9200/_cat/indices/kibana_sample_data_logs*?v"

出力は次のようになります。

{
  "id": "logs",
  "name": "Sample web logs",
  "status": "installed"
}
health status index                                         uuid                   pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open   .ds-kibana_sample_data_logs-2026.09.22-000001 0YivG3oFQQmb-mYqmULr_g   1   1      14074            0      8.7mb          8.7mb        8.7mb

LLM サービスの要件

GDC エアギャップ環境では、クラウドホスト型の Elastic マネージド LLM と外部のパブリック SaaS LLM プロバイダにアクセスできません。GDC 環境に応じて、次の操作を行います。

  • GDC のマネージド Gemini: Gemini をマネージド LLM サービスとして提供する GDC 環境では、利用可能な場合、Kibana をマネージド Gemini サービスに直接接続できます。
  • セルフマネージド LLM サービス: または、GDC 環境(GDC GPU ノードなど)でホストされているセルフマネージド LLM サービスに Elastic を接続することもできます。

セルフマネージド LLM サービスのデプロイは、このガイドの範囲外です。Elastic 用のセルフマネージド LLM サービスをプロビジョニングまたは選択する場合は、Elastic の公式ガイドから派生した要件を満たしていることを確認してください。

技術要件の概要

次のポイントは、セルフマネージド LLM サービスが Elastic の AI Agent、Agent Builder、Attack Discovery と確実に連携するために必要な、主要な API、モデル機能、コンテキスト ウィンドウ、ランタイム要件をまとめたものです。次のセクションで引用されている特定の構成パラメータ名(num_ctx、num_predict、repeat_penalty、repeat_last_n、OLLAMA_FLASH_ATTENTION、OLLAMA_NUM_PARALLEL など)は、Ollama を例として使用しています。vLLM などの別の推論エンジンを使用する場合は、同等の設定を適用します。

  • ネイティブ関数呼び出しを備えた OpenAI 互換の Chat Completions API(/v1/chat/completions):
    • OpenAI 互換の /v1/chat/completions エンドポイント("stream": true 経由の Server-Sent Events ストリーミングを含む)を公開するセルフマネージド推論サーバー(Ollama や vLLM など)は、Kibana の OpenAI コネクタを使用して統合できます。プロバイダは Other(OpenAI 互換サービス)に設定します。
    • クラウドネイティブ ツール呼び出しをサポートするモデル(gemma4:31b など)を使用する場合は、Kibana コネクタでネイティブ関数呼び出しを有効にします("enableNativeFunctionCalling": true)。デフォルト("enableNativeFunctionCalling": false)では、Kibana はプロンプトベースのシミュレートされた関数呼び出しを使用します。これは、ネストされたツール呼び出し中に信頼性が低く、解析エラー(Error: Missing name for tool use など)で失敗する可能性があります。ネイティブ関数呼び出しを有効にすると、ツール呼び出しのフォーマットが推論サーバーの OpenAI tools プロトコルに直接委任されます。
  • ネイティブ ツールと関数呼び出し機能を備えた指示チューニング型モデル:
    • Elastic AI Agent(elastic-ai-agent)とカスタム Agent Builder エージェントは、マルチステップ ツール呼び出し(platform.core.list_indices、platform.core.get_index_mapping、platform.core.search、platform.core.execute_esql、platform.core.cases)に依存しています。これらのツールを使用して、インデックス マッピングの検出、Elasticsearch に対する ES|QL クエリの生成と実行、結果の合成を行います。
    • 選択するモデルは、指示に基づいてチューニングされ、有効なクラウドネイティブ ツール呼び出し、構造化された JSON 出力、ES|QL クエリを生成できる必要があります。モデルの評価基準については、Elastic の LLM performance matrix for Observability と LLM performance matrix for Elastic Security をご覧ください。
  • 128,000 トークン以上のコンテキスト ウィンドウ(例: Ollama の num_ctx: 131072):
    • Elastic の一般的なセルフマネージド カスタム LLM のドキュメントでは、最小 64,000 トークンが引用されていますが、Elastic AI エージェント(elastic-ai-agent)には 128,000 トークンのコンテキスト ウィンドウ(131,072 トークン)を強くおすすめします。
    • マルチステップの調査では、エージェントはシステム指示とツール定義をインデックス スキーマ、ES|QL ドキュメント、未加工の検索結果と組み合わせます(たとえば、集計前にフィールド値を検査するために最大 100 個のサンプル ドキュメントを取得します)。1 回の調査で会話履歴が 70,000 トークンを超えることがあり、64,000 トークンのコンテキスト ウィンドウでは会話の途中でプロンプトが切り捨てられます。
    • Ollama では、モデルの Modelfile に組み込まれた PARAMETER num_ctx ディレクティブがコンテナの OLLAMA_CONTEXT_LENGTH 環境変数をオーバーライドします。モデルの Modelfile で OLLAMA_CONTEXT_LENGTH=131072 と PARAMETER num_ctx 131072 の両方が構成されていることを確認します(80 GB の NVIDIA A100 または H100 GPU では、128K コンテキストの gemma4:31b が VRAM に快適に収まります)。
  • 出力トークンの上限と緩やかな繰り返しペナルティ(Ollama の num_predict: 8192 や repeat_penalty: 1.05 など):
    • Kibana の OpenAI ストリーム アダプタは、"finish_reason": "length" を致命的な Token limit reached エラーとして扱い、すべてのリクエストで "temperature": 0(貪欲デコード)を送信します。
    • 推論可能なモデル(ツール呼び出しを発行する前に内部思考トークンを生成する gemma4:31b など)の場合は、出力トークン上限を 8,192 トークン以上に設定し、穏やかな繰り返しペナルティを構成します。たとえば、Ollama で num_predict: 8192、repeat_penalty: 1.05、repeat_last_n: 64 を設定します。過度に積極的な繰り返しペナルティ(大きなウィンドウでの 1.15+ など)は避けてください。モデルが内部の推論ブロックから最終的なツール呼び出し JSON に ES|QL クエリをコピーする際に、ペナルティが課せられます。
  • GPU アクセラレーション、Flash Attention、並列 KV キャッシュ スロット(1 ターンあたり 60 秒未満):
    • Kibana の OpenAI コネクタは、ストリーミング ターンごとにデフォルトの 60 秒の HTTP タイムアウトを適用します。
    • 1 回のエージェント ターンでネストされた LLM サブコールがトリガーされることがあるため(たとえば、メインのエージェント会話がアクティブな間に自然言語の検索リクエストを ES|QL クエリに変換するなど)、KV キャッシュ スロットが 1 つしかない推論サーバーを実行すると、サブコールによってメインの会話のキャッシュに保存されたコンテキストが削除され、後続のターンが遅くなる可能性があります。
    • Ollama で OLLAMA_FLASH_ATTENTION=1 と OLLAMA_NUM_PARALLEL=2 を設定して、Flash Attention が長いコンテキストの評価を高速化し、メインの会話とネストされたサブ呼び出しが GPU メモリに個別の KV キャッシュ スロットを保持するようにします。

Kibana で LLM コネクタを構成する

OpenAI 互換の LLM サービスが実行され、Kibana Pod からアクセスできるようになったら、CLI または Kibana UI を使用して Kibana で OpenAI コネクタを構成します。

オプション 1: CLI を使用してコネクタを作成する

  1. 次の POST /api/actions/connector コマンドを実行して、ネイティブ関数呼び出しが有効("enableNativeFunctionCalling": true)な LLM サービス(${LLM_API_URL})を指す OpenAI(.gen-ai)コネクタを作成します。Kibana は、一意のコネクタ ID を自動的に生成して割り当てます。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/actions/connector" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"name\": \"${LLM_CONNECTOR_NAME}\",
        \"connector_type_id\": \".gen-ai\",
        \"config\": {
          \"apiProvider\": \"Other\",
          \"apiUrl\": \"${LLM_API_URL}\",
          \"defaultModel\": \"${LLM_MODEL_NAME}\",
          \"enableNativeFunctionCalling\": true
        },
        \"secrets\": {
          \"apiKey\": \"${LLM_API_KEY}\"
        }
      }" | jq .
    
  2. Kibana のコネクタを一覧表示し、名前でフィルタして、${LLM_CONNECTOR_NAME} コネクタが存在し、シークレットが欠落していないことを確認します。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${KIBANA_IP}:5601/api/actions/connectors" \
      -H "kbn-xsrf: true" \
      | jq ".[] | select(.name == \"${LLM_CONNECTOR_NAME}\")"
    

    出力は次のようになります。

    {
      "id": "<GENERATED_CONNECTOR_ID>",
      "name": "<LLM_CONNECTOR_NAME>",
      "config": {
        "apiProvider": "Other",
        "apiUrl": "<LLM_API_URL>",
        "defaultModel": "<LLM_MODEL_NAME>",
        "enableNativeFunctionCalling": true
      },
      "connector_type_id": ".gen-ai",
      "is_preconfigured": false,
      "is_deprecated": false,
      "referenced_by_count": 0,
      "is_missing_secrets": false,
      "is_system_action": false
    }
    
  3. CLI から Kibana と LLM サービス間のエンドツーエンドの接続をテストするには、名前で自動生成されたコネクタ ID を検索し、その _execute エンドポイントを呼び出します。

    export CONNECTOR_ID=$(curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${KIBANA_IP}:5601/api/actions/connectors" \
      -H "kbn-xsrf: true" \
      | jq -r ".[] | select(.name == \"${LLM_CONNECTOR_NAME}\") | .id")
    
    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/actions/connector/${CONNECTOR_ID}/_execute" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"params\": {
          \"subAction\": \"invokeAI\",
          \"subActionParams\": {
            \"model\": \"${LLM_MODEL_NAME}\",
            \"messages\": [
              {
                \"role\": \"user\",
                \"content\": \"Reply with the single word Pong.\"
              }
            ]
          }
        }
      }" | jq .
    

    出力は次のようになります。"status": "ok" とモデルのレスポンスが表示されます。

    {
      "status": "ok",
      "data": {
        "message": "Pong",
        "usage": {
          "prompt_tokens": 23,
          "prompt_tokens_details": {
            "cached_tokens": 0
          },
          "completion_tokens": 38,
          "total_tokens": 61
        }
      },
      "connector_id": "<GENERATED_CONNECTOR_ID>"
    }
    

オプション 2: Kibana UI でコネクタを作成する

  1. Kibana(https://${KIBANA_IP}:5601)にログインします。
  2. メイン ナビゲーション メニューを開き、[管理] > [アラートと分析情報] > [コネクタ] に移動します。
  3. [コネクタを作成] をクリックし、[OpenAI] を選択します。
  4. コネクタのフィールドを構成します。
    • コネクタ名: ${LLM_CONNECTOR_NAME} の値を入力します(例: Gemma4-31B)。
    • OpenAI プロバイダを選択する: [その他(OpenAI 互換サービス)] を選択します。
    • URL: ${LLM_API_URL}(LLM サービスの /v1/chat/completions エンドポイント)の値を入力します。
    • デフォルト モデル: ${LLM_MODEL_NAME} の値を入力します(例: gemma4:31b)。
    • API キー: ${LLM_API_KEY} の値を入力します(サーバーがベアラー認証を適用していない場合は空でない文字列、またはサーバーの API キー)。
    • ネイティブ関数呼び出しを有効にする: 切り替えをオンにします。
  5. [Save & test] をクリックしてコネクタを保存し、[Test] タブをすぐに開きます。
  6. [テスト] タブで、サンプルのプロンプト(Reply with the single word Pong. など)を入力し、[実行] をクリックして、Kibana が LLM サービスから有効なレスポンスを受信することを確認します。

LLM 統合をテストする

kibana_sample_data_logs データセットがインストールされ、LLM コネクタが検証されたら、Kibana で Elastic の AI 機能をテストできます。

Elastic AI Agent

Elastic AI Agent(elastic-ai-agent)は、自然言語を使用して Elasticsearch 全体でデータを探索、分析するために Kibana に統合されているデフォルトの会話エージェントです。構成済みの LLM コネクタと組み込みの Agent Builder ツールを活用して、エージェントは Elasticsearch のインデックスとマッピング(platform.core.list_indices、platform.core.get_index_mapping)を自動的に検出し、ES|QL クエリ(platform.core.search、platform.core.execute_esql)を生成して実行し、基盤となるスキーマを事前に把握していなくても、結果を合成します。また、カスタマイズされた指示とツールを使用して独自のカスタム エージェントを構築することもできます。詳細については、Agent Builder を使用したカスタム エージェントをご覧ください。

Kibana UI で Elastic AI エージェントを開いて構成するには:

  1. Kibana のメイン ナビゲーション メニューを開き、[AI Agent] を選択します(または、上部のヘッダーバーにある [Ask AI] ボタンをクリックするか、https://${KIBANA_IP}:5601/app/chat に直接移動します)。
  2. アクティブなエージェントとして Elastic AI Agent が選択され、モデルコネクタとして LLM コネクタ(${LLM_CONNECTOR_NAME})が選択されていることを確認します。

例 1: 失敗した HTTP リクエストと潜在的なスキャナーを調査する

kibana_sample_data_logs で 200 以外の HTTP レスポンスを生成している上位のクライアント IP アドレス、その送信元の国、それらの IP のいずれかで HTTP 200 トラフィックが成功したかどうかを特定するよう Elastic AI エージェントに依頼します。

  • オプション 1: Kibana UI を使用する: AI Agent チャット プロンプトに次の質問を入力します。

    In kibana_sample_data_logs, which client IP addresses had the most failed HTTP requests (non-200 status codes), what countries did they come from, and did any of those IPs also have successful HTTP 200 requests?

  • オプション 2: CLI を使用する

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"agent_id\": \"elastic-ai-agent\",
        \"connector_id\": \"${CONNECTOR_ID}\",
        \"input\": \"In kibana_sample_data_logs, which client IP addresses had the most failed HTTP requests (non-200 status codes), what countries did they come from, and did any of those IPs also have successful HTTP 200 requests?\"
      }" | jq '{
        status: .status,
        tools_called: [.steps[] | select(.type == "tool_call") | {tool_id: .tool_id, params: .params}],
        response: .response.message
      }'
    

具体的な仕組みを説明しましょう。

  1. エージェントは platform.core.get_index_mapping(indices=["kibana_sample_data_logs"]) を呼び出して、使用可能なフィールド(clientip、geo.src、response、bytes、request など)を検査します。
  2. platform.core.search を呼び出してサンプル ドキュメントを検査し、HTTP ステータス コードが response に保存されていることを確認します。次に、ES|QL 集計を実行して、clientip と geo.src で 200 以外のリクエストをランク付けし、上位の IP のうち response == "200" トラフィックも記録した IP を確認します。

    FROM kibana_sample_data_logs
    | WHERE response != "200"
    | STATS failed_count = COUNT(*) BY clientip, geo.src
    | SORT failed_count DESC
    | LIMIT 10
    
    FROM kibana_sample_data_logs
    | WHERE clientip IN ("30.156.16.164", "213.235.30.99", "221.241.228.46", "209.94.234.133", "7.127.73.138", "247.240.202.244", "130.125.65.24", "164.85.94.243", "81.194.200.150", "30.145.143.210") AND response == "200"
    | STATS success_count = COUNT(*) BY clientip
    

Elastic AI エージェントのレスポンスは次のようになります。

kibana_sample_data_logs で失敗した HTTP リクエスト(200 以外のステータス コード)が最も多いクライアント IP アドレスは 30.156.16.164 です。これは米国から発信され、100 件の失敗したリクエストが記録されています。

リクエストが失敗したその他の上位 IP アドレス(すべて米国から発信されたもの)は次のとおりです。

  • 213.235.30.99: 6 件の失敗
  • 221.241.228.46: 6 回の失敗
  • 209.94.234.133: 6 件の失敗
  • 7.127.73.138: 5 件の失敗
  • 247.240.202.244: 5 回の失敗
  • 130.125.65.24: 5 件の失敗
  • 164.85.94.243: 4 件の失敗
  • 81.194.200.150: 4 件の失敗
  • 30.145.143.210: 4 件の障害

成功したリクエストについては、上記の IP はすべて、少なくとも一部の HTTP 200 レスポンスが成功しています。ただし、30.156.16.164 は成功したリクエストがありません。

例 2: HTTP ステータス コード別にトラフィック量とペイロード バイト数を分析する

Elastic AI エージェントに、kibana_sample_data_logs のすべてのリクエストにおける HTTP レスポンス ステータス コードの内訳と、各ステータス コードで転送された合計バイト数を尋ねます。

  • オプション 1: Kibana UI を使用する: AI Agent で新しい会話を開始し、次の質問を入力します。

    In kibana_sample_data_logs, what is the breakdown of HTTP response status codes across all requests, and how much total data (in bytes) was transferred for each status code?

  • オプション 2: CLI を使用する:

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"agent_id\": \"elastic-ai-agent\",
        \"connector_id\": \"${CONNECTOR_ID}\",
        \"input\": \"In kibana_sample_data_logs, what is the breakdown of HTTP response status codes across all requests, and how much total data (in bytes) was transferred for each status code?\"
      }" | jq '{
        status: .status,
        tools_called: [.steps[] | select(.type == "tool_call") | {tool_id: .tool_id, params: .params}],
        response: .response.message
      }'
    

バックグラウンドで起きていること:

  1. エージェントは、platform.core.get_index_mapping を介して kibana_sample_data_logs スキーマを検査し、response.keyword フィールドと bytes フィールドを特定します。
  2. platform.core.search を呼び出し、次の ES|QL 集計を生成して実行します。

    FROM kibana_sample_data_logs
    | STATS request_count = COUNT(*), total_bytes = SUM(bytes) BY response.keyword
    | LIMIT 100
    

Elastic AI エージェントのレスポンスは次のようになります。

kibana_sample_data_logs で転送された HTTP レスポンス ステータス コードの内訳と合計データは次のとおりです。

ステータス コード リクエスト数 転送されたデータの合計(バイト)
200 12,832 75,681,246
404 801 4,044,443
503 441 0

Agent Builder を使用したカスタム エージェント

Agent Builder を使用すると、カスタム システム指示を組み込みツール(platform.core.cases、platform.core.execute_esql、platform.core.search など)やユーザー定義ツール(esql、index_search、workflow、mcp)と組み合わせることで、特定の運用ワークフローに合わせてカスタマイズされた AI エージェントを作成できます。

次の例では、kibana_sample_data_logs データセットを直接操作する Web Security Incident Responder エージェントを構築します。自然言語プロンプトが指定されると、エージェントは次の処理を行います。

  1. カスタム ES|QL ツール(investigate_suspicious_web_clients)を実行して、kibana_sample_data_logs 内の 200 以外の HTTP レスポンスをクライアント IP 別に集計します。
  2. 悪意のある偵察や悪用アクティビティ(30.156.16.164)を、無害なダウンロード エラーと区別します。
  3. カスタム Kibana ワークフロー ツール(create_security_case)を呼び出して、検出結果と修復手順を含む構造化された Kibana セキュリティ ケースを自動的に開きます。

カスタム ES|QL とケース作成ツールを作成する

  1. 200 以外の HTTP ステータス コードを生成する上位のクライアント IP アドレス、ステータス コード、送信元の国(geo.src)、ユーザー エージェント(agent)、リクエストされた URL(request)を kibana_sample_data_logs にクエリする、investigate_suspicious_web_clients という名前のカスタム esql ツールを作成します。

    • オプション 1: Kibana UI を使用する:

      1. Kibana で、メイン ナビゲーション メニューを開き、[Elasticsearch] > [エージェント] に移動して、[すべてのツールを管理](または https://${KIBANA_IP}:5601/app/agent_builder/tools に直接移動)をクリックし、[新しいツール] をクリックします。
      2. ツール フィールドを構成します。

        • ツール ID: investigate_suspicious_web_clients
        • タイプ: ES|QL
        • 説明: Finds the top client IP addresses generating non-200 HTTP error responses in kibana_sample_data_logs, returning their error count, status codes, source country, user agents, and requested URL paths.
        • ラベル: security、logs
        • ES|QL クエリ:

          FROM kibana_sample_data_logs | WHERE response != "200" | STATS error_count = COUNT(*), status_codes = VALUES(response), country = VALUES(geo.src), user_agents = VALUES(agent), sample_requests = VALUES(request) BY clientip | SORT error_count DESC | LIMIT 5
          
      3. [保存] をクリックします。

    • オプション 2: CLI を使用する

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/tools" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "id": "investigate_suspicious_web_clients",
          "type": "esql",
          "description": "Finds the top client IP addresses generating non-200 HTTP error responses in kibana_sample_data_logs, returning their error count, status codes, source country, user agents, and requested URL paths.",
          "tags": ["security", "logs"],
          "configuration": {
            "query": "FROM kibana_sample_data_logs | WHERE response != \"200\" | STATS error_count = COUNT(*), status_codes = VALUES(response), country = VALUES(geo.src), user_agents = VALUES(agent), sample_requests = VALUES(request) BY clientip | SORT error_count DESC | LIMIT 5",
            "params": {}
          }
        }' | jq '{id: .id, type: .type}'
      
  2. Agent Builder の組み込み platform.core.cases ツールは読み取り専用(既存のケースの検索と取得に使用)であるため、Kibana のネイティブ cases.createCase ステップを呼び出す Kibana ワークフロー(create-security-case)を作成し、create_security_case という名前の workflow ツールとして登録します。

    • オプション 1: Kibana UI を使用する

      1. メイン ナビゲーション メニューを開き、[管理] > [ワークフロー] に移動します(または https://${KIBANA_IP}:5601/app/workflows に直接移動します)。[新しいワークフローを作成] をクリックします。
      2. YAML エディタで、次のワークフロー定義を貼り付けて、[保存] をクリックします。

        version: "1"
        name: Create Security Case
        description: Creates a new Kibana Case in Elastic Security
        enabled: true
        triggers:
          - type: manual
        inputs:
          - name: title
            type: string
            required: true
            description: Title of the security case
          - name: description
            type: string
            required: true
            description: Detailed markdown description of the incident findings
          - name: severity
            type: string
            default: high
            description: Severity of the case (low, medium, high, critical)
        steps:
          - name: create_case
            type: cases.createCase
            with:
              title: ${{ inputs.title }}
              description: ${{ inputs.description }}
              severity: ${{ inputs.severity }}
              owner: securitySolution
              tags:
                - agent-builder
                - web-logs
        
      3. Agent Builder の [ツール] ページ(https://${KIBANA_IP}:5601/app/agent_builder/tools、または Elasticsearch > [エージェント] に移動して [すべてのツールを管理] をクリック)に戻り、[新しいツール] をクリックします。

      4. ワークフロー ツールのフィールドを構成して、[保存] をクリックします。

        • ツール ID: create_security_case
        • タイプ: ワークフロー
        • Workflow: [Create Security Case](create-security-case)を選択します。
        • 説明: Creates a new Kibana Case in Elastic Security with a title, markdown description of the incident findings, and severity (low, medium, high, critical).
        • ラベル: security、cases
    • オプション 2: CLI を使用する

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/workflows" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "workflows": [
            {
              "yaml": "version: \"1\"\nname: Create Security Case\ndescription: Creates a new Kibana Case in Elastic Security\nenabled: true\ntriggers:\n  - type: manual\ninputs:\n  - name: title\n    type: string\n    required: true\n    description: Title of the security case\n  - name: description\n    type: string\n    required: true\n    description: Detailed markdown description of the incident findings\n  - name: severity\n    type: string\n    default: high\n    description: Severity of the case (low, medium, high, critical)\nsteps:\n  - name: create_case\n    type: cases.createCase\n    with:\n      title: ${{ inputs.title }}\n      description: ${{ inputs.description }}\n      severity: ${{ inputs.severity }}\n      owner: securitySolution\n      tags:\n        - agent-builder\n        - web-logs\n"
            }
          ]
        }' | jq '.created[] | {id: .id, valid: .valid}'
      
      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/tools" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "id": "create_security_case",
          "type": "workflow",
          "description": "Creates a new Kibana Case in Elastic Security with a title, markdown description of the incident findings, and severity (low, medium, high, critical).",
          "tags": ["security", "cases"],
          "configuration": {
            "workflow_id": "create-security-case"
          }
        }' | jq '{id: .id, type: .type}'
      

カスタム インシデント対応エージェントを作成する

web-security-incident-responder エージェントを作成し、investigate_suspicious_web_clients、create_security_case、platform.core.cases ツールを割り当てます。

  • オプション 1: Kibana UI を使用する

    1. メイン ナビゲーション メニューを開き、[Elasticsearch] > [Agents] に移動するか、https://${KIBANA_IP}:5601/app/agent_builder/agents に直接移動して、[New agent] をクリックします。
    2. エージェントの詳細を構成します。

      • エージェント ID: web-security-incident-responder
      • 表示名: Web Security Incident Responder
      • 表示の説明: Investigates suspicious HTTP error activity in web access logs and opens Kibana Security Cases with the findings.
      • ラベル: security、incident-response
      • カスタム指示:

        You are an automated Web Security Incident Responder. When asked to investigate suspicious web traffic:
        1. Call the `investigate_suspicious_web_clients` tool to inspect the top client IPs generating non-200 HTTP responses in `kibana_sample_data_logs`.
        2. Identify any client IP exhibiting malicious reconnaissance or exploitation behavior (such as Nmap scans, WordPress login probing, `/etc/passwd` path traversal, or `/level/15/exec` command execution attempts) versus benign download errors.
        3. Call the `create_security_case` tool to open a new Kibana Security Case for the malicious client IP. Set an appropriate `title`, `severity` (`high`), and a well-structured Markdown `description` summarizing the source IP, geolocation, total failed requests, observed attack categories (mapped to MITRE ATT&CK where applicable), sample malicious URLs, and recommended remediation steps.
        4. Return a concise summary of your investigation and confirm the created Kibana Case details.
        
      • ツール: investigate_suspicious_web_clients、create_security_case、platform.core.cases を選択します。

    3. [保存] をクリックします。

  • オプション 2: CLI を使用する

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/agents" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "id": "web-security-incident-responder",
        "name": "Web Security Incident Responder",
        "description": "Investigates suspicious HTTP error activity in web access logs and opens Kibana Security Cases with the findings.",
        "labels": ["security", "incident-response"],
        "avatar_color": "#E7664C",
        "avatar_symbol": "IR",
        "configuration": {
          "instructions": "You are an automated Web Security Incident Responder. When asked to investigate suspicious web traffic:\n1. Call the `investigate_suspicious_web_clients` tool to inspect the top client IPs generating non-200 HTTP responses in `kibana_sample_data_logs`.\n2. Identify any client IP exhibiting malicious reconnaissance or exploitation behavior (such as Nmap scans, WordPress login probing, `/etc/passwd` path traversal, or `/level/15/exec` command execution attempts) versus benign download errors.\n3. Call the `create_security_case` tool to open a new Kibana Security Case for the malicious client IP. Set an appropriate `title`, `severity` (`high`), and a well-structured Markdown `description` summarizing the source IP, geolocation, total failed requests, observed attack categories (mapped to MITRE ATT&CK where applicable), sample malicious URLs, and recommended remediation steps.\n4. Return a concise summary of your investigation and confirm the created Kibana Case details.",
          "tools": [
            {
              "tool_ids": [
                "investigate_suspicious_web_clients",
                "create_security_case",
                "platform.core.cases"
              ]
            }
          ]
        }
      }' | jq '{id: .id, name: .name}'
    

エージェントを実行して Kibana ケースを確認する

  1. Web Security Incident Responder エージェントとの会話を実行します。

    • オプション 1: Kibana UI を使用する

      1. メイン ナビゲーション メニューを開き、[Elasticsearch] > [Agents] に移動します(または https://${KIBANA_IP}:5601/app/agent_builder に直接移動します)。
      2. エージェント セレクタから [Web Security Incident Responder] を選択し、LLM コネクタ(${LLM_CONNECTOR_NAME})が選択されていることを確認します。
      3. 次のプロンプトを入力します。

        Investigate the top client IPs generating HTTP errors in our web logs, identify any attacker performing reconnaissance or exploitation, and open a high-severity Kibana Security Case with your findings.

    • オプション 2: CLI を使用する

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d "{
          \"agent_id\": \"web-security-incident-responder\",
          \"connector_id\": \"${CONNECTOR_ID}\",
          \"input\": \"Investigate the top client IPs generating HTTP errors in our web logs, identify any attacker performing reconnaissance or exploitation, and open a high-severity Kibana Security Case with your findings.\"
        }" | jq '{
          status: .status,
          tools_called: [.steps[] | select(.type == "tool_call") | .tool_id],
          response: .response.message
        }'
      

      出力は次のようになります。

      {
        "status": "completed",
        "tools_called": [
          "investigate_suspicious_web_clients",
          "create_security_case"
        ],
        "response": "I have completed the investigation into the web logs. \n\nThe analysis identified client IP **`30.156.16.164`** (located in the US) as a malicious actor. This IP generated 100 failed requests (HTTP 404) exhibiting clear patterns of reconnaissance and exploitation:\n\n*   **Reconnaissance:** Extensive probing for WordPress installations and plugins, as well as Nmap scan signatures (e.g., `/nmaplowercheck...`).\n*   **Path Traversal:** Attempts to access sensitive system files via `/etc/passwd`.\n*   **Remote Command Execution (RCE):** Attempts to execute commands through paths such as `/level/15/exec`.\n*   **Application Probing:** Targeting common management interfaces including Tomcat, Axis2, and JMX consoles.\n\nAs a result, I have opened a **high-severity** Kibana Security Case to track this incident:\n\n*   **Case ID:** `1f0e00a5-cc89-4f62-b305-f286fae3d70e`\n*   **Title:** Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164\n*   **Recommended Action:** Immediately block the IP at the firewall/WAF and verify that no unauthorized access was achieved."
      }
      
  2. 新しく作成された Kibana セキュリティ ケースを確認します。

    • オプション 1: Kibana UI を使用する

      1. メイン ナビゲーション メニューを開き、[セキュリティ] > [ケース] に移動します(または、https://${KIBANA_IP}:5601/app/security/cases に直接移動します)。
      2. [Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164] をクリックして、フォーマットされた Markdown インシデントの概要、MITRE ATT&CK 分析、悪意のある URL のサンプル、修復チェックリストを表示します。
    • オプション 2: CLI を使用する

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        "https://${KIBANA_IP}:5601/api/cases/_find" \
        -H "kbn-xsrf: true" \
        | jq '.cases[0] | {id: .id, title: .title, severity: .severity, status: .status, tags: .tags, description: .description}'
      

      出力は次のようになります。

      {
        "id": "1f0e00a5-cc89-4f62-b305-f286fae3d70e",
        "title": "Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164",
        "severity": "high",
        "status": "open",
        "tags": [
          "agent-builder",
          "web-logs"
        ],
        "description": "### Incident Summary\n**Source IP:** `30.156.16.164`\n**Geolocation:** US\n**Total Failed Requests:** 100\n**Status Codes Observed:** 404\n\n### Analysis\nThe client IP exhibited clear patterns of malicious reconnaissance and exploitation attempts:\n\n- **Reconnaissance (MITRE ATT&CK T1595):** Extensive probing for WordPress installations (`/wp-login.php`, various plugin paths) and Nmap scan signatures (`/nmaplowercheck...`).\n- **Path Traversal (MITRE ATT&CK T1083):** Attempted to access sensitive system files via `/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd`.\n- **Remote Command Execution (MITRE ATT&CK T1203):** Attempts to execute commands via paths like `/level/15/exec/-/configure/http`.\n- **Application Probing:** Targeted common management interfaces such as `/tomcat/manager/html/`, `/axis2/axis2-admin/`, and `/jmx-console/`.\n\n### Sample Malicious URLs\n- `/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd`\n- `/level/15/exec/-/configure/http`\n- `/nmaplowercheck1331872976`\n- `/wp-login.php`\n\n### Recommended Remediation\n1. **Block IP:** Immediately block `30.156.16.164` at the network firewall/WAF.\n2. **Log Review:** Verify that all requests from this IP resulted in 404 errors and no unauthorized access was gained.\n3. **Hardening:** Ensure that sensitive directories (e.g., `/cgi-bin/`, `/manager/`) are properly restricted."
      }
      

攻撃の検出

Attack Discovery は、Elastic Security の AI 機能です。未解決のセキュリティ通知を分析し、MITRE ATT&CK フレームワークにマッピングして、関連するセキュリティ通知、IP アドレス、ユーザー アカウントを視覚的な攻撃チェーンに関連付けます。攻撃検出では、複数のアラートの構造化された JSON 出力を確実に合成するために大規模なモデルが必要となるため、30B クラス以上のモデル(gemma4:31b など)を使用します。

サンプルログからセキュリティ通知を生成する

デフォルトでは、kibana_sample_data_logs データセットには未加工のウェブアクセス ログが含まれており、Elastic Security アラート インデックス(.alerts-security.alerts-default)は入力されません。さらに、Kibana のサンプルデータ ジェネレータは、30.156.16.164 からの 100 件のスキャン リクエストとエクスプロイト リクエストに将来の @timestamp 値(最大 30 日先)を割り当てます。これにより、Elastic Security 検出ルールによってクエリされる now-30d から now のウィンドウの外に配置されます。

サンプルログを使用して Attack Discovery をデモするには、30.156.16.164 のレコードの @timestamp を 30 日戻し、Elastic Common Schema(ECS)の source.ip フィールドと user.name フィールドで拡充し、これらのイベントからセキュリティ通知を生成する 2 つの検出ルールを作成します。

  1. clientip を source.ip にマッピングし、user.name(www-data)を設定して、kibana_sample_data_logs* の 30.156.16.164 レコードで @timestamp を 30 日前にシフトします。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${ELASTICSEARCH_IP}:9200/kibana_sample_data_logs*/_update_by_query?conflicts=proceed&refresh=true" \
      -H "Content-Type: application/json" \
      -d '{
        "query": {
          "term": {
            "clientip": "30.156.16.164"
          }
        },
        "script": {
          "source": "ctx._source.source = [\"ip\": ctx._source.clientip]; ctx._source.user = [\"name\": \"www-data\"]; ctx._source[\"@timestamp\"] = ZonedDateTime.parse(ctx._source[\"@timestamp\"]).minusDays(30).toString();",
          "lang": "painless"
        }
      }' | jq '{updated: .updated, failures: .failures}'
    
  2. kibana_sample_data_logs* をターゲットとする 2 つの Elastic Security 検出ルールを作成します。最初のルールは Nmap と WordPress ログイン プロービング(Reconnaissance)を検出し、2 番目のルールは /etc/passwd パス トラバーサルとリモート コマンド実行の試行(Initial Access)を検出します。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/detection_engine/rules" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Suspicious Web Reconnaissance and Vulnerability Scanning",
        "description": "Detects Nmap active scanning and WordPress login/plugin probing in web access logs.",
        "risk_score": 47,
        "severity": "medium",
        "type": "query",
        "language": "kuery",
        "query": "clientip: \"30.156.16.164\" and (request: *nmap* or request: *wp-login*)",
        "index": ["kibana_sample_data_logs*"],
        "from": "now-30d",
        "interval": "1h",
        "max_signals": 2,
        "enabled": true,
        "threat": [
          {
            "framework": "MITRE ATT&CK",
            "tactic": {
              "id": "TA0043",
              "name": "Reconnaissance",
              "reference": "https://attack.mitre.org/tactics/TA0043/"
            },
            "technique": [
              {
                "id": "T1595",
                "name": "Active Scanning",
                "reference": "https://attack.mitre.org/techniques/T1595/"
              }
            ]
          }
        ]
      }' | jq '{id: .id, name: .name, enabled: .enabled}'
    
    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/detection_engine/rules" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Web Path Traversal and Remote Command Execution Attempt",
        "description": "Detects path traversal attempts targeting /etc/passwd and Cisco IOS privilege level 15 remote command execution (/level/15/exec).",
        "risk_score": 73,
        "severity": "high",
        "type": "query",
        "language": "kuery",
        "query": "clientip: \"30.156.16.164\" and (request: *passwd* or request: *exec*)",
        "index": ["kibana_sample_data_logs*"],
        "from": "now-30d",
        "interval": "1h",
        "max_signals": 2,
        "enabled": true,
        "threat": [
          {
            "framework": "MITRE ATT&CK",
            "tactic": {
              "id": "TA0001",
              "name": "Initial Access",
              "reference": "https://attack.mitre.org/tactics/TA0001/"
            },
            "technique": [
              {
                "id": "T1190",
                "name": "Exploit Public-Facing Application",
                "reference": "https://attack.mitre.org/techniques/T1190/"
              }
            ]
          }
        ]
      }' | jq '{id: .id, name: .name, enabled: .enabled}'
    
  3. 最初のルール実行が完了するまで約 60 秒待ってから、4 つのアラートが .alerts-security.alerts-default にインデックス登録されていることを確認します。

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${ELASTICSEARCH_IP}:9200/.alerts-security.alerts-default/_search?size=4" \
      | jq '.hits.hits[]._source | {rule: .["kibana.alert.rule.name"], source_ip: .source.ip, user_name: .user.name, request: .request}'
    

    出力は次のようになります。

    {
      "rule": "Web Path Traversal and Remote Command Execution Attempt",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd"
    }
    {
      "rule": "Web Path Traversal and Remote Command Execution Attempt",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd"
    }
    {
      "rule": "Suspicious Web Reconnaissance and Vulnerability Scanning",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/nmaplowercheck1331872976"
    }
    {
      "rule": "Suspicious Web Reconnaissance and Vulnerability Scanning",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/nmaplowercheck1331873003"
    }
    

Attack Discovery を実行する

  1. Kibana で、メイン ナビゲーション メニューを開き、[セキュリティ] > [攻撃の検出] に移動します(または、https://${KIBANA_IP}:5601/app/security/attack_discovery に直接移動します)。
  2. ページ上部のコネクタ メニューから LLM コネクタ(${LLM_CONNECTOR_NAME})を選択します。
  3. [生成] をクリックして、.alerts-security.alerts-default で未対応のアラートを分析します。

CLI から Attack Discovery をトリガーして検査することもできます。

export EXECUTION_UUID=$(curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  -X POST "https://${KIBANA_IP}:5601/api/attack_discovery/_generate" \
  -H "kbn-xsrf: true" \
  -H "elastic-api-version: 2023-10-31" \
  -H "Content-Type: application/json" \
  -d "{
    \"alertsIndexPattern\": \".alerts-security.alerts-default\",
    \"anonymizationFields\": [
      {\"id\": \"_id\", \"field\": \"_id\", \"allowed\": true},
      {\"id\": \"@timestamp\", \"field\": \"@timestamp\", \"allowed\": true},
      {\"id\": \"kibana.alert.original_time\", \"field\": \"kibana.alert.original_time\", \"allowed\": true},
      {\"id\": \"message\", \"field\": \"message\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.name\", \"field\": \"kibana.alert.rule.name\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.description\", \"field\": \"kibana.alert.rule.description\", \"allowed\": true},
      {\"id\": \"kibana.alert.severity\", \"field\": \"kibana.alert.severity\", \"allowed\": true},
      {\"id\": \"kibana.alert.risk_score\", \"field\": \"kibana.alert.risk_score\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.tactic.id\", \"field\": \"kibana.alert.rule.threat.tactic.id\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.tactic.name\", \"field\": \"kibana.alert.rule.threat.tactic.name\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.technique.id\", \"field\": \"kibana.alert.rule.threat.technique.id\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.technique.name\", \"field\": \"kibana.alert.rule.threat.technique.name\", \"allowed\": true},
      {\"id\": \"source.ip\", \"field\": \"source.ip\", \"allowed\": true},
      {\"id\": \"user.name\", \"field\": \"user.name\", \"allowed\": true}
    ],
    \"apiConfig\": {
      \"actionTypeId\": \".gen-ai\",
      \"connectorId\": \"${CONNECTOR_ID}\"
    },
    \"replacements\": {},
    \"size\": 20,
    \"subAction\": \"invokeAI\"
  }" | jq -r '.execution_uuid')

echo "Attack Discovery execution UUID: ${EXECUTION_UUID}"

生成が完了したら(約 60 ~ 90 秒)、検出された攻撃チェーンを取得します。

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${KIBANA_IP}:5601/api/attack_discovery/_find" \
  -H "kbn-xsrf: true" \
  -H "elastic-api-version: 2023-10-31" \
  | jq '.data[0]'

生成された攻撃検出では、4 つのアラートが 1 つのインシデントに関連付けられ、ソース IP、ユーザー アカウント、MITRE ATT&CK 戦術がリンクされます。

{
  "alert_ids": [
    "05c30118398ea2a8b70c55ce81fdb559dbe14c460a8a95253bd81719415a28b5",
    "73ebaeca440b463ffa5812da5b68a2485d465a648ffaf16385a54d8bae9a53d2",
    "60ae9f16b40cec1bdc27674ade0e5eb69680ccca3d956fbf2a75418843799987",
    "de503ae52444db7712d062c54750f413b90a75521cdfff0de9bdf41a64141346"
  ],
  "alert_rule_uuid": "attack_discovery_ad_hoc_rule_id",
  "alert_start": "2026-10-03T01:05:00.384Z",
  "alert_updated_at": "2026-10-03T01:05:03.460Z",
  "alert_workflow_status": "open",
  "alert_workflow_status_updated_at": "2026-10-03T01:05:03.460Z",
  "connector_id": "<GENERATED_CONNECTOR_ID>",
  "connector_name": "<GENERATED_CONNECTOR_ID>",
  "details_markdown": "- The attack sequence began at `2026-10-01T12:43:41.756Z` when an actor using IP `30.156.16.164` performed active reconnaissance scanning against the server.\n- Following the initial scan, the attacker attempted to exploit a path traversal vulnerability at `2026-10-01T12:43:51.756Z` and again at `2026-10-01T12:44:02.756Z`, specifically targeting the `/etc/passwd` file via the `mj_wwwusr` CGI endpoint.\n- The activity concluded with further reconnaissance scanning at `2026-10-01T12:44:21.756Z` from the same source IP `30.156.16.164`.\n- All requests were associated with the user `www-data` and resulted in 404 errors, suggesting the attempts were unsuccessful.",
  "entity_summary_markdown": "Activity targeting user `www-data` from source IP `30.156.16.164`.",
  "generation_uuid": "<EXECUTION_UUID>",
  "id": "1f71ed004c7340d5088d242ca2483525a164a9ea9a80a3f90bd1f514d43231bc",
  "mitre_attack_tactics": [
    "Reconnaissance",
    "Initial Access"
  ],
  "risk_score": 240,
  "summary_markdown": "An attacker from `30.156.16.164` performed reconnaissance and path traversal attempts against the system.",
  "timestamp": "2026-10-03T01:05:00.384Z",
  "title": "Web Reconnaissance and Exploitation Attempt",
  "user_id": "elastic",
  "user_name": "elastic",
  "users": [
    {
      "id": "elastic",
      "name": "elastic"
    }
  ],
  "index": ".internal.adhoc.alerts-security.attack.discovery.alerts-default-000001"
}