WordPress Hardening — LXC / Proxmox
Indice
- 1. Ambiente Macchina
- 1.1. Installazione ambiente LAMP
- 1.2. Cloudflare Tunnel
- 1.3. Firewall
- 1.4. Apache
- 1.5. Configurazione IP reale client (Cloudflare + Apache)
- 1.6. Fail2Ban
- 1.7. Cloudflare WAF
- 1.8. Backup
- 2. Ambiente WordPress
- 2.1. Permessi cartelle e file
- 2.2. WP-CLI
- 2.3. Aggiornamenti e manutenzione con WP-CLI
- 2.4. Esempio di cron di sistema
- 2.5. Gestione del log rotate del file di log generato dallo script di manutenzione
- 2.6. Configurazione wp-config.php
- 2.7. Hardening PHP
- 2.8. Hardening MySQL
- 2.9. Cron per WP-Cron
- 2.10. Cron di sistema per WP-Cron
- 2.11. Cron di sistema per manutenzione e aggiornamenti con WP-CLI
- 2.12. Hardening enumeration
- 2.13. File Integrity Monitoring (AIDE)
- 2.14. Plugins
- 2.15. Limit Login Attempts Reloaded
1. Ambiente Macchina
Container LXC, Ubuntu 24.04 LTS.
1.1. Installazione ambiente LAMP
sudo apt update
# Apache
# Nota: con Cloudflare Tunnel (§1.2) il traffico transita via tunnel locale.
# Non aprire porte Apache su UFW — il firewall mantiene default deny incoming (§1.3).
# I moduli Apache (ssl, headers, rewrite, remoteip) vengono abilitati in §1.4.
sudo apt install apache2 -y
sudo systemctl restart apache2
# MySQL
# Nota: mysql_secure_installation viene eseguito in §2.8 con le istruzioni complete
# per la configurazione dell'utente WordPress con privilegi minimi.
sudo apt install mysql-server -y
# PHP — rilevare la versione installata e usarla come prefisso per i moduli aggiuntivi
sudo apt install php libapache2-mod-php php-mysql -y
PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
sudo apt install \
php${PHP_VERSION}-curl \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-imagick \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-zip \
php${PHP_VERSION}-intl \
php${PHP_VERSION}-gd \
-y
# Nota: DOMDocument è incluso in php-xml, già installato sopra.
# Certbot
sudo apt install certbot python3-certbot-apache -y
1.2. Cloudflare Tunnel
La macchina comunica con internet tramite un tunnel Cloudflare, con certificato SSL attivato tramite Let's Encrypt. Su Cloudflare la verifica del certificato è disabilitata (No TLS Verify), in modo da accettare qualsiasi certificato presentato dalla macchina, anche se scaduto o auto-firmato. In questo modo, anche se il certificato Let's Encrypt scade o non viene rinnovato, il tunnel continuerà a funzionare senza interruzioni.
[!WARNING] No TLS Verify è una soluzione temporanea. Per ambienti di produzione è fortemente raccomandato utilizzare un Origin Certificate Cloudflare e abilitare la verifica TLS verso l'origine. L'opzione No TLS Verify va considerata una soluzione di compatibilità, non la configurazione finale.
1.3. Firewall
Container LXC con firewall UFW attivo: blocco di tutte le porte in entrata, consenso di tutte le porte in uscita, blocco del traffico instradato.
sudo ufw reset # Resetta le regole del firewall
sudo ufw default deny incoming # Blocca tutto il traffico in entrata
sudo ufw default allow outgoing # Consente tutto il traffico in uscita
sudo ufw default deny routed # Blocca tutto il traffico instradato
sudo ufw enable # Abilita il firewall
sudo ufw status verbose # Verifica lo stato del firewall
[!WARNING] Questo blocca qualsiasi porta in entrata. Accertarsi di avere accesso tramite la console del nodo Proxmox prima di abilitare il firewall.
1.4. Apache
1.4.1. Moduli necessari
1.4.2. File di configurazione VirtualHost
File: /etc/apache2/sites-available/example.com.conf
# VirtualHost HTTP → redirect permanente a HTTPS
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
# VirtualHost HTTPS con sicurezza e ottimizzazione
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example.com
# SSL/TLS
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# Se si usa un Origin Certificate Cloudflare, sostituire con:
# SSLCertificateFile /etc/cloudflare/cert.pem
# SSLCertificateKeyFile /etc/cloudflare/key.pem
# Protocolli e cipher suite
# - Disabilita tutti i protocolli obsoleti, consente solo TLS 1.2 e 1.3.
# - Le cipher suite seguono il profilo "Intermediate" di Mozilla SSL Config Generator
# (https://ssl-config.mozilla.org/) e le raccomandazioni NIST SP 800-52 Rev. 2.
# - SSLHonorCipherOrder off: in TLS 1.3 la selezione della cipher è gestita dal client;
# su TLS 1.2 lascia al server la precedenza, ma con TLS 1.3 disabilitarlo è preferibile.
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:\
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:\
ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\
DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLCompression Off
SSLSessionTickets Off
# Directory principale WordPress
# - AllowOverride All: consente ai file .htaccess di sovrascrivere la configurazione Apache.
# - Options -Indexes: disabilita il directory listing. +FollowSymLinks: consente symlink.
# - Require all granted: accesso HTTP consentito (regole specifiche sotto la restringono).
<Directory /var/www/example.com>
AllowOverride All
Options -Indexes +FollowSymLinks
Require all granted
</Directory>
# Blocca esecuzione PHP nella directory uploads
<Directory /var/www/example.com/wp-content/uploads>
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
</Directory>
# Protegge file sensibili, metadata e file informativi
<FilesMatch "^(\.htaccess|\.htpasswd|\.git|\.svn|composer\.(json|lock)|package(-lock)?\.json|yarn\.lock|phpunit\.xml|readme\.html|license\.txt)$">
Require all denied
</FilesMatch>
# Impedisce l'accesso diretto a wp-config.php
# Nota: il file dovrebbe risiedere fuori dalla webroot (vedi §2.6).
# Questa regola è un secondo livello di protezione.
<Files wp-config.php>
Require all denied
</Files>
# Blocca XML-RPC se non utilizzato
<Files xmlrpc.php>
Require all denied
</Files>
# Restrizione accesso wp-login.php e /wp-admin/ per IP
# - Sostituire 1.2.3.4 con il proprio IP statico o il range della VPN (es. Tailscale 100.64.0.0/10).
# - Questa è la difesa più efficace contro brute-force e credential stuffing
# (fonte: WordPress.org Hardening Guide — https://developer.wordpress.org/advanced-administration/security/hardening/#admin-url-access).
<Files wp-login.php>
Require ip 1.2.3.4
Require ip 100.64.0.0/10
</Files>
<LocationMatch "^/wp-admin/">
Require ip 1.2.3.4
Require ip 100.64.0.0/10
</LocationMatch>
# Security Headers
# - X-Content-Type-Options: impedisce MIME sniffing su tipi di contenuto non dichiarati correttamente.
# - X-Frame-Options: impedisce il framing del sito da domini esterni (clickjacking).
# - Referrer-Policy: limita le informazioni del referrer verso altri origin.
# - Permissions-Policy: disabilita API browser sensibili non necessarie al sito.
# - X-Permitted-Cross-Domain-Policies: blocca policy cross-domain legacy (Adobe/Flash).
# - Strict-Transport-Security: forza HTTPS per 1 anno su dominio e sottodomini; preload
# iscrive il dominio nella lista HSTS preload dei browser (https://hstspreload.org/),
# proteggendo anche il primo accesso da attacchi SSLStrip.
# - Content-Security-Policy: principale difesa contro XSS e data injection (OWASP).
# unsafe-inline è necessario per WordPress base; rimuoverlo richiede nonce support
# tramite plugin dedicati.
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set X-Permitted-Cross-Domain-Policies "none"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
# Log del VirtualHost
ErrorLog ${APACHE_LOG_DIR}/example-error.log
CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>
1.4.3. Abilitare il sito e ricaricare Apache
1.4.4. Direttive di sicurezza globali
Creare, se non esiste, il file /etc/apache2/conf-available/security.conf:
# - ServerSignature Off: disabilita la firma del server nelle pagine generate da Apache.
# - ServerTokens Prod: espone solo "Apache" nell'header Server, senza versione o moduli.
# - TraceEnable Off: disabilita il metodo HTTP TRACE (information disclosure e abuse).
ServerSignature Off
ServerTokens Prod
TraceEnable Off
1.5. Configurazione IP reale client (Cloudflare + Apache)
Quando si utilizza un tunnel Cloudflare, Apache vede l'IP del proxy e non quello reale del client. Questo compromette logging, rate limiting e Fail2Ban.
Per ripristinare l'IP reale del client, abilitare il modulo remoteip:
Creare o modificare il file /etc/apache2/conf-available/remoteip.conf:
Aggiornare il formato dei log per usare l'IP reale:
[!NOTE] Questa configurazione è obbligatoria con Cloudflare (Tunnel o Proxy): garantisce log corretti, rende efficace Fail2Ban e permette rate limiting affidabile.
La direttiva
RemoteIPTrustedProxy 127.0.0.1è corretta con Cloudflare Tunnel locale. Se si usa Cloudflare Proxy diretto (senza tunnel), configurare i range IP ufficiali Cloudflare come trusted:
1.6. Fail2Ban
Fail2Ban blocca a livello di rete (iptables/nftables) gli IP che generano troppi errori di autenticazione, complementando il rate limiting applicativo di LLAR.
1.6.1. Installazione
1.6.2. Jail per WordPress
File: /etc/fail2ban/jail.d/wordpress.conf
[wordpress-auth]
enabled = true
port = http,https
filter = wordpress-auth
logpath = /var/log/apache2/example-access.log
maxretry = 5
bantime = 3600
findtime = 600
[wordpress-xmlrpc]
enabled = true
port = http,https
filter = wordpress-xmlrpc
logpath = /var/log/apache2/example-access.log
maxretry = 2
bantime = 86400
findtime = 600
1.6.3. Filtri
File: /etc/fail2ban/filter.d/wordpress-auth.conf
File: /etc/fail2ban/filter.d/wordpress-xmlrpc.conf
[!NOTE] Il
logpathdeve puntare al log Apache del sito configurato conremoteip(§1.5), altrimenti Fail2Ban banna l'IP del proxy Cloudflare invece di quello del client reale.
1.7. Cloudflare WAF
Il piano Free di Cloudflare include un WAF con ruleset gestiti. Va abilitato dalla Dashboard Cloudflare come prima linea di difesa, prima che il traffico raggiunga Apache.
Abilitare dalla Dashboard Cloudflare:
- Security → WAF → Managed Rules → Cloudflare Free Managed Ruleset — abilita
- Security → WAF → Managed Rules → Cloudflare OWASP Core Ruleset — abilita (sensitivity: Low per iniziare, poi aumentare)
- Security → Bot Fight Mode — abilita
[!TIP] Configurare le Rate Limiting Rules di Cloudflare per
POST /wp-login.phpcome ulteriore layer prima che le richieste raggiungano Fail2Ban e LLAR.
1.8. Backup
La protezione avviene su due livelli:
- Backup infrastrutturale: backup giornaliero della VM tramite Proxmox Backup Server.
- Backup applicativo: dump SQL cifrato generato automaticamente dallo script di manutenzione prima degli aggiornamenti (vedi §2.3).
2. Ambiente WordPress
2.1. Permessi cartelle e file
# Permessi base WordPress
sudo chown -R www-data:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
# .htaccess
sudo touch /var/www/example.com/.htaccess
sudo chgrp www-data /var/www/example.com/.htaccess
sudo chmod 644 /var/www/example.com/.htaccess
# wp-content
sudo chown -R www-data:www-data /var/www/example.com/wp-content
sudo find /var/www/example.com/wp-content -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/wp-content -type f -exec chmod 644 {} \;
# uploads e cache
# Nota: 755/644 è sufficiente con owner www-data. Permessi 775/664 consentono
# la scrittura al gruppo e andrebbero usati solo se strettamente necessario
# (fonte: WordPress.org — Changing File Permissions).
sudo chown -R www-data:www-data /var/www/example.com/wp-content/uploads
sudo chown -R www-data:www-data /var/www/example.com/wp-content/cache
sudo find /var/www/example.com/wp-content/uploads -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/wp-content/uploads -type f -exec chmod 644 {} \;
sudo find /var/www/example.com/wp-content/cache -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/wp-content/cache -type f -exec chmod 644 {} \;
# wp-config.php (fuori dalla webroot — vedi §2.6)
sudo chown www-data:www-data /var/www/wp-config.php
sudo chmod 640 /var/www/wp-config.php
2.2. WP-CLI
WP-CLI permette di gestire WordPress da terminale in modo veloce, scriptabile e senza passare dal backend.
sudo apt update
sudo apt install php-cli curl -y
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info
sudo -u www-data wp --info
2.3. Aggiornamenti e manutenzione con WP-CLI
Script da eseguire periodicamente tramite cron: aggiorna WordPress, esegue un backup SQL cifrato con GPG, esegue health check HTTP con verifica dello status code, e invia alert email in caso di errore.
2.3.1. Prerequisiti — Chiave di cifratura backup
# Generare una passphrase sicura e salvarla in un file protetto
sudo bash -c 'openssl rand -base64 32 > /etc/wp-backup.key'
sudo chmod 400 /etc/wp-backup.key
sudo chown root:root /etc/wp-backup.key
[!WARNING] Conservare
/etc/wp-backup.keyin un luogo sicuro e separato dal server (es. password manager). Senza la chiave, i backup cifrati non sono recuperabili.
2.3.2. Script di manutenzione
File: /usr/local/bin/wp-maintenance.sh
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/bin:/usr/bin:/bin
SITE_PATH="/var/www/example.com"
SITE_URL="https://example.com"
LOG_DIR="/var/log/wp"
LOG_FILE="${LOG_DIR}/example.com-maintenance.log"
BACKUP_DIR="/var/backups/wp"
BACKUP_RETENTION_DAYS=14
DB_BACKUP_FILE="${BACKUP_DIR}/example.com-$(date '+%Y-%m-%d-%H%M%S').sql.gz.gpg"
GPG_PASSPHRASE_FILE="/etc/wp-backup.key"
ADMIN_EMAIL="admin@example.com"
mkdir -p "${LOG_DIR}"
mkdir -p "${BACKUP_DIR}"
touch "${LOG_FILE}"
chown www-data:www-data "${LOG_FILE}"
chmod 640 "${LOG_FILE}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "${LOG_FILE}"
}
run_wp() {
sudo -u www-data wp "$@" --path="${SITE_PATH}" >> "${LOG_FILE}" 2>&1
}
# Invia email di alert in caso di errore e registra la riga del fallimento.
# Fonte: Bash manual — trap (https://www.gnu.org/software/bash/manual/bash.html#index-trap)
send_alert() {
local msg="$1"
log "ERRORE: ${msg}"
echo "${msg}" | mail -s "[WP Maintenance] ERRORE: ${SITE_URL}" "${ADMIN_EMAIL}" 2>/dev/null || true
}
trap 'send_alert "Lo script ha terminato con errore alla riga ${LINENO}. Controllare ${LOG_FILE}."' ERR
cleanup_old_backups() {
find "${BACKUP_DIR}" -type f -name 'example.com-*.sql.gz.gpg' -mtime +"${BACKUP_RETENTION_DAYS}" -delete
}
# Verifica che il sito risponda HTTP 200.
# curl -f fallisce su status >= 400 ma non distingue 200 da 301/503.
# La verifica esplicita del codice è necessaria per un health check affidabile.
check_http() {
local url="$1"
local http_code
http_code=$(curl -o /dev/null -s -w "%{http_code}" --max-time 15 "$url")
if [ "$http_code" != "200" ]; then
log "ERRORE: health check ${url} ha restituito HTTP ${http_code}"
send_alert "Health check fallito: ${url} ha restituito HTTP ${http_code}"
exit 1
fi
log "OK: ${url} risponde HTTP ${http_code}"
}
log "=== INIZIO MANUTENZIONE WORDPRESS ==="
if ! sudo -u www-data wp core is-installed --path="${SITE_PATH}" >> "${LOG_FILE}" 2>&1; then
log "ERRORE: WordPress non risulta installato in ${SITE_PATH}"
exit 1
fi
# Backup database cifrato con GPG (AES-256).
# Il dump SQL viene generato da www-data, compresso con gzip e cifrato.
# La cifratura dei backup è richiesta per proteggere dati personali e credenziali
# (fonte: CIS Controls v8 — Control 11.3; GDPR Art. 32).
log "Esporto e cifro backup database: ${DB_BACKUP_FILE}"
sudo -u www-data wp db export - --path="${SITE_PATH}" 2>>"${LOG_FILE}" \
| gzip \
| gpg --batch --yes --symmetric --cipher-algo AES256 \
--passphrase-file "${GPG_PASSPHRASE_FILE}" \
--output "${DB_BACKUP_FILE}"
log "Pulizia backup più vecchi di ${BACKUP_RETENTION_DAYS} giorni"
cleanup_old_backups >> "${LOG_FILE}" 2>&1
log "Aggiornamento plugin"
run_wp plugin update --all
log "Aggiornamento core WordPress"
run_wp core update
log "Aggiornamento temi"
run_wp theme update --all
log "Pulizia transient"
run_wp transient delete --all
log "Ottimizzazione database"
run_wp db optimize
log "Health check homepage"
check_http "${SITE_URL}/"
log "Health check pagina login"
check_http "${SITE_URL}/wp-login.php"
log "=== FINE MANUTENZIONE WORDPRESS ==="
Rendere lo script eseguibile:
sudo chmod 750 /usr/local/bin/wp-maintenance.sh
sudo chown root:root /usr/local/bin/wp-maintenance.sh
2.3.3. Predisposizione directory
sudo mkdir -p /var/log/wp
sudo mkdir -p /var/backups/wp
sudo chown -R www-data:www-data /var/log/wp
sudo chown -R www-data:www-data /var/backups/wp
sudo chmod 750 /var/log/wp
sudo chmod 750 /var/backups/wp
2.4. Esempio di cron di sistema
# flock -n evita esecuzioni parallele dello script.
# In caso di lock già acquisito (run in corso), il job viene saltato e viene inviata
# una notifica email. Eseguire silenziosamente senza notifica lascerebbe l'admin
# ignaro di run saltate.
0 4 * * * /usr/bin/flock -n /tmp/wp-maintenance.lock /usr/local/bin/wp-maintenance.sh \
|| echo "wp-maintenance già in esecuzione, run saltata." \
| mail -s "[WP] Maintenance skip: $(date)" admin@example.com
2.5. Gestione del log rotate del file di log generato dallo script di manutenzione
File: /etc/logrotate.d/wp-maintenance
/var/log/wp/example.com-maintenance.log {
daily # Ruota il log ogni giorno
rotate 14 # Mantieni 14 file ruotati (2 settimane)
compress # Comprimi i log ruotati per risparmiare spazio
delaycompress # Ritarda la compressione alla rotazione successiva (evita di comprimere log ancora in uso)
missingok # Non generare errori se il file non esiste
notifempty # Non ruotare se il file è vuoto
copytruncate # Copia e tronca invece di rinominare (compatibilità con processi che tengono il file aperto)
create 640 www-data www-data # Ricrea il file con permessi corretti
}
2.6. Configurazione wp-config.php
2.6.1. Posizione del file
Spostare wp-config.php fuori dalla webroot per ridurre l'esposizione in caso di misconfiguration Apache. WordPress cerca automaticamente il file nella directory padre.
sudo mv /var/www/example.com/wp-config.php /var/www/wp-config.php
sudo chown www-data:www-data /var/www/wp-config.php
sudo chmod 640 /var/www/wp-config.php
2.6.2. Configurazioni consigliate
<?php
# [...]
// Prefisso tabelle database — impostare solo durante l'installazione iniziale.
// Un prefisso non standard riduce la superficie in caso di SQL injection.
// (fonte: WordPress.org Hardening — Database table prefix)
$table_prefix = 'x7k_'; // generare un valore random
// Modalità debug — sempre false in produzione per evitare information disclosure
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', false );
// Forza HTTPS nel backend
define( 'FORCE_SSL_ADMIN', true );
// Disabilita l'editor file nel backend (previene modifiche via UI compromessa)
define( 'DISALLOW_FILE_EDIT', true );
// Disabilita installazione e aggiornamento plugin/temi dal backend.
// La gestione avviene esclusivamente tramite WP-CLI (vedi §2.2).
define( 'DISALLOW_FILE_MODS', true );
// Disabilita WP-Cron interno — viene gestito con cron di sistema (vedi §2.9)
define( 'DISABLE_WP_CRON', true );
/* That's all, stop editing! Happy publishing. */
# [...]
2.7. Hardening PHP
La configurazione di default di PHP espone la versione, permette funzioni pericolose e non limita inclusioni remote. Modificare /etc/php/8.x/apache2/php.ini (sostituire 8.x con la versione installata):
; Sicurezza — information disclosure
expose_php = Off
; Gestione errori — non esporre mai errori in produzione
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; Prevenzione RFI (Remote File Inclusion) e SSRF
allow_url_fopen = Off
allow_url_include = Off
; Disabilita funzioni che permettono esecuzione di comandi di sistema.
; Fonte: OWASP PHP Configuration Cheat Sheet
; (https://cheatsheetseries.owasp.org/cheatsheets/PHP_Configuration_Cheat_Sheet.html)
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,\
curl_exec,curl_multi_exec,parse_ini_file,show_source
; Limiti di upload — adattare alle necessità del sito
upload_max_filesize = 10M
post_max_size = 12M
max_execution_time = 60
; Cookie di sessione — HttpOnly e Secure prevengono accesso JS e trasmissione su HTTP
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
Creare la directory di log PHP:
sudo mkdir -p /var/log/php
sudo chown www-data:www-data /var/log/php
sudo chmod 750 /var/log/php
sudo systemctl restart apache2
2.8. Hardening MySQL
2.8.1. Eseguire la procedura di hardening guidata
Rispondere Yes a tutte le domande: rimozione utenti anonimi, disabilitazione accesso root remoto, rimozione database di test.
2.8.2. Utente database con privilegi minimi
WordPress richiede solo i permessi necessari alle proprie operazioni. Un utente GRANT ALL è una superficie d'attacco non necessaria.
-- Connettersi come root
sudo mysql -u root -p
-- Creare utente con privilegi minimi
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'password_sicura';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
-- Verificare che root non sia accessibile da remoto
SELECT User, Host FROM mysql.user WHERE User = 'root';
Fonte: WordPress.org — Hardening — Database Security + CIS MySQL Benchmark
2.9. Cron per WP-Cron
Con DISABLE_WP_CRON true in wp-config.php, WP-Cron va gestito tramite cron di sistema:
2.10. Cron di sistema per WP-Cron
# (CONSIGLIATO) Esecuzione tramite WP-CLI ogni 15 minuti.
# Evita il caricamento dell'intero stack PHP/WordPress per ogni richiesta HTTP,
# eseguendo solo gli eventi dovuti senza processare richieste inutili.
*/15 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/example.com > /dev/null 2>&1
# Alternativa senza WP-CLI:
# */15 * * * * /usr/bin/php /var/www/example.com/wp-cron.php > /dev/null 2>&1
2.11. Cron di sistema per manutenzione e aggiornamenti con WP-CLI
# Esecuzione script di manutenzione ogni notte alle 4:00.
# flock -n evita esecuzioni parallele; in caso di lock già acquisito, invia alert email.
0 4 * * * /usr/bin/flock -n /tmp/wp-maintenance.lock /usr/local/bin/wp-maintenance.sh \
|| echo "wp-maintenance già in esecuzione, run saltata." \
| mail -s "[WP] Maintenance skip: $(date)" admin@example.com
2.12. Hardening enumeration
MU Plugin per bloccare l'enumerazione degli utenti, che può essere sfruttata per identificare nomi utente validi per brute-force e altri attacchi.
File: /var/www/example.com/wp-content/mu-plugins/deeplab-hardening-enumeration.php
<?php
/**
* Deep Lab - Hardening enumerazione utenti
*
* Mitiga:
* - /?author=1
* - archivi autore pubblici (se non necessari)
* - endpoint REST /wp/v2/users per utenti anonimi
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Blocca l'enumerazione via query string ?author=ID.
*/
add_action( 'template_redirect', function () {
if ( is_admin() ) {
return;
}
if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
}, 0 );
/**
* Disabilita gli archivi autore.
* Attivare solo se il sito NON usa pagine autore pubbliche.
*/
add_action( 'template_redirect', function () {
if ( is_admin() ) {
return;
}
if ( is_author() ) {
global $wp_query;
$wp_query->set_404();
status_header( 404 );
nocache_headers();
exit;
}
}, 1 );
/**
* Rimuove gli endpoint REST utenti per i visitatori anonimi.
* Non disabilita tutta la REST API.
*/
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( is_user_logged_in() ) {
return $endpoints;
}
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
return $endpoints;
} );
2.13. File Integrity Monitoring (AIDE)
AIDE monitora l'integrità del filesystem e rileva modifiche non autorizzate ai file WordPress. Fondamentale per individuare compromissioni post-exploit.
sudo apt install aide -y
# Inizializzare il database di riferimento
sudo aideinit
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Configurazione cron per check giornaliero:
# Check AIDE ogni notte alle 3:00 — alert via email in caso di modifiche
0 3 * * * /usr/bin/aide --check >> /var/log/aide/aide.log 2>&1 \
|| echo "AIDE ha rilevato modifiche al filesystem. Controllare /var/log/aide/aide.log." \
| mail -s "[AIDE] Alert integrità filesystem: $(hostname)" admin@example.com
[!NOTE] Dopo ogni aggiornamento legittimo (plugin, core, temi) aggiornare il database AIDE:
Fonte: CIS Controls v8 — Control 10 + SANS Institute — File Integrity Monitoring
2.14. Plugins
2.15. Limit Login Attempts Reloaded
Installazione:
sudo -u www-data wp plugin install limit-login-attempts-reloaded --activate \
--path=/var/www/example.com
2.15.1. Rate Limit
| Parametro | Valore |
|---|---|
| Tentativi consentiti | 2 |
| Durata blocco iniziale | 40 minuti |
| Dopo 2 lockout | Blocco esteso a 8 ore |
| Reset conteggio tentativi dopo | 300 ore |
| Safe list | IP pubblici autorizzati |
| Origine IP affidabile | REMOTE_ADDR |
[!NOTE] L'origine IP affidabile deve essere coerente con la configurazione Apache/Cloudflare. Con
mod_remoteipcorrettamente configurato (§1.5),REMOTE_ADDRriflette l'IP reale del client. Verificare sul singolo ambiente prima di andare in produzione.
2.15.2. Autenticazione a due fattori (2FA)
2FA abilitato per il ruolo Amministratore.