mirror of
https://github.com/deunix-educ/Fail2banMqttActionBanishment.git
synced 2026-08-24 03:11:58 +02:00
First commit
This commit is contained in:
Executable
+45
@@ -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."
|
||||
Executable
+39
@@ -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)."
|
||||
Executable
+70
@@ -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"
|
||||
Executable
+40
@@ -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
|
||||
Reference in New Issue
Block a user