First commit

This commit is contained in:
2026-07-20 11:05:07 +02:00
commit b592ab669f
159 changed files with 10294 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
# generate-node-cert.sh — génère une clé + un certificat client mTLS pour
# un noeud, signé par la CA privée du master (voir master-ca-init.sh).
#
# Flux MANUEL (clé + CSR + signature générées ici, au même endroit) :
# réservé à l'usage local sur le master lui-même (ex. certificat
# master-internal, généré par install.sh::install-master sans jamais
# passer par le réseau). Pour un noeud DISTANT, préférer le flux par
# jeton (manage.py create_join_token puis sudo ./install.sh join sur le
# noeud, voir ROADMAP.md) : la clé y est générée SUR le noeud et ne
# transite jamais par le réseau, contrairement à ce script qui la génère
# ici puis suppose qu'elle sera copiée à la main vers le noeud.
#
# Usage : sudo ./generate-node-cert.sh <nom_du_noeud> [répertoire_sortie]
# Le nom du noeud devient le CN du certificat, donc son identité MQTT côté
# master (use_identity_as_username) — sert de base aux ACL par noeud.
set -euo pipefail
NODE_NAME="${1:?Usage: $0 <nom_du_noeud> [répertoire_sortie]}"
CA_DIR="${CA_DIR:-/etc/mosquitto/master-ca}"
OUT_DIR="${2:-./certs/$NODE_NAME}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ ! -f "$CA_DIR/ca.key" ]; then
echo "CA introuvable dans $CA_DIR — lancer master-ca-init.sh d'abord." >&2
exit 1
fi
install -d "$OUT_DIR"
openssl genrsa -out "$OUT_DIR/$NODE_NAME.key" 2048
openssl req -new -key "$OUT_DIR/$NODE_NAME.key" -subj "/CN=$NODE_NAME" -out "$OUT_DIR/$NODE_NAME.csr"
# Signature + entitlement ACL + restart mosquitto : logique partagée avec
# l'endpoint /api/join/ (banevents/views.py::join_node), voir
# sign-node-csr.sh — un seul chemin de code qui touche ca.key.
CA_DIR="$CA_DIR" "$SCRIPT_DIR/sign-node-csr.sh" "$NODE_NAME" "$OUT_DIR/$NODE_NAME.csr" > "$OUT_DIR/$NODE_NAME.crt"
cp "$CA_DIR/ca.crt" "$OUT_DIR/ca.crt"
rm "$OUT_DIR/$NODE_NAME.csr"
chmod 600 "$OUT_DIR/$NODE_NAME.key"
echo "Certificat généré dans $OUT_DIR :"
echo " - $NODE_NAME.key, $NODE_NAME.crt (ca.crt copié aussi mais pas utilisé par le noeud :"
echo " le certificat serveur du master est vérifié via le magasin de CA système)"
echo "À copier sur le noeud (ex. emitter/master-tls/) et référencer via"
echo "MQTT_MASTER_CLIENT_KEY / MQTT_MASTER_CLIENT_CERT / MQTT_MASTER_NODE_NAME=$NODE_NAME."
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# master-ca-init.sh — initialise la CA privée du broker master (Phase 3),
# utilisée uniquement pour signer les certificats clients des noeuds (mTLS).
# Distincte du certificat Let's Encrypt du broker (qui sert l'identité TLS
# serveur, pas l'authentification des clients).
#
# À lancer UNE SEULE FOIS sur le VPS master. Usage : sudo ./master-ca-init.sh [répertoire CA]
set -euo pipefail
CA_DIR="${1:-/etc/mosquitto/master-ca}"
if [ -f "$CA_DIR/ca.key" ]; then
echo "CA déjà présente dans $CA_DIR — rien à faire." >&2
exit 1
fi
if ! getent group mosquitto >/dev/null; then
echo "Groupe système 'mosquitto' introuvable — le paquet mosquitto doit être installé avant." >&2
exit 1
fi
# Le process mosquitto (utilisateur dédié 'mosquitto', pas root) doit pouvoir
# lire ca.crt pour vérifier les certificats clients (cafile du listener
# 8883) — d'où le groupe 'mosquitto' sur le répertoire et sur ce fichier.
# ca.key (la clé privée de la CA, qui sert à SIGNER de nouveaux certificats
# clients via generate-node-cert.sh) reste root-only : mosquitto n'en a
# jamais besoin.
install -d -o root -g mosquitto -m 750 "$CA_DIR"
openssl genrsa -out "$CA_DIR/ca.key" 4096
openssl req -x509 -new -nodes -key "$CA_DIR/ca.key" -sha256 -days 3650 \
-subj "/CN=Fail2banActionBanisher Master CA" \
-out "$CA_DIR/ca.crt"
chown root:root "$CA_DIR/ca.key"
chmod 600 "$CA_DIR/ca.key"
chown root:mosquitto "$CA_DIR/ca.crt"
chmod 640 "$CA_DIR/ca.crt"
echo "CA créée dans $CA_DIR (ca.crt à référencer comme cafile du listener 8883)."
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# sign-node-csr.sh — signe une CSR de noeud avec la CA privée du master,
# ajoute l'entitlement ACL nécessaire, redémarre mosquitto. Coeur commun à
# generate-node-cert.sh (flux manuel) et à l'endpoint /api/join/ (flux par
# jeton, banevents/views.py::join_node) — un seul chemin de code qui
# touche ca.key, pas deux implémentations à maintenir en parallèle.
#
# Usage : sudo ./sign-node-csr.sh <nom_du_noeud> <fichier_csr>
# Écrit le certificat signé (PEM) sur STDOUT — tous les messages
# informatifs/d'erreur vont sur STDERR, pour que stdout reste exploitable
# programmatiquement (ex. capturé tel quel par un appelant Python).
set -euo pipefail
NODE_NAME="${1:?Usage: $0 <nom_du_noeud> <fichier_csr>}"
CSR_FILE="${2:?Usage: $0 <nom_du_noeud> <fichier_csr>}"
CA_DIR="${CA_DIR:-/etc/mosquitto/master-ca}"
if [ ! -f "$CA_DIR/ca.key" ]; then
echo "CA introuvable dans $CA_DIR — lancer master-ca-init.sh d'abord." >&2
exit 1
fi
if [ ! -f "$CSR_FILE" ]; then
echo "CSR introuvable : $CSR_FILE" >&2
exit 1
fi
TMP_CERT="$(mktemp)"
trap 'rm -f "$TMP_CERT"' EXIT
openssl x509 -req -in "$CSR_FILE" \
-CA "$CA_DIR/ca.crt" -CAkey "$CA_DIR/ca.key" -CAcreateserial \
-out "$TMP_CERT" -days 825 -sha256 2>/dev/null
# Entitlement ACL pour ce noeud dans master-acl.conf (idempotent). Les
# règles globales non scopées (pattern write fail2ban/%u/ban, pattern
# read fail2ban/%u/action, topic read fail2ban/broadcast/action, cf.
# generic/mosquitto/master-acl.conf) suffisent pour la publication (%u),
# mais PAS pour la lecture du canal de diffusion : SUBSCRIBE sur
# fail2ban/broadcast/action reçoit bien un SUBACK positif, mais aucun
# message n'est jamais livré (constaté avec mosquitto_sub -v : SUBACK OK,
# zéro PUBLISH reçu). Seule une règle "topic read" explicite sous un bloc
# "user <nom>" dédié fait effectivement recevoir les messages (piège
# mosquitto : un SUBACK positif ne prouve pas qu'un SUBSCRIBE est
# autorisé, cf. CONTEXT.md).
ACL_FILE="${ACL_FILE:-/etc/mosquitto/master-acl.conf}"
if [ -f "$ACL_FILE" ]; then
if ! grep -q "^user $NODE_NAME\$" "$ACL_FILE"; then
echo "-> ajout de l'entitlement ACL pour '$NODE_NAME' dans $ACL_FILE" >&2
{
echo ""
echo "user $NODE_NAME"
echo "topic read fail2ban/broadcast/action"
echo "topic read fail2ban/broadcast/roster"
} >> "$ACL_FILE"
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet mosquitto.service; then
systemctl restart mosquitto.service
fi
else
echo "-> entitlement ACL pour '$NODE_NAME' déjà présent dans $ACL_FILE, inchangé" >&2
fi
else
echo "-> ATTENTION : $ACL_FILE introuvable (pas sur le master ?) — pas d'entitlement ACL ajouté." >&2
echo " Le noeud '$NODE_NAME' ne recevra aucun SYNC_BAN tant que ce bloc n'est pas ajouté à la main :" >&2
echo " user $NODE_NAME" >&2
echo " topic read fail2ban/broadcast/action" >&2
echo " topic read fail2ban/broadcast/roster" >&2
fi
cat "$TMP_CERT"
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# Vérifie les filtres fail2ban custom avec fail2ban-regex et des logs d'exemple.
# Usage: scripts/test-fail2ban-filters.sh
set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
FILTER_DIR="$REPO_DIR/generic/fail2ban/filter.d"
FIXTURE_DIR="$REPO_DIR/tests/fail2ban-filters"
if ! command -v fail2ban-regex >/dev/null 2>&1; then
echo "fail2ban-regex introuvable (paquet fail2ban) — impossible de lancer les tests." >&2
exit 1
fi
# filtre:fichier_log:nombre_attendu_de_lignes_bannies
CASES=(
"coturn:coturn.log:3"
"mosquitto:mosquitto.log:2"
"openvpn:openvpn.log:4"
"recidive-filter:recidive.log:2"
)
failed=0
for case in "${CASES[@]}"; do
IFS=':' read -r filter logfile expected <<< "$case"
output="$(fail2ban-regex "$FIXTURE_DIR/$logfile" "$FILTER_DIR/$filter.conf" 2>&1)"
matched="$(echo "$output" | sed -n 's/.*, \([0-9]\+\) matched,.*/\1/p' | head -n1)"
if [ "$matched" = "$expected" ]; then
echo "[OK] $filter : $matched/$expected lignes bannies"
else
echo "[FAIL] $filter : $matched/$expected lignes bannies attendues"
echo "$output"
failed=1
fi
done
exit $failed