Introduction
Wazuh index data contains valuable security information, including alerts, vulnerability findings, and compliance results. Protecting this data is essential to ensure it can be recovered following accidental deletion, index corruption, or infrastructure failure.
The Wazuh indexer supports snapshot management through its underlying OpenSearch capabilities, allowing snapshots to be stored in external repositories. While the Wazuh documentation demonstrates using an NFS shared filesystem as a snapshot repository, organizations may prefer an S3-compatible object storage solution such as MinIO for environments where shared storage is unavailable or undesirable.
In this blog post, we demonstrate how to configure MinIO as a self-hosted S3-compatible snapshot repository for the Wazuh indexer. We also show how to automate snapshot creation and retention using Snapshot Management and how to restore index data from a snapshot.
NFS or MinIO - What Works for You
Wazuh documents NFS as a snapshot repository and it does the job well. If NFS is already part of your infrastructure, there is no reason to move away from it. Both approaches support automated snapshot policies through the same Snapshot Management interface in the Wazuh Dashboard — the scheduler, retention rules, and restore workflow are identical regardless of which repository type you use.
MinIO is simply another option that some environments fit better. It uses the S3 API over HTTP instead of a shared filesystem mount, which means no path.repo change in opensearch.yml, no per-node mount configuration, and no dependency on a network filesystem staying healthy. If the MinIO endpoint is unreachable, the snapshot fails with a visible connection error rather than silently writing nothing. And because it speaks S3, the same repository configuration works against AWS S3 or any other S3-compatible storage by changing the endpoint — useful if your environment might move to cloud storage later.
That said, if NFS works for you, keep using it. This guide just uses MinIO as the repository because it fits the two-server lab setup cleanly and demonstrates an approach that is not yet covered in the official Wazuh documentation.
One thing worth knowing before deploying MinIO in production: the open-source community edition was archived in April 2026 and is no longer actively maintained. For a long-term production setup, Garage or SeaweedFS are solid S3-compatible alternatives actively maintained, fully open-source, and the Wazuh repository configuration works identically with both.
Lab Architecture
This setup requires two servers both can be Ubuntu 22.04 LTS virtual machines or cloud instances.
Server | Role | Minimum Specs |
Server 1 — 192.168.1.10 | Wazuh All-in-One (Manager + Indexer + Dashboard) | 4 vCPU · 8 GB RAM · 50 GB disk · Ubuntu 22.04 |
Server 2 — 192.168.1.20 | MinIO S3-compatible object storage | 2 vCPU · 4 GB RAM · 100 GB disk · Ubuntu 22.04 |
Note: Both servers must be able to reach each other on port 9000 (MinIO S3 API). Verify connectivity before starting: nc -zv 192.168.1.20 9000
Part 1 Install and Configure MinIO on Server 2
Step 1 Create a dedicated system user
MinIO should never run as root. Create a system user with no login shell:
sudo useradd -r minio-user -s /sbin/nologinStep 2 Create the data directory
sudo mkdir -p /mnt/minio/data
sudo chown -R minio-user:minio-user /mnt/minio
sudo chmod -R 750 /mnt/minio
Step 3 Download and install the MinIO binary
curl -o minio https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
minio --versionStep 4 Create the MinIO environment configuration file
sudo tee /etc/default/minio > /dev/null <<EOF
MINIO_VOLUMES="/mnt/minio/data"
MINIO_ROOT_USER="minioadmin"
MINIO_ROOT_PASSWORD="ChangeThisPassword123!"
MINIO_OPTS="--address 192.168.1.20:9000 --console-address 192.168.1.20:9001"
EOFNote: Replace ChangeThisPassword123! with a strong password. These credentials will be stored in the Wazuh Indexer keystore in Part 3.
Step 5 Create the MinIO systemd service
sudo tee /etc/systemd/system/minio.service > /dev/null <<EOF
[Unit]
Description=MinIO Object Storage
Wants=network-online.target
After=network-online.target
AssertFileIsExecutable=/usr/local/bin/minio
[Service]
WorkingDirectory=/mnt/minio/data
User=minio-user
Group=minio-user
EnvironmentFile=/etc/default/minio
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
Restart=always
LimitNOFILE=65536
TimeoutStopSec=infinity
SendSIGKILL=no
[Install]
WantedBy=multi-user.target
EOFStep 6 Start and enable MinIO
sudo systemctl daemon-reload
sudo systemctl enable minio
sudo systemctl start minio
sudo systemctl status minio
Step 7 Create the snapshot bucket
Open the MinIO console at http://192.168.1.20:9001. Log in with your credentials, navigate to Buckets → Create Bucket, name it wazuh-snapshots, and click Create Bucket.
Alternatively, use the MinIO client (mc) from the command line:
# Install mc on Server 2
curl -o mc https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
sudo mv mc /usr/local/bin/
# Configure and create bucket
mc alias set local http://192.168.1.20:9000 minioadmin ChangeThisPassword123!
mc mb local/wazuh-snapshots
mc ls local/
Part 2 Install the repository-s3 Plugin on the Wazuh Indexer
Step 1 Install the repository-s3 plugin
The repository-s3 plugin allows the Wazuh Indexer to communicate with any S3-compatible storage including MinIO.
/usr/share/wazuh-indexer/bin/opensearch-plugin install repository-s3
When prompted with the permissions warning, type y and press Enter. Verify the plugin installed:
/usr/share/wazuh-indexer/bin/opensearch-plugin list | grep repository-s3Note: Use the Wazuh Indexer plugin path /usr/share/wazuh-indexer/bin/opensearch-plugin — not the standard OpenSearch path. These are different binaries.
Step 2 Store MinIO credentials in the Wazuh Indexer keystore
The Wazuh Indexer uses a secure keystore to store S3 credentials. These commands prompt for values without echoing them on screen.
without echoing them on screen.
# Add the MinIO access key
echo "minioadmin" | sudo /usr/share/wazuh-indexer/bin/opensearch-keystore add --stdin s3.client.default.access_key
# Add the MinIO secret key
echo "ChangeThisPassword123!" | sudo /usr/share/wazuh-indexer/bin/opensearch-keystore add --stdin s3.client.default.secret_key
Verify the keystore permissions:
ls -la /etc/wazuh-indexer/opensearch.keystoreStep 3 Restart the Wazuh Indexer
sudo systemctl restart wazuh-indexer
# Wait 30 seconds then verify cluster health
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
https://192.168.1.10:9200/_cluster/health?prettyNote: The status field should show green or yellow after restart. Red means unassigned shards — investigate before proceeding.
Part 3 Register the MinIO Repository
Step 1 Register the repository via the API
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X PUT "https://192.168.1.10:9200/_snapshot/wazuh-minio-repo?pretty" \
-H "Content-Type: application/json" \
-d '{
"type": "s3",
"settings": {
"bucket": "wazuh-snapshots",
"endpoint": "192.168.1.20:9000",
"protocol": "http",
"path_style_access": "true",
"client": "default"
}
}'
A successful response returns: { "acknowledged": true }Note: path_style_access: true is required for MinIO. Without it, the indexer tries virtual-hosted-style URLs that do not work with self-hosted S3-compatible storage.
Step 2 Take a manual test snapshot
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X PUT "https://192.168.1.10:9200/_snapshot/wazuh-minio-repo/test-snapshot-01?wait_for_completion=true&pretty" \
-H "Content-Type: application/json" \
-d '{
"indices": "wazuh-alerts-*",
"ignore_unavailable": true,
"include_global_state": false
}'
A successful response includes "state": "SUCCESS" and lists all snapshotted indices. Verify the files landed in MinIO:
mc ls local/wazuh-snapshots/
Part 4 Create a Snapshot Management Policy in the Wazuh Dashboard
Instead of manually triggering snapshots, a Snapshot Management (SM) policy runs on a schedule and automatically manages retention. Everything in this part is done through the Wazuh Dashboard no command line required.
Step 1 Navigate to Snapshot Management
In the Wazuh Dashboard: ☰ (Menu) → Indexer management → Snapshot Management

Step 2 Create a new policy
Click Create policy and fill in:
- Policy name: wazuh-daily-snapshot
- Description: Daily automated snapshot of Wazuh alert indices to MinIO

Step 3 Configure the snapshot source
Under Source, set Indices to: wazuh-alerts-*,wazuh-archives-*
This pattern captures all daily alert indices and archive indices. Add any custom Wazuh indices here separated by commas.

Step 4 Configure the snapshot destination
Under Destination:
Repository: wazuh-minio-repo
Snapshot name prefix: wazuh-daily-
The policy appends the date and sequence number automatically, producing names like wazuh-daily-2026-07-03-1.
Step 5 Set the schedule
Under Schedule, toggle to Custom cron expression and enter:
0 2 * * *This fires every day at 2:00 AM. Select your local timezone from the dropdown.

Step 6 Configure retention
Under Retention:
Max count: 30 (keep 30 days of daily backups)
Min count: 7 (never delete below 7 snapshots)
Max age: 30d

Step 7 Review and create
Review the full policy configuration and click Create. The policy list will show wazuh-daily-snapshot with status Enabled and the next scheduled run time.

Part 5 Fixing Snapshot Restore Permissions (Security Plugin)
When attempting a restore, you may encounter a 403 security_exception even when using the admin user. This is a known behavior in the Wazuh Indexer security plugin — the admin backend role maps to all_access but does not automatically inherit snapshot restore permissions from the reserved manage_snapshots role.
The fix is to create a custom role with the required cluster permissions and map the admin user to it.
Step 1 Create a custom snapshot restore role
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X PUT "https://localhost:9200/_plugins/_security/api/roles/custom_snapshot_restore?pretty" \
-H "Content-Type: application/json" \
-d '{
"cluster_permissions": [
"cluster:admin/snapshot/restore",
"cluster:admin/snapshot/get",
"cluster:admin/snapshot/create",
"cluster:admin/snapshot/status",
"cluster:admin/repository/get",
"cluster:admin/repository/put"
],
"index_permissions": [
{
"index_patterns": ["*"],
"allowed_actions": [
"indices:admin/create",
"indices:data/write/index",
"indices:admin/mapping/put"
]
}
]
}'
Expected response: { "status": "CREATED", "message": "'custom_snapshot_restore' created." }Step 2 Map admin user to the custom role
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X PUT "https://localhost:9200/_plugins/_security/api/rolesmapping/custom_snapshot_restore?pretty" \
-H "Content-Type: application/json" \
-d '{
"backend_roles": ["admin"],
"hosts": [],
"users": ["admin"]
}'
Expected response: { "status": "CREATED", "message": "'custom_snapshot_restore' created." }Note: The manage_snapshots role is reserved in Wazuh Indexer and cannot be modified via PUT or PATCH. Creating a custom role is the correct approach — do not attempt to modify reserved roles.
Part 6 Restoring From a Snapshot
Knowing how to restore is as important as knowing how to create backups. This section covers both full restore and targeted single-index recovery both tested in this lab.
Restore a specific index from a snapshot
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X POST "https://localhost:9200/_snapshot/wazuh-minio-repo/test-snapshot-01/_restore?pretty" \
-H "Content-Type: application/json" \
-d '{
"indices": "wazuh-alerts-4.x-2026.07.02",
"ignore_unavailable": true,
"include_global_state": false,
"rename_pattern": "(.+)",
"rename_replacement": "restored-$1"
}'
Expected response: { "accepted": true }
Note: The rename_pattern and rename_replacement fields are required. They prefix the restored index with restored- to avoid conflicts with any existing index of the same name. An index cannot be restored into a name already in use.Check restore progress
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
"https://localhost:9200/_cat/recovery/restored-wazuh-alerts-4.x-2026.07.02?v&pretty"Verify the restored index
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
"https://localhost:9200/_cat/indices/restored-wazuh-alerts-4.x-2026.07.02?v&pretty"Making restored data visible in Threat Hunting
The restored index is named restored-wazuh-alerts-4.x-2026.07.02 — it does not match the wazuh-alerts-* dashboard pattern. To make it searchable in Threat Hunting, reindex the restored data back into the original index name:
curl -sk -u admin:<YOUR_ADMIN_PASSWORD> \
-X POST "https://localhost:9200/_reindex?pretty" \
-H "Content-Type: application/json" \
-d '{
"source": {
"index": "restored-wazuh-alerts-4.x-2026.07.02"
},
"dest": {
"index": "wazuh-alerts-4.x-2026.07.02"
}
}'After reindexing completes, open Threat Hunting in the Wazuh Dashboard and filter by date the alerts from that date will appear exactly as before.

Conclusion
The question that started all of this "If the Wazuh Indexer disk dies tonight, how much alert history do we lose?" now has a real answer: nothing, as long as the last snapshot ran.
That shift matters more than it sounds. Most teams spend enormous energy making Wazuh collect the right data and catch the right threats. Very few think about what happens to that data when something goes wrong at the infrastructure level. A corrupted index, an accidental deletion, a disk failure during an upgrade these are not edge cases.
Share
