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.ymlEnvironment Variables#
Create the .env file from the example:
cp .env.example .envMinimum 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.comUse 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 generatorThis creates config/wazuh_indexer_ssl_certs/ with:
root-ca.pem— self-signed root CAwazuh.manager.pem/wazuh.manager-key.pemwazuh.indexer.pem/wazuh.indexer-key.pemwazuh.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#
| Rules | Control area | Description |
|---|---|---|
| 100001–100002 | Attack detection | Brute force and account lockout |
| 100003, 100010 | Privileged access | Sudo and user creation |
| 100020 | System integrity | Changes to critical files |
| 100030 | Availability | Unexpected 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-agentDashboard 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.