Implementación de las funciones potenciadas por IA generativa de Elastic Stack en GDC aislado

Descripción general

En esta guía, se describe la implementación de extremo a extremo de Elastic Stack (Elasticsearch y Kibana) en un clúster compartido aislado de Google Distributed Cloud (GDC) y se muestra cómo integrarlo con un servicio de modelo de lenguaje grande (LLM) para evaluar las funciones potenciadas por IA generativa de Elastic.

La implementación usa el operador oficial de Elastic Cloud on Kubernetes (ECK) disponible en el Marketplace de GDC aislado. El operador se instala en un espacio de nombres de proyecto en un clúster de Kubernetes compartido y automatiza el aprovisionamiento y la administración del ciclo de vida de los recursos de Elasticsearch y Kibana. Una vez implementado, Kibana se conecta a un extremo de inferencia de LLM (como Gemini administrado en GDC o un servicio autoadministrado compatible con OpenAI, como Ollama o vLLM). Esta integración potencia las funciones de IA generativa de Elastic, incluido el agente de IA de Elastic, los agentes personalizados creados con Agent Builder y Attack Discovery.

Esta guía se escribió para Elastic v9.4.0, Elastic Kubernetes Operator v3.4.0 y GDC v1.16.2. Algunas instrucciones pueden variar un poco si usas versiones diferentes.

Arquitectura

La arquitectura consiste en una implementación de Elastic Stack administrada por el operador de ECK dentro de un espacio de nombres de proyecto en un clúster de usuario compartido de GDC. Kibana se comunica a través de la red interna de GDC con un extremo de inferencia de LLM.

Diagrama de arquitectura de Elastic Stack.

Entre los componentes clave de esta arquitectura, se incluyen los siguientes:

  • Clúster compartido de GDC: Es un clúster de Kubernetes compartido con alcance de organización que se conecta a varios proyectos y aloja paquetes de software y cargas de trabajo de aplicaciones de GDC Marketplace.
  • Proyecto de GDC: Es el espacio de nombres del proyecto en el clúster compartido que aísla el operador de ECK, el clúster de Elasticsearch, la instancia de Kibana y los Secrets y las NetworkPolicies de Kubernetes asociados.
  • Operador de Elastic Cloud on Kubernetes (ECK): Se instala desde el Marketplace de GDC en el espacio de nombres del proyecto. El operador administra los recursos personalizados Elasticsearch y Kibana, y concilia la licencia de Elastic necesaria para las funciones potenciadas por IA generativa y los conectores de LLM externos.
  • Elasticsearch: Almacena y, además, indexa datos operativos y de seguridad, y ejecuta consultas de búsqueda y ES|QL generadas por las funciones potenciadas por IA y los agentes de Kibana.
  • Kibana: Aloja las funciones potenciadas por IA generativa de Elastic, incluido el agente de IA de Elastic (para la investigación de registros en lenguaje natural y la generación de ES|QL), el compilador de agentes (para crear agentes y herramientas conversacionales personalizados sobre los datos de Elasticsearch) y Attack Discovery (para la correlación de alertas de seguridad y el análisis de la cadena de ataques). Kibana también administra los conectores de acciones de LLM que organizan las llamadas a herramientas de varios pasos entre el usuario, Elasticsearch y el servicio de LLM.
  • Servicio de LLM: Es un servicio de LLM administrado (como Gemini en GDC) o un servidor de inferencia autoadministrado (como Ollama o vLLM) que proporciona capacidades de finalización de chat y llamada a herramientas a Kibana a través de la red interna de GDC.

Antes de comenzar

Asegúrate de que tu entorno de GDC cumpla con los siguientes requisitos previos y suposiciones antes de comenzar la implementación:

  • Clúster de Kubernetes y proyecto de GDC compartidos: Tienes acceso a un clúster compartido de GDC existente y a un proyecto de GDC conectado a ese clúster. El proyecto funciona como el contenedor de espacio de nombres para todos los recursos de Kubernetes que se implementan en esta guía. Asegúrate de que el clúster tenga suficientes recursos de procesamiento y almacenamiento persistente para alojar las cargas de trabajo del operador de ECK, Elasticsearch y Kibana.
  • Servicio de LLM: Tienes acceso de red a un servicio de LLM, ya sea el servicio administrado de Gemini (si está disponible en tu entorno de GDC) o un servicio de LLM autoadministrado compatible con OpenAI (por ejemplo, gemma4:31b entregado a través de Ollama o vLLM en un proyecto de GPU). La implementación de la infraestructura del LLM en sí está fuera del alcance de esta guía, pero los requisitos específicos del modelo y de la API se detallan en Requisitos para el servicio de LLM.
  • Recupera la versión de Elastic que se incluye en tu entorno de GDC:

    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')
    
  • Configura las variables de entorno a las que se hará referencia en esta guía. Modifica estos valores según sea necesario para que coincidan con tu entorno específico:

    # 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"
    
  • Configura gdcloud:

    gdcloud config set project ${PROJECT_ID}
    gdcloud config set core/zone ${ZONE}
    
  • Solicita o vincula los roles de IAM a nivel de proyecto necesarios para tu usuario en el proyecto de GDC de destino (${PROJECT_ID}). Según la documentación del ciclo de vida de GDC Marketplace y los requisitos de redes de GDC, se requieren, al menos, los siguientes roles de proyecto:

    • Administrador de espacio de nombres (namespace-admin): Acceso de administrador completo para crear y administrar cargas de trabajo, Secrets, Services y recursos personalizados de Kubernetes dentro del espacio de nombres del proyecto en el clúster compartido.
    • Visualizador de proyectos (project-viewer): Acceso de lectura para ver el proyecto existente y sus recursos en la consola y la CLI de GDC.
    • Editor de Marketplace (marketplace-editor): Permiso para instalar, actualizar y desinstalar paquetes de software de GDC Marketplace en el proyecto.
    • Administrador de NetworkPolicy del proyecto (project-networkpolicy-admin): Permiso para configurar reglas globales de ProjectNetworkPolicy que permitan que el tráfico externo y entre proyectos llegue a los servicios de LoadBalancer de Kibana y Elasticsearch.

    Pídele al administrador de IAM de tu organización o al administrador de IAM del proyecto que te otorgue estos roles (o ejecuta los siguientes comandos si tienes privilegios de administrador de 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
    
  • Recupera el kubeconfig del clúster compartido y configura un alias de shell de ku para kubectl:

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

Instala el paquete de Marketplace de Elastic

En esta sección, instalarás el operador de Elastic Cloud on Kubernetes (ECK) en el espacio de nombres de tu proyecto desde el Marketplace de GDC aislado siguiendo la guía del ciclo de vida de Marketplace de GDC.

Instala a través de la CLI de gdcloud (o la consola de GDC)

  1. Enumera los paquetes de software disponibles en Marketplace para confirmar el nombre del servicio y la versión disponible:

    gdcloud marketplace services list
    

    El resultado debería ser similar al siguiente:

    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. Instala el paquete de Elastic de Marketplace en tu clúster compartido de destino:

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

    *(Como alternativa, en la consola de GDC, selecciona tu proyecto ${PROJECT_ID}, navega a Marketplace, selecciona la ficha Elastic Cloud on Kubernetes, haz clic en Instalar, selecciona el clúster ${CLUSTER_NAME} y haz clic en Instalar).*

  3. Verifica que el StatefulSet y el Pod elastic-operator se estén ejecutando en el espacio de nombres de tu proyecto (${PROJECT_ID}):

    ku get statefulset,pods -n ${PROJECT_ID}
    

    El resultado debería ser similar al siguiente:

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

Implementa Elastic Stack

Crea el manifiesto de Elastic Stack

El paquete de GDC Marketplace implementa el operador de ECK preconfigurado para extraer imágenes de contenedor de Elasticsearch y Kibana del registro local de Harbor aislado.

Crea elastic-stack.yaml para implementar un clúster de Elasticsearch de 1 nodo, una instancia de IU de Kibana, servicios HTTP de LoadBalancer y un allow-all NetworkPolicy en el espacio de nombres de tu proyecto (${PROJECT_ID}). El manifiesto también incluye una licencia Secret conciliada por el operador de ECK, configurada aquí como una prueba de Enterprise de 30 días (eck-trial-license) en caso de que aún no tengas tu propia licencia de Enterprise:

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

Aplica y verifica la implementación

Aplica el manifiesto

ku apply -f elastic-stack.yaml

Aplica un ProjectNetworkPolicy global para que las reglas de firewall de GDC permitan el tráfico entrante a las IP de LoadBalancer y el tráfico entre proyectos (como la comunicación con el servicio de 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

Supervisa los Pods, los Services y los recursos personalizados hasta que Elasticsearch y Kibana alcancen el estado Running y Ready, y obtengan las IP de LoadBalancer en ${CLUSTER_NAME}:

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

Una vez que los recursos estén listos, el resultado debería ser similar al siguiente:

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

Exporta las IP externas de LoadBalancer asignadas a Elasticsearch y Kibana para usarlas en los pasos siguientes:

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"

Recupera las credenciales y accede a la IU de Kibana

Recupera la contraseña de superusuario de elastic

ECK genera automáticamente un Secret de Kubernetes (elastic-cluster-es-elastic-user) que contiene la contraseña predeterminada del superusuario elastic. Recupérala y decodifícala con el siguiente comando:

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

Verifica la conectividad y accede

  1. Verifica que se pueda acceder a Elasticsearch a través de HTTPS desde tu estación de trabajo:

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" "https://${ELASTICSEARCH_IP}:9200"
    
  2. Abre https://${KIBANA_IP}:5601 en tu navegador, acepta la advertencia del certificado autofirmado y accede con lo siguiente:

    • Nombre de usuario: elastic
    • Contraseña: <output from ELASTIC_PASSWORD above>

Verifica la licencia de Elastic

Las funciones potenciadas por IA generativa de Elastic (incluidos el agente de IA de Elastic, Agent Builder y Attack Discovery) y los conectores de LLM externos requieren una licencia Enterprise (o una licencia de prueba de Enterprise activa de 30 días).

Cuando un operador de ECK administra un clúster de Elasticsearch, el license-controller de ECK concilia de forma continua la licencia del clúster con los Secrets de Kubernetes en el espacio de nombres del operador (${PROJECT_ID}). Si aún no tienes una licencia de Enterprise de producción, el Secret eck-trial-license incluido en elastic-stack.yaml (license.k8s.elastic.co/type: enterprise_trial) le indica a ECK que aprovisione y mantenga una licencia de prueba de Enterprise de 30 días de forma automática. (Ten en cuenta que, si inicias una prueba de forma manual a través de POST /_license/start_trial sin un Secret de licencia de ECK, ECK revertirá el clúster a basic en el siguiente ciclo de conciliación).

Para verificar la licencia activa a través de la CLI, haz lo siguiente:

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

*(Como alternativa, en Kibana, abre el menú de navegación y ve a Management > Dev Tools y, luego, ejecuta GET /_license en la pestaña Console).*

El resultado debería ser similar al siguiente, que muestra "status": "active" y "type": "enterprise" (o "type": "trial" si usas la licencia de prueba):

{
  "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
  }
}

Carga un conjunto de datos de muestra integrado

Para evaluar las funciones de IA de Elastic sin escribir canalizaciones de transferencia de datos personalizadas ni requerir acceso a Internet externo, puedes cargar el conjunto de datos de registros web de muestra integrado de Kibana (kibana_sample_data_logs). Como el archivo de datos de muestra se incluye en la imagen de contenedor de Kibana, se instala en segundos en un entorno aislado. El conjunto de datos propaga 14,074 documentos de registro de acceso web que contienen direcciones IP de clientes (clientip), códigos de estado HTTP (response), países de origen y destino (geo.src, geo.dest), bytes transferidos (bytes), URLs y cadenas de usuario-agente.

Consulta Agrega datos de muestra en Kibana y elastic-datasets-builtin.md para obtener conjuntos de datos integrados adicionales (flights y ecommerce).

Carga el conjunto de datos

Ejecuta el siguiente comando para cargar kibana_sample_data_logs a través del extremo de muestra de datos interno de Kibana:

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"

Verifica el conjunto de datos de muestra

Verifica que el conjunto de datos de muestra logs esté marcado como installed en Kibana y confirma que el flujo de datos de respaldo contenga documentos 14,074 en Elasticsearch:

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"

El resultado debería ser similar al siguiente:

{
  "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

Requisitos para el servicio de LLM

En un entorno aislado de GDC, no se puede acceder a los LLM administrados de forma elástica alojados en la nube ni a los proveedores externos de LLM de SaaS públicos. Según tu entorno de GDC, haz lo siguiente:

  • Gemini administrado en GDC: En los entornos de GDC que ofrecen Gemini como un servicio de LLM administrado, puedes conectar Kibana directamente al servicio de Gemini administrado si está disponible.
  • Servicio de LLM autoadministrado: Como alternativa, puedes conectar Elastic a un servicio de LLM autoadministrado alojado en tu entorno de GDC (por ejemplo, en nodos de GPU de GDC).

La implementación de un servicio de LLM autoadministrado está fuera del alcance de esta guía. Cuando aprovisiones o selecciones un servicio de LLM autoadministrado para Elastic, asegúrate de que cumpla con los requisitos derivados de las guías oficiales de Elastic:

Resumen de los requisitos técnicos

En los siguientes puntos, se resumen los requisitos clave de la API, la capacidad del modelo, la ventana de contexto y el tiempo de ejecución necesarios para que un servicio de LLM autoadministrado funcione de manera confiable con el agente de IA, el Creador de agentes y el Descubrimiento de ataques de Elastic. Ten en cuenta que los nombres de los parámetros de configuración específicos que se citan en las siguientes secciones (como num_ctx, num_predict, repeat_penalty, repeat_last_n, OLLAMA_FLASH_ATTENTION y OLLAMA_NUM_PARALLEL) se proporcionan con Ollama como ejemplo. Aplica la configuración equivalente si usas otro motor de inferencia, como vLLM.

  • API de finalizaciones de chat compatible con OpenAI (/v1/chat/completions) con llamada a funciones nativa:
    • Cualquier servidor de inferencia autoadministrado (como Ollama o vLLM) que exponga un extremo /v1/chat/completions compatible con OpenAI (incluido el streaming de eventos enviados por el servidor a través de "stream": true) se puede integrar con el conector de OpenAI de Kibana con el proveedor establecido en Otro (servicio compatible con OpenAI).
    • Habilita la llamada a funciones nativa ("enableNativeFunctionCalling": true) en el conector de Kibana cuando uses modelos que admitan la llamada a herramientas nativa de la nube (como gemma4:31b). De forma predeterminada ("enableNativeFunctionCalling": false), Kibana usa la llamada a funciones simulada basada en instrucciones, que es menos confiable durante las llamadas a herramientas anidadas y puede fallar con errores de análisis (como Error: Missing name for tool use). Habilitar la llamada a funciones nativa delega el formato de la llamada a la herramienta directamente al protocolo OpenAI tools del servidor de inferencia.
  • Modelo ajustado según las instrucciones con capacidades nativas de llamadas a funciones y herramientas:
    • El agente de IA de Elastic (elastic-ai-agent) y los agentes personalizados de Agent Builder dependen de la llamada a funciones de varios pasos (platform.core.list_indices, platform.core.get_index_mapping, platform.core.search, platform.core.execute_esql y platform.core.cases). Usan estas herramientas para descubrir asignaciones de índices, generar y ejecutar consultas de ES|QL en Elasticsearch y sintetizar los hallazgos.
    • El modelo elegido debe estar ajustado según las instrucciones y ser capaz de generar invocaciones válidas de herramientas nativas de la nube, resultados JSON estructurados y consultas de ES|QL. Consulta la matriz de rendimiento de LLM para Observability y la matriz de rendimiento de LLM para Elastic Security de Elastic para conocer los criterios de evaluación de modelos.
  • Ventana de contexto de al menos 128,000 tokens (por ejemplo, num_ctx: 131072 en Ollama):
    • Si bien la documentación general de LLM personalizados autoadministrados de Elastic cita un mínimo de 64,000 tokens, se recomienda una ventana de contexto de 128,000 tokens (tokens de 131,072) para el agente de IA de Elastic (elastic-ai-agent).
    • Durante las investigaciones de varios pasos, el agente combina instrucciones del sistema y definiciones de herramientas con esquemas de índice, documentación de ES|QL y resultados de búsqueda sin procesar (por ejemplo, recupera hasta 100 documentos de muestra para inspeccionar los valores de los campos antes de agregarlos). Una sola investigación puede superar los 70,000 tokens en el historial de conversaciones, lo que hace que una ventana de contexto de 64,000 tokens trunque la instrucción a mitad de la conversación.
    • Ten en cuenta que, en Ollama, una directiva PARAMETER num_ctx integrada en el Modelfile de un modelo anula la variable de entorno OLLAMA_CONTEXT_LENGTH del contenedor. Asegúrate de que OLLAMA_CONTEXT_LENGTH=131072 y PARAMETER num_ctx 131072 en el Modelfile del modelo estén configurados (en una GPU NVIDIA A100 o H100 de 80 GB, gemma4:31b en el contexto 128K cabe cómodamente en la VRAM).
  • Límite de tokens de salida y penalización por repetición leve (por ejemplo, num_predict: 8192 y repeat_penalty: 1.05 en Ollama):
    • El adaptador de transmisión de OpenAI de Kibana trata "finish_reason": "length" como un error fatal de Token limit reached y envía "temperature": 0 (decodificación codiciosa) en cada solicitud.
    • En el caso de los modelos capaces de razonar (como gemma4:31b, que genera tokens de pensamiento internos antes de emitir llamadas a herramientas), configura un límite de tokens de salida de al menos 8,192 tokens y una penalización por repetición suave. Por ejemplo, en Ollama, configura num_predict: 8192, repeat_penalty: 1.05 y repeat_last_n: 64. Evita las penalizaciones por repetición demasiado agresivas (como 1.15+ en ventanas grandes), ya que penalizan al modelo cuando copia una consulta de ES|QL de su bloque de razonamiento interno en el JSON final de la llamada a la herramienta.
  • Aceleración de GPU, Flash Attention y ranuras de caché de KV paralelas (menos de 60 s por turno):
    • El conector de OpenAI de Kibana aplica un tiempo de espera HTTP predeterminado de 60 segundos por turno de transmisión.
    • Debido a que un solo turno de agente puede activar llamadas secundarias de LLM anidadas (por ejemplo, para traducir solicitudes de búsqueda en lenguaje natural en consultas de ES|QL mientras la conversación del agente principal está activa), ejecutar un servidor de inferencia con solo una ranura de caché de KV puede hacer que las llamadas secundarias expulsen el contexto almacenado en caché de la conversación principal y ralenticen los turnos posteriores.
    • En Ollama, configura OLLAMA_FLASH_ATTENTION=1 y OLLAMA_NUM_PARALLEL=2 para que Flash Attention acelere la evaluación de contexto largo y la conversación principal y las llamadas secundarias anidadas mantengan ranuras de caché de KV separadas en la memoria de la GPU.

Configura el conector de LLM en Kibana

Una vez que tu servicio de LLM compatible con OpenAI esté en ejecución y sea accesible desde el pod de Kibana, configura un conector de OpenAI en Kibana con la CLI o la IU de Kibana.

Opción 1: Crea el conector a través de la CLI

  1. Ejecuta el siguiente comando POST /api/actions/connector para crear un conector de OpenAI (.gen-ai) que apunte a tu servicio de LLM (${LLM_API_URL}) con la llamada a funciones nativa habilitada ("enableNativeFunctionCalling": true). Kibana genera y asigna automáticamente un ID de conector único:

    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. Verifica que el conector ${LLM_CONNECTOR_NAME} exista y no le falten secretos. Para ello, enumera los conectores de Kibana y filtra por nombre:

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

    El resultado debería ser similar al siguiente:

    {
      "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. Para probar la conectividad de extremo a extremo en vivo entre Kibana y el servicio de LLM desde la CLI, busca el ID del conector generado de forma automática por nombre y llama a su extremo _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 .
    

    El resultado debería ser similar al siguiente, en el que se muestra "status": "ok" y la respuesta del modelo:

    {
      "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>"
    }
    

Opción 2: Crea el conector a través de la IU de Kibana

  1. Accede a Kibana (https://${KIBANA_IP}:5601).
  2. Abre el menú de navegación principal y ve a Administración > Alertas y estadísticas > Conectores.
  3. Haz clic en Crear conector y selecciona OpenAI.
  4. Configura los campos del conector:
    • Nombre del conector: Ingresa el valor de ${LLM_CONNECTOR_NAME} (por ejemplo, Gemma4-31B).
    • Selecciona un proveedor de OpenAI: Selecciona Otro (servicio compatible con OpenAI).
    • URL: Ingresa el valor de ${LLM_API_URL} (el extremo /v1/chat/completions de tu servicio de LLM).
    • Modelo predeterminado: Ingresa el valor de ${LLM_MODEL_NAME} (por ejemplo, gemma4:31b).
    • Clave de API: Ingresa el valor de ${LLM_API_KEY} (cualquier cadena no vacía si tu servidor no aplica la autenticación de portador o la clave de API de tu servidor).
    • Habilitar la llamada a funciones nativas: Activa el botón de activación.
  5. Haz clic en Guardar y probar para guardar el conector y abrir de inmediato la pestaña Probar.
  6. En la pestaña Test, ingresa una instrucción de ejemplo (por ejemplo, Reply with the single word Pong.) y haz clic en Run para verificar que Kibana reciba una respuesta válida del servicio de LLM.

Prueba las integraciones de LLM

Con el conjunto de datos kibana_sample_data_logs instalado y el conector de LLM verificado, puedes probar las capacidades de IA de Elastic en Kibana.

Agente de IA de Elastic

El agente de IA de Elastic (elastic-ai-agent) es el agente conversacional predeterminado integrado en Kibana para explorar y analizar datos en Elasticsearch con lenguaje natural. Con la tecnología de tu conector de LLM configurado y las herramientas integradas de Agent Builder, el agente descubre automáticamente los índices y las asignaciones de Elasticsearch (platform.core.list_indices, platform.core.get_index_mapping), genera y ejecuta consultas de ES|QL (platform.core.search, platform.core.execute_esql) y sintetiza los hallazgos sin que necesites conocer el esquema subyacente de antemano. También puedes crear tus propios agentes personalizados con instrucciones y herramientas adaptadas. Consulta Agentes personalizados con Agent Builder para obtener más detalles.

Para abrir y configurar el agente de IA de Elastic en la IU de Kibana, haz lo siguiente:

  1. Abre el menú de navegación principal en Kibana y selecciona Agente de IA (o haz clic en el botón Preguntar a la IA en la barra de encabezado superior, o navega directamente a https://${KIBANA_IP}:5601/app/chat).
  2. Asegúrate de que Elastic AI Agent esté seleccionado como el agente de IA activo y que tu conector de LLM (${LLM_CONNECTOR_NAME}) esté seleccionado como el conector del modelo.

Ejemplo 1: Investiga las solicitudes HTTP con errores y los posibles escáneres

Pídele al agente de IA de Elastic que identifique las principales direcciones IP de clientes que generan respuestas HTTP que no sean 200 en kibana_sample_data_logs, sus países de origen y si alguna de esas IP también tuvo tráfico HTTP 200 exitoso:

  • Opción 1: A través de la IU de Kibana: Ingresa la siguiente pregunta en la instrucción de chat del agente de IA:

    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?

  • Opción 2: Con la 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
      }'
    

Qué sucede tras bambalinas:

  1. El agente llama a platform.core.get_index_mapping(indices=["kibana_sample_data_logs"]) para inspeccionar los campos disponibles (clientip, geo.src, response, bytes, request, etc.).
  2. Llama a platform.core.search para inspeccionar documentos de muestra y confirmar que los códigos de estado HTTP se almacenen en response. Luego, ejecuta agregaciones de ES|QL para clasificar las solicitudes que no sean 200 por clientip y geo.src, y verificar cuáles de las IPs principales también registraron tráfico de response == "200":

    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
    

La respuesta del agente de IA de Elastic debería ser similar a la siguiente:

La dirección IP del cliente con la mayor cantidad de solicitudes HTTP fallidas (códigos de estado que no son 200) en kibana_sample_data_logs es 30.156.16.164, que se originó en Estados Unidos y registró 100 solicitudes fallidas.

Otras direcciones IP principales con solicitudes con errores, todas las cuales también se originaron en Estados Unidos, incluyen las siguientes:

  • 213.235.30.99: 6 fallas
  • 221.241.228.46: 6 fallas
  • 209.94.234.133: 6 fallas
  • 7.127.73.138: 5 fallas
  • 247.240.202.244: 5 fallas
  • 130.125.65.24: 5 fallas
  • 164.85.94.243: 4 fallas
  • 81.194.200.150: 4 fallas
  • 30.145.143.210: 4 fallas

En cuanto a las solicitudes exitosas, todas las IPs que se mencionaron anteriormente tuvieron al menos algunas respuestas HTTP 200 exitosas, excepto 30.156.16.164, que no tuvo solicitudes exitosas.

Ejemplo 2: Analiza el volumen de tráfico y los bytes de carga útil por código de estado HTTP

Pídele al agente de IA de Elastic el desglose de los códigos de estado de respuesta HTTP de todas las solicitudes en kibana_sample_data_logs y el total de bytes transferidos para cada código de estado:

  • Opción 1: A través de la IU de Kibana: Inicia una nueva conversación en Agente de IA y escribe la siguiente pregunta:

    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?

  • Opción 2: Con la 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
      }'
    

Qué sucede tras bambalinas:

  1. El agente inspecciona el esquema kibana_sample_data_logs a través de platform.core.get_index_mapping, identificando los campos response.keyword y bytes.
  2. Invoca a platform.core.search, que genera y ejecuta la siguiente agregación de ES|QL:

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

La respuesta del agente de IA de Elastic debería ser similar a la siguiente:

El desglose de los códigos de estado de respuesta HTTP y el total de datos transferidos en kibana_sample_data_logs es el siguiente:

Código de estado Recuento de solicitudes Total de datos transferidos (bytes)
200 12,832 75,681,246
404 801 4,044,443
503 441 0

Agentes personalizados con Agent Builder

Agent Builder te permite crear agentes de IA personalizados adaptados a flujos de trabajo operativos específicos combinando instrucciones del sistema personalizadas con herramientas integradas (como platform.core.cases, platform.core.execute_esql y platform.core.search) y herramientas definidas por el usuario (esql, index_search, workflow y mcp).

En el siguiente ejemplo, se compila un agente de respuesta ante incidentes de seguridad web que opera directamente en el conjunto de datos kibana_sample_data_logs. Dada una instrucción en lenguaje natural, el agente hace lo siguiente:

  1. Ejecuta una herramienta de ES|QL personalizada (investigate_suspicious_web_clients) para agregar respuestas HTTP que no sean 200 en kibana_sample_data_logs por IP de cliente.
  2. Distingue la actividad maliciosa de reconocimiento y explotación (30.156.16.164) de las fallas de descarga benignas.
  3. Invoca una herramienta personalizada de flujo de trabajo de Kibana (create_security_case) para abrir automáticamente un caso de seguridad de Kibana estructurado con sus hallazgos y pasos de solución.

Crear herramientas personalizadas de ES|QL y de creación de casos

  1. Crea una herramienta personalizada de esql llamada investigate_suspicious_web_clients que consulte kibana_sample_data_logs para obtener las principales direcciones IP de clientes que generan códigos de estado HTTP que no sean 200, junto con sus códigos de estado, país de origen (geo.src), usuarios-agentes (agent) y URLs solicitadas (request):

    • Opción 1: A través de la IU de Kibana:

      1. En Kibana, abre el menú de navegación principal y ve a Elasticsearch > Agents y, luego, haz clic en Manage all tools (o navega directamente a https://${KIBANA_IP}:5601/app/agent_builder/tools) y haz clic en New tool.
      2. Configura los campos de la herramienta:

        • ID de la herramienta: investigate_suspicious_web_clients
        • Tipo: ES|QL
        • Descripción: 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.
        • Etiquetas: security, logs
        • Consulta de 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. Haz clic en Guardar.

    • Opción 2: Con la 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. Debido a que la herramienta platform.core.cases integrada de Agent Builder es de solo lectura (se usa para buscar y recuperar casos existentes), crea un flujo de trabajo de Kibana (create-security-case) que invoque el paso cases.createCase nativo de Kibana y regístralo como una herramienta workflow llamada create_security_case:

    • Opción 1: A través de la IU de Kibana:

      1. Abre el menú de navegación principal y ve a Administración > Flujos de trabajo (o navega directamente a https://${KIBANA_IP}:5601/app/workflows) y, luego, haz clic en Crear un flujo de trabajo nuevo.
      2. En el editor de YAML, pega la siguiente definición de flujo de trabajo y haz clic en Guardar:

        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. Regresa a la página Herramientas de Agent Builder (https://${KIBANA_IP}:5601/app/agent_builder/tools o ve a Elasticsearch > Agentes y haz clic en Administrar todas las herramientas) y haz clic en Nueva herramienta.

      4. Configura los campos de la herramienta de flujo de trabajo y haz clic en Guardar:

        • ID de la herramienta: create_security_case
        • Tipo: Flujo de trabajo
        • Flujo de trabajo: Selecciona Crear caso de seguridad (create-security-case)
        • Descripción: Creates a new Kibana Case in Elastic Security with a title, markdown description of the incident findings, and severity (low, medium, high, critical).
        • Etiquetas: security, cases
    • Opción 2: Con la 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}'
      

Crea el agente personalizado de Incident Responder

Crea el agente web-security-incident-responder y asígnale las herramientas investigate_suspicious_web_clients, create_security_case y platform.core.cases:

  • Opción 1: A través de la IU de Kibana:

    1. Abre el menú de navegación principal y ve a Elasticsearch > Agentes (o navega directamente a https://${KIBANA_IP}:5601/app/agent_builder/agents) y, luego, haz clic en Nuevo agente.
    2. Configura los detalles del agente:

      • ID del agente: web-security-incident-responder
      • Nombre visible: Web Security Incident Responder
      • Descripción visible: Investigates suspicious HTTP error activity in web access logs and opens Kibana Security Cases with the findings.
      • Etiquetas: security, incident-response
      • Instrucciones personalizadas:

        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.
        
      • Herramientas: Selecciona investigate_suspicious_web_clients, create_security_case y platform.core.cases.

    3. Haz clic en Guardar.

  • Opción 2: Con la 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}'
    

Ejecuta el agente y verifica el caso de Kibana

  1. Inicia una conversación con el agente de Web Security Incident Responder:

    • Opción 1: A través de la IU de Kibana:

      1. Abre el menú de navegación principal y ve a Elasticsearch > Agentes (o navega directamente a https://${KIBANA_IP}:5601/app/agent_builder).
      2. Selecciona Web Security Incident Responder en el selector de agentes y asegúrate de que esté seleccionado tu conector de LLM (${LLM_CONNECTOR_NAME}).
      3. Escribe la siguiente instrucción:

        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.

    • Opción 2: Con la 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
        }'
      

      El resultado debería ser similar al siguiente:

      {
        "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. Verifica el caso de seguridad de Kibana recién creado:

    • Opción 1: A través de la IU de Kibana:

      1. Abre el menú de navegación principal y ve a Seguridad > Casos (o navega directamente a https://${KIBANA_IP}:5601/app/security/cases).
      2. Haz clic en Intento de reconocimiento y explotación maliciosos desde 30.156.16.164 para ver el resumen del incidente en formato Markdown, el análisis de MITRE ATT&CK, las URLs maliciosas de muestra y la lista de tareas para la corrección.
    • Opción 2: Con la 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}'
      

      El resultado debería ser similar al siguiente:

      {
        "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."
      }
      

Descubrimiento de ataques

Attack Discovery es una función de IA de Elastic Security que analiza las alertas de seguridad abiertas, las asigna al marco de MITRE ATT&CK y correlaciona las alertas, las direcciones IP y las cuentas de usuario relacionadas en cadenas de ataque visuales. Dado que la detección de ataques requiere un modelo grande para sintetizar de forma confiable resultados JSON estructurados con varias alertas, usa un modelo de clase 30B o más grande (como gemma4:31b).

Genera alertas de seguridad a partir de los registros de muestra

De forma predeterminada, el conjunto de datos de kibana_sample_data_logs contiene registros de acceso web sin procesar y no completa el índice de alertas de Elastic Security (.alerts-security.alerts-default). Además, el generador de datos de muestra de Kibana asigna valores de @timestamp futuros (hasta 30 días antes) a las 100 solicitudes de análisis y explotación de 30.156.16.164, lo que las coloca fuera del período de now-30d a now que consultan las reglas de detección de Elastic Security.

Para demostrar el Descubrimiento de ataques con los registros de muestra, desplaza 30 días hacia atrás el @timestamp de los registros de 30.156.16.164, enriquécelos con los campos source.ip y user.name del esquema común de Elastic (ECS) y crea dos reglas de detección que generen alertas de seguridad a partir de esos eventos:

  1. Asigna clientip a source.ip, establece user.name (www-data) y desplaza @timestamp 30 días hacia atrás en los registros de 30.156.16.164 en kibana_sample_data_logs*:

    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. Crea dos reglas de detección de Elastic Security dirigidas a kibana_sample_data_logs*. La primera regla detecta el sondeo de inicio de sesión de Nmap y WordPress (Reconnaissance), y la segunda regla detecta intentos de recorrido de ruta de /etc/passwd y ejecución de comandos remotos (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. Espera aproximadamente 60 segundos para que se complete la ejecución inicial de la regla y, luego, verifica que se hayan indexado 4 alertas en .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}'
    

    El resultado debería ser similar al siguiente:

    {
      "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"
    }
    

Ejecuta el descubrimiento de ataques

  1. En Kibana, abre el menú de navegación principal y ve a Seguridad > Detección de ataques (o navega directamente a https://${KIBANA_IP}:5601/app/security/attack_discovery).
  2. Selecciona tu conector de LLM (${LLM_CONNECTOR_NAME}) en el menú de conectores que se encuentra en la parte superior de la página.
  3. Haz clic en Generar para analizar las alertas abiertas en .alerts-security.alerts-default.

También puedes activar e inspeccionar Attack Discovery desde la CLI:

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}"

Una vez que finalice la generación (aproximadamente entre 60 y 90 segundos), recupera la cadena de ataque descubierta:

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]'

El descubrimiento de ataque generado correlaciona las cuatro alertas en un solo incidente con la IP de origen, la cuenta de usuario y las tácticas de MITRE ATT&CK vinculadas:

{
  "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"
}