Automating Wazuh Hot/Cold Storage on Azure
If you’ve spent any time in the trenches as a Cloud & DevOps Engineer, you know the lifecycle of a SIEM deployment. Day 1: "Look at all these amazing security events!" Day 90: "Why is our Premium SSD bill higher than our office rent, and why is the cluster throwing disk_watermark alerts?"
Managing a Wazuh deployment for enterprise clients—especially those bound by strict EU regulations like DORA—means you have to retain logs for a long time. Usually, this means 90 days of instant, searchable "Hot" storage, and up to a year of "Cold" archive storage.
Leaving a year's worth of logs on high-IOPS SSDs is a great way to burn money. On the flip side, mismanaging your cold storage transitions is a great way to accidentally delete critical compliance data.
Here is the exact, battle-tested pipeline we deployed to automate our Wazuh log lifecycle using Azure Blob Storage, complete with a self-destructing restore mechanism for when the auditors come knocking.
The Architecture: Hot to Cold Pipeline
Our goal was simple and completely hands-free:
0–90 days: Data lives on local Wazuh Indexer SSDs (Hot tier).
Day 91: The index is automatically snapshotted to a low-cost Azure Blob Storage container.
Day 91 (Step 2): Once the snapshot is confirmed successful, the local heavy data is permanently deleted from the expensive SSD.
Day 365: Azure automatically drops the cold data.
Let's dive into the implementation.
Phase 1: Preparing the Azure Landing Zone
First, we need a secure place to dump the cold data.
Head into the Azure Portal.
Navigate to your Storage Account (e.g.,
siemstorage) ➔ Containers.Create a new container named
wazuh-archive. Make absolutely sure the Public Access level is set to Private.
Keep your Storage Account Name and Access Key handy; we will need them in the next step.
Phase 2: Equipping the Wazuh Indexer
Wazuh Indexer (forked from OpenSearch) doesn't natively know how to speak to Azure Blob out of the box. We need to install the correct translation plugin and securely inject our credentials.
SSH into your Wazuh Indexer node(s) and run:
Bash
sudo /usr/share/wazuh-indexer/bin/opensearch-plugin install repository-azure
sudo systemctl restart wazuh-indexer
⚠️ The Keystore Warning
Never put your raw Azure Access Keys in the opensearch.yml file. If someone dumps that file, your entire compliance archive is compromised. Use the built-in secure keystore:
Bash
# Add the Storage Account Name
sudo /usr/share/wazuh-indexer/bin/opensearch-keystore add azure.client.default.account
# (Type 'siemstorage' when prompted)
# Add the Access Key
sudo /usr/share/wazuh-indexer/bin/opensearch-keystore add azure.client.default.key
# (Paste your massive Azure key when prompted)
# Verify and restart
sudo /usr/share/wazuh-indexer/bin/opensearch-keystore list
sudo systemctl restart wazuh-indexer
Phase 3: Making the Connection
Now we tell the cluster where to send the data. We register the Azure container as a valid snapshot repository via the API.
(Note: Replace 10.3.2.7 with your indexer IP and use your actual admin credentials).
Bash
curl -k -u admin -XPUT https://10.3.2.7:9200/_snapshot/azure_repo \
-H "Content-Type: application/json" -d '
{
"type": "azure",
"settings": {
"container": "wazuh-archive",
"base_path": "snapshots"
}
}'
Crucial Step: Test it before you trust it. Let's force a manual test snapshot to ensure data actually flows.
Bash
curl -k -u admin -XPUT "https://10.3.2.7:9200/_snapshot/azure_repo/test-snapshot?wait_for_completion=true"
If it returns "state": "SUCCESS", check your Azure portal. You should see index-0, metadata, and segments files populating your container.
Phase 4: The Brains of the Operation (ISM Policy)
This is where the magic happens. We use Index State Management (ISM) to automate the 90-day transition.
Notice the structure of this policy. It doesn't just transition and delete. It moves the index to a backup state, forces the snapshot to Azure, and only when that action successfully completes does it transition to the delete state to wipe the local disk. This prevents catastrophic data loss if Azure has an outage.
Bash
curl -k -u admin -XPUT https://10.3.2.7:9200/_plugins/_ism/policies/wazuh-tiering \
-H "Content-Type: application/json" -d '
{
"policy": {
"description": "Safe Snapshot to Azure Blob followed by Local Deletion",
"default_state": "hot",
"ism_template": [
{
"index_patterns": ["wazuh-alerts-4.x-*"],
"priority": 100
}
],
"states": [
{
"name": "hot",
"actions": [],
"transitions": [
{
"state_name": "backup",
"conditions": {
"min_index_age": "90d"
}
}
]
},
{
"name": "backup",
"actions": [
{
"retry": {
"count": 3,
"backoff": "exponential",
"delay": "1m"
},
"snapshot": {
"repository": "azure_repo",
"snapshot": "{{ctx.index}}-snapshot"
}
}
],
"transitions": [
{
"state_name": "delete"
}
]
},
{
"name": "delete",
"actions": [
{
"retry": {
"count": 3,
"backoff": "exponential",
"delay": "1m"
},
"delete": {}
}
],
"transitions": []
}
]
}
}'
Because we included the ism_template block, every new index born at midnight automatically inherits this lifecycle. To attach it to your existing historical indices, run:
Bash
curl -k -u admin -XPOST "https://10.3.2.7:9200/_plugins/_ism/add/wazuh-alerts-*" \
-H "Content-Type: application/json" -d '
{
"policy_id": "wazuh-tiering"
}'
The Plot Twist: Handling the Auditors (And Avoiding Disk Disasters)
Fast forward six months. An auditor demands to see logs from February 27th. You dutifully restore the snapshot from Azure to your local SSD. The auditor gives you a thumbs up, leaves, and you go on PTO.
The Problem: Restored indices are "dumb." They lose their original ISM policy. If you don't manually delete that restored index, it will sit on your Premium SSD forever, eventually taking down your cluster.
The Solution: Self-Destructing Restores.
We implemented a "Smart Restore" pipeline. First, we create a cleanup policy that looks for any index starting with restored- and blindly deletes it after 7 days:
Bash
# Create the Cleanup Policy via Dev Tools or Curl
PUT _plugins/_ism/policies/wazuh-restored-cleanup
{
"policy": {
"description": "Auto-delete restored audit logs after 7 days",
"default_state": "active",
"ism_template": [
{
"index_patterns": ["restored-wazuh-alerts-*"],
"priority": 100
}
],
"states": [
{
"name": "active",
"actions": [],
"transitions": [
{
"state_name": "delete",
"conditions": {
"min_index_age": "7d"
}
}
]
},
{
"name": "delete",
"actions": [
{
"retry": { "count": 3, "backoff": "exponential", "delay": "1m" },
"delete": {}
}
],
"transitions": []
}
]
}
}
Now, update your runbook. When an auditor asks for data, you don't just restore it; you rename it on the fly using Regex.
Bash
curl -k -u admin:'<password>' -X POST "https://10.3.2.7:9200/_snapshot/azure_repo/test-snapshot/_restore" -H 'Content-Type: application/json' -d '{
"indices": "wazuh-alerts-4.x-2026.02.27",
"ignore_unavailable": true,
"include_global_state": false,
"rename_pattern": "wazuh-alerts-(.+)",
"rename_replacement": "restored-wazuh-alerts-$1"
}'
The logs drop onto your disk as restored-wazuh-alerts-4.x-2026.02.27. The auditor searches the data. Exactly 7 days later, the index deletes itself. No disk exhaustion, no pager duty alerts at 3 AM.
Final Thoughts
Setting up a SIEM is easy; engineering it to survive the realities of production, compliance, and cost constraints is the actual job. By offloading cold storage to Azure and automating the cleanup of audit restorations, you buy yourself peace of mind—and save a massive amount on compute costs.

