Wazuh + AWS Bedrock: AI Security in Docker (Part 1)

Introduction

In the previous article we embedded a local Ollama model directly into the Wazuh Dashboard chat via ML Commons. That approach provides full control over data with no cloud dependencies. In this series we take a parallel path: using AWS Bedrock - specifically Claude Sonnet 4.5 - as the inference backend, while all security data stays strictly within the local Docker network.

The architecture we are building consists of three layers:

  • Part 1 (this article) - a conversational ML Commons agent powered by Bedrock Claude with PPLTool for natural-language queries against alerts and a chat widget in Wazuh Dashboard
  • Part 2 - opensearch-mcp-server-py as a sidecar service in Docker, exposing Wazuh Indexer data via Model Context Protocol for external clients like Claude Desktop
  • Part 3 - a RAG pipeline using Bedrock Titan Embeddings v2 and an OpenSearch k-NN index for semantic search over threat intelligence and SOC playbooks

By the end of Part 1 you will have a working AI analyst in the Wazuh Dashboard chat that answers questions about alerts in natural language, generates threat summaries, and translates English-language queries into PPL queries against wazuh-alerts-* indices.

Prerequisites

  • Docker and Docker Compose v2
  • The wazuh-docker repository, branch v4.14.3
  • An AWS account with Bedrock model access enabled (Claude Sonnet 4.5 and Titan Embed Text v2) in the desired region
  • An IAM user with bedrock:InvokeModel permission on the corresponding model ARNs
  • At least 8 GB of RAM allocated to Docker

Privacy note. Alert content sent to Bedrock for inference is processed by AWS according to their data handling terms. For environments with strict data residency requirements, use the local Ollama approach from the previous article.

Preparing the Stack

Cloning and Generating Certificates

git clone https://github.com/wazuh/wazuh-docker.git -b v4.14.3
cd wazuh-docker/single-node
docker compose -f generate-indexer-certs.yml run --rm generator

Creating the .env File

All AWS credentials are stored in a single .env file at the root of the single-node/ directory. Add it to .gitignore right away.

cat > single-node/.env << 'EOF'
# AWS Bedrock
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_SESSION_TOKEN=
AWS_REGION=us-east-1
EOF

Add the .env file to .gitignore:

echo ".env" >> .gitignore

Leave AWS_SESSION_TOKEN empty if you are using long-lived IAM user credentials. For temporary STS credentials, insert the session token value.

Downloading Dashboard ML Commons Plugins

The Wazuh Dashboard image does not include the ML Commons UI plugins. We extract them from the corresponding OpenSearch Dashboards archive.

curl https://artifacts.opensearch.org/releases/bundle/opensearch-dashboards/2.19.4/opensearch-dashboards-2.19.4-linux-x64.tar.gz -o opensearch-dashboards.tar.gz
tar -xzf opensearch-dashboards.tar.gz

mkdir -p config/wazuh_dashboard/plugins
cp -r opensearch-dashboards-2.19.4/plugins/observabilityDashboards  config/wazuh_dashboard/plugins/
cp -r opensearch-dashboards-2.19.4/plugins/mlCommonsDashboards       config/wazuh_dashboard/plugins/
cp -r opensearch-dashboards-2.19.4/plugins/assistantDashboards       config/wazuh_dashboard/plugins/

rm -rf opensearch-dashboards-2.19.4* opensearch-dashboards.tar.gz

Downloading Indexer ML Commons Plugins

On the Indexer side we need two additional plugins: opensearch-skills (contains PPLTool, VectorDBTool, and other agent tools) and opensearch-flow-framework.

mkdir -p config/wazuh_indexer/plugins
cd config/wazuh_indexer/plugins

curl -L -O https://repo1.maven.org/maven2/org/opensearch/plugin/opensearch-flow-framework/2.19.4.0/opensearch-flow-framework-2.19.4.0.zip
curl -L -O https://repo1.maven.org/maven2/org/opensearch/plugin/opensearch-skills/2.19.4.0/opensearch-skills-2.19.4.0.zip

mkdir -p opensearch-flow-framework opensearch-skills
unzip opensearch-flow-framework-2.19.4.0.zip -d opensearch-flow-framework/
unzip opensearch-skills-2.19.4.0.zip         -d opensearch-skills/
rm -f *.zip

cd ../../..

Updating opensearch_dashboards.yml

Add the following lines to config/wazuh_dashboard/opensearch_dashboards.yml:

assistant.chat.enabled: true
observability.query_assist.enabled: true
opensearch.requestTimeout: 300000
opensearch.shardTimeout: 300000
opensearch.pingTimeout: 300000

Updating docker-compose.yml

Add plugin volume mounts to the wazuh.indexer and wazuh.dashboard services.

For wazuh.indexer, add to the volumes section:

- ./config/wazuh_indexer/plugins/opensearch-flow-framework:/usr/share/wazuh-indexer/plugins/opensearch-flow-framework
- ./config/wazuh_indexer/plugins/opensearch-skills:/usr/share/wazuh-indexer/plugins/opensearch-skills

For wazuh.dashboard, add to the volumes section:

- ./config/wazuh_dashboard/plugins/assistantDashboards:/usr/share/wazuh-dashboard/plugins/assistantDashboards
- ./config/wazuh_dashboard/plugins/mlCommonsDashboards:/usr/share/wazuh-dashboard/plugins/mlCommonsDashboards
- ./config/wazuh_dashboard/plugins/observabilityDashboards:/usr/share/wazuh-dashboard/plugins/observabilityDashboards

The resulting file should look roughly like this:

# Wazuh App Copyright (C) 2017, Wazuh Inc. (License GPLv2)
services:
  wazuh.manager:
    image: wazuh/wazuh-manager:4.14.3
    hostname: wazuh.manager
    restart: always
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 655360
        hard: 655360
    ports:
      - "1514:1514"
      - "1515:1515"
      - "514:514/udp"
      - "55000:55000"
    environment:
      - INDEXER_URL=https://wazuh.indexer:9200
      - INDEXER_USERNAME=admin
      - INDEXER_PASSWORD=SecretPassword
      - FILEBEAT_SSL_VERIFICATION_MODE=full
      - SSL_CERTIFICATE_AUTHORITIES=/etc/ssl/root-ca.pem
      - SSL_CERTIFICATE=/etc/ssl/filebeat.pem
      - SSL_KEY=/etc/ssl/filebeat.key
      - API_USERNAME=wazuh-wui
      - API_PASSWORD=MyS3cr37P450r.*-
    volumes:
      - wazuh_api_configuration:/var/ossec/api/configuration
      - wazuh_etc:/var/ossec/etc
      - wazuh_logs:/var/ossec/logs
      - wazuh_queue:/var/ossec/queue
      - wazuh_var_multigroups:/var/ossec/var/multigroups
      - wazuh_integrations:/var/ossec/integrations
      - wazuh_active_response:/var/ossec/active-response/bin
      - wazuh_agentless:/var/ossec/agentless
      - wazuh_wodles:/var/ossec/wodles
      - filebeat_etc:/etc/filebeat
      - filebeat_var:/var/lib/filebeat
      - ./config/wazuh_indexer_ssl_certs/root-ca-manager.pem:/etc/ssl/root-ca.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.manager.pem:/etc/ssl/filebeat.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key
      - ./config/wazuh_cluster/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf

  wazuh.indexer:
    image: wazuh/wazuh-indexer:4.14.3
    hostname: wazuh.indexer
    restart: always
    ports:
      - "9200:9200"
    environment:
      - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g"
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    volumes:
      - wazuh-indexer-data:/var/lib/wazuh-indexer
      - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/config/certs/root-ca.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/config/certs/wazuh.indexer.key
      - ./config/wazuh_indexer_ssl_certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/config/certs/wazuh.indexer.pem
      - ./config/wazuh_indexer_ssl_certs/admin.pem:/usr/share/wazuh-indexer/config/certs/admin.pem
      - ./config/wazuh_indexer_ssl_certs/admin-key.pem:/usr/share/wazuh-indexer/config/certs/admin-key.pem
      - ./config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/config/opensearch.yml
      - ./config/wazuh_indexer/internal_users.yml:/usr/share/wazuh-indexer/config/opensearch-security/internal_users.yml
      - ./config/wazuh_indexer/plugins/opensearch-flow-framework:/usr/share/wazuh-indexer/plugins/opensearch-flow-framework
      - ./config/wazuh_indexer/plugins/opensearch-skills:/usr/share/wazuh-indexer/plugins/opensearch-skills

  wazuh.dashboard:
    image: wazuh/wazuh-dashboard:4.14.3
    hostname: wazuh.dashboard
    restart: always
    ports:
      - 443:5601
    environment:
      - INDEXER_USERNAME=admin
      - INDEXER_PASSWORD=SecretPassword
      - WAZUH_API_URL=https://wazuh.manager
      - DASHBOARD_USERNAME=kibanaserver
      - DASHBOARD_PASSWORD=kibanaserver
      - API_USERNAME=wazuh-wui
      - API_PASSWORD=MyS3cr37P450r.*-
    volumes:
      - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem
      - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem
      - ./config/wazuh_dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml
      - ./config/wazuh_dashboard/wazuh.yml:/usr/share/wazuh-dashboard/data/wazuh/config/wazuh.yml
      - wazuh-dashboard-config:/usr/share/wazuh-dashboard/data/wazuh/config
      - wazuh-dashboard-custom:/usr/share/wazuh-dashboard/plugins/wazuh/public/assets/custom
      - ./config/wazuh_dashboard/plugins/assistantDashboards:/usr/share/wazuh-dashboard/plugins/assistantDashboards
      - ./config/wazuh_dashboard/plugins/mlCommonsDashboards:/usr/share/wazuh-dashboard/plugins/mlCommonsDashboards
      - ./config/wazuh_dashboard/plugins/observabilityDashboards:/usr/share/wazuh-dashboard/plugins/observabilityDashboards
    depends_on:
      - wazuh.indexer
    links:
      - wazuh.indexer:wazuh.indexer
      - wazuh.manager:wazuh.manager

volumes:
  wazuh_api_configuration:
  wazuh_etc:
  wazuh_logs:
  wazuh_queue:
  wazuh_var_multigroups:
  wazuh_integrations:
  wazuh_active_response:
  wazuh_agentless:
  wazuh_wodles:
  filebeat_etc:
  filebeat_var:
  wazuh-indexer-data:
  wazuh-dashboard-config:
  wazuh-dashboard-custom:

Starting the Stack

docker compose up -d
docker compose ps

You can also monitor the startup process with docker compose logs -f.

All three containers - wazuh.indexer, wazuh.manager, wazuh.dashboard - should have the Up status. The Dashboard is available at https://localhost:443.

Configuring ML Commons

Open the Wazuh Dashboard, navigate to Indexer Management -> Dev Tools, and run the following request.

Enabling the Agent Framework

PUT /_cluster/settings
{
  "persistent": {
    "plugins.ml_commons.only_run_on_ml_node": false,
    "plugins.ml_commons.native_memory_threshold": 99,
    "plugins.ml_commons.agent_framework_enabled": true,
    "plugins.ml_commons.memory_feature_enabled": true,
    "plugins.ml_commons.rag_pipeline_feature_enabled": true,
    "plugins.ml_commons.connector_access_control_enabled": true,
    "plugins.ml_commons.connector.private_ip_enabled": true,
    "plugins.ml_commons.trusted_connector_endpoints_regex": [
      "^https://bedrock-runtime\\.[a-z0-9-]+\\.amazonaws\\.com/.*$"
    ]
  }
}

Creating the Bedrock Connector

POST /_plugins/_ml/connectors/_create
{
  "name": "Bedrock Claude Sonnet 4.5",
  "description": "Bedrock Claude Sonnet 4.5 for Wazuh security analytics",
  "version": 1,
  "protocol": "aws_sigv4",
  "credential": {
    "access_key": "<AWS_ACCESS_KEY_ID>",
    "secret_key": "<AWS_SECRET_ACCESS_KEY>"
  },
  "parameters": {
    "region": "us-east-1",
    "service_name": "bedrock",
    "response_filter": "$.content[0].text",
    "max_tokens_to_sample": "8000",
    "anthropic_version": "bedrock-2023-05-31",
    "model": "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
  },
  "actions": [{
    "action_type": "predict",
    "method": "POST",
    "headers": { "content-type": "application/json" },
    "url": "https://bedrock-runtime.${parameters.region}.amazonaws.com/model/${parameters.model}/invoke",
    "request_body": "{\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"${parameters.prompt}\"}]},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"{\"}]}],\"anthropic_version\":\"${parameters.anthropic_version}\",\"max_tokens\":${parameters.max_tokens_to_sample}}"
  }]
}

Save the connector_id from the response.

Registering and Deploying the Model

POST /_plugins/_ml/models/_register?deploy=true
{
  "name": "claude-sonnet-4-5",
  "function_name": "remote",
  "description": "Bedrock Claude Sonnet 4.5 for Wazuh",
  "connector_id": "<connector_id>"
}

Save the model_id from the response.

Run a quick smoke test:

POST /_plugins/_ml/models/<model_id>/_predict
{
  "parameters": {
    "inputs": "Reply with exactly one word: WORKING"
  }
}

The response should contain WORKING.

Creating the Security Analyst Agent

POST /_plugins/_ml/agents/_register
{
  "name": "wazuh_bedrock_analyst",
  "type": "conversational",
  "description": "Wazuh Bedrock Claude",
  "llm": {
    "model_id": "<model_id>",
    "parameters": {
      "max_iteration": 5,
      "stop_when_no_tool_found": true,
      "response_filter": "$.content[0].text",
      "system_instruction": "You are a senior SOC analyst specialized in Wazuh SIEM. Analyze alerts, identify threats, map findings to MITRE ATT&CK, and provide actionable recommendations. Be concise and precise."
    }
  },
  "memory": { "type": "conversation_index" },
  "tools": [
    {
      "type": "PPLTool",
      "name": "WazuhAlertQuery",
      "description": "Translates natural language security questions to PPL queries over wazuh-alerts-4.x-* indices. Use for: counting alerts, filtering by agent, rule level, source IP, or time range, and top-N aggregations.",
      "parameters": {
        "model_id": "<model_id>",
        "index": "wazuh-alerts-4.x-*",
        "execute": true
      }
    },
    {
      "type": "SearchIndexTool",
      "name": "WazuhAlertSearch",
      "description": "Searches wazuh-alerts-4.x-* using Query DSL. Use for exact field filtering on rule.level, rule.id, agent.name, data.srcip, and @timestamp.",
      "parameters": { "index": "wazuh-alerts-4.x-*" }
    },
    {
      "type": "CatIndexTool",
      "name": "ListWazuhIndices",
      "description": "Lists all wazuh-* indices with document counts and disk size."
    },
    {
      "type": "IndexMappingTool",
      "name": "DiscoverAlertFields",
      "description": "Returns field mappings for any Wazuh index. Use before constructing a query to confirm field names."
    }
  ],
  "app_type": "os_chat"
}

Save the agent_id.

Connecting the Agent to the Dashboard Chat

docker compose exec wazuh.indexer curl -k -X PUT \
  "https://localhost:9200/.plugins-ml-config/_doc/os_chat" \
  --cert /usr/share/wazuh-indexer/config/certs/admin.pem \
  --key /usr/share/wazuh-indexer/config/certs/admin-key.pem \
  --cacert /usr/share/wazuh-indexer/config/certs/root-ca.pem \
  -H "Content-Type: application/json" \
  -d '{
    "type": "os_chat_root_agent",
    "configuration": {
      "agent_id": "<agent_id>"
    }
  }'

Expected response:

{
   "_index":".plugins-ml-config",
   "_id":"os_chat",
   "_version":2,
   "result":"updated",
   "_shards":{
      "total":1,
      "successful":1,
      "failed":0
   },
   "_seq_no":2,
   "_primary_term":1
}

Testing the Agent

Open the Wazuh Dashboard and click the chat icon in the top-right corner. Try the following queries:

Show me the top 10 rules by alert count in the last 24 hours

or

Summarize all critical alerts (rule level 7 or above) from the past 6 hours.
Include affected agents, source IPs, and recommended actions.

The agent operates on a ReAct reasoning loop: it may first call DiscoverAlertFields to verify field names, then WazuhAlertQuery to generate and execute a PPL query, and finally synthesize a natural-language response using Bedrock Claude.

Conclusion

In this part we deployed a full Wazuh stack in Docker with ML Commons support, connected AWS Bedrock Claude Sonnet 4.5 as the LLM backend, and created a conversational agent with a set of tools for natural-language security alert analysis. In the next part we will add opensearch-mcp-server-py as a sidecar service to expose Wazuh Indexer data via Model Context Protocol for external clients.

Series Navigation:

  • Part 1: AI Security Analytics in Docker (you are here)
  • Part 2: MCP Server for External Clients (coming soon)
  • Part 3: RAG Pipeline with Bedrock Titan Embeddings (coming soon)

See also