+ Note: Cette alerte est automatiquement déclenchée lorsque plus de ${ALERT_THRESHOLD} IPs sont bannies en moins d'une heure.
+ Fail2Ban continue de protéger votre serveur en bannissant automatiquement les adresses IP suspectes.
+
+
+
+
+
+
+ `;
+
+ const textContent = `
+🚨 ALERTE SÉCURITÉ SSH
+
+Une attaque massive a été détectée sur votre serveur.
+
+STATISTIQUES:
+- IPs bannies (dernière heure): ${banCount}
+- Seuil d'alerte: ${ALERT_THRESHOLD}
+- Date de détection: ${new Date().toLocaleString('fr-FR')}
+
+ACTIONS RECOMMANDÉES:
+1. Vérifiez le tableau de bord Fail2Ban
+2. Examinez les logs SSH
+3. Renforcez les règles Fail2Ban si nécessaire
+4. Vérifiez la robustesse de vos mots de passe
+
+Fail2Ban continue de protéger votre serveur automatiquement.
+
+---
+Gestion des Formations Manager Itinova
+ `;
+
+ await sendEmail({
+ to: ownerEmail,
+ subject,
+ html: htmlContent,
+ text: textContent,
+ });
+}
+
+/**
+ * Initialise le système d'alertes Fail2Ban
+ */
+export function initFail2BanAlertSystem() {
+ console.log('[Fail2Ban Alert] Initialisation du système d\'alertes');
+ console.log(`[Fail2Ban Alert] Seuil: ${ALERT_THRESHOLD} IPs/heure, Vérification toutes les ${CHECK_INTERVAL / 60000} minutes`);
+
+ // Première vérification immédiate
+ checkAndSendAlert();
+
+ // Vérifications périodiques
+ setInterval(checkAndSendAlert, CHECK_INTERVAL);
+
+ console.log('[Fail2Ban Alert] Système d\'alertes initialisé');
+}
diff --git a/server/fail2banDb.ts b/server/fail2banDb.ts
new file mode 100644
index 0000000..b26282e
--- /dev/null
+++ b/server/fail2banDb.ts
@@ -0,0 +1,139 @@
+import { exec } from 'child_process';
+import { promisify } from 'util';
+
+const execAsync = promisify(exec);
+
+export interface BannedIP {
+ ip: string;
+ banTime: string;
+ failCount: number;
+}
+
+export interface Fail2BanStats {
+ currentlyBanned: number;
+ totalBanned: number;
+ currentlyFailed: number;
+ totalFailed: number;
+ bannedIPs: BannedIP[];
+}
+
+/**
+ * Récupère le statut de Fail2Ban pour SSH
+ */
+export async function getFail2BanStatus(): Promise {
+ try {
+ const { stdout } = await execAsync('sudo fail2ban-client status sshd');
+
+ const stats: Fail2BanStats = {
+ currentlyBanned: 0,
+ totalBanned: 0,
+ currentlyFailed: 0,
+ totalFailed: 0,
+ bannedIPs: [],
+ };
+
+ // Parser le statut
+ const currentlyBannedMatch = stdout.match(/Currently banned:\s+(\d+)/);
+ const totalBannedMatch = stdout.match(/Total banned:\s+(\d+)/);
+ const currentlyFailedMatch = stdout.match(/Currently failed:\s+(\d+)/);
+ const totalFailedMatch = stdout.match(/Total failed:\s+(\d+)/);
+ const bannedIPsMatch = stdout.match(/Banned IP list:\s+(.+)/);
+
+ if (currentlyBannedMatch) stats.currentlyBanned = parseInt(currentlyBannedMatch[1]);
+ if (totalBannedMatch) stats.totalBanned = parseInt(totalBannedMatch[1]);
+ if (currentlyFailedMatch) stats.currentlyFailed = parseInt(currentlyFailedMatch[1]);
+ if (totalFailedMatch) stats.totalFailed = parseInt(totalFailedMatch[1]);
+
+ // Récupérer les IPs bannies avec détails
+ if (bannedIPsMatch && bannedIPsMatch[1].trim()) {
+ const ips = bannedIPsMatch[1].trim().split(/\s+/);
+
+ for (const ip of ips) {
+ try {
+ // Récupérer les détails de chaque IP bannie
+ const { stdout: ipInfo } = await execAsync(`sudo fail2ban-client get sshd actionban | grep -A 10 "${ip}" || echo ""`);
+
+ stats.bannedIPs.push({
+ ip,
+ banTime: new Date().toISOString(), // Approximation, Fail2Ban ne stocke pas la date exacte facilement
+ failCount: 3, // Valeur par défaut basée sur maxretry
+ });
+ } catch (err) {
+ // Si on ne peut pas récupérer les détails, on ajoute quand même l'IP
+ stats.bannedIPs.push({
+ ip,
+ banTime: new Date().toISOString(),
+ failCount: 3,
+ });
+ }
+ }
+ }
+
+ return stats;
+ } catch (error) {
+ console.error('[Fail2Ban] Erreur lors de la récupération du statut:', error);
+ throw new Error('Impossible de récupérer le statut Fail2Ban');
+ }
+}
+
+/**
+ * Débannit une IP spécifique
+ */
+export async function unbanIP(ip: string): Promise {
+ try {
+ await execAsync(`sudo fail2ban-client set sshd unbanip ${ip}`);
+ console.log(`[Fail2Ban] IP ${ip} débannie avec succès`);
+ return true;
+ } catch (error) {
+ console.error(`[Fail2Ban] Erreur lors du débannissement de ${ip}:`, error);
+ return false;
+ }
+}
+
+/**
+ * Récupère l'historique des bannissements depuis les logs
+ */
+export async function getBanHistory(limit: number = 50): Promise> {
+ try {
+ const { stdout } = await execAsync(`sudo grep "Ban" /var/log/fail2ban.log | tail -${limit}`);
+
+ const history: Array<{ ip: string; date: string; action: string }> = [];
+ const lines = stdout.trim().split('\n');
+
+ for (const line of lines) {
+ // Format: 2026-01-18 19:00:00,123 fail2ban.actions [12345]: NOTICE [sshd] Ban 1.2.3.4
+ const match = line.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*\[(sshd)\] (Ban|Unban) ([\d.]+)/);
+
+ if (match) {
+ history.push({
+ date: match[1],
+ action: match[3],
+ ip: match[4],
+ });
+ }
+ }
+
+ return history.reverse(); // Plus récent en premier
+ } catch (error) {
+ console.error('[Fail2Ban] Erreur lors de la récupération de l\'historique:', error);
+ return [];
+ }
+}
+
+/**
+ * Compte le nombre d'IPs bannies dans la dernière heure
+ */
+export async function countRecentBans(hours: number = 1): Promise {
+ try {
+ const since = new Date();
+ since.setHours(since.getHours() - hours);
+ const sinceStr = since.toISOString().slice(0, 19).replace('T', ' ');
+
+ const { stdout } = await execAsync(`sudo grep "Ban" /var/log/fail2ban.log | grep -v "Unban" | awk '{print $1, $2}' | awk -v since="${sinceStr}" '$0 >= since' | wc -l`);
+
+ return parseInt(stdout.trim()) || 0;
+ } catch (error) {
+ console.error('[Fail2Ban] Erreur lors du comptage des bannissements récents:', error);
+ return 0;
+ }
+}
diff --git a/server/routers.ts b/server/routers.ts
index 9be9b77..0f0729e 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -6,6 +6,7 @@ import { parseLocalDateTime } from "./dateUtils";
import { z } from "zod";
import * as db from "./db";
import * as analyticsDb from "./analyticsDb";
+import * as fail2banDb from "./fail2banDb";
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
import { TRPCError } from "@trpc/server";
import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation, sendResetPasswordEmail } from "./emailService";
@@ -2668,6 +2669,48 @@ export const appRouter = router({
}),
}),
+
+ // Monitoring Fail2Ban
+ fail2ban: router({
+ // Récupérer le statut et les statistiques Fail2Ban
+ getStatus: adminProcedure.query(async () => {
+ return await fail2banDb.getFail2BanStatus();
+ }),
+
+ // Récupérer l'historique des bannissements
+ getHistory: adminProcedure
+ .input(z.object({
+ limit: z.number().optional().default(50),
+ }))
+ .query(async ({ input }) => {
+ return await fail2banDb.getBanHistory(input.limit);
+ }),
+
+ // Débannir une IP
+ unbanIP: adminProcedure
+ .input(z.object({
+ ip: z.string(),
+ }))
+ .mutation(async ({ input }) => {
+ const success = await fail2banDb.unbanIP(input.ip);
+ if (!success) {
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: `Impossible de débannir l'IP ${input.ip}`,
+ });
+ }
+ return { success: true };
+ }),
+
+ // Compter les bannissements récents
+ countRecentBans: adminProcedure
+ .input(z.object({
+ hours: z.number().optional().default(1),
+ }))
+ .query(async ({ input }) => {
+ return await fail2banDb.countRecentBans(input.hours);
+ }),
+ }),
});
export type AppRouter = typeof appRouter;
diff --git a/todo.md b/todo.md
index 2c0abb0..271f04a 100644
--- a/todo.md
+++ b/todo.md
@@ -873,3 +873,7 @@
- [x] Identifier la cause de l'erreur "banner line contains invalid characters"
- [x] Optimiser la sécurité SSH avec Fail2Ban renforcé (maxretry=3, bantime=3600)
- [x] Créer une whitelist d'IPs de confiance pour éviter les blocages
+- [x] Créer les fonctions backend pour récupérer les données Fail2Ban (IPs bannies, statistiques)
+- [x] Créer l'interface admin du tableau de bord Fail2Ban avec possibilité de débannir
+- [x] Implémenter le système d'alertes email pour les attaques massives (>10 IPs/heure)
+- [x] Tester et déployer le monitoring Fail2Ban sur le VPS