# CLAUDE.md — Sécurisation serveur Linux Debian
> Contexte spécialisé pour Claude Code. Coller ce fichier à la racine du projet pour auditer et sécuriser un serveur Debian dédié (SSH, fail2ban, firewall, auditd, Apache, monitoring automatisé).
---
## Section 1 : SSH hardening
Configuration minimale à appliquer dans `/etc/ssh/sshd_config` :
```bash
# Désactiver le login root par mot de passe (garder les clés pour urgence)
PermitRootLogin prohibit-password
# Logging SFTP détaillé dans auth.log
Subsystem sftp internal-sftp -l INFO -f AUTH
# Recharger sans coupure de service
sshd -t && systemctl reload sshd
```
### Groupe sftponly avec ChrootDirectory
Isoler les utilisateurs SFTP dans leur home — ils ne peuvent ni exécuter de shell, ni accéder à d'autres répertoires.
```bash
# Créer le groupe
groupadd sftponly
# Ajouter les utilisateurs SFTP
usermod -aG sftponly alice
usermod -aG sftponly bob
```
Bloc à ajouter **à la fin** de `/etc/ssh/sshd_config` (après toute directive `Match` existante) :
```bash
Match Group sftponly
ForceCommand internal-sftp -l INFO -f AUTH
ChrootDirectory %h
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no
AllowAgentForwarding no
```
**Piège du chroot :** `ChrootDirectory` exige que le répertoire racine appartienne à `root:root` en `755`. Si le home appartient à l'utilisateur, SSH refuse la connexion silencieusement.
```bash
# Structure correcte pour chroot SFTP
chown root:root /home/alice # Obligatoire
chmod 755 /home/alice
# Sous-dossier accessible en écriture
mkdir -p /home/alice/uploads
chown alice:alice /home/alice/uploads
chmod 755 /home/alice/uploads
```
---
## Section 2 : fail2ban — Protection brute force
```bash
apt install fail2ban
systemctl enable fail2ban
```
Toutes les personnalisations dans `/etc/fail2ban/jail.local` — **ne jamais modifier `jail.conf`** (écrasé lors des mises à jour).
```bash
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 86400 ; 24h
findtime = 600 ; fenêtre : 10 minutes
maxretry = 5
banaction = iptables-multiport
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 5
[proftpd]
enabled = true
port = ftp,ftp-data,ftps,ftps-data
logpath = /var/log/proftpd/proftpd.log
maxretry = 6
bantime = 86400
```
### Filtre custom pour Apache HTTP Digest
Le filtre `apache-auth` par défaut ne détecte pas les erreurs HTTP Digest (`AH01790` / `AH01794`). Créer un filtre custom :
```bash
# /etc/fail2ban/filter.d/apache-auth.local
[Definition]
failregex = \[client <HOST>:.*\] AH01790: user .+: password mismatch
\[client <HOST>:.*\] AH01794: user .+ in realm .+ not found
\[client <HOST>:.*\] AH01788: .+nonce from .+ received on .+ - not found
```
```bash
# /etc/fail2ban/jail.local — jail correspondante
[apache-auth]
enabled = true
filter = apache-auth
port = http,https
logpath = /var/log/apache2/error.log
maxretry = 8
bantime = 86400
findtime = 300
```
Tester le filtre avant activation :
```bash
fail2ban-regex /var/log/apache2/error.log /etc/fail2ban/filter.d/apache-auth.local --print-all-matched
```
Commandes de diagnostic :
```bash
# Vue d'ensemble
fail2ban-client status
# Détail d'une jail
fail2ban-client status apache-auth
fail2ban-client status sshd
# Débannir une IP (si tu te bans toi-même)
fail2ban-client set sshd unbanip 1.2.3.4
# Logs en temps réel
tail -f /var/log/fail2ban.log
```
---
## Section 3 : Audit post-incident — vérifier l'absence d'intrusion
```bash
# Connexions SSH réussies (chercher des logins inattendus)
grep "Accepted" /var/log/auth.log | tail -50
# Tentatives depuis une IP spécifique
grep "198.51.100.1" /var/log/auth.log
# Timestamps des fichiers sensibles
stat /etc/passwd /etc/shadow /etc/sudoers
ls -la /root/.ssh/
# Fichiers modifiés dans /etc (dernières 24h)
touch -d "24 hours ago" /tmp/ref_file
find /etc -newer /tmp/ref_file -ls 2>/dev/null
# Compter les tentatives auth Digest par IP
grep "AH0179" /var/log/apache2/error.log | grep -oP '\[client \K[0-9.]+' | sort | uniq -c | sort -rn | head -20
# Identifier une IP attaquante
curl -s https://ipinfo.io/198.51.100.1/json
```
---
## Section 4 : auditd — Détection d'intrusion en continu
```bash
apt install auditd audispd-plugins
systemctl enable auditd
```
Créer `/etc/audit/rules.d/security.rules` :
```bash
# Modifications sudoers
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# Configuration SSH
-w /etc/ssh/sshd_config -p wa -k sshd_config
# Clés SSH root
-w /root/.ssh/authorized_keys -p wa -k root_keys
# Comptes système
-w /etc/passwd -p wa -k accounts
-w /etc/shadow -p wa -k accounts
-w /etc/group -p wa -k accounts
# Exécution su et sudo
-w /usr/bin/su -p x -k su_exec
-w /usr/bin/sudo -p x -k sudo_exec
# Crontab
-w /etc/crontab -p wa -k crontab
-w /etc/cron.d/ -p wa -k crontab
-w /var/spool/cron/ -p wa -k crontab
# Configuration applicative sensible
-w /var/www/html/config.php -p rwa -k app_config
-w /etc/apache2/ -p wa -k apache_config
-w /etc/fail2ban/ -p wa -k fail2ban
```
```bash
# Charger sans redémarrer
augenrules --load
# Vérifier les règles actives
auditctl -l
# Consulter les événements (dernières 24h)
ausearch -ts yesterday -te now | aureport -f -i | head -50
```
Outils complémentaires :
```bash
# Process accounting — qui a exécuté quoi et quand
apt install acct
accton on
lastcomm --user alice | head -30
lastcomm --user root | head -50
# Rootkit scanner
apt install rkhunter
rkhunter --update && rkhunter --propupd # Initialiser la baseline
rkhunter --check --skip-keypress # Scan complet
# Après apt upgrade, toujours régénérer la baseline :
apt upgrade && rkhunter --propupd
```
---
## Section 5 : Apache hardening
### Bloquer les répertoires sensibles
Utiliser `RewriteRule` plutôt que `Require all denied` — la RewriteRule s'exécute **avant** l'authentification dans le pipeline Apache.
```apacheconf
RewriteEngine On
# Bloquer l'accès direct aux données internes
RewriteRule ^/data/internal - [F,L]
RewriteRule ^/uploads/private - [F,L]
# Bloquer les fichiers de config exposés par erreur
RewriteRule \.(env|log|sql|bak)$ - [F,L]
```
### Permissions sur les fichiers htdigest et config
```bash
# htdigest — root:www-data 640
chown root:www-data /etc/apache2/.htdigest
chmod 640 /etc/apache2/.htdigest
# Config PHP avec credentials
chown root:www-data /var/www/html/config.php
chmod 640 /var/www/html/config.php
```
### Principe du moindre privilège pour sudo
Ne jamais mettre `www-data` en sudo général. Créer `/etc/sudoers.d/www-data` :
```bash
# /etc/sudoers.d/www-data — toujours chemins absolus
www-data ALL=(ALL) NOPASSWD: /usr/sbin/apachectl graceful
www-data ALL=(ALL) NOPASSWD: /usr/local/bin/maintenance-script.sh
```
```bash
chmod 440 /etc/sudoers.d/www-data
visudo -c -f /etc/sudoers.d/www-data # Vérifier la syntaxe avant d'enregistrer
```
Validation côté PHP pour tout paramètre passé à shell_exec :
```php
<?php
$user = $_POST['username'] ?? '';
if (!preg_match('/^[a-z][a-z0-9_]{2,31}$/', $user)) {
die('Invalid username format');
}
$escaped = escapeshellarg($user);
$output = shell_exec("sudo /usr/local/bin/create-user-dir.sh $escaped");
?>
```
---
## Section 6 : Mise à jour automatique et sécurité PHP
```bash
apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades
```
Sessions PHP sécurisées — à configurer AVANT `session_start()` :
```php
<?php
ini_set('session.cookie_httponly', 1); // Inaccessible depuis JavaScript
ini_set('session.cookie_secure', 1); // HTTPS uniquement
ini_set('session.cookie_samesite', 'Lax'); // Protection CSRF partielle
ini_set('session.use_strict_mode', 1); // Rejeter les IDs non générés par le serveur
session_start();
?>
```
Ou globalement dans `/etc/php/8.x/apache2/php.ini` :
```bash
session.cookie_httponly = 1
session.cookie_secure = 1
```
---
## Section 7 : Monitoring nocturne avec filtrage IA
Script cron qui collecte les logs, les fait analyser par Claude CLI, et n'envoie un mail que si un incident réel est détecté.
```bash
#!/bin/bash
# /root/scripts/security-audit.sh
# Crontab root : 0 3 * * * /root/scripts/security-audit.sh
REPORT_DIR="/var/log/security-audit"
TODAY=$(date +%Y-%m-%d)
REPORT="$REPORT_DIR/$TODAY.txt"
mkdir -p "$REPORT_DIR"
chmod 700 "$REPORT_DIR"
{
echo "=== RAPPORT DE SECURITE - $TODAY ==="
ausearch -ts yesterday -te now 2>/dev/null | aureport -f -i 2>/dev/null | tail -100
fail2ban-client status sshd 2>/dev/null
grep "Accepted" /var/log/auth.log | grep "$(date +%b)" || echo "aucune connexion SSH reussie"
ss -tlnp
} > "$REPORT" 2>&1
chmod 600 "$REPORT"
/root/scripts/security-analyze.sh "$REPORT"
```
Script d'analyse avec fallback robuste — si l'IA est indisponible, le rapport brut est envoyé quand même :
```bash
#!/bin/bash
# /root/scripts/security-analyze.sh
REPORT="${1:-}"
ADMIN_EMAIL="admin@example.com"
TODAY=$(date +%Y-%m-%d)
PROMPT="Analyse ce rapport de securite Linux. Les tentatives SSH depuis IPs aleatoires = bruit normal. Incident reel = connexion SSH reussie depuis IP inconnue, modification fichier sensible dans auditd, port inattendu. JSON UNIQUEMENT sans markdown : {\"alert\": true/false, \"summary\": \"2-3 phrases\", \"details\": [\"point1\"]}"
AI_RESPONSE=""
AI_ERROR=0
if command -v claude >/dev/null 2>&1; then
AI_RESPONSE=$(timeout 60 claude --print --model claude-haiku-4-5 "$PROMPT" < "$REPORT" 2>/dev/null) || AI_ERROR=1
else
AI_ERROR=1
fi
# Fallback : toujours envoyer le rapport si l'analyse IA échoue
if [ "$AI_ERROR" -eq 1 ] || [ -z "$AI_RESPONSE" ]; then
mail -s "[SECURITE] Rapport $TODAY - IA indisponible" "$ADMIN_EMAIL" < "$REPORT"
exit 0
fi
ALERT=$(echo "$AI_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('alert', False))" 2>/dev/null || echo "parse_error")
if [ "$ALERT" = "parse_error" ]; then
mail -s "[SECURITE] Rapport $TODAY - reponse IA invalide" "$ADMIN_EMAIL" < "$REPORT"
exit 0
fi
if [ "$ALERT" = "True" ]; then
SUMMARY=$(echo "$AI_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('summary', 'N/A'))" 2>/dev/null || echo "N/A")
{ echo "IA : $SUMMARY"; echo ""; cat "$REPORT"; } | mail -s "[ALERTE SECURITE] $TODAY - Incident detecte" "$ADMIN_EMAIL"
fi
# Si alert=False : rapport archivé dans REPORT_DIR, pas de mail.
```
---
## Section 8 : Checklist sécurité complète
```
SSH
[ ] PermitRootLogin prohibit-password dans sshd_config
[ ] Groupe sftponly avec ChrootDirectory pour utilisateurs SFTP
[ ] Home dirs sftponly : root:root 755, sous-dossiers user:user
[ ] sshd -t && systemctl reload sshd après chaque modification
fail2ban
[ ] jail.local créé (ne pas toucher jail.conf)
[ ] Jail sshd : bantime 86400, maxretry 5
[ ] Filtre apache-auth.local custom pour erreurs Digest AH0179x
[ ] fail2ban-regex testé sur vrais logs avant activation
[ ] fail2ban-client status pour valider toutes les jails
auditd
[ ] Règles sur sudoers, ssh config, root keys, comptes système
[ ] Règles sur su, sudo, crontab, config applicative
[ ] augenrules --load après modification
[ ] acct installé (lastcomm) + rkhunter baseline initialisée
Apache
[ ] RewriteRule [F,L] sur répertoires sensibles
[ ] htdigest : root:www-data 640
[ ] sudoers www-data : chemins absolus, visudo -c validé
PHP
[ ] session.cookie_httponly = 1
[ ] session.cookie_secure = 1
[ ] session.use_strict_mode = 1
Monitoring
[ ] security-audit.sh en cron 0 3 * * * (root)
[ ] chmod 700 REPORT_DIR, chmod 600 rapports
[ ] Fallback mail si Claude CLI indisponible
[ ] unattended-upgrades configuré
```