Vai al contenuto

Hardening LXC Wordpress

Indice


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:

  1. Eseguire un backup completo dal nodo Proxmox: pct backup <CT_ID>
  2. Fermare i servizi prima dello spegnimento: systemctl stop apache2 mysql fail2ban
  3. 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:

Text Only
unprivileged: 1

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)

Bash
# 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:

Bash
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

Bash
pct enter <CT_ID>
mount | grep -E "/tmp|/var/tmp|/var/log|/home|/root"

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:

/etc/fstab
tmpfs /tmp tmpfs rw,nodev,nosuid,noexec,size=512M 0 0
Bash
mount /tmp
# oppure riavviare il container
mount | grep /tmp

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

Bash
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
Bash
sudo mkdir -p /etc/cloudflare
sudo chmod 700 /etc/cloudflare

Creare il file della chiave privata (/etc/cloudflare/example.com.key):

Bash
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):

Bash
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:

ApacheConf
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)
Bash
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:

Bash
sudo ufw status numbered
Se sono presenti regole personalizzate (es. porte SSH, VPN, servizi interni), ripristinarle manualmente dopo il reset. Con Cloudflare Tunnel non è necessaria alcuna porta in entrata, quindi il reset è solitamente sicuro — ma verificare sempre prima.

Warning

Questo blocca qualsiasi porta in entrata. Accertarsi di avere accesso tramite la console del nodo Proxmox prima di abilitare il firewall.

Bash
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

Bash
sudo a2enmod ssl headers rewrite remoteip
sudo systemctl restart apache2

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

ApacheConf
# 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:

Bash
mkdir -p /var/www/example.com

Configurare il nome di dominio in /etc/hosts

Bash
# 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

Bash
sudo a2ensite example.com
sudo apache2ctl configtest
sudo systemctl reload apache2

1.5.5. Direttive di sicurezza globali

Creare, se non esiste, il file /etc/apache2/conf-available/security.conf:

ApacheConf
# - 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
Bash
sudo a2enconf security
sudo apache2ctl configtest
sudo systemctl reload apache2

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:

Bash
sudo a2enmod remoteip

Creare o modificare il file /etc/apache2/conf-available/remoteip.conf:

ApacheConf
RemoteIPHeader       CF-Connecting-IP
RemoteIPTrustedProxy 127.0.0.1

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

ApacheConf
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:

ApacheConf
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:

Bash
sudo a2enconf remoteip
sudo apache2ctl configtest
sudo systemctl restart apache2

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:

Bash
curl https://api.cloudflare.com/client/v4/ips


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

Bash
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

1.7.2. Jail per WordPress

File: /etc/fail2ban/jail.d/wordpress.conf

INI
[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

INI
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
ignoreregex =

File: /etc/fail2ban/filter.d/wordpress-xmlrpc.conf

INI
[Definition]
failregex = ^<HOST> .* "POST /xmlrpc\.php
ignoreregex =

Riavviare Fail2Ban e verificare lo stato della jail:

Bash
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-auth

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

Text Only
(ip.geoip.country eq "RU" or ip.geoip.country eq "CN")
Blocca completamente il traffico da questi paesi.

Regola 2 – Protezione wp-login.php e wp-admin solo dall'Italia
Azione: Block

Text Only
(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")
- Consente l'accesso a 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

Text Only
(http.request.uri.path contains "xmlrpc.php")
Blocca qualsiasi richiesta a 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.php risponda 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:

Bash
sudo mysql_secure_installation

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):

Text Only
All done!

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:

SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY 'nuova_password_sicura';
FLUSH PRIVILEGES;

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):

Bash
openssl rand -base64 24

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:

Bash
sudo mysql -u root -p

Eseguire i seguenti comandi:

SQL
-- 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.

SQL
-- 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:

SQL
SELECT User, Host FROM mysql.user WHERE User = 'root';

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.

Bash
# 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.

Bash
# 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.

Bash
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

Bash
# 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

Bash
#!/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:

Bash
sudo chmod 750 /usr/local/bin/wp-maintenance.sh
sudo chown root:root /usr/local/bin/wp-maintenance.sh

2.3.3. Predisposizione directory

Bash
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:

Bash
sudo crontab -e

ed inserire:

Bash
# 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

Text Only
/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.

Bash
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

Fonte: WordPress.org — Hardening — Securing wp-config.php

2.6.2. Configurazioni consigliate

PHP
<?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):

INI
; 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:

Bash
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:

Bash
sudo crontab -u www-data -e

2.9. Cron di sistema per WP-Cron

Bash
# (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

Bash
# 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
<?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:

Bash
# Cerca file PHP fuori dai percorsi attesi
find /var/www/example.com -name "*.php" -newer /var/www/example.com/wp-settings.php
# Cerca file con permessi di esecuzione inattesi
find /var/www/example.com/wp-content/uploads -type f -perm /111
Investigare qualsiasi file sospetto prima di procedere con l'inizializzazione.

Bash
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:

Bash
sudo crontab -e
Bash
# 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
Bash
sudo mkdir -p /var/log/aide
sudo chmod 750 /var/log/aide

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:

Bash
sudo -u www-data wp plugin install wordfence --activate --path=/var/www/example.com

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:

Bash
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:

  1. Andare su Wordfence → Login Security → 2FA.
  2. Cliccare su "Activate" accanto al proprio profilo.
  3. Scansionare il codice QR con un'app TOTP (Google Authenticator, FreeOTP, Authy, ecc.).
  4. Inserire il codice OTP generato dall'app per confermare l'attivazione.
  5. 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:

  1. Navigare su Wordfence → Firewall → Manage WAF.
  2. Impostare Protection Level su Wordfence Full Protection.
  3. 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:

  1. Verificare il funzionamento del sito: navigare nel frontend e nel backend per assicurarsi che non ci siano falsi positivi.
  2. Testare il login con 2FA: disconnettersi e riconnettersi, verificando che il codice OTP venga richiesto e funzioni.
  3. Monitorare i log: controllare Wordfence → Tools → Live Traffic per vedere le richieste in tempo reale.
  4. Verificare i lockout: provare a inserire credenziali errate per verificare che il blocco brute force funzioni.