Skip to main content

Wazuh SIEM with Docker: complete deployment with SSL and compliance rules

Rogelio Guerra Riverón
Author
Rogelio Guerra Riverón
Building my own web infrastructure from scratch. Here I document each step: servers, networks, containers and everything that comes along.

What is ENS and Why It Matters
#

The National Security Framework (ENS) is the mandatory cybersecurity regulatory framework for Spanish Public Administrations and private companies that provide services to them. It is regulated by the Royal Decree 311/2022 and establishes the principles, requirements, and security measures that must be applied to information systems that handle public data or services.

In practice, the ENS classifies systems based on the impact that a security incident would have on the organization and citizens. The level of measures to be implemented depends on that classification.

The Three ENS Categories
#

The ENS defines three categorization levels:

Basic Category For systems whose compromise would have limited impact. It usually applies to informational websites, low-risk internal services, or systems with non-sensitive data. The required measures are the minimum of the framework.

Medium Category For systems whose compromise would cause considerable harm to the organization or third parties. It is the most common category in internal management environments: ERP, employee portals, HR systems, document management platforms. It requires active monitoring controls, incident management, and access control.

High Category For critical systems whose compromise could cause serious or very serious harm. It applies to systems that manage critical infrastructure, health data, judicial or defense systems. It requires the strictest measures of the framework.

Medium Category in Practice
#

A system classified as medium category requires, among other controls:

  • Continuous monitoring of security events
  • Active detection and incident management
  • Privileged access control and action traceability
  • System file integrity
  • Centralized log recording and analysis

This is exactly where Wazuh comes in.


Wazuh as a Compliance Platform
#

Wazuh is an open source SIEM (Security Information and Event Management) platform that natively covers most of the monitoring controls required by medium category: real-time log analysis, intrusion detection, file integrity monitoring, software inventory, and active incident response.

In this article I deploy a single node with Docker, generate the necessary TLS certificates, and add custom rules oriented to the most common security controls in medium category environments.


Stack Architecture
#

The single-node Wazuh stack has three components:

  • wazuh.manager — event analysis and correlation engine
  • wazuh.indexer — OpenSearch-based storage
  • wazuh.dashboard — web interface (OpenSearch Dashboards)

Communication between components uses mutual TLS with custom certificates, which we generate before the first startup.

Project Structure
#

wazuh/
├── docker-compose.yml
├── generate-certs.yml
├── gen-certs.sh
├── deploy.sh
├── .env
└── config/
    ├── certs.yml
    ├── wazuh_manager/
    │   ├── wazuh_manager.conf
    │   └── local_rules.xml
    ├── wazuh_indexer/
    │   ├── wazuh.indexer.yml
    │   └── internal_users.yml
    └── wazuh_dashboard/
        ├── opensearch_dashboards.yml
        └── wazuh.yml

Environment Variables
#

Create the .env file from the example:

cp .env.example .env

Minimum content:

INDEXER_ADMIN_PASSWORD=TU_PASSWORD_SEGURO
KIBANA_PASSWORD=TU_PASSWORD_KIBANA
API_PASSWORD=TU_PASSWORD_API

SMTP_HOST=mail.tudominio.com
SMTP_FROM=alertas@tudominio.com
SMTP_TO=admin@tudominio.com

Use passwords of at least 12 characters with uppercase letters, numbers, and symbols. The indexer validates them at startup.

Generate TLS Certificates
#

Wazuh requires TLS certificates for internal communication between manager, indexer, and dashboard. The stack includes a generator container:

docker compose -f generate-certs.yml run --rm generator

This creates config/wazuh_indexer_ssl_certs/ with:

  • root-ca.pem — self-signed root CA
  • wazuh.manager.pem / wazuh.manager-key.pem
  • wazuh.indexer.pem / wazuh.indexer-key.pem
  • wazuh.dashboard.pem / wazuh.dashboard-key.pem

docker-compose.yml
#

services:
  wazuh.manager:
    image: wazuh/wazuh-manager:4.9.2
    hostname: wazuh.manager
    restart: always
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 655360
        hard: 655360
    ports:
      - "1514:1514/tcp"   # agentes TCP
      - "1515:1515/tcp"   # enrollment
      - "5140:5140/udp"   # syslog externo
    environment:
      INDEXER_URL: https://wazuh.indexer:9200
      INDEXER_USERNAME: admin
      INDEXER_PASSWORD: ${INDEXER_ADMIN_PASSWORD}
      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: ${API_PASSWORD}
    volumes:
      - wazuh_api_configuration:/var/ossec/api/configuration
      - wazuh_etc:/var/ossec/etc
      - wazuh_logs:/var/ossec/logs
      - ./config/wazuh_indexer_ssl_certs/root-ca.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_manager/local_rules.xml:/var/ossec/etc/rules/local_rules.xml
      - ./config/wazuh_manager/wazuh_manager.conf:/wazuh-config-mount/etc/ossec.conf

  wazuh.indexer:
    image: wazuh/wazuh-indexer:4.9.2
    hostname: wazuh.indexer
    restart: always
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536
    environment:
      OPENSEARCH_JAVA_OPTS: "-Xms1g -Xmx1g"
    volumes:
      - wazuh-indexer-data:/var/lib/wazuh-indexer
      - ./config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/opensearch.yml
      - ./config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/certs/root-ca.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.pem
      - ./config/wazuh_indexer_ssl_certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer-key.pem

  wazuh.dashboard:
    image: wazuh/wazuh-dashboard:4.9.2
    hostname: wazuh.dashboard
    restart: always
    ports:
      - "8443:5601"
    environment:
      INDEXER_USERNAME: admin
      INDEXER_PASSWORD: ${INDEXER_ADMIN_PASSWORD}
      WAZUH_API_URL: https://wazuh.manager
      DASHBOARD_USERNAME: kibanaserver
      DASHBOARD_PASSWORD: ${KIBANA_PASSWORD}
      API_USERNAME: wazuh-wui
      API_PASSWORD: ${API_PASSWORD}
    volumes:
      - ./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
      - ./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
    depends_on:
      - wazuh.indexer

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:

Deployment script
#

#!/usr/bin/env bash
set -euo pipefail

echo "=== [1/4] Generando certificados SSL ==="
docker compose -f generate-certs.yml run --rm generator

echo "=== [2/4] Abriendo puertos en UFW ==="
ufw allow 1514/tcp comment "Wazuh agentes TCP"
ufw allow 1515/tcp comment "Wazuh enrollment"
ufw allow 5140/udp comment "Wazuh syslog"

echo "=== [3/4] Iniciando stack ==="
docker compose up -d

echo "=== [4/4] Esperando indexer (2-3 min) ==="
for i in $(seq 1 30); do
  if docker compose exec wazuh.indexer \
      curl -ks https://localhost:9200/_cat/health 2>/dev/null | grep -qE 'green|yellow'; then
    echo "Indexer OK"
    break
  fi
  echo "Esperando... ($i/30)"
  sleep 10
done

docker compose ps
echo "Dashboard disponible en https://TU-IP:8443"

Manager configuration
#

The wazuh_manager.conf file defines global behavior. Key points:

<ossec_config>
  <global>
    <jsonout_output>yes</jsonout_output>
    <alerts_log>yes</alerts_log>
    <email_notification>no</email_notification>
    <smtp_server>mail.tudominio.com</smtp_server>
    <email_from>alertas@tudominio.com</email_from>
    <email_to>admin@tudominio.com</email_to>
    <email_maxperhour>12</email_maxperhour>
    <agents_disconnection_time>10m</agents_disconnection_time>
    <white_list>127.0.0.1</white_list>
    <white_list>192.168.0.0/16</white_list>
  </global>

  <alerts>
    <log_alert_level>3</log_alert_level>
    <email_alert_level>12</email_alert_level>
  </alerts>

  <!-- Agentes via TCP -->
  <remote>
    <connection>secure</connection>
    <port>1514</port>
    <protocol>tcp</protocol>
    <queue_size>131072</queue_size>
  </remote>

  <!-- Syslog UDP para firewalls y dispositivos de red -->
  <remote>
    <connection>syslog</connection>
    <port>5140</port>
    <protocol>udp</protocol>
    <allowed-ips>0.0.0.0/0</allowed-ips>
  </remote>

  <syscheck>
    <disabled>no</disabled>
    <frequency>43200</frequency>
    <scan_on_start>yes</scan_on_start>
    <alert_new_files>yes</alert_new_files>
    <directories check_all="yes" report_changes="yes">/etc,/usr/bin,/usr/sbin</directories>
    <directories check_all="yes">/bin,/sbin,/boot</directories>
  </syscheck>
</ossec_config>

Custom security rules
#

Wazuh includes thousands of predefined rules. Custom ones go in local_rules.xml with IDs from 100000 onwards. This block implements the most common monitoring controls in environments with medium-category compliance requirements: attack detection, privileged access control, file integrity, and service availability.

<group name="cumplimiento_custom,">

  <!-- DETECCIÓN DE ATAQUES — autenticación -->

  <!-- Fuerza bruta SSH: 8 intentos fallidos en 120 segundos -->
  <rule id="100001" level="10" frequency="8" timeframe="120">
    <if_matched_sid>5760</if_matched_sid>
    <description>Posible ataque de fuerza bruta SSH — $(attempts) intentos fallidos</description>
    <group>authentication_failures,</group>
  </rule>

  <!-- Cuenta de usuario bloqueada -->
  <rule id="100002" level="10">
    <if_sid>5503</if_sid>
    <description>Cuenta bloqueada por múltiples intentos fallidos de autenticación</description>
    <group>authentication_failures,</group>
  </rule>

  <!-- CONTROL DE ACCESO PRIVILEGIADO -->

  <!-- Escalada de privilegios con sudo -->
  <rule id="100003" level="9">
    <if_sid>5402</if_sid>
    <description>Escalada de privilegios detectada mediante sudo</description>
    <group>priv_escalation,</group>
  </rule>

  <!-- Nuevo usuario creado en el sistema -->
  <rule id="100010" level="8">
    <if_sid>5902</if_sid>
    <description>Nuevo usuario creado en el sistema — revisión recomendada</description>
    <group>account_changes,</group>
  </rule>

  <!-- INTEGRIDAD DE FICHEROS -->

  <!-- Modificación de fichero crítico del sistema -->
  <rule id="100020" level="10">
    <if_sid>550</if_sid>
    <description>Fichero crítico del sistema modificado — $(file)</description>
    <group>syscheck,integrity_check_host,</group>
  </rule>

  <!-- DISPONIBILIDAD DE SERVICIOS -->

  <!-- Servicio detenido inesperadamente -->
  <rule id="100030" level="8">
    <if_sid>2904</if_sid>
    <description>Servicio detenido de forma inesperada — $(service)</description>
    <group>service_control,</group>
  </rule>

</group>

What each block covers
#

RulesControl areaDescription
100001–100002Attack detectionBrute force and account lockout
100003, 100010Privileged accessSudo and user creation
100020System integrityChanges to critical files
100030AvailabilityUnexpected service failures

The alert levels (8-10) determine which alerts generate email notifications based on the threshold configured in email_alert_level.

Enroll a Linux agent
#

From the server where the agent will be installed:

# Descargar e instalar (ajusta versión y arquitectura)
wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.9.2-1_amd64.deb
dpkg -i wazuh-agent_4.9.2-1_amd64.deb

# Apuntar al manager
sed -i 's/MANAGER_IP/192.168.X.X/' /var/ossec/etc/ossec.conf

# Registrar y arrancar
/var/ossec/bin/agent-auth -m 192.168.X.X
systemctl enable wazuh-agent && systemctl start wazuh-agent

Dashboard access
#

Once the stack is UP (the indexer takes 2-3 minutes to initialize):

https://TU-IP:8443
Usuario: admin
Contraseña: (la definida en INDEXER_ADMIN_PASSWORD)

Important: change all default passwords before exposing the service to the network.

Conclusion
#

With this stack you have a complete SIEM on a single server capable of covering the monitoring controls required by the most common compliance frameworks. The natural next step is to add more agents, configure email or webhook alerts for high-level events, and review the compliance dashboards that Wazuh includes out of the box for PCI-DSS, GDPR, HIPAA, and ENS itself. To verify the level of compliance of a system with ENS, CCN-CERT publishes the security guides (CCN-STIC series) as reference.