Vai al contenuto

WordPress Hardening — LXC / Proxmox

Indice


1. Ambiente Macchina

Container LXC, Ubuntu 24.04 LTS.

1.1. Installazione ambiente LAMP

Bash
sudo apt update

# Apache
# Nota: con Cloudflare Tunnel (§1.2) il traffico transita via tunnel locale.
# Non aprire porte Apache su UFW — il firewall mantiene default deny incoming (§1.3).
# I moduli Apache (ssl, headers, rewrite, remoteip) vengono abilitati in §1.4.
sudo apt install apache2 -y
sudo systemctl restart apache2

# MySQL
# Nota: mysql_secure_installation viene eseguito in §2.8 con le istruzioni complete
# per la configurazione dell'utente WordPress con privilegi minimi.
sudo apt install mysql-server -y

# PHP — rilevare la versione installata e usarla come prefisso per i moduli aggiuntivi
sudo apt install php libapache2-mod-php php-mysql -y
PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
sudo apt install \
    php${PHP_VERSION}-curl \
    php${PHP_VERSION}-xml \
    php${PHP_VERSION}-imagick \
    php${PHP_VERSION}-mbstring \
    php${PHP_VERSION}-zip \
    php${PHP_VERSION}-intl \
    php${PHP_VERSION}-gd \
    -y
# Nota: DOMDocument è incluso in php-xml, già installato sopra.

# Certbot
sudo apt install certbot python3-certbot-apache -y

1.2. Cloudflare Tunnel

La macchina comunica con internet tramite un tunnel Cloudflare, con certificato SSL attivato tramite Let's Encrypt. Su Cloudflare la verifica del certificato è disabilitata (No TLS Verify), in modo da accettare qualsiasi certificato presentato dalla macchina, anche se scaduto o auto-firmato. In questo modo, anche se il certificato Let's Encrypt scade o non viene rinnovato, il tunnel continuerà a funzionare senza interruzioni.

[!WARNING] No TLS Verify è una soluzione temporanea. Per ambienti di produzione è fortemente raccomandato utilizzare un Origin Certificate Cloudflare e abilitare la verifica TLS verso l'origine. L'opzione No TLS Verify va considerata una soluzione di compatibilità, non la configurazione finale.


1.3. Firewall

Container LXC con firewall UFW attivo: blocco di tutte le porte in entrata, consenso di tutte le porte in uscita, blocco del traffico instradato.

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

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


1.4. Apache

1.4.1. Moduli necessari

Bash
sudo a2enmod ssl headers rewrite remoteip
sudo systemctl restart apache2

1.4.2. File di configurazione VirtualHost

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
    DocumentRoot /var/www/example.com

    # SSL/TLS
    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # Se si usa un Origin Certificate Cloudflare, sostituire con:
    # SSLCertificateFile    /etc/cloudflare/cert.pem
    # SSLCertificateKeyFile /etc/cloudflare/key.pem

    # Protocolli e cipher suite
    # - Disabilita tutti i protocolli obsoleti, consente solo TLS 1.2 e 1.3.
    # - Le cipher suite seguono il profilo "Intermediate" di Mozilla SSL Config Generator
    #   (https://ssl-config.mozilla.org/) e le raccomandazioni NIST SP 800-52 Rev. 2.
    # - SSLHonorCipherOrder off: in TLS 1.3 la selezione della cipher è gestita dal client;
    #   su TLS 1.2 lascia al server la precedenza, ma con TLS 1.3 disabilitarlo è preferibile.
    SSLProtocol             -all +TLSv1.2 +TLSv1.3
    SSLCipherSuite          ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:\
                            ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:\
                            ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\
                            DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder     off
    SSLCompression          Off
    SSLSessionTickets       Off

    # Directory principale WordPress
    # - AllowOverride All: consente ai file .htaccess di sovrascrivere la configurazione Apache.
    # - Options -Indexes: disabilita il directory listing. +FollowSymLinks: consente symlink.
    # - Require all granted: accesso HTTP consentito (regole specifiche sotto la restringono).
    <Directory /var/www/example.com>
        AllowOverride All
        Options -Indexes +FollowSymLinks
        Require all granted
    </Directory>

    # Blocca esecuzione PHP nella directory uploads
    <Directory /var/www/example.com/wp-content/uploads>
        <FilesMatch "\.php$">
            Require all denied
        </FilesMatch>
    </Directory>

    # Protegge file sensibili, metadata e file informativi
    <FilesMatch "^(\.htaccess|\.htpasswd|\.git|\.svn|composer\.(json|lock)|package(-lock)?\.json|yarn\.lock|phpunit\.xml|readme\.html|license\.txt)$">
        Require all denied
    </FilesMatch>

    # Impedisce l'accesso diretto a wp-config.php
    # Nota: il file dovrebbe risiedere fuori dalla webroot (vedi §2.6).
    # Questa regola è un secondo livello di protezione.
    <Files wp-config.php>
        Require all denied
    </Files>

    # Blocca XML-RPC se non utilizzato
    <Files xmlrpc.php>
        Require all denied
    </Files>

    # Restrizione accesso wp-login.php e /wp-admin/ per IP
    # - Sostituire 1.2.3.4 con il proprio IP statico o il range della VPN (es. Tailscale 100.64.0.0/10).
    # - Questa è la difesa più efficace contro brute-force e credential stuffing
    #   (fonte: WordPress.org Hardening Guide — https://developer.wordpress.org/advanced-administration/security/hardening/#admin-url-access).
    <Files wp-login.php>
        Require ip 1.2.3.4
        Require ip 100.64.0.0/10
    </Files>

    <LocationMatch "^/wp-admin/">
        Require ip 1.2.3.4
        Require ip 100.64.0.0/10
    </LocationMatch>

    # Security Headers
    # - X-Content-Type-Options: impedisce MIME sniffing su tipi di contenuto non dichiarati correttamente.
    # - X-Frame-Options: impedisce il framing del sito da domini esterni (clickjacking).
    # - Referrer-Policy: limita le informazioni del referrer verso altri origin.
    # - Permissions-Policy: disabilita API browser sensibili non necessarie al sito.
    # - X-Permitted-Cross-Domain-Policies: blocca policy cross-domain legacy (Adobe/Flash).
    # - Strict-Transport-Security: forza HTTPS per 1 anno su dominio e sottodomini; preload
    #   iscrive il dominio nella lista HSTS preload dei browser (https://hstspreload.org/),
    #   proteggendo anche il primo accesso da attacchi SSLStrip.
    # - Content-Security-Policy: principale difesa contro XSS e data injection (OWASP).
    #   unsafe-inline è necessario per WordPress base; rimuoverlo richiede nonce support
    #   tramite plugin dedicati.
    Header always set X-Content-Type-Options          "nosniff"
    Header always set X-Frame-Options                 "SAMEORIGIN"
    Header always set Referrer-Policy                 "strict-origin-when-cross-origin"
    Header always set Permissions-Policy              "geolocation=(), microphone=(), camera=()"
    Header always set X-Permitted-Cross-Domain-Policies "none"
    Header always set Strict-Transport-Security       "max-age=31536000; includeSubDomains; preload"
    Header always set Content-Security-Policy         "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"

    # Log del VirtualHost
    ErrorLog  ${APACHE_LOG_DIR}/example-error.log
    CustomLog ${APACHE_LOG_DIR}/example-access.log combined
</VirtualHost>

1.4.3. Abilitare il sito e ricaricare Apache

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

1.4.4. 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.5. Configurazione IP reale client (Cloudflare + Apache)

Quando si utilizza un tunnel Cloudflare, Apache vede l'IP del proxy e non quello reale del client. Questo compromette logging, rate limiting e Fail2Ban.

Per ripristinare l'IP reale del client, abilitare il modulo remoteip:

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:

ApacheConf
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
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.6. Fail2Ban

Fail2Ban blocca a livello di rete (iptables/nftables) gli IP che generano troppi errori di autenticazione, complementando il rate limiting applicativo di LLAR.

1.6.1. Installazione

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

1.6.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.6.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 =
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.5), altrimenti Fail2Ban banna l'IP del proxy Cloudflare invece di quello del client reale.


1.7. Cloudflare WAF

Il piano Free di Cloudflare include un WAF con ruleset gestiti. Va abilitato dalla Dashboard Cloudflare come prima linea di difesa, prima che il traffico raggiunga Apache.

Abilitare dalla Dashboard Cloudflare:

  1. Security → WAF → Managed Rules → Cloudflare Free Managed Ruleset — abilita
  2. Security → WAF → Managed Rules → Cloudflare OWASP Core Ruleset — abilita (sensitivity: Low per iniziare, poi aumentare)
  3. Security → Bot Fight Mode — abilita

[!TIP] Configurare le Rate Limiting Rules di Cloudflare per POST /wp-login.php come ulteriore layer prima che le richieste raggiungano Fail2Ban e LLAR.


1.8. Backup

La protezione avviene su due livelli:

  • Backup infrastrutturale: backup giornaliero della VM tramite Proxmox Backup Server.
  • Backup applicativo: dump SQL cifrato generato automaticamente dallo script di manutenzione prima degli aggiornamenti (vedi §2.3).

2. Ambiente WordPress


2.1. Permessi cartelle e file

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 {} \;

# 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 /etc/wp-backup.key in un luogo sicuro e separato dal server (es. password manager). Senza la chiave, i backup cifrati non sono recuperabili.

2.3.2. Script di manutenzione

File: /usr/local/bin/wp-maintenance.sh

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

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.

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.9)
define( 'DISABLE_WP_CRON', true );

/* That's all, stop editing! Happy publishing. */
# [...]

2.7. Hardening PHP

La configurazione di default di PHP espone la versione, permette funzioni pericolose e non limita inclusioni remote. Modificare /etc/php/8.x/apache2/php.ini (sostituire 8.x con la versione installata):

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. Hardening MySQL

2.8.1. Eseguire la procedura di hardening guidata

Bash
sudo mysql_secure_installation

Rispondere Yes a tutte le domande: rimozione utenti anonimi, disabilitazione accesso root remoto, rimozione database di test.

2.8.2. Utente database con privilegi minimi

WordPress richiede solo i permessi necessari alle proprie operazioni. Un utente GRANT ALL è una superficie d'attacco non necessaria.

SQL
-- Connettersi come root
sudo mysql -u root -p

-- Creare utente con privilegi minimi
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'password_sicura';

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
    ON wordpress_db.* TO 'wp_user'@'localhost';

FLUSH PRIVILEGES;

-- Verificare che root non sia accessibile da remoto
SELECT User, Host FROM mysql.user WHERE User = 'root';

Fonte: WordPress.org — Hardening — Database Security + CIS MySQL Benchmark


2.9. Cron per WP-Cron

Con DISABLE_WP_CRON true in wp-config.php, WP-Cron va gestito tramite cron di sistema:

Bash
sudo crontab -u www-data -e

2.10. 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.11. 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.12. Hardening enumeration

MU Plugin per bloccare l'enumerazione degli utenti, che può essere sfruttata per identificare nomi utente validi per brute-force e altri attacchi.

File: /var/www/example.com/wp-content/mu-plugins/deeplab-hardening-enumeration.php

PHP
<?php
/**
 * Deep Lab - Hardening enumerazione utenti
 *
 * Mitiga:
 * - /?author=1
 * - archivi autore pubblici (se non necessari)
 * - endpoint REST /wp/v2/users per utenti anonimi
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Blocca l'enumerazione via query string ?author=ID.
 */
add_action( 'template_redirect', function () {
    if ( is_admin() ) {
        return;
    }

    if ( isset( $_GET['author'] ) && is_numeric( $_GET['author'] ) ) {
        wp_safe_redirect( home_url( '/' ), 301 );
        exit;
    }
}, 0 );

/**
 * Disabilita gli archivi autore.
 * Attivare solo se il sito NON usa pagine autore pubbliche.
 */
add_action( 'template_redirect', function () {
    if ( is_admin() ) {
        return;
    }

    if ( is_author() ) {
        global $wp_query;
        $wp_query->set_404();
        status_header( 404 );
        nocache_headers();
        exit;
    }
}, 1 );

/**
 * Rimuove gli endpoint REST utenti per i visitatori anonimi.
 * Non disabilita tutta la REST API.
 */
add_filter( 'rest_endpoints', function ( $endpoints ) {
    if ( is_user_logged_in() ) {
        return $endpoints;
    }

    unset( $endpoints['/wp/v2/users'] );
    unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );

    return $endpoints;
} );

2.13. File Integrity Monitoring (AIDE)

AIDE monitora l'integrità del filesystem e rileva modifiche non autorizzate ai file WordPress. Fondamentale per individuare compromissioni post-exploit.

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

[!NOTE] Dopo ogni aggiornamento legittimo (plugin, core, temi) aggiornare il database AIDE:

Bash
sudo aideinit --force
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

Fonte: CIS Controls v8 — Control 10 + SANS Institute — File Integrity Monitoring


2.14. Plugins


2.15. Limit Login Attempts Reloaded

Installazione:

Bash
sudo -u www-data wp plugin install limit-login-attempts-reloaded --activate \
    --path=/var/www/example.com

2.15.1. Rate Limit

Parametro Valore
Tentativi consentiti 2
Durata blocco iniziale 40 minuti
Dopo 2 lockout Blocco esteso a 8 ore
Reset conteggio tentativi dopo 300 ore
Safe list IP pubblici autorizzati
Origine IP affidabile REMOTE_ADDR

[!NOTE] L'origine IP affidabile deve essere coerente con la configurazione Apache/Cloudflare. Con mod_remoteip correttamente configurato (§1.5), REMOTE_ADDR riflette l'IP reale del client. Verificare sul singolo ambiente prima di andare in produzione.

2.15.2. Autenticazione a due fattori (2FA)

2FA abilitato per il ruolo Amministratore.