UTMStack v11.2.8 Community Edition — Procédures de déploiement pour lab PME Suisse. Installation VMware, Suricata, CrowdSec, SOAR, OPNsense.
Les règles de corrélation built-in d’un SIEM (system_owner=true côté UTMStack) sont calibrées pour un périmètre générique — elles doivent fonctionner sur n’importe quel déploiement, donc elles pèchent presque toujours par excès de largeur plutôt que par excès de précision. Le tuning post-déploiement n’est pas une anomalie, c’est une discipline à part entière du métier SOC (le detection engineering), et ce comportement se retrouve sur tous les produits du marché (Splunk ES, QRadar, Sentinel, Elastic Security, Wazuh…).
Ce lab amplifie particulièrement le phénomène : la plupart des déploiements SIEM en entreprise ne reçoivent les logs IDS/IPS qu’après un premier filtrage côté firewall managé. Ici, HOME_NET a été délibérément élargi pour inclure le réseau WAN (voir 07 — Règles Suricata avancées), afin de capturer tout le trafic de scan Internet à des fins pédagogiques et documentaires. Conséquence directe : 100% du bruit de reconnaissance passive (scanners connus, listes de réputation IP) remonte jusqu’aux règles de corrélation UTMStack, alors qu’un déploiement pro filtrerait une bonne partie de ce volume en amont.
Échelle observée sur un mois de lab (v11-alert-*, 2026-05-31 → 2026-06-29) : 150 004 alertes “Open”, dont trois règles built-in concentraient à elles seules 97% du volume.
Architecture confirmée par inspection directe (OpenSearch + PostgreSQL) :
| Composant | Contenu |
|---|---|
OpenSearch — index v11-alert-YYYY-MM-DD |
Instances d’alertes (un index par jour, géré par ISM) |
PostgreSQL — table utm_correlation_rules |
Définitions des règles de corrélation (rule_definition_def) |
Le port OpenSearch (9200) n’est pas exposé sur l’hôte — toute requête passe par docker exec dans le conteneur utmstack_node1 :
docker exec -it <container_id> curl -s -u 'admin:<password>' -k -X POST "https://localhost:9200/v11-alert-*/_search?pretty" \
-H 'Content-Type: application/json' -d '{ ... }'
⚠️ Le mot de passe OpenSearch contenant un
!, il déclenche l’expansion d’historique bash s’il n’est pas entre guillemets simples ('admin:motdepasse!xxx'). Toujours encadrer les credentials de guillemets simples.
Agrégation terms sur name.keyword (le nom de la règle de corrélation associée à chaque alerte) :
{
"size": 0,
"aggs": {
"by_statusLabel": {"terms": {"field": "statusLabel.keyword"}},
"by_name": {"terms": {"field": "name.keyword", "size": 20}}
}
}
Résultat obtenu (extrait, 1 mois de lab) :
| Règle | Volume | % du total |
|---|---|---|
| Tunneling Detection | 118 932 | 77% |
| High level Suricata alert | 28 195 | 18% |
| Medium level Suricata alert | 3 184 | 2% |
| Known Malicious IP Detected (SOAR flow 5000) | 2 254 | 1,5% |
| Suricata Network Anomaly Detected (SOAR flow 5001) | 632 | 0,4% |
| (13 autres règles) | ~900 | 0,6% |
Une fois la règle identifiée, sa définition se récupère côté PostgreSQL :
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -x -c \
"SELECT id, rule_name, rule_category, system_owner, rule_active, rule_definition_def FROM utm_correlation_rules WHERE rule_name = '<nom>';"
⚠️ Ne jamais exclure les règles
Known Malicious IP DetectedetSuricata Network Anomaly Detected— ce sont les déclencheurs des flows SOAR 5000/5001 (ban CrowdSec automatique, voir 05 — SOAR & Automatisation). Toute modification de filtre doit explicitement les laisser intactes.
rule_definition_def:
(
(equals("log.eventType", "alert") &&
(contains("log.alert.signature", "SSH") &&
!equals("target.port", 22))) ||
(equals("log.appProto", "ssh") &&
!equals("target.port", 22)) ||
(equals("protocol", "TCP") &&
equals("target.port", 443) &&
!equals("log.appProto", "tls")) ||
(equals("log.eventType", "alert") &&
contains("log.alert.signature", "tunnel")) ||
(equals("target.port", 53) &&
equals("protocol", "TCP") &&
greaterThan("log.flow.bytes_toserver", 5000))
)
La branche 3 (target.port=443 && !appProto=tls) est conçue pour détecter du C2 caché sur le port 443 — un signal légitime. Mais en environnement WAN-facing, ce critère matche aussi massivement les scans Internet qui touchent le port 443 sans jamais compléter de handshake TLS.
Validation par agrégation sur les hits “Tunneling Detection” :
| Critère | Résultat |
|---|---|
target.port |
443 → 118 994 / 119 105 (99,9%) |
log.direction |
to_server → 118 191 (quasi 100%, donc trafic entrant uniquement) |
log.appProto identifié |
1 775 / ~119 000 (1,5% seulement — le reste n’a simplement jamais atteint un protocole identifiable) |
Exemple représentatif : signature SURICATA STREAM 3way handshake SYN resend different seq on SYN recv, origine externe, cible 192.168.1.203 (WAN OPNsense) — une anomalie de stream TCP typique d’un scan superficiel, pas un vrai tunnel applicatif.
Ajout d’une exclusion ciblée sur la seule branche 443, sans toucher aux 4 autres branches (SSH, port 53) ni à la capacité de détection d’un vrai C2 sortant depuis un hôte interne :
UPDATE utm_correlation_rules
SET rule_definition_def = $$(
(equals("log.eventType", "alert") &&
(contains("log.alert.signature", "SSH") &&
!equals("target.port", 22))) ||
(equals("log.appProto", "ssh") &&
!equals("target.port", 22)) ||
(equals("protocol", "TCP") &&
equals("target.port", 443) &&
!equals("log.appProto", "tls") &&
!equals("target.ip", "192.168.1.203")) ||
(equals("log.eventType", "alert") &&
contains("log.alert.signature", "tunnel")) ||
(equals("target.port", 53) &&
equals("protocol", "TCP") &&
greaterThan("log.flow.bytes_toserver", 5000))
)$$
WHERE id = 530;
Résultat observé : volume quotidien de “Tunneling Detection” passé de plusieurs milliers à ~200 sur les jours suivant le fix.
rule_definition_def:
equals("log.eventType", "alert") && equals("severity", "medium")
Règle volontairement générique — capte tout event Suricata de sévérité moyenne, sans distinction de direction ni de cible. Validation par agrégation :
| Critère | Résultat |
|---|---|
action: blocked total |
1 712 |
dont target.ip = 192.168.1.203 (WAN) |
1 708 (99,8% du volume bloqué) |
action: allowed (à préserver) |
1 472 |
Le trafic blocked + cible WAN est composé de signatures déjà neutralisées par Suricata (Dshield, NF Known Scanner) — risque réel nul. Le trafic allowed, lui, doit rester visible quelle que soit sa cible.
UPDATE utm_correlation_rules
SET rule_definition_def = $$(
equals("log.eventType", "alert") &&
equals("severity", "medium") &&
!(equals("target.ip", "192.168.1.203") &&
equals("log.alert.action", "blocked"))
)$$
WHERE id = 876;
Résultat attendu : ~54% de réduction sur cette règle, sans toucher au trafic allowed.
⚠️ Cette version du fix a été complétée par la suite — voir Fix 6 plus bas pour un second profil de bruit découvert sur cette même règle. Si tu construis ton fichier
update-rule876.sqlpour la première fois, utilise directement la version complète du Fix 6, pas celle-ci.
rule_definition_def:
equals("log.eventType", "alert") && equals("severity", "high")
Contrairement à la règle 876, l’agrégation sur by_action a montré l’inverse de ce qui était attendu :
log.alert.action |
Volume |
|---|---|
allowed |
28 413 |
blocked |
10 |
Le critère “WAN + blocked” ne pouvait donc réduire que 10 documents sur 28 195 — inutile tel quel. L’agrégation sur by_signature a révélé le vrai dénominateur commun :
| Signature | Volume | % du bucket |
|---|---|---|
| SCAN Very slow stealth port scan | 15 758 | 56% |
| SCAN Ultra-slow paranoid stealth scan | 12 162 | 43% |
| (8 autres signatures + reste) | ~275 | 1% |
Ces deux signatures sont des détections comportementales de scan furtif (paquets espacés dans le temps) — Suricata les classe en alert/allowed par nature, car ce ne sont jamais des règles de blocage par correspondance de paquet. D’où l’inefficacité du critère action.
Critère corrigé sur la sous-chaîne "stealth" (commune aux deux signatures) combinée à la cible WAN :
UPDATE utm_correlation_rules
SET rule_definition_def = $$(
equals("log.eventType", "alert") &&
equals("severity", "high") &&
!(equals("target.ip", "192.168.1.203") &&
contains("log.alert.signature", "stealth"))
)$$
WHERE id = 875;
Tout vrai scan furtif visant le réseau LAN (reconnaissance interne réelle) reste détecté — seul le bruit de reconnaissance passive contre le WAN est filtré.
rule_definition_def:
(
(equals("log.eventType", "alert") &&
contains("log.alert.signature", ["scan", "SCAN", "portscan"])) ||
(equals("protocol", "TCP") &&
equals("log.tcp.flags", "S") &&
equals("log.flow.state", "new") &&
lessThan("log.flow.duration", 1)) ||
(equals("protocol", "TCP") &&
oneOf("log.tcp.flags", ["", "F", "FPU"])) ||
(equals("log.eventType", "anomaly") &&
contains("log.anomaly.event", "scan"))
)
Règle large (4 branches OR). Volume : 241 docs sur 1 mois, 100% sur target.ip = 192.168.1.203 (WAN), répartition 151 blocked / 90 allowed. Même pattern que la règle 876 : le trafic déjà bloqué visant le WAN ne mérite pas de triage manuel.
ℹ️ Pour ce type de règle comportementale (scan furtif basé sur le timing des paquets), ne pas passer Suricata en
drop— des services légitimes (Google, Azure, CDN) déclenchent régulièrement ces patterns sans intention malveillante. Si une IP est réellement malveillante, elle apparaîtra aussi dans les listes Dshield/CINS/Spamhaus déjà endrop. La couche comportementale reste enalertcomme observateur, pas comme bloqueur.
UPDATE utm_correlation_rules
SET rule_definition_def = $$(
(
(equals("log.eventType", "alert") &&
contains("log.alert.signature", ["scan", "SCAN", "portscan"])) ||
(equals("protocol", "TCP") &&
equals("log.tcp.flags", "S") &&
equals("log.flow.state", "new") &&
lessThan("log.flow.duration", 1)) ||
(equals("protocol", "TCP") &&
oneOf("log.tcp.flags", ["", "F", "FPU"])) ||
(equals("log.eventType", "anomaly") &&
contains("log.anomaly.event", "scan"))
) &&
!(equals("target.ip", "192.168.1.203") &&
equals("log.alert.action", "blocked"))
)$$
WHERE id = 525;
Règle endpoint (event Windows 4104 = PowerShell ScriptBlock Logging), 3 occurrences sur 1 mois. Toutes issues du même chemin :
C:\ProgramData\Microsoft\Windows Defender Advanced Threat Protection\DataCollection\...\36dc1ba5-....ps1
Fausse alerte : c’est le scanner Log4Shell/CVE-2021-44228 intégré à Microsoft Defender for Endpoint, exécuté en tâche de fond par MDE lui-même. Le script contient Invoke-WebRequestWithRootCaVerification (contient le mot-clé Invoke-WebRequest) et FromBase64String (décodage légitime de certificats) — combinaison matchant la deuxième branche regex de détection sans qu’il s’agisse d’une vraie tentative de contournement AMSI.
Ce cas illustre une source de bruit d’une nature différente des précédents : pas du bruit réseau périmétrique, mais un faux positif endpoint généré par l’outil de sécurité Microsoft lui-même. Signal à retenir pour tout déploiement SIEM avec MDE : les agents de sécurité endpoint (MDE, CrowdStrike Falcon, etc.) génèrent régulièrement des scripts PowerShell qui peuvent déclencher des règles AMSI/obfuscation — une exclusion par chemin d’exécution est préférable à une désactivation de la règle.
Exclusion ciblée sur le chemin MDE, sans toucher aux deux branches de détection (actives pour tout autre processus) :
UPDATE utm_correlation_rules
SET rule_definition_def = $$equals("log.eventCode", "4104") &&
(regexMatch("log.eventDataScriptBlockText", "(?i)(amsiutils|amsiinitfailed|amsiscanbuffer|virtualalloc|writeprocessmemory|getdelegateforfunctionpointer|invoke-mimikatz|invoke-shellcode|invoke-dllinjection|createremotethread)") ||
(regexMatch("log.eventDataScriptBlockText", "(?i)(downloadstring|downloadfile|downloaddata|invoke-webrequest|net.webclient|start-bitstransfer)") &&
regexMatch("log.eventDataScriptBlockText", "(?i)(iex|invoke-expression|-enc |-encodedcommand|-w hidden|-windowstyle hidden|frombase64string)"))) &&
!contains("log.data.Path", "Windows Defender Advanced Threat Protection")$$
WHERE id = 1424;
⚠️ ID corrigé — ce fix visait initialement l’ID 1436, qui après un revert de snapshot pointait vers une règle différente (“GCP Firewall Rule Created”). La vraie règle PowerShell vit sous l’ID 1424 en v11.2.11. Voir la section Les ID de règles ne sont pas stables entre versions pour la méthode de recherche utilisée pour la retrouver.
Après la mise en place du fix 2 (WAN + action=blocked), un nouveau profil de bruit est apparu sur la même règle : le trafic sortant légitime d’OPNsense lui-même (mise à jour des définitions Microsoft Defender via Delivery Optimization) déclenchait toujours la règle, puisqu’il n’est pas bloqué (action: allowed) et ne correspond donc pas à l’exclusion existante.
lastEvent.log.alert.signature_id: 2021076
signature: "ET HUNTING SUSPICIOUS Dotted Quad Host MZ Response"
http.http_user_agent: "Microsoft-Delivery-Optimization/10.1" / "10.0"
target.ip: 192.168.1.203 (100% des 36 occurrences)
La signature détecte un exécutable Windows (en-tête “MZ”) téléchargé depuis un hôte désigné par IP brute plutôt qu’un nom de domaine — un pattern généralement associé à du C2 évitant le DNS. Mais Microsoft Delivery Optimization (le CDN interne de Windows Update) utilise couramment des IP directes pour ses téléchargements, sans lien avec une activité malveillante. Confirmé : 100% des occurrences ont ce User-Agent précis, 100% ciblent le WAN local (donc c’est bien OPNsense qui télécharge, pas une machine tierce qui le contacte).
Ajout d’une exclusion supplémentaire sur le User-Agent, en plus (pas en remplacement) de l’exclusion WAN+blocked du fix 2 :
UPDATE utm_correlation_rules
SET rule_definition_def = $$(
equals("log.eventType", "alert") &&
equals("severity", "medium") &&
!(equals("target.ip", "192.168.1.203") &&
equals("log.alert.action", "blocked")) &&
!contains("log.http.http_user_agent", "Microsoft-Delivery-Optimization")
)$$
WHERE id = 876;
Un ban CrowdSec erroné a alerté sur ce faux positif le 15 juillet 2026 : l’IP 74.161.173.129 (infrastructure CDN Fastly utilisée par Microsoft) s’est retrouvée bannie suite à une alerte UTMStack sur du trafic Windows Update/Defender parfaitement légitime.
rule_definition_def:
equals("log.eventType", "alert") &&
(contains("log.alert.signature", ["executable", "EXE download", "PE file", "script download", "malware download"]) ||
oneOf("log.alert.category", ["A Network Trojan was detected", "Potentially Bad Traffic", "Misc Attack"]) &&
contains("log.alert.signature", ["download", "file"])) &&
exists("origin.ip") &&
exists("target.ip")
La signature Suricata déclenchée était NF - Generic - Exe file masquerading using Right-to-Left Override - MITRE T1036 (SID 5018812, catégorie A Network Trojan was detected) — une détection légitime en soi (technique RTLO utilisée pour déguiser des exécutables malveillants), mais qui matche par accident sur certains noms de fichiers de mise à jour Windows/Defender. Le trafic contenait déjà le flowbit ET.INFO.WindowsUpdate, confirmant son origine légitime — mais la règle de corrélation 521 ne tenait pas compte de ce marqueur : le mot-clé "file" présent dans la signature RTLO, combiné à la catégorie A Network Trojan was detected, suffisait à faire matcher la règle et déclencher le SOAR.
Exclusion sur le flowbit ET.INFO.WindowsUpdate, déjà présent nativement dans le trafic légitime concerné :
UPDATE utm_correlation_rules
SET rule_definition_def = $$equals("log.eventType", "alert") &&
(contains("log.alert.signature", ["executable", "EXE download", "PE file", "script download", "malware download"]) ||
oneOf("log.alert.category", ["A Network Trojan was detected", "Potentially Bad Traffic", "Misc Attack"]) &&
contains("log.alert.signature", ["download", "file"])) &&
exists("origin.ip") &&
exists("target.ip") &&
!contains("log.metadata.flowbits", "ET.INFO.WindowsUpdate")$$
WHERE id = 521;
ℹ️ Ce fix illustre une source de faux positif différente des précédents (pas du bruit de reconnaissance réseau, ni un faux positif endpoint MDE comme le Fix 5) : une règle de détection légitime et bien conçue (RTLO masquerading) qui matche accidentellement sur un contenu bénin partageant les mêmes mots-clés génériques (“file”, catégorie “Network Trojan”). La leçon : toujours vérifier si un flowbit ou un marqueur de contexte déjà présent dans les données peut servir d’exclusion ciblée, avant d’envisager de désactiver ou d’affaiblir une règle de détection par ailleurs légitime.
Pour éviter les problèmes d’échappement de guillemets imbriqués ($$...$$, guillemets doubles dans equals("...")) à travers docker exec -c, le SQL est écrit dans un fichier, copié dans le conteneur, puis exécuté via -f :
# 1. Backup avant modification
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -c \
"SELECT rule_definition_def FROM utm_correlation_rules WHERE id = <id>;" > /root/backup-rule<id>-$(date +%Y%m%d).txt
# 2. Fichier SQL (via nano)
nano /root/update-rule<id>.sql
# 3. Copie + exécution dans le conteneur
docker cp /root/update-rule<id>.sql $(docker ps -q -f name=utmstack_postgres):/tmp/update-rule<id>.sql
docker exec -i $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -f /tmp/update-rule<id>.sql
# 4. Vérification
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -c \
"SELECT rule_definition_def FROM utm_correlation_rules WHERE id = <id>;"
ℹ️
docker exec -itavec une commande non-interactive (-c "SELECT..."redirigée vers un fichier) peut sembler « figer » le terminal — c’est en réalité le pager (less) du résultat psql qui attend une touche.qdébloque immédiatement. Pour éviter le piège, ne pas utiliser-itquand la sortie est redirigée.
Une modification de utm_correlation_rules en base ne suffit pas. Constat fait le 2026-06-30 : malgré un UPDATE confirmé en base (vérifié par SELECT), une alerte Dshield correspondant exactement au nouveau critère d’exclusion de la règle 876 a continué à être créée plus de 3 heures après l’application du fix.
Cause identifiée : utm_correlation_rules est lue en mémoire au démarrage des services utmstack_event-processor-worker et utmstack_event-processor-manager, et n’est pas relue à chaud. Tant que ces services tournent depuis avant la modification SQL, ils continuent d’évaluer les alertes avec l’ancienne définition de règle.
Action requise après tout UPDATE sur utm_correlation_rules — redémarrer les deux services, un par un pour limiter le risque (prudence justifiée par un historique d’instabilité Docker Swarm sur ce lab) :
docker service update --force utmstack_event-processor-worker
docker service ps utmstack_event-processor-worker --no-trunc
# Attendre "Running X secondes ago" sans "Failed", puis :
docker service update --force utmstack_event-processor-manager
docker service ps utmstack_event-processor-manager --no-trunc
Validation finale : observer dans le Log Explorer qu’un nouvel événement correspondant au critère d’exclusion n’apparaît plus dans la file Alerts/incidents.
Les ~118 932 alertes “Tunneling Detection” déjà générées avant le fix ne sont pas supprimées rétroactivement par la modification de règle — celle-ci n’agit que sur les futures alertes.
Vérification de la politique ISM (_plugins/_ism/policies/utmstack_ism_policy) :
{
"name": "open",
"transitions": [
{"state_name": "delete", "conditions": {"min_index_age": "30d"}}
]
}
Rétention fixe à 30 jours sur les index v11-alert-* (template partagé avec v11-log-*). Le backlog s’autopurge donc progressivement sans action manuelle, au rythme où chaque index quotidien atteint son seuil d’âge.
⚠️ Un nettoyage manuel via
_update_by_queryest possible (changement destatus/statusLabelen masse) mais déconseillé en routine : risque de timeout sur ~30 index, conflits de version avec les processus actifs (cron de purge, SOC-AI), et opération irréversible sur des documents existants. La rétention naturelle à 30 jours est suffisante dans la plupart des cas. Si un nettoyage immédiat est nécessaire, valider d’abord avec/_count(non destructif) sur un seul index avant d’élargir àv11-alert-*, et utiliserwait_for_completion=false+requests_per_secondpour throttler l’opération.
Architecture à considérer pour une solution durable : découpler la politique ISM des alertes (cycle de vie court — objet de triage opérationnel) de celle des logs bruts (cycle de vie long — donnée forensique/historique), plutôt que de partager utmstack_ism_policy entre v11-log-* et v11-alert-*.
Constaté empiriquement le 2026-07-01 après un reboot d’UTMStack : les règles system_owner=true sont réinitialisées à leur définition d’origine au démarrage. Les fixes 530, 525, 875 et 876 ont tous été écrasés. La règle 1436 (PowerShell MDE) a survécu à ce reboot — comportement non encore expliqué, à surveiller.
Ce n’est plus une hypothèse : tout reboot ou update UTMStack nécessite une réapplication des fixes.
Plutôt que de réappliquer manuellement après chaque reboot (procédure décrite dans la section précédente), un service systemd gère la réapplication automatiquement.
/root/utmstack-fixes/mkdir -p /root/utmstack-fixes
cp /root/update-rule530.sql /root/utmstack-fixes/
cp /root/update-rule875.sql /root/utmstack-fixes/
cp /root/update-rule876.sql /root/utmstack-fixes/
cp /root/update-rule525.sql /root/utmstack-fixes/
cp /root/update-rule1424.sql /root/utmstack-fixes/
cp /root/update-rule521.sql /root/utmstack-fixes/
ℹ️ Pour un lecteur qui reconstruit ces fichiers depuis zéro : voici le contenu final et à jour de chacun des 5 fichiers, prêt à copier-coller directement (
nano /root/update-rule<id>.sql, coller, sauvegarder). Ce sont les mêmes contenus que dans les sections Fix 1 à 6 plus haut — regroupés ici pour éviter d’avoir à les rechercher un par un.
/usr/local/bin/utmstack-fix-rules.sh#!/bin/bash
# Réapplication automatique des fixes de règles de corrélation UTMStack
# Comportement confirmé : UTMStack réinitialise utm_correlation_rules
# au démarrage pour les règles system_owner=true (530, 525, 875, 876)
LOG="/var/log/utmstack-fix-rules.log"
echo "$(date) — Démarrage réapplication des fixes de règles" >> $LOG
# Attendre que PostgreSQL soit prêt (timeout 300s)
TIMEOUT=300
ELAPSED=0
POSTGRES_ID=""
while true; do
POSTGRES_ID=$(docker ps -q -f name=utmstack_postgres)
if [ -n "$POSTGRES_ID" ]; then
docker exec $POSTGRES_ID psql -U postgres -d utmstack -c "SELECT 1" > /dev/null 2>&1
if [ $? -eq 0 ]; then
break
fi
fi
sleep 5
ELAPSED=$((ELAPSED + 5))
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "$(date) — ERREUR : timeout attente PostgreSQL après ${TIMEOUT}s" >> $LOG
exit 1
fi
done
echo "$(date) — PostgreSQL prêt après ${ELAPSED}s" >> $LOG
# Réappliquer les fixes
for sql in update-rule530.sql update-rule875.sql update-rule876.sql update-rule525.sql update-rule1424.sql update-rule521.sql; do
if [ -f "/root/utmstack-fixes/$sql" ]; then
docker cp /root/utmstack-fixes/$sql $POSTGRES_ID:/tmp/$sql
docker exec -i $POSTGRES_ID psql -U postgres -d utmstack -f /tmp/$sql >> $LOG 2>&1
echo "$(date) — Fix appliqué : $sql" >> $LOG
else
echo "$(date) — ERREUR : fichier manquant /root/utmstack-fixes/$sql" >> $LOG
fi
done
# Attendre 120s que Docker Swarm et les event-processor soient stables
echo "$(date) — Attente 120s avant redémarrage des event-processor" >> $LOG
sleep 120
echo "$(date) — Redémarrage event-processor-worker" >> $LOG
docker service update --force utmstack_event-processor-worker >> $LOG 2>&1
echo "$(date) — Attente 30s avant redémarrage event-processor-manager" >> $LOG
sleep 30
echo "$(date) — Redémarrage event-processor-manager" >> $LOG
docker service update --force utmstack_event-processor-manager >> $LOG 2>&1
echo "$(date) — Terminé — fixes de règles appliqués avec succès" >> $LOG
chmod +x /usr/local/bin/utmstack-fix-rules.sh
/etc/systemd/system/utmstack-fix-rules.service[Unit]
Description=UTMStack — Réapplication des fixes de règles de corrélation
After=docker.service
Requires=docker.service
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/utmstack-fix-rules.sh
RemainAfterExit=yes
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable utmstack-fix-rules.service
systemctl status utmstack-fix-rules.service
# Attendu : enabled + active (exited) + status=0/SUCCESS
tail -50 /var/log/utmstack-fix-rules.log
UPDATE 1 en séquence : ~1 secondeℹ️ Le service est
oneshot— il s’exécute une fois au démarrage puis reste en étatactive (exited). C’est le comportement attendu, pas une erreur.
Incident du 2026-07-01/02 : après un revert vers un snapshot antérieur (v11.2.8) puis une remise à jour vers v11.2.11, le fix prévu pour l’ID 1436 (“Windows: Suspicious PowerShell…”) s’est retrouvé appliqué à une règle totalement différente — “GCP Firewall Rule Created — Open Ingress”. Entre les versions, UTMStack avait réattribué cet ID à une autre règle ; la vraie règle PowerShell vivait désormais sous l’ID 1424.
Un script qui réapplique un fix par UPDATE ... WHERE id = X sans vérifier au préalable que X correspond toujours au bon rule_name peut donc silencieusement corrompre une règle sans rapport. C’est arrivé une fois dans ce lab — voir plus bas la procédure de retour en arrière pour ce cas précis.
Ne jamais appliquer un UPDATE ... WHERE id = X sur une règle system_owner=true sans avoir d’abord confirmé son identité. La méthode utilisée dans ce lab, à chaque fois qu’un ID semblait suspect :
Étape 1 — Chercher par nom plutôt que par ID. Le rule_name est plus stable que l’id entre versions (mais pas garanti à 100% non plus — vérifier aussi le contenu si un doute persiste) :
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -c \
"SELECT id, rule_name FROM utm_correlation_rules WHERE rule_name ILIKE '%PowerShell%';"
Si plusieurs résultats correspondent (cas réel rencontré : 4 règles contenant “PowerShell”), identifier la bonne par son rule_category et par un extrait de sa rule_definition_def :
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -x -c \
"SELECT id, rule_name, rule_category, rule_definition_def FROM utm_correlation_rules WHERE id IN (1424,1140,1362,1341);"
Étape 2 — Avant d’appliquer un UPDATE, toujours vérifier ce qu’il y a déjà à cet ID. Une simple lecture avant écriture aurait évité l’incident du 1436 :
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -x -c \
"SELECT id, rule_name, rule_definition_def FROM utm_correlation_rules WHERE id = 1436;"
Si le rule_name retourné ne correspond pas à ce qu’on s’apprête à modifier, s’arrêter — l’ID a bougé, il faut relancer l’étape 1.
Étape 3 — Si une règle a été écrasée par erreur, deux options selon la disponibilité de sa vraie définition d’origine :
UPDATE normal.docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -c \
"UPDATE utm_correlation_rules SET rule_active = false WHERE id = <id>;"
C’est réversible et explicite — préférable à une règle active dont le nom et la logique ne correspondent plus.
Le script utmstack-fix-rules.sh (section précédente) applique ses UPDATE par ID, sans cette vérification de nom — c’est un choix assumé pour la vitesse d’exécution automatique au démarrage, mais le fichier SQL de chaque fix doit être revalidé manuellement (étape 1 et 2 ci-dessus) après tout revert de snapshot vers une version différente, avant de laisser le service tourner en confiance. Un revert n’est pas un reboot ordinaire — c’est le seul scénario où ce lab a rencontré une dérive d’ID.
État actuel des IDs vérifiés dans ce lab (v11.2.11, au 2026-07-15) :
| Règle | ID | rule_name |
|---|---|---|
| Tunneling Detection | 530 | Tunneling Detection |
| High level Suricata alert | 875 | High level Suricata alert |
| Medium level Suricata alert | 876 | Medium level Suricata alert |
| Port Scan Detection | 525 | Port Scan Detection |
| Windows Suspicious PowerShell | 1424 (anciennement 1436) | Windows: Suspicious PowerShell (Encoded / Download Cradle / AMSI Bypass) |
| Malicious File Download Detection | 521 | Malicious File Download Detection |
| GCP Firewall Rule Created | 1436 | GCP Firewall Rule Created — Open Ingress (désactivée, rule_active = false — définition d’origine non restaurée après collision d’ID) |
Comportement confirmé empiriquement le 2026-07-01, et régulièrement depuis : un reboot UTMStack — ou même le simple redémarrage d’un conteneur event-processor sans reboot serveur (confirmé le 5 juillet) — réinitialise les règles system_owner=true à leur définition d’origine. Aucune règle n’est à l’abri de façon fiable et permanente ; l’automatisation (service au boot + cron horaire de vérification, voir section précédente) est nécessaire, pas optionnelle.
Depuis le 2026-07-01, le service utmstack-fix-rules gère automatiquement la réapplication au démarrage, complété depuis le 5 juillet par un cron horaire de vérification (utmstack-fix-rules-check.sh) qui couvre le cas des redémarrages de conteneur isolés. La vérification manuelle ci-dessous reste utile après un update majeur, un revert de snapshot, ou en cas de doute :
# Vérifier l'état du service après chaque reboot
systemctl status utmstack-fix-rules.service
# Attendu : enabled + active (exited) + status=0/SUCCESS
# Consulter le log d'exécution
tail -50 /var/log/utmstack-fix-rules.log
# Vérification manuelle des définitions si doute — par nom, pas seulement par ID
docker exec $(docker ps -q -f name=utmstack_postgres) psql -U postgres -d utmstack -c \
"SELECT id, rule_name FROM utm_correlation_rules WHERE id IN (530,875,876,525,1424,521);"
Si un fix est manquant, forcer une réexécution du service :
systemctl start utmstack-fix-rules.service
tail -f /var/log/utmstack-fix-rules.log
| ← Règles Suricata avancées | → SOC AI |