Deploying the Wazuh stack via Docker in production (either in single-node or multi-node topologies) requires encrypted communication by default. Traffic between the Manager, Indexer, and Dashboard runs strictly over mTLS (Mutual TLS).
To save you from creating these keys by hand, the official Wazuh repository provides a utility compose file called generate-indexer-certs.yml. It spins up a temporary container (generator) that reads instances.yml and outputs all necessary certificates into the ./config/certs/ host directory.
However, the headache usually begins during the main stack startup, where the wazuh.indexer container enters a crash loop or rejects the TLS keys. If you are facing this, the issue is almost always centered around two specific bottlenecks described below.
Prerequisites#
Before executing the certificate generation commands, ensure you have:
- Docker Engine 20.10+ and Docker Compose v2 installed on the host.
- The official repository cloned locally:
git clone https://github.com/wazuh/wazuh-docker.git. In recent repository releases (4.7+),generate-indexer-certs.ymlresides insingle-node/ormulti-node/— adjust the path:docker compose -f single-node/generate-indexer-certs.yml run --rm generator. - Sufficient disk space available in the
./configdirectory. - Root access or
sudoprivileges to manipulate system files.
1. File permission failure (accessdeniedexception)#
The certificate generator container runs as root and writes the directory and files to the host volume. However, the Wazuh Indexer daemon inside the container executes under the system user wazuh (which owns UID/GID 1000).
If you don't adjust the file ownership on the host filesystem, the indexer will fail to read its private key (.key) during startup:
wazuh-indexer | java.nio.file.AccessDeniedException: /usr/share/wazuh-indexer/config/certs/wazuh.indexer.key
How to fix via host chown#
Align the file ownership and read permissions with the correct UID on the host immediately after generation:
# Force directory ownership to Wazuh's UID 1000
sudo chown -R 1000:1000 ./config/certs
# Restrict private key and certificate read permissions
sudo chmod 600 ./config/certs/*.key
sudo chmod 644 ./config/certs/*.pem
Alternative: user mapping in compose#
If you want to avoid running chown on the host, declare the user mapping directly inside the docker-compose.yml file to match the generated certificate ownership:
services:
wazuh.indexer:
image: wazuh/wazuh-indexer:4.x
user: "1000:1000" # Forces the container to run as the correct UID/GID
volumes:
- ./config/certs:/usr/share/wazuh-indexer/config/certs:ro
This configuration ensures Docker grants read access over the bind mounts without requiring manual host-level root ownership changes.
2. Hostname mismatch during TLS handshake#
The TLS engine strictly validates the SAN (Subject Alternative Name) fields in the certificates. If the container name or hostname mapped in docker-compose.yml (e.g., wazuh.indexer) does not match the identities defined in the generation config, the connection fails.
The resulting error output varies across components:
- Wazuh Dashboard (Node.js — OpenSearch Dashboards):
ERR_TLS_CERT_ALTNAME_INVALIDin container logs / HTTP 503 on dashboard endpoint - Wazuh Indexer (Java):
java.security.cert.CertificateException: No Subject Alternative Names present - Wazuh Manager (C):
ERROR: SSL server certificate verification failed
How to fix#
Open config/certs/instances.yml and ensure that the name block and the values under dns map exactly to the service names defined in your docker-compose.yml:
# config/certs/instances.yml
instances:
- name: wazuh.indexer
ip:
- 127.0.0.1
dns:
- wazuh.indexer
- localhost
- name: wazuh.manager
dns:
- wazuh.manager
- name: wazuh.dashboard
dns:
- wazuh.dashboard
Security: mandatory backups before cleaning certificates#
If you modify these configs, never wipe the old certificate directory without taking a backup first. Other active services depending on the previous signatures will lose connectivity. Create a timestamped archive and verify its creation success before executing any physical deletion:
# Create a compressed backup and clean the directory only on success
BACKUP_DATE=$(date +%Y%m%d-%H%M%S)
sudo tar czf "./config/certs-backup-${BACKUP_DATE}.tar.gz" ./config/certs/ && \
sudo rm -rf ./config/certs/* || { echo "❌ Backup failed! Aborting deletion."; exit 1; }
echo "✅ Backup created successfully and folder cleaned for regeneration."
3. Mandatory security controls: SELinux and AppArmor#
On environments with active SELinux (RHEL, Rocky Linux, CentOS) or AppArmor (Ubuntu), the kernel's Mandatory Access Control (MAC) policies can block the container engine from reading host-mounted certificates, even if the file permissions (chmod/chown) match perfectly on the host.
Checking and adjusting SELinux contexts#
If getenforce reports Enforcing, you must label the certificates directory correctly.
# Option 1: Temporary/volatile context adjustment (fast testing)
sudo chcon -R -t container_file_t ./config/certs/
# Option 2: Persistent context adjustment (recommended for production)
# 1. Register the default context for the absolute certificates folder path
sudo semanage fcontext -a -t container_file_t "/absolute/path/to/project/config/certs(/.*)?"
# 2. Apply the registered security context
sudo restorecon -R -v ./config/certs
4. Automated certificate provisioning script#
Use this script to initialize certificates safely, validating environment prerequisites and setting permissions. In recent repository releases (4.7+), generate-indexer-certs.yml resides in single-node/ or multi-node/ — adjust the path: docker compose -f single-node/generate-indexer-certs.yml run --rm generator. You can inspect the full code below without leaving the page, or access the maintained version on GitHub Gist:
wazuh-certs-setup.sh — Automated certificate setup for Wazuh DockerGistExpand code
#!/usr/bin/env bash
# wazuh-certs-setup.sh - Automated certificate setup for Wazuh Docker
set -euo pipefail
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
# Root privilege validation
if [ "$(id -u)" -ne 0 ]; then
log "❌ Run as root (sudo su -) or use: sudo bash $0"; exit 1
fi
# 1. Dependency Checks
for cmd in docker openssl; do
command -v "$cmd" >/dev/null 2>&1 || { log "❌ Missing dependency: $cmd"; exit 1; }
done
# 2. Preventative Backup with success validation
if [ -d "./config/certs" ] && [ "$(ls -A ./config/certs)" ]; then
BACKUP_PATH="./config/certs-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
log "💾 Archiving old certificates into $BACKUP_PATH..."
tar -czf "$BACKUP_PATH" ./config/certs/ && rm -rf ./config/certs/* || { log "❌ Critical error archiving old certificates! Aborting cleanup."; exit 1; }
fi
mkdir -p ./config/certs
# 3. Certificate Generation (in 4.7+ repositories, the file resides in single-node/ or multi-node/)
log "🔑 Launching Wazuh certificate generator container..."
COMPOSE_FILE="generate-indexer-certs.yml"
[ ! -f "$COMPOSE_FILE" ] && [ -f "single-node/generate-indexer-certs.yml" ] && COMPOSE_FILE="single-node/generate-indexer-certs.yml"
docker compose -f "$COMPOSE_FILE" run --rm generator
# 4. Permission Adjustments
log "🔧 Aligning owner and permissions (UID 1000)..."
chown -R 1000:1000 ./config/certs
chmod 600 ./config/certs/*.key
chmod 644 ./config/certs/*.pem
# 5. Apply SELinux Contexts if active (persistent if semanage is available)
if command -v getenforce >/dev/null && [ "$(getenforce)" = "Enforcing" ]; then
log "🛡️ Setting SELinux context (container_file_t)..."
if command -v semanage >/dev/null; then
ABS_PATH=$(realpath ./config/certs)
semanage fcontext -a -t container_file_t "$ABS_PATH(/.*)?" 2>/dev/null && restorecon -R ./config/certs || chcon -R -t container_file_t ./config/certs
else
chcon -R -t container_file_t ./config/certs
fi
fi
log "✅ Certificate provisioning finished successfully!"5. Certification diagnostics and troubleshooting script#
If you face TLS handshakes issues or boot loops, execute this diagnosis tool on the host to analyze permissions, error logs, and Subject Alternative Names (SAN). You can inspect the full code below without leaving the page, or access the maintained version on GitHub Gist:
diagnose-wazuh-certs.sh — Diagnostics script for Wazuh Docker certificatesGistExpand code
#!/usr/bin/env bash
# diagnose-wazuh-certs.sh - Diagnostics script for Wazuh Docker certificates
set -eo pipefail
echo "=== WAZUH DOCKER DIAGNOSTICS ==="
echo "Execution Date: $(date)"
echo ""
# 1. Verify Container States
echo "📦 Stack Containers Status:"
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -i wazuh || echo "No Wazuh containers found."
echo ""
# 2. Check File Ownerships
echo "👤 Certificate Owners and Permissions:"
if [ -d "./config/certs" ]; then
for file in ./config/certs/*; do
if [ -f "$file" ]; then
stat -c " %n -> Owner: %U:%G | Perms: %a" "$file"
fi
done
else
echo "❌ Directory ./config/certs not found on host."
fi
echo ""
# 3. Validate SAN Parameters
echo "🔍 Subject Alternative Name (SAN) Audit:"
INDEXER_CERT="./config/certs/wazuh.indexer.pem"
if [ -f "$INDEXER_CERT" ]; then
openssl x509 -in "$INDEXER_CERT" -text -noout | grep -A2 "Subject Alternative Name" || echo "⚠️ SAN parameters not found."
else
echo "❌ File wazuh.indexer.pem not found."
fi
echo ""
# 4. Check Container Logs for Errors (Note: in multi-node topologies, container names can vary, e.g., wazuh-indexer-1)
echo "📋 Core Error Logs from Indexer:"
INDEXER_CONTAINER=$(docker ps --format '{{.Names}}' | grep -i -E "wazuh.*indexer" | head -n 1 || echo "wazuh.indexer")
echo "Reading logs from container: $INDEXER_CONTAINER"
docker logs --tail 50 "$INDEXER_CONTAINER" 2>&1 | grep -i -E "error|exception|denied|mismatch|failed" || echo "No critical errors found in recent log lines."
echo ""
# 5. Recovery Notes / Generator Reset
echo "⚠️ RECOVERY NOTE:"
echo "If you need to reset the certificate state completely, run:"
echo " docker compose -f generate-indexer-certs.yml down -v"
echo " rm -rf ./config/certs/*"
echo "==================================="Provisioning and boot pipeline (troubleshooting workflow)#
Below is the visual mapping of the certificate generation pipeline, permission fixes, SELinux policies, and stack boot validation for Wazuh Docker:
instances.yml configured
│
▼
docker compose -f generate-indexer-certs.yml run --rm generator
│
▼
Certificates written to host (root)
│
▼
chown -R 1000:1000 ./config/certs
│
▼
chmod 600 *.key · chmod 644 *.pem
│
▼
SELinux active? ──Yes──▶ semanage fcontext + restorecon
│ │
No │
└──────────▼────────────────┘
Validate SAN in certificate (openssl x509)
│
▼
docker compose up -d
Conclusion#
In this guide, I covered the two most frequent failure modes during the indexer boot — key read permissions (UID 1000) and strict SAN validation in the mTLS handshake — alongside persistent SELinux hardening. With automated setup and diagnostic tooling in place, crash loops become trivial to resolve in minutes.
Was this article helpful?
Leave a quick reaction to help prioritize future technical guides:
This post is licensed under CC BY-NC.



Comments
Join the discussion below.
0 comments