Hardening LXC Wordpress
Indice
- 1. Ambiente Macchina
- 1.1. Isolamento directory critiche (partizionamento LXC)
- 1.2. Installazione ambiente LAMP
- 1.3. Cloudflare Tunnel
- 1.4. Firewall
- 1.5. Apache
- 1.6. Configurazione IP reale client (Cloudflare + Apache)
- 1.7. Fail2Ban
- 1.8. Cloudflare Security Rules (WAF Free)
- 1.9. Hardening MySQL
- 1.9.1. Eseguire la procedura di hardening guidata
- 1.10. Backup
- 2. Ambiente WordPress
- 2.0. Download di 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. Cron per WP-Cron
- 2.9. Cron di sistema per WP-Cron
- 2.10. Cron di sistema per manutenzione e aggiornamenti con WP-CLI
- 2.11. Hardening enumeration
- 2.12. File Integrity Monitoring (AIDE)
- 2.13. Plugins
- 2.13.1 Wordfence Security
- 2.13.1.2 Installazione
- 2.13.1.3 Configurazione della Two-Factor Authentication (2FA)
- 2.13.1.4 Configurazione Brute Force Protection
- 2.13.1.5 Configurazione Rate Limiting
- 2.13.1.6 Gestione del reCAPTCHA
- 2.13.1.7 Firewall e Learning Mode
- 2.13.1.8 Verifica della configurazione
1. Ambiente Macchina
Container LXC, Ubuntu 24.04 LTS.
Note
Questa guida è applicabile sia a container nuovi che a container già operativi. Nelle sezioni dove la procedura differisce tra i due scenari è presente un callout Ambiente esistente con le istruzioni specifiche. In assenza di tale callout, la procedura è identica in entrambi i casi.
1.1. Isolamento directory critiche (partizionamento LXC)
In un container LXC il kernel è condiviso con l'host Proxmox, ma i namespace di mount permettono di applicare opzioni restrittive specifiche per singole directory. Separare alcune directory su volumi dedicati offre due vantaggi di sicurezza fondamentali:
- Limitazione dei diritti di esecuzione — impedisce che un attaccante, dopo aver caricato un file malevolo in una directory scrivibile (es.
/tmp), possa eseguirlo. - Isolamento dello spazio — evita che una directory riempia l'intero filesystem di root, bloccando il sistema o cancellando dati importanti.
Warning
Container nuovo: eseguire questa operazione prima di installare qualsiasi software (§1.2). Non ci sono dati da migrare e la procedura è immediata.
Note
Ambiente esistente: su un container già operativo i servizi scrivono attivamente in /var/log e usano /tmp. Prima di procedere:
- Eseguire un backup completo dal nodo Proxmox:
pct backup <CT_ID> - Fermare i servizi prima dello spegnimento:
systemctl stop apache2 mysql fail2ban - Dopo aver aggiunto i mount point, copiare i dati esistenti prima del riavvio (vedi nota nella procedura sotto)
Verificare che il container sia in modalità unprivileged, aprendo sul nodo Proxmox il file /etc/pve/lxc/<CT_ID>.conf e controllando la presenza di:
Opzioni di mount
| Opzione | Effetto |
|---|---|
noexec |
Impedisce l'esecuzione di qualsiasi binario o script |
nosuid |
Ignora i bit setuid e setgid |
nodev |
Impedisce la creazione di file di dispositivo (device node) |
noatime |
Non aggiorna il timestamp di accesso (migliora le performance) |
Directory da isolare
| Directory | Opzioni consigliate | Dimensione tipica | Note |
|---|---|---|---|
/tmp |
noexec,nosuid,nodev |
1–2 GB | Alternativa: tmpfs in RAM (vedi sotto) |
/var/tmp |
noexec,nosuid,nodev |
1 GB | Usato da alcune applicazioni, spesso trascurato |
/var/log |
nosuid,nodev |
2–4 GB | Deve contenere i log di Apache, PHP, WP e AIDE (§2.12) |
/home |
nosuid,nodev |
2–5 GB | Home degli utenti |
/root |
nosuid,nodev |
1 GB | Home di root |
Note
/var/log non monta noexec perché alcuni tool di log rotation e monitoring potrebbero richiedere script nella directory. nosuid,nodev è sufficiente per questo volume.
Procedura (da eseguire sul nodo Proxmox, container spento)
# 1. Spegnere il container
pct shutdown <CT_ID>
# 2. Aggiungere i mount point con le opzioni di sicurezza
# La sintassi è: <storage>:<size_GB>,mp=<path>,mountoptions=<opt1>;<opt2>
# Sostituire "local-lvm" con lo storage disponibile (es. local-zfs)
pct set <CT_ID> -mp0 "local-lvm:2,mp=/tmp,mountoptions=noexec;nosuid;nodev"
pct set <CT_ID> -mp1 "local-lvm:1,mp=/var/tmp,mountoptions=noexec;nosuid;nodev"
pct set <CT_ID> -mp2 "local-lvm:4,mp=/var/log,mountoptions=nosuid;nodev"
pct set <CT_ID> -mp3 "local-lvm:3,mp=/home,mountoptions=nosuid;nodev"
pct set <CT_ID> -mp4 "local-lvm:1,mp=/root,mountoptions=nosuid;nodev"
# 3. Riavviare il container (vedi WARNING sotto)
pct start <CT_ID>
Note
Container nuovo: le directory sono vuote, nessuna copia necessaria.
Warning
Ambiente esistente: copiare i dati prima del riavvio per evitare di perdere log e file in uso. Dal nodo Proxmox, con il container ancora spento:
pct mount <CT_ID>
# /var/log (mp2)
cp -a /var/lib/lxc/<CT_ID>/rootfs/var/log/* /var/lib/lxc/<CT_ID>/mnt/mp2/
# /home (mp3) — se presenti home utenti
cp -a /var/lib/lxc/<CT_ID>/rootfs/home/* /var/lib/lxc/<CT_ID>/mnt/mp3/
# /root (mp4)
cp -a /var/lib/lxc/<CT_ID>/rootfs/root/. /var/lib/lxc/<CT_ID>/mnt/mp4/
pct unmount <CT_ID>
/tmp e /var/tmp contengono solo file temporanei: non è necessaria la copia.
Verifica
I flag noexec, nosuid, nodev devono comparire nell'output per le rispettive directory.
Alternativa per /tmp: tmpfs in RAM
Invece di un volume su disco, /tmp può essere montato come tmpfs — un filesystem volatile in memoria RAM che si svuota automaticamente al riavvio. Consuma meno I/O ed è più veloce.
Aggiungere questa riga a /etc/fstab dentro il container:
Note
tmpfs non richiede la creazione di un mount point Proxmox. L'opzione noexec è ugualmente applicata dal kernel. PHP scrive file temporanei in /tmp ma non li esegue da lì: noexec non interferisce con il funzionamento di WordPress o WP-CLI.
1.2. Installazione ambiente LAMP
sudo apt update
# Apache
# Nota: con Cloudflare Tunnel (§1.3) il traffico transita via tunnel locale.
# Non aprire porte Apache su UFW — il firewall mantiene default deny incoming (§1.4).
# I moduli Apache (ssl, headers, rewrite, remoteip) vengono abilitati in §1.5.
sudo apt install apache2 -y
sudo systemctl restart apache2
# MySQL
# Nota: mysql_secure_installation viene eseguito in §1.9 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
# Nota: DOMDocument è incluso in php-xml
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
# Certbot (SOLO SE NON SI USANO GLI ORIGIN CERTIFICATE DI CLOUDFLARE — vedi §1.3)
sudo apt install certbot python3-certbot-apache -y
# Utilità necessarie
sudo apt install curl gnupg mailutils unzip -y
Note
gnupg è richiesto dallo script di manutenzione per cifrare i backup SQL (§2.3).
mailutils (con un MTA locale come postfix) è richiesto da tutti i comandi mail usati per gli alert nel documento. Durante l'installazione di mailutils, selezionare Internet Site e inserire il FQDN (Fully Qualified Domain Name) del server.
1.3. 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.1. Alternativa consigliata: Origin Certificate Cloudflare
Per ambienti di produzione è raccomandato sostituire la configurazione Let's Encrypt + No TLS Verify con un Origin Certificate emesso da Cloudflare. I vantaggi:
- Validità di 15 anni (nessun rinnovo automatico, nessuna scadenza da monitorare).
- Nessuna porta in ingresso richiesta per la validazione (compatibile con UFW §1.4).
- Verifica TLS Full (strict) attivabile su Cloudflare.
- Non richiede Certbot né altri ACME client sul server.
Procedura:
1.3.1.1. Generare l'Origin Certificate
Dalla Dashboard Cloudflare: SSL/TLS → Origin Server → Create Certificate.
- Private key type: ECDSA (consigliato) o RSA.
- Hostnames: inserire il dominio del sito e il suo alias (es.
example.com,www.example.com). Per sicurezza, evitare wildcard*a meno che non siano strettamente necessarie. - Certificate Validity: 15 years.
- Copiare immediatamente la chiave privata e il certificato (PEM).
1.3.1.2. Installare il certificato sul server
Creare il file della chiave privata (/etc/cloudflare/example.com.key):
sudo nano /etc/cloudflare/example.com.key
# Incollare la chiave privata
sudo chmod 600 /etc/cloudflare/example.com.key
sudo chown root:root /etc/cloudflare/example.com.key
Creare il file del certificato (/etc/cloudflare/example.com.pem):
sudo nano /etc/cloudflare/example.com.pem
# Incollare il certificato (inclusi BEGIN e END)
sudo chmod 644 /etc/cloudflare/example.com.pem
sudo chown root:root /etc/cloudflare/example.com.pem
1.3.1.3. Aggiornare il VirtualHost Apache
Nel file /etc/apache2/sites-available/example.com.conf (§1.5.2), sostituire i percorsi dei certificati:
SSLCertificateFile /etc/cloudflare/example.com.pem
SSLCertificateKeyFile /etc/cloudflare/example.com.key
1.3.1.4. Attivare la verifica TLS su Cloudflare
Nella Dashboard Cloudflare, SSL/TLS → Overview, impostare "Full (strict)".
1.3.1.5. Rimuovere Certbot (se presente)
sudo apt purge certbot python3-certbot-apache -y
sudo rm -rf /etc/letsencrypt /var/lib/letsencrypt
1.4. 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.
Warning
Ambiente esistente: ufw reset cancella tutte le regole esistenti. Prima di procedere, documentare le regole correnti:
Warning
Questo blocca qualsiasi porta in entrata. Accertarsi di avere accesso tramite la console del nodo Proxmox prima di abilitare il firewall.
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
1.5. Apache
1.5.1. Moduli necessari
1.5.2. File di configurazione VirtualHost
Note
Ambiente esistente: se il file /etc/apache2/sites-available/example.com.conf esiste già, non sovrascriverlo interamente. Integrare selettivamente le direttive mancanti (cipher suite, security headers, restrizione IP su wp-login, blocco uploads) confrontando il contenuto attuale con la configurazione sotto. Usare apache2ctl configtest dopo ogni modifica prima di ricaricare.
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
ServerAlias www.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.5.3. DocumentRoot e configurazione nome dominio
Creare, se non già non esiste, la cartella DocumentRoot per il sito:
Configurare il nome di dominio in /etc/hosts
# Verifica e aggiunge la riga se manca
DOMAIN="example.com"
if ! grep -q "127.0.0.1.*$DOMAIN" /etc/hosts; then
echo "127.0.0.1 $DOMAIN www.$DOMAIN" | sudo tee -a /etc/hosts
fi
1.5.4. Abilitare il sito e ricaricare Apache
1.5.5. 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.6. 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, modificando la seguente parte nel file /etc/apache2/apache2.conf, sostituendo %h (host remoto) con %a (IP remoto dopo remoteip).
da
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
a:
LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%a %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
Infine, abilitare la configurazione e riavviare Apache:
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.7. Fail2Ban
Fail2Ban blocca a livello di rete (iptables/nftables) gli IP che generano troppi errori di autenticazione, complementando il rate limiting applicativo di Wordfence e le regole di Cloudflare WAF. In questo modo, anche se un attaccante riesce a superare i limiti di Wordfence o WAF, Fail2Ban può bannare l'IP a livello di firewall, impedendo ulteriori tentativi.
1.7.1. Installazione
1.7.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.7.3. Filtri
File: /etc/fail2ban/filter.d/wordpress-auth.conf
File: /etc/fail2ban/filter.d/wordpress-xmlrpc.conf
Riavviare Fail2Ban e verificare lo stato della jail:
Note
Il logpath deve puntare al log Apache del sito configurato con remoteip (§1.6), altrimenti Fail2Ban banna l'IP del proxy Cloudflare invece di quello del client reale.
1.8. Cloudflare Security Rules (WAF Free)
Il piano Free di Cloudflare include Security Rules personalizzabili che permettono di filtrare il traffico in base a criteri come paese di provenienza, URI richiesta, IP, host e altro. Queste regole costituiscono la prima linea di difesa prima che il traffico raggiunga Apache, Fail2Ban e WordPress.
Vantaggi delle Security Rules - Blocco geografico e per path senza consumare risorse del server - Mitigazione di brute‑force, scansioni e traffico indesiderato già all'edge - Logging centralizzato nella dashboard Cloudflare
1.8.1. Creazione delle regole (Cloudflare Dashboard)
Accedere a Security → Rules → Create Rule e inserire i blocchi nell'ordine consigliato.
L'ordine di esecuzione è importante: le regole vengono valutate dall'alto verso il basso e la prima corrispondenza determina l'azione.
Regola 1 – Blocco geografico Cina e Russia
Azione: Block
Regola 2 – Protezione wp-login.php e wp-admin solo dall'Italia
Azione: Block
(http.request.uri.path contains "/wp-login.php" and ip.geoip.country ne "IT")
or
(http.request.uri.path contains "/wp-admin/" and http.request.uri.path ne "/wp-admin/admin-ajax.php" and ip.geoip.country ne "IT")
wp-login.php e alle pagine di amministrazione solo da IP italiani.
- Esclude admin-ajax.php per non interferire con chiamate AJAX pubbliche (es. plugin frontend).
Regola 3 – Blocco XML‑RPC
Azione: Block
xmlrpc.php, a meno che non sia strettamente necessario (es. app mobile tramite WordPress.com). Se il sito non utilizza XML‑RPC, questa regola elimina un vettore di attacco comune.
1.8.2. Verifica e monitoraggio
Dopo aver attivato le regole, verificare nella dashboard Cloudflare (Security → Events) che il traffico legittimo non venga bloccato. In particolare:
- Controllare che
admin-ajax.phprisponda correttamente da qualsiasi paese. - Assicurarsi che i propri IP (italiani) possano accedere a
wp-admin. - Se si utilizza Jetpack o altre integrazioni che chiamano XML‑RPC, disabilitare la Regola 3 o restringerla a specifici IP di servizio.
1.8.3. Aggiunta di Bot Fight Mode
Per una protezione aggiuntiva contro bot malevoli, abilitare Security → Bots → Bot Fight Mode.
È una funzionalità gratuita che blocca il traffico bot noto prima che raggiunga le Security Rules, riducendo ulteriormente il carico sul server.
Tip
Le Security Rules sono il primo filtro. A valle operano Fail2Ban (§1.7) e Limit Login Attempts Reloaded (§2.14), creando una difesa a più livelli.
1.9. Hardening MySQL
1.9.1. Eseguire la procedura di hardening guidata
Warning
Ambiente esistente: mysql_secure_installation è idempotente e può essere eseguito più volte senza danni. Se era già stato eseguito in precedenza, le domande relative a operazioni già completate (es. rimozione utenti anonimi) non avranno effetto.
Avviare la procedura:
Il programma è interattivo. Rispondere come indicato di seguito:
| Domanda | Risposta consigliata | Note |
|---|---|---|
| Validate Password Component? | y |
Attiva il controllo della forza delle password |
| Scegli livello di policy: | 1 |
MEDIUM (lunghezza ≥ 8, maiuscole, minuscole, numeri e caratteri speciali). La password generata con openssl rand -base64 24 è conforme a questo livello. |
| Impostare password per root? | (Invio) | Con auth_socket (default su Ubuntu) non è necessario impostare una password per root. L'accesso è garantito da sudo mysql. Se si desidera comunque una password, eseguire ALTER USER dopo la procedura (vedi nota sotto). |
| Remove anonymous users? | y |
Rimuove gli utenti anonimi che permettono accesso senza autenticazione. |
| Disallow root login remotely? | y |
Impedisce connessioni remote come root. |
| Remove test database? | y |
Rimuove il database di test accessibile a tutti. |
| Reload privilege tables now? | y |
Applica tutte le modifiche. |
Output atteso (al termine):
Nota sulla password di root
Su Ubuntu, l'utente root di MySQL è configurato con il plugin auth_socket (autenticazione tramite socket Unix). Questo significa che:
- Puoi accedere a MySQL come root senza password usando
sudo mysql. - L'accesso remoto come root è già disabilitato.
Se per esigenze particolari (es. accesso da applicazioni esterne) volessi impostare una password per root, puoi farlo in un secondo momento con:
Ma nella configurazione standard di questa guida non è necessario.
1.9.2. Creazione del database e utente WordPress
Questa sezione copre sia la creazione da zero (per nuove installazioni) sia la revisione/adeguamento di un ambiente già esistente.
Nuova installazione
Generare una password sicura per l'utente del database (da conservare in un password manager):
Prendere nota dei seguenti valori:
- Nome database:
db_wordpress_name - Nome utente:
db_wordpress_user - Password: quella generata con
openssl
Collegarsi a MySQL come root:
Eseguire i seguenti comandi:
-- Creare il database con charset UTF-8 (supporto completo per emoji e lingue)
CREATE DATABASE db_wordpress_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Creare l'utente con la password generata in precedenza
CREATE USER 'db_wordpress_user'@'localhost' IDENTIFIED BY 'password_sicura';
-- Concedere i privilegi minimi necessari per WordPress
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
ON db_wordpress_name.* TO 'db_wordpress_user'@'localhost';
-- Applicare le modifiche
FLUSH PRIVILEGES;
-- Verificare che l'utente sia stato creato correttamente
SELECT User, Host FROM mysql.user WHERE User = 'db_wordpress_user';
Warning
Sostituire password_sicura con la password generata tramite openssl. Non utilizzare password deboli o riutilizzate.
Note
WordPress richiede permessi come CREATE, DROP, ALTER per eseguire aggiornamenti automatici di plugin e core tramite WP‑CLI. I privilegi elencati sono il minimo indispensabile (fonte: WordPress.org — Database Security).
Ambiente esistente
Se l'utente WordPress è già configurato con GRANT ALL (o con privilegi eccessivi), non è necessario ricrearlo. È sufficiente revocare i privilegi in eccesso e riassegnare solo quelli necessari.
-- Collegarsi come root
sudo mysql -u root -p
-- Revocare tutti i privilegi esistenti
REVOKE ALL PRIVILEGES ON db_wordpress_name.* FROM 'db_wordpress_user'@'localhost';
-- Assegnare solo i privilegi minimi
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
ON db_wordpress_name.* TO 'db_wordpress_user'@'localhost';
FLUSH PRIVILEGES;
Se il database o l'utente non esistono ancora, seguire la procedura per Nuova installazione sopra.
Verifica di sicurezza aggiuntiva
Dopo aver configurato l'utente, verificare che l'utente root non sia accessibile da remoto:
L'output deve mostrare solo localhost (o 127.0.0.1), mai un host esterno o %.
Fonte: WordPress.org — Hardening — Database Security + CIS MySQL Benchmark
1.10. Backup
La protezione avviene su due livelli:
- Backup infrastrutturale: backup giornaliero del LXC 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.0. Download di WordPress
Questa procedura scarica l'ultima versione di WordPress, la estrae e posiziona i file nella DocumentRoot configurata in Apache (§1.5.2).
Warning
Ambiente esistente: se nella DocumentRoot è già presente un'installazione WordPress, questa procedura la sovrascrive. Utilizzarla solo per nuove installazioni.
# Scaricare l'ultima versione di WordPress in /tmp
wget https://wordpress.org/latest.zip -P /tmp
# Estrarre l'archivio in /tmp
cd /tmp
unzip latest.zip
# Spostare i file nella DocumentRoot
sudo mv /tmp/wordpress/* /var/www/example.com/
# Rimuovere l'archivio e la directory temporanea
rm /tmp/latest.zip
rm -rf /tmp/wordpress/
2.1. Permessi cartelle e file
Note
Ambiente esistente: il reset massivo dei permessi con find è sicuro su una installazione WordPress standard. Se sono presenti script custom con bit di esecuzione intenzionali (es. in wp-content/mu-plugins o in tool di deploy), documentarli prima con find /var/www/example.com -perm /111 -type f e ripristinarli manualmente dopo.
# 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 {} \;
# spostare wp-config.php fuori dalla webroot per maggiore sicurezza
sudo mv /var/www/example.com/wp-config.php /var/www/wp-config.php
# 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 una copia di /etc/wp-backup.key in un luogo sicuro e separato dal server (es. password manager). La chiave deve rimanere presente sul server per la cifratura automatica, ma senza la copia offline i backup cifrati non sono recuperabili in caso di guasto.
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
Per eseguire lo script di manutenzione:
ed inserire:
# 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.
Note
Ambiente esistente: lo spostamento causa un'interruzione del sito di pochi secondi (il tempo di eseguire mv). Se il sito è ad alto traffico, pianificare l'operazione in una finestra di bassa frequentazione. WordPress rileva automaticamente il file nella directory padre senza necessità di ulteriori configurazioni.
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.8)
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. Cron per WP-Cron
Con DISABLE_WP_CRON true in wp-config.php, WP-Cron va gestito tramite cron di sistema:
2.9. 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.10. 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.11. 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/hardening-enumeration.php
<?php
/**
* 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.12. File Integrity Monitoring (AIDE)
AIDE monitora l'integrità del filesystem e rileva modifiche non autorizzate ai file WordPress. Fondamentale per individuare compromissioni post-exploit.
Warning
Ambiente esistente: il database AIDE fotografa lo stato del filesystem al momento dell'inizializzazione. Su un sistema già operativo, assicurarsi che non siano presenti file non autorizzati o malware prima di eseguire aideinit — altrimenti lo stato compromesso diventa il baseline di riferimento. Eseguire una verifica preliminare:
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
# Refresh baseline AIDE dopo la manutenzione notturna (che parte alle 4:00)
# Se il database AIDE esiste, lo rigenera così da includere gli aggiornamenti legittimi.
30 4 * * * [ -f /var/lib/aide/aide.db ] && /usr/bin/aideinit --force > /dev/null 2>&1 && mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Note
Il database AIDE viene rigenerato automaticamente ogni notte alle 4:30 (dopo la manutenzione WordPress delle 4:00) tramite un cron di root. Non è richiesto alcun intervento manuale dopo gli aggiornamenti.
Fonte: CIS Controls v8 — Control 10 + SANS Institute — File Integrity Monitoring
2.13. Plugins
2.13.1 Wordfence Security
Wordfence è una suite di sicurezza completa per WordPress che integra firewall WAF, brute force protection, rate limiting e two-factor authentication (2FA) in un unico plugin.
2.13.1.2 Installazione
Installare Wordfence tramite WP-CLI:
Per ambienti esistenti che utilizzano LLAR (Limit Login Attempts Reloaded), non è necessario disinstallarlo immediatamente. Wordfence può coesistere con LLAR durante la fase di transizione. Dopo aver configurato Wordfence e verificato che tutto funzioni correttamente, si può disattivare e rimuovere LLAR:
sudo -u www-data wp plugin deactivate limit-login-attempts-reloaded --path=/var/www/example.com
sudo -u www-data wp plugin delete limit-login-attempts-reloaded --path=/var/www/example.com
2.13.1.3 Configurazione della Two-Factor Authentication (2FA)
Accedere al backend di WordPress e navigare su Wordfence → Login Security → 2FA.
Impostazioni consigliate
| Parametro | Valore consigliato | Note |
|---|---|---|
| 2FA Roles | Amministratore: Required; Editor/Author/Contributor/Subscriber: Disabled | Solo gli amministratori devono avere 2FA obbligatoria. |
| Grace Period | 10 giorni | Tempo concesso agli utenti per attivare la 2FA prima che venga loro impedito l'accesso. |
| Allow remembering device for 30 days | Attivato | Riduce la frequenza delle richieste OTP per dispositivi fidati. |
| Require 2FA for XML-RPC | Required (se NON si usa app WordPress o Jetpack) | Richiede 2FA per le richieste XML-RPC (Skipped se si usano app WordPress o Jetpack). |
| Disable XML-RPC authentication | Attivo | Disabilita l'autenticazione XML-RPC (Oppure attivarla nel caso sia necessaria per app e Jetpack). |
2FA per l'amministratore
Per attivare la 2FA per l'utente corrente:
- Andare su Wordfence → Login Security → 2FA.
- Cliccare su "Activate" accanto al proprio profilo.
- Scansionare il codice QR con un'app TOTP (Google Authenticator, FreeOTP, Authy, ecc.).
- Inserire il codice OTP generato dall'app per confermare l'attivazione.
- Salvare i recovery codes in un luogo sicuro (password manager). Questi codici permettono di recuperare l'accesso in caso di perdita del dispositivo.
Warning
Conservare i recovery codes in un luogo sicuro e separato dal server (es. password manager). Senza di essi, in caso di perdita del dispositivo che genera gli OTP, l'accesso al sito potrebbe essere irrecuperabile.
2.13.1.4 Configurazione Brute Force Protection
Navigare su Wordfence → All Options → Brute Force Protection.
Impostazioni consigliate
| Parametro | Valore consigliato | Note |
|---|---|---|
| Enable brute force protection | Attivato | Attiva tutte le opzioni di protezione. |
| Lock out after how many login failures | 2 |
Dopo 2 tentativi falliti, l'IP viene bloccato. |
| Lock out after how many forgot password attempts | 2 |
Dopo 2 tentativi di reset password falliti, l'IP viene bloccato. |
| Count failures over what time period | 4 hours |
Le finestra di conteggio dei tentativi. |
| Amount of time a user is locked out | 5 days |
Durata del lockout. |
| Immediately lock out invalid usernames | Attivato | Blocca immediatamente gli IP che provano username inesistenti. |
| Immediately block the IP of users who try to sign in as these usernames | admin, administrator, test |
Blocca IP che provano username comuni (modificare a piacere). |
| Prevent the use of passwords leaked in data breaches | For admin only |
Controlla le password degli amministratori contro database di data breach. |
| Block IPs who send POST requests with blank User-Agent and Referer | Disattivato | Può bloccare servizi legittimi che non inviano questi header. |
| Check password strength on profile update | Attivato | Impone password forti agli amministratori. |
2.13.1.5 Configurazione Rate Limiting
Navigare su Wordfence → All Options → Rate Limiting.
Impostazioni consigliate
| Parametro | Valore consigliato | Note |
|---|---|---|
| Enable Rate Limiting and Advanced Blocking | Attivato | Attiva tutte le funzioni di rate limiting. |
| How should we treat Google's crawlers | Unlimited | Non limitare i crawler Google per non penalizzare il SEO. |
| If anyone's requests exceed | 240 per minute |
Limite per utenti normali. |
| then | Block for 30 minutes |
Blocco per IP che superano il limite. |
| If a crawler's page views exceed | 480 per minute |
Limite più permissivo per crawler legittimi. |
| then | Block for 30 minutes |
Blocco breve per crawler aggressivi. |
| If a crawler's pages not found (404s) exceed | 240 per minute |
Limite per 404 generati da crawler. |
| then | Block for 30 minutes |
Blocco per crawler che generano troppi 404. |
2.13.1.6 Gestione del reCAPTCHA
Navigare su Wordfence → Login Security → reCAPTCHA.
Impostazioni consigliate
| Parametro | Valore consigliato | Note |
|---|---|---|
| Enable reCAPTCHA | Disabilitato (toggle off) | Con Cloudflare Tunnel, Fail2Ban (§1.7) e le Security Rules (§1.8), il reCAPTCHA è ridondante. Inoltre, in assenza di chiavi API, causa il blocco dei login legittimi. |
2.13.1.7 Firewall e Learning Mode
Wordfence include un Web Application Firewall (WAF) che, appena installato, entra in Learning Mode.
Cos'è il Learning Mode
Durante la prima settimana di utilizzo, Wordfence osserva il traffico del sito per apprendere quali richieste sono legittime e quali sono malevole. Questo permette al firewall di adattarsi automaticamente alle specificità del sito senza causare falsi positivi.
Configurazione consigliata
| Parametro | Valore consigliato | Note |
|---|---|---|
| Learning Mode | Attivo per 7 giorni | Lasciare attivo per almeno una settimana per permettere al firewall di apprendere. |
| Protection Level | Basic WordPress Protection | Protezione sufficiente per la maggior parte dei siti. |
Dopo il Learning Mode
Trascorsa una settimana, Wordfence uscirà automaticamente dal Learning Mode e attiverà la protezione completa. In alternativa, è possibile uscire manualmente dopo aver verificato che non ci siano falsi positivi:
- Navigare su Wordfence → Firewall → Manage WAF.
- Impostare Protection Level su Wordfence Full Protection.
- Verificare il funzionamento del sito.
Note
Durante il Learning Mode, il firewall non blocca attivamente il traffico sospetto ma registra solo le richieste. La protezione brute force e il rate limiting rimangono comunque attivi e funzionanti.
2.13.1.8 Verifica della configurazione
Dopo aver configurato Wordfence:
- Verificare il funzionamento del sito: navigare nel frontend e nel backend per assicurarsi che non ci siano falsi positivi.
- Testare il login con 2FA: disconnettersi e riconnettersi, verificando che il codice OTP venga richiesto e funzioni.
- Monitorare i log: controllare Wordfence → Tools → Live Traffic per vedere le richieste in tempo reale.
- Verificare i lockout: provare a inserire credenziali errate per verificare che il blocco brute force funzioni.