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-pyas 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:InvokeModelpermission 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)
Related Reading
- Ollama in Wazuh Dashboard: AI Security Analysis
- Enhancing Wazuh with Ollama: Cybersecurity Boost (Part 1)
- Wazuh LLM: Fine-Tuned Llama 3.1 for Security Analysis
- Wazuh Documentation RAG: Part 1