2025-08-08 20:15:53 +03:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# WOO v12 — WordPress Operations Orchestrator (Interactive, One-Command)
|
|
|
|
|
# Target: Ubuntu 22.04 / 24.04 LTS
|
2025-08-05 23:15:48 +03:00
|
|
|
|
#
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# - Run once to set up the server. After that, just type `woo` for the menu.
|
|
|
|
|
# - Always installs the latest stable PHP from Ondřej’s PPA (tries 8.3, falls back to 8.2).
|
|
|
|
|
# - Per-site isolation: separate PHP-FPM pool, DB+user, Nginx vhost, filesystem.
|
|
|
|
|
# - No Multisite, No Staging, No Cloudflare, No remote backups (local only).
|
|
|
|
|
# - XML-RPC kept ON but protected by a global whitelist (for Odoo).
|
|
|
|
|
# - Smart, safe, zero-fail mindset with rollback on errors and clear reports.
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
IFS=$'\n\t'
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# --------------------------- Auto-sudo (OVH-friendly) -----------------------
|
|
|
|
|
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
|
|
|
|
|
exec sudo -E bash "$0" "$@"
|
|
|
|
|
fi
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# --------------------------- Colors & Log -----------------------------------
|
|
|
|
|
c_blue='\033[0;34m'; c_green='\033[0;32m'; c_yellow='\033[1;33m'; c_red='\033[0;31m'; c_nc='\033[0m'
|
2025-08-06 19:21:42 +03:00
|
|
|
|
readonly LOG_DIR="/var/log/woo-toolkit"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
readonly LOG_FILE="${LOG_DIR}/woo-$(date +%Y%m%d_%H%M%S).log"
|
|
|
|
|
mkdir -p "$LOG_DIR"; touch "$LOG_FILE"
|
2025-08-06 19:16:08 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
log() { echo -e "${c_blue}[$(date '+%F %T')]${c_nc} $*" | tee -a "$LOG_FILE"; }
|
|
|
|
|
ok() { echo -e "${c_green}✓${c_nc} $*" | tee -a "$LOG_FILE"; }
|
|
|
|
|
warn() { echo -e "${c_yellow}‼${c_nc} $*" | tee -a "$LOG_FILE"; }
|
|
|
|
|
fail(){ echo -e "${c_red}✗${c_nc} $*" | tee -a "$LOG_FILE"; exit 1; }
|
|
|
|
|
|
|
|
|
|
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
|
|
|
|
|
|
|
|
|
# --------------------------- Global Defaults --------------------------------
|
2025-08-06 17:45:54 +03:00
|
|
|
|
readonly WEBROOT="/var/www"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
readonly CONFIG_DIR="/etc/woo"
|
|
|
|
|
readonly PHP_CANDIDATES=("8.3" "8.2")
|
|
|
|
|
PHP_VERSION=""
|
|
|
|
|
readonly MIN_RAM_MB=2048
|
|
|
|
|
readonly MIN_DISK_MB=10240
|
2025-08-06 19:01:20 +03:00
|
|
|
|
readonly XMLRPC_WHITELIST_FILE="/etc/nginx/conf.d/xmlrpc_whitelist.conf"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
readonly LIMITS_FILE="/etc/nginx/conf.d/limits.conf"
|
|
|
|
|
readonly CACHE_FILE="/etc/nginx/conf.d/fastcgi_cache.conf"
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
declare -A CTX=() # rollback context keys: DOMAIN, SITE_DIR, DB_NAME, DB_USER
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# --------------------------- Helpers ----------------------------------------
|
2025-08-08 20:15:53 +03:00
|
|
|
|
have(){ command -v "$1" &>/dev/null; }
|
|
|
|
|
gen_pass(){ openssl rand -base64 "${1:-24}"; }
|
|
|
|
|
rand_hex(){ openssl rand -hex "${1:-6}"; }
|
|
|
|
|
press_any(){ read -n 1 -s -r -p "Press any key to continue..."; echo; }
|
2025-08-05 23:42:21 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
error_handler(){
|
|
|
|
|
local l="$1" c="$2"
|
|
|
|
|
log "Error on line $l: $c"
|
|
|
|
|
rollback_safe
|
|
|
|
|
fail "Aborted. See log: $LOG_FILE"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rollback_safe(){
|
|
|
|
|
if [[ -n "${CTX[DOMAIN]:-}" ]]; then
|
|
|
|
|
rm -f "/etc/nginx/sites-available/${CTX[DOMAIN]}" "/etc/nginx/sites-enabled/${CTX[DOMAIN]}" || true
|
|
|
|
|
rm -f "/etc/php/${PHP_VERSION}/fpm/pool.d/${CTX[DOMAIN]}.conf" || true
|
|
|
|
|
systemctl reload "php${PHP_VERSION}-fpm" nginx || true
|
|
|
|
|
fi
|
|
|
|
|
[[ -n "${CTX[DB_NAME]:-}" ]] && mysql --defaults-file=/root/.my.cnf -e "DROP DATABASE IF EXISTS \`${CTX[DB_NAME]}\`;" || true
|
|
|
|
|
[[ -n "${CTX[DB_USER]:-}" ]] && mysql --defaults-file=/root/.my.cnf -e "DROP USER IF EXISTS '${CTX[DB_USER]}'@'localhost';" || true
|
|
|
|
|
[[ -n "${CTX[SITE_DIR]:-}" ]] && rm -rf "${CTX[SITE_DIR]}" || true
|
|
|
|
|
}
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
# --------------------------- Preflight & Detection --------------------------
|
|
|
|
|
preflight(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Preflight checks…"
|
|
|
|
|
[[ -f /etc/os-release ]] || fail "Unsupported OS."
|
2025-08-08 20:15:53 +03:00
|
|
|
|
. /etc/os-release
|
2025-08-08 20:33:36 +03:00
|
|
|
|
[[ "${NAME}" =~ Ubuntu ]] || fail "Ubuntu required."
|
|
|
|
|
if [[ "${VERSION_ID}" != "22.04" && "${VERSION_ID}" != "24.04" ]]; then
|
|
|
|
|
warn "Tested on 22.04/24.04. You are ${VERSION_ID}."
|
|
|
|
|
fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local free_mb; free_mb=$(df -Pm / | awk 'NR==2 {print $4}')
|
2025-08-08 20:33:36 +03:00
|
|
|
|
(( free_mb >= MIN_DISK_MB )) || fail "Low disk: ${free_mb}MB (< ${MIN_DISK_MB})."
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ok "Preflight OK."
|
|
|
|
|
}
|
2025-08-08 19:30:16 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ensure_swap(){
|
|
|
|
|
local ram_mb; ram_mb=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
|
|
|
|
|
if (( ram_mb < MIN_RAM_MB )); then
|
2025-08-08 20:33:36 +03:00
|
|
|
|
warn "RAM ${ram_mb}MB < ${MIN_RAM_MB}MB. Creating 2G swap for stability…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
if ! swapon --show | grep -q /swapfile; then
|
|
|
|
|
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
|
|
|
|
|
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
|
|
|
|
|
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
|
|
|
|
ok "Swap enabled."
|
2025-08-08 19:30:16 +03:00
|
|
|
|
fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
fi
|
2025-08-05 21:25:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
select_php_version(){
|
|
|
|
|
for v in "${PHP_CANDIDATES[@]}"; do
|
2025-08-08 20:33:36 +03:00
|
|
|
|
if apt-cache policy "php${v}-fpm" | grep -q Candidate; then PHP_VERSION="$v"; break; fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
done
|
2025-08-08 20:33:36 +03:00
|
|
|
|
[[ -n "$PHP_VERSION" ]] || PHP_VERSION="8.2"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ok "Using PHP ${PHP_VERSION}."
|
2025-08-06 19:01:20 +03:00
|
|
|
|
}
|
2025-08-06 17:45:54 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# --------------------------- Stack Install ----------------------------------
|
|
|
|
|
install_stack(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Installing & updating packages…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
apt-get update -y
|
2025-08-08 20:33:36 +03:00
|
|
|
|
apt-get install -y software-properties-common curl wget unzip git rsync psmisc jq dnsutils ca-certificates gnupg lsb-release
|
2025-08-08 20:15:53 +03:00
|
|
|
|
add-apt-repository -y ppa:ondrej/php
|
|
|
|
|
add-apt-repository -y ppa:ondrej/nginx
|
|
|
|
|
apt-get update -y
|
2025-08-05 21:25:52 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
select_php_version
|
|
|
|
|
|
|
|
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
|
|
|
nginx mariadb-server redis-server fail2ban ufw \
|
|
|
|
|
certbot python3-certbot-nginx unattended-upgrades haveged \
|
|
|
|
|
php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-curl php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml \
|
|
|
|
|
php${PHP_VERSION}-zip php${PHP_VERSION}-gd php${PHP_VERSION}-opcache php${PHP_VERSION}-redis php${PHP_VERSION}-imagick php${PHP_VERSION}-bcmath
|
|
|
|
|
|
|
|
|
|
if ! have wp; then
|
|
|
|
|
curl -fsSL -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
|
|
|
|
chmod +x /usr/local/bin/wp
|
|
|
|
|
fi
|
|
|
|
|
ok "Stack installed."
|
|
|
|
|
}
|
2025-08-06 19:27:38 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
secure_mariadb(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Securing MariaDB…"
|
|
|
|
|
if [[ -f /root/.my.cnf ]]; then warn "MariaDB already secured. Skipping."; return; fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local pass; pass="$(gen_pass 32)"
|
|
|
|
|
mysql -u root <<SQL
|
|
|
|
|
ALTER USER 'root'@'localhost' IDENTIFIED BY '${pass}';
|
2025-08-06 20:49:10 +03:00
|
|
|
|
DELETE FROM mysql.user WHERE User='';
|
2025-08-08 20:15:53 +03:00
|
|
|
|
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1');
|
2025-08-06 20:49:10 +03:00
|
|
|
|
DROP DATABASE IF EXISTS test;
|
2025-08-06 19:32:21 +03:00
|
|
|
|
FLUSH PRIVILEGES;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
SQL
|
|
|
|
|
cat >/root/.my.cnf <<EOF
|
|
|
|
|
[client]
|
|
|
|
|
user=root
|
|
|
|
|
password=${pass}
|
|
|
|
|
EOF
|
|
|
|
|
chmod 600 /root/.my.cnf
|
|
|
|
|
ok "MariaDB secured."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tune_mariadb(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Tuning MariaDB…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local ram_kb ib_k ib
|
|
|
|
|
ram_kb=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
|
2025-08-08 20:33:36 +03:00
|
|
|
|
ib_k=$(( ram_kb / 4 )); if (( ib_k > 4194304 )); then ib="4096M"; else ib="${ib_k}K"; fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >/etc/mysql/mariadb.conf.d/99-woo-tuned.cnf <<EOF
|
|
|
|
|
[mysqld]
|
|
|
|
|
innodb_buffer_pool_size=${ib}
|
|
|
|
|
innodb_log_file_size=256M
|
|
|
|
|
innodb_file_per_table=1
|
|
|
|
|
max_allowed_packet=256M
|
|
|
|
|
tmp_table_size=128M
|
|
|
|
|
max_heap_table_size=128M
|
|
|
|
|
innodb_flush_log_at_trx_commit=1
|
|
|
|
|
EOF
|
|
|
|
|
systemctl restart mariadb
|
|
|
|
|
ok "MariaDB tuned."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
harden_php(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Hardening PHP ${PHP_VERSION}…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
mkdir -p "/etc/php/${PHP_VERSION}/fpm/conf.d"
|
|
|
|
|
cat >"/etc/php/${PHP_VERSION}/fpm/conf.d/99-woo-opcache.ini" <<EOF
|
|
|
|
|
opcache.enable=1
|
|
|
|
|
opcache.enable_cli=1
|
|
|
|
|
opcache.memory_consumption=256
|
|
|
|
|
opcache.interned_strings_buffer=16
|
|
|
|
|
opcache.max_accelerated_files=100000
|
|
|
|
|
opcache.validate_timestamps=0
|
|
|
|
|
opcache.jit=1255
|
|
|
|
|
opcache.jit_buffer_size=64M
|
|
|
|
|
realpath_cache_size=4096k
|
|
|
|
|
realpath_cache_ttl=600
|
2025-08-06 19:32:21 +03:00
|
|
|
|
EOF
|
2025-08-08 20:33:36 +03:00
|
|
|
|
sed -i 's/^expose_php = On/expose_php = Off/' "/etc/php/${PHP_VERSION}/fpm/php.ini" || true
|
|
|
|
|
sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' "/etc/php/${PHP_VERSION}/fpm/php.ini" || true
|
2025-08-08 20:15:53 +03:00
|
|
|
|
systemctl restart "php${PHP_VERSION}-fpm"
|
|
|
|
|
ok "PHP hardened."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
harden_imagick(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Applying safe ImageMagick policy…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local pol="/etc/ImageMagick-6/policy.xml"; [[ -f "$pol" ]] || pol="/etc/ImageMagick/policy.xml"
|
|
|
|
|
if [[ -f "$pol" ]]; then
|
2025-08-08 20:33:36 +03:00
|
|
|
|
cp "$pol" "${pol}.bak.$(date +%s)" || true
|
2025-08-08 20:15:53 +03:00
|
|
|
|
if ! grep -q 'policy domain="resource" name="memory"' "$pol"; then
|
|
|
|
|
cat >>"$pol" <<'EOF'
|
|
|
|
|
<policymap>
|
|
|
|
|
<policy domain="resource" name="memory" value="512MiB"/>
|
|
|
|
|
<policy domain="resource" name="map" value="1GiB"/>
|
|
|
|
|
<policy domain="resource" name="width" value="8000"/>
|
|
|
|
|
<policy domain="resource" name="height" value="8000"/>
|
|
|
|
|
<policy domain="coder" rights="none" pattern="PDF"/>
|
|
|
|
|
<policy domain="coder" rights="none" pattern="PS"/>
|
|
|
|
|
</policymap>
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2025-08-05 21:25:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
harden_firewall_fail2ban(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "UFW + Fail2Ban…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ufw default deny incoming || true
|
|
|
|
|
ufw default allow outgoing || true
|
|
|
|
|
ufw allow OpenSSH || true
|
|
|
|
|
ufw allow 'Nginx Full' || true
|
|
|
|
|
echo "y" | ufw enable || true
|
|
|
|
|
|
|
|
|
|
cat >/etc/fail2ban/jail.d/wordpress.conf <<EOF
|
2025-08-06 19:01:20 +03:00
|
|
|
|
[sshd]
|
2025-08-06 17:45:54 +03:00
|
|
|
|
enabled = true
|
2025-08-06 19:01:20 +03:00
|
|
|
|
maxretry = 3
|
|
|
|
|
bantime = 1d
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
[nginx-wp-login]
|
2025-08-06 19:01:20 +03:00
|
|
|
|
enabled = true
|
2025-08-08 20:15:53 +03:00
|
|
|
|
filter = nginx-wp-login
|
2025-08-06 17:45:54 +03:00
|
|
|
|
logpath = /var/log/nginx/*access.log
|
2025-08-08 20:15:53 +03:00
|
|
|
|
maxretry = 10
|
|
|
|
|
findtime = 600
|
|
|
|
|
bantime = 1d
|
|
|
|
|
|
|
|
|
|
[nginx-xmlrpc]
|
|
|
|
|
enabled = true
|
|
|
|
|
filter = nginx-xmlrpc
|
|
|
|
|
logpath = /var/log/nginx/*access.log
|
|
|
|
|
maxretry = 20
|
|
|
|
|
findtime = 600
|
|
|
|
|
bantime = 1d
|
2025-08-06 19:01:20 +03:00
|
|
|
|
EOF
|
2025-08-08 20:33:36 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >/etc/fail2ban/filter.d/nginx-wp-login.conf <<'EOF'
|
2025-08-06 19:16:08 +03:00
|
|
|
|
[Definition]
|
2025-08-08 20:15:53 +03:00
|
|
|
|
failregex = <HOST> - .* "(POST|GET) /wp-login.php
|
2025-08-06 19:01:20 +03:00
|
|
|
|
ignoreregex =
|
2025-08-05 23:15:48 +03:00
|
|
|
|
EOF
|
2025-08-08 20:33:36 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >/etc/fail2ban/filter.d/nginx-xmlrpc.conf <<'EOF'
|
|
|
|
|
[Definition]
|
|
|
|
|
failregex = <HOST> - .* "POST /xmlrpc.php
|
|
|
|
|
ignoreregex =
|
|
|
|
|
EOF
|
2025-08-08 20:33:36 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
systemctl restart fail2ban || true
|
2025-08-06 19:01:20 +03:00
|
|
|
|
}
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
nginx_global(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Writing global Nginx configs (limits, cache, xmlrpc whitelist, default)…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >"$LIMITS_FILE" <<'EOF'
|
|
|
|
|
limit_req_zone $binary_remote_addr zone=logins:10m rate=5r/m;
|
|
|
|
|
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=10r/m;
|
|
|
|
|
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
2025-08-06 19:01:20 +03:00
|
|
|
|
EOF
|
2025-08-06 21:07:08 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >"$CACHE_FILE" <<'EOF'
|
2025-08-08 19:30:16 +03:00
|
|
|
|
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
|
|
|
|
|
fastcgi_cache_key "$scheme$request_method$host$request_uri";
|
|
|
|
|
fastcgi_cache_use_stale error timeout invalid_header http_500;
|
|
|
|
|
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
|
|
|
|
|
EOF
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >"$XMLRPC_WHITELIST_FILE" <<'EOF'
|
|
|
|
|
# Global XML-RPC whitelist for all sites (0 = blocked by default)
|
|
|
|
|
geo $xmlrpc_allowed {
|
|
|
|
|
default 0;
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# Add IPs via the WOO menu -> XML-RPC whitelist
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# Example: 203.0.113.10 1;
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
cat >/etc/nginx/sites-available/default <<'EOF'
|
2025-08-06 21:07:08 +03:00
|
|
|
|
server {
|
|
|
|
|
listen 80 default_server;
|
|
|
|
|
listen [::]:80 default_server;
|
|
|
|
|
server_name _;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
return 444;
|
2025-08-06 21:07:08 +03:00
|
|
|
|
}
|
|
|
|
|
EOF
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ln -sf /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
|
|
|
|
|
nginx -t && systemctl reload nginx
|
2025-08-05 21:25:52 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
systemd_logrotate(){
|
|
|
|
|
log "Systemd & logrotate…"
|
|
|
|
|
mkdir -p /etc/systemd/system/nginx.service.d
|
|
|
|
|
cat >/etc/systemd/system/nginx.service.d/override.conf <<'EOF'
|
|
|
|
|
[Service]
|
|
|
|
|
Restart=always
|
|
|
|
|
RestartSec=2
|
|
|
|
|
LimitNOFILE=100000
|
2025-08-05 23:15:48 +03:00
|
|
|
|
EOF
|
2025-08-08 20:15:53 +03:00
|
|
|
|
systemctl daemon-reload
|
|
|
|
|
systemctl restart nginx
|
|
|
|
|
|
|
|
|
|
cat >/etc/logrotate.d/woo-nginx <<'EOF'
|
|
|
|
|
/var/log/nginx/*.log {
|
|
|
|
|
daily
|
|
|
|
|
rotate 14
|
|
|
|
|
compress
|
|
|
|
|
missingok
|
|
|
|
|
notifempty
|
|
|
|
|
create 0640 www-data adm
|
|
|
|
|
sharedscripts
|
|
|
|
|
postrotate
|
|
|
|
|
[ -s /run/nginx.pid ] && kill -USR1 `cat /run/nginx.pid`
|
|
|
|
|
endscript
|
2025-08-06 17:45:54 +03:00
|
|
|
|
}
|
2025-08-08 20:15:53 +03:00
|
|
|
|
EOF
|
2025-08-06 17:45:54 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >/etc/logrotate.d/woo-phpfpm <<'EOF'
|
|
|
|
|
/var/log/php-fpm/*.log {
|
|
|
|
|
daily
|
|
|
|
|
rotate 14
|
|
|
|
|
compress
|
|
|
|
|
missingok
|
|
|
|
|
notifempty
|
|
|
|
|
create 0640 www-data adm
|
2025-08-06 19:01:20 +03:00
|
|
|
|
}
|
2025-08-08 20:15:53 +03:00
|
|
|
|
EOF
|
2025-08-06 17:45:54 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# --------------------------- Per-Site Functions -----------------------------
|
|
|
|
|
php_pool_create(){
|
|
|
|
|
local domain="$1"
|
|
|
|
|
cat >"/etc/php/${PHP_VERSION}/fpm/pool.d/${domain}.conf" <<EOF
|
2025-08-06 19:01:20 +03:00
|
|
|
|
[${domain}]
|
|
|
|
|
user = www-data
|
|
|
|
|
group = www-data
|
|
|
|
|
listen = /run/php/php${PHP_VERSION}-${domain}.sock
|
|
|
|
|
listen.owner = www-data
|
|
|
|
|
listen.group = www-data
|
|
|
|
|
pm = ondemand
|
|
|
|
|
pm.max_children = 50
|
|
|
|
|
pm.process_idle_timeout = 10s
|
|
|
|
|
pm.max_requests = 500
|
|
|
|
|
slowlog = /var/log/php-fpm/${domain}-slow.log
|
|
|
|
|
php_admin_value[error_log] = /var/log/php-fpm/${domain}-error.log
|
|
|
|
|
php_admin_flag[log_errors] = on
|
|
|
|
|
php_value[session.save_handler] = redis
|
|
|
|
|
php_value[session.save_path] = "tcp://127.0.0.1:6379"
|
|
|
|
|
EOF
|
2025-08-08 20:15:53 +03:00
|
|
|
|
mkdir -p /var/log/php-fpm
|
|
|
|
|
touch "/var/log/php-fpm/${domain}-error.log" "/var/log/php-fpm/${domain}-slow.log"
|
|
|
|
|
chown -R www-data:www-data /var/log/php-fpm
|
|
|
|
|
systemctl restart "php${PHP_VERSION}-fpm"
|
2025-08-06 17:45:54 +03:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
nginx_site_write(){
|
2025-08-08 20:33:36 +03:00
|
|
|
|
local domain="$1" docroot="$2" cache_on="$3"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local cache_block=""
|
|
|
|
|
if [[ "$cache_on" == "on" ]]; then
|
|
|
|
|
cache_block=$(cat <<'EOS'
|
2025-08-06 19:01:20 +03:00
|
|
|
|
set $skip_cache 0;
|
2025-08-06 19:21:42 +03:00
|
|
|
|
if ($request_method = POST) { set $skip_cache 1; }
|
|
|
|
|
if ($query_string != "") { set $skip_cache 1; }
|
|
|
|
|
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") { set $skip_cache 1; }
|
|
|
|
|
if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass") { set $skip_cache 1; }
|
2025-08-06 19:01:20 +03:00
|
|
|
|
fastcgi_cache WORDPRESS;
|
2025-08-06 17:45:54 +03:00
|
|
|
|
fastcgi_cache_valid 200 60m;
|
2025-08-06 19:01:20 +03:00
|
|
|
|
fastcgi_cache_bypass $skip_cache;
|
|
|
|
|
fastcgi_no_cache $skip_cache;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
EOS
|
2025-08-06 17:45:54 +03:00
|
|
|
|
)
|
2025-08-08 20:15:53 +03:00
|
|
|
|
fi
|
2025-08-06 17:45:54 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
cat >"/etc/nginx/sites-available/${domain}" <<EOF
|
2025-08-06 16:48:57 +03:00
|
|
|
|
server {
|
2025-08-08 19:30:16 +03:00
|
|
|
|
listen 443 ssl http2;
|
2025-08-06 19:01:20 +03:00
|
|
|
|
server_name ${domain} www.${domain};
|
2025-08-08 20:15:53 +03:00
|
|
|
|
root ${docroot};
|
2025-08-06 19:01:20 +03:00
|
|
|
|
index index.php;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
|
|
|
|
|
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
|
|
|
|
|
ssl_protocols TLSv1.3;
|
|
|
|
|
ssl_prefer_server_ciphers on;
|
|
|
|
|
|
2025-08-06 19:01:20 +03:00
|
|
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
|
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
2025-08-06 19:01:20 +03:00
|
|
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
include ${LIMITS_FILE};
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
${cache_block}
|
2025-08-06 16:48:57 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
location / { try_files \$uri \$uri/ /index.php\$is_args\$args; }
|
|
|
|
|
|
|
|
|
|
location = /wp-login.php {
|
|
|
|
|
limit_req zone=logins burst=10 nodelay;
|
2025-08-06 16:48:57 +03:00
|
|
|
|
include snippets/fastcgi-php.conf;
|
|
|
|
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-06 19:01:20 +03:00
|
|
|
|
location = /xmlrpc.php {
|
2025-08-06 19:21:42 +03:00
|
|
|
|
if (\$xmlrpc_allowed = 0) { return 403; }
|
2025-08-08 20:15:53 +03:00
|
|
|
|
limit_req zone=xmlrpc burst=20 nodelay;
|
|
|
|
|
include snippets/fastcgi-php.conf;
|
|
|
|
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
location ~ \.php\$ {
|
2025-08-06 19:01:20 +03:00
|
|
|
|
include snippets/fastcgi-php.conf;
|
|
|
|
|
fastcgi_pass unix:/run/php/php${PHP_VERSION}-${domain}.sock;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
limit_conn perip 20;
|
2025-08-06 16:48:57 +03:00
|
|
|
|
}
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
|
|
|
|
location ~* /(?:uploads|files)/.*\.php\$ { deny all; }
|
|
|
|
|
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)\$ { deny all; }
|
|
|
|
|
location ~ /\. { deny all; }
|
|
|
|
|
location = /readme.html { deny all; }
|
|
|
|
|
location = /license.txt { deny all; }
|
|
|
|
|
}
|
|
|
|
|
server {
|
|
|
|
|
listen 80;
|
|
|
|
|
server_name ${domain} www.${domain};
|
|
|
|
|
return 301 https://\$host\$request_uri;
|
2025-08-06 16:48:57 +03:00
|
|
|
|
}
|
|
|
|
|
EOF
|
2025-08-08 20:15:53 +03:00
|
|
|
|
ln -sf "/etc/nginx/sites-available/${domain}" "/etc/nginx/sites-enabled/${domain}"
|
|
|
|
|
nginx -t && systemctl reload nginx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
create_wp_site(){
|
|
|
|
|
clear; echo -e "${c_green}--- Add New WordPress Site ---${c_nc}\n"
|
|
|
|
|
read -rp "Domain (example.com): " domain
|
2025-08-08 20:33:36 +03:00
|
|
|
|
[[ -n "$domain" ]] || { warn "Domain is required."; press_any; return; }
|
2025-08-08 20:15:53 +03:00
|
|
|
|
read -rp "Admin email: " admin_email
|
2025-08-08 20:33:36 +03:00
|
|
|
|
[[ -n "$admin_email" ]] || { warn "Admin email is required."; press_any; return; }
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
local admin_user=""
|
|
|
|
|
while true; do
|
|
|
|
|
read -rp "Admin username (avoid 'admin'): " admin_user
|
|
|
|
|
[[ -z "$admin_user" ]] && { warn "Username required."; continue; }
|
|
|
|
|
[[ "$admin_user" == "admin" ]] && { warn "Please choose something other than 'admin'."; continue; }
|
|
|
|
|
break
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
local admin_pass; admin_pass="$(gen_pass 16)"
|
|
|
|
|
local site_dir="${WEBROOT}/${domain}"
|
2025-08-08 20:33:36 +03:00
|
|
|
|
[[ -d "$site_dir" ]] && { warn "Directory already exists: ${site_dir}"; press_any; return; }
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
CTX[DOMAIN]="$domain"; CTX[SITE_DIR]="$site_dir"
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
mkdir -p "${site_dir}"; chown -R www-data:www-data "${site_dir}"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
|
|
|
|
local db_name="wp_$(echo "$domain" | tr '.' '_' | cut -c1-20)_$(rand_hex 4)"
|
|
|
|
|
local db_user="usr_$(rand_hex 6)"
|
|
|
|
|
local db_pass; db_pass="$(gen_pass 24)"
|
|
|
|
|
CTX[DB_NAME]="$db_name"; CTX[DB_USER]="$db_user"
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Creating database & user…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
mysql --defaults-file=/root/.my.cnf <<SQL
|
|
|
|
|
CREATE DATABASE \`${db_name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
|
|
|
CREATE USER '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';
|
|
|
|
|
GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';
|
|
|
|
|
FLUSH PRIVILEGES;
|
|
|
|
|
SQL
|
2025-08-06 19:47:34 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Downloading WordPress core…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
sudo -u www-data WP_CLI_CACHE_DIR='/tmp/wp-cli-cache' wp core download --path="${site_dir}" --locale=en_US
|
2025-08-05 21:25:52 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Creating wp-config.php…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local table_prefix="wp_$(rand_hex 3)_"
|
|
|
|
|
sudo -u www-data wp config create --path="${site_dir}" --dbname="${db_name}" --dbuser="${db_user}" --dbpass="${db_pass}" --dbprefix="${table_prefix}" --extra-php <<PHP
|
|
|
|
|
define('WP_CACHE', true);
|
|
|
|
|
define('WP_REDIS_HOST', '127.0.0.1');
|
|
|
|
|
define('WP_REDIS_PORT', 6379);
|
|
|
|
|
define('FS_METHOD', 'direct');
|
|
|
|
|
define('FORCE_SSL_ADMIN', true);
|
|
|
|
|
define('DISALLOW_FILE_EDIT', true);
|
|
|
|
|
define('WP_AUTO_UPDATE_CORE', 'minor');
|
|
|
|
|
define('WP_DEBUG', false);
|
|
|
|
|
define('WP_MEMORY_LIMIT', '128M');
|
|
|
|
|
define('WP_MAX_MEMORY_LIMIT', '256M');
|
|
|
|
|
define('AUTOSAVE_INTERVAL', 120);
|
|
|
|
|
define('WP_POST_REVISIONS', 10);
|
|
|
|
|
PHP
|
|
|
|
|
sudo -u www-data wp config set WP_CACHE_KEY_SALT "'${domain}'" --path="${site_dir}" --raw
|
|
|
|
|
sudo -u www-data wp config set DISABLE_WP_CRON true --path="${site_dir}" --raw
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Installing WordPress (single site)…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
sudo -u www-data wp core install --path="${site_dir}" --url="https://${domain}" --title="${domain}" --admin_user="${admin_user}" --admin_password="${admin_pass}" --admin_email="${admin_email}"
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Enabling Redis object cache…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
sudo -u www-data wp plugin install redis-cache --activate --path="${site_dir}"
|
|
|
|
|
sudo -u www-data wp redis enable --path="${site_dir}"
|
|
|
|
|
|
|
|
|
|
mkdir -p "${site_dir}/wp-content/mu-plugins"
|
|
|
|
|
cat >"${site_dir}/wp-content/mu-plugins/woo-hardening.php" <<'PHP'
|
|
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* MU-hardening for WOO
|
|
|
|
|
*/
|
|
|
|
|
add_filter('admin_init', function(){ remove_action('wp_head', 'wp_generator'); });
|
|
|
|
|
add_filter('rest_endpoints', function($endpoints){ unset($endpoints['/wp/v2/users']); return $endpoints; });
|
|
|
|
|
if (!defined('DISALLOW_FILE_EDIT')) define('DISALLOW_FILE_EDIT', true);
|
|
|
|
|
if (!defined('WP_POST_REVISIONS')) define('WP_POST_REVISIONS', 10);
|
|
|
|
|
if (!defined('AUTOSAVE_INTERVAL')) define('AUTOSAVE_INTERVAL', 120);
|
|
|
|
|
PHP
|
|
|
|
|
chown -R www-data:www-data "${site_dir}"
|
|
|
|
|
|
|
|
|
|
php_pool_create "$domain"
|
2025-08-08 20:33:36 +03:00
|
|
|
|
nginx_site_write "$domain" "$site_dir" "on"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Requesting Let's Encrypt SSL…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
if certbot --nginx --hsts --redirect --staple-ocsp -d "$domain" -d "www.${domain}" -m "$admin_email" --agree-tos --non-interactive; then
|
|
|
|
|
ok "SSL issued."
|
|
|
|
|
else
|
2025-08-08 20:33:36 +03:00
|
|
|
|
warn "SSL issuance failed (DNS/ports?). Retry later with: certbot --nginx -d $domain -d www.$domain"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
(crontab -l 2>/dev/null | grep -v "wp cron event run --due-now --path=${site_dir}" ; \
|
|
|
|
|
echo "* * * * * sudo -u www-data WP_CLI_CACHE_DIR=/tmp/wp-cli-cache wp cron event run --due-now --path=${site_dir} >/dev/null 2>&1") | crontab -
|
|
|
|
|
|
|
|
|
|
find "${site_dir}" -type d -exec chmod 755 {} \;
|
|
|
|
|
find "${site_dir}" -type f -exec chmod 644 {} \;
|
|
|
|
|
chmod 600 "${site_dir}/wp-config.php"
|
|
|
|
|
|
|
|
|
|
mkdir -p /root/woo_credentials
|
|
|
|
|
local cred="/root/woo_credentials/${domain}.txt"
|
|
|
|
|
cat >"$cred" <<EOF
|
|
|
|
|
========================================
|
|
|
|
|
WordPress Credentials for: ${domain}
|
|
|
|
|
========================================
|
2025-08-05 23:42:21 +03:00
|
|
|
|
Admin URL: https://${domain}/wp-admin
|
2025-08-08 20:15:53 +03:00
|
|
|
|
Admin User: ${admin_user}
|
|
|
|
|
Admin Pass: ${admin_pass}
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
Database: ${db_name}
|
|
|
|
|
DB User: ${db_user}
|
|
|
|
|
DB Pass: ${db_pass}
|
|
|
|
|
========================================
|
2025-08-05 23:42:21 +03:00
|
|
|
|
EOF
|
2025-08-08 20:15:53 +03:00
|
|
|
|
chmod 600 "$cred"
|
|
|
|
|
|
|
|
|
|
clear
|
|
|
|
|
echo -e "${c_green}✔ Site installed successfully!${c_nc}"
|
|
|
|
|
echo
|
|
|
|
|
echo "Installation Report:"
|
|
|
|
|
echo " - Domain: https://${domain}"
|
|
|
|
|
echo " - Admin URL: https://${domain}/wp-admin"
|
|
|
|
|
echo " - Admin User: ${admin_user}"
|
|
|
|
|
echo " - Admin Pass: ${admin_pass}"
|
|
|
|
|
echo " - DB Name: ${db_name}"
|
|
|
|
|
echo " - DB User: ${db_user}"
|
|
|
|
|
echo " - SSL: $( [[ -f /etc/letsencrypt/live/${domain}/fullchain.pem ]] && echo Issued || echo Pending/Failed )"
|
|
|
|
|
echo " - Redis: Enabled"
|
|
|
|
|
echo " - Cache: FastCGI (ON)"
|
|
|
|
|
echo " - Cron: System cron enabled"
|
|
|
|
|
echo
|
|
|
|
|
echo "Credentials saved at: ${cred}"
|
|
|
|
|
press_any
|
2025-08-08 20:33:36 +03:00
|
|
|
|
CTX=()
|
2025-08-08 20:15:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove_wp_site(){
|
|
|
|
|
clear; echo -e "${c_red}--- Remove WordPress Site ---${c_nc}\n"
|
|
|
|
|
local domain
|
|
|
|
|
read -rp "Domain to remove (example.com): " domain
|
|
|
|
|
[[ -n "$domain" ]] || { warn "No domain provided."; press_any; return; }
|
|
|
|
|
|
|
|
|
|
local site_dir="${WEBROOT}/${domain}"
|
|
|
|
|
if [[ ! -d "$site_dir" ]]; then
|
|
|
|
|
warn "No site found at ${site_dir}."
|
|
|
|
|
press_any; return
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo -e "${c_yellow}This will permanently delete files, DB, user, and Nginx/PHP configs for ${domain}.${c_nc}"
|
|
|
|
|
read -rp "Type the domain again to confirm: " confirm_domain
|
|
|
|
|
[[ "$confirm_domain" == "$domain" ]] || { warn "Confirmation mismatch. Aborting."; press_any; return; }
|
|
|
|
|
|
|
|
|
|
local db_name=""; local db_user=""
|
|
|
|
|
if [[ -f "${site_dir}/wp-config.php" ]]; then
|
|
|
|
|
db_name=$(grep "DB_NAME" "${site_dir}/wp-config.php" | cut -d \' -f 4 || true)
|
|
|
|
|
db_user=$(grep "DB_USER" "${site_dir}/wp-config.php" | cut -d \' -f 4 || true)
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
rm -f "/etc/nginx/sites-available/${domain}" "/etc/nginx/sites-enabled/${domain}"
|
|
|
|
|
rm -f "/etc/php/${PHP_VERSION}/fpm/pool.d/${domain}.conf"
|
|
|
|
|
systemctl reload nginx "php${PHP_VERSION}-fpm" || true
|
|
|
|
|
|
|
|
|
|
if [[ -n "$db_name" ]]; then
|
|
|
|
|
mysql --defaults-file=/root/.my.cnf -e "DROP DATABASE IF EXISTS \`${db_name}\`;"
|
|
|
|
|
mysql --defaults-file=/root/.my.cnf -e "DROP USER IF EXISTS '${db_user}'@'localhost';"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
rm -rf "${site_dir}"
|
|
|
|
|
|
|
|
|
|
ok "Site ${domain} removed."
|
|
|
|
|
press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list_sites(){
|
|
|
|
|
clear; echo -e "${c_blue}--- Managed Sites ---${c_nc}\n"
|
2025-08-08 20:33:36 +03:00
|
|
|
|
if ls -1 "${WEBROOT}" | grep -v '^html$' 2>/dev/null; then :; else echo "No sites found."; fi
|
2025-08-08 20:15:53 +03:00
|
|
|
|
echo; press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manage_cache(){
|
|
|
|
|
clear; echo -e "${c_blue}--- Manage Cache ---${c_nc}\n"
|
|
|
|
|
read -rp "Domain: " domain
|
|
|
|
|
local conf="/etc/nginx/sites-available/${domain}"
|
|
|
|
|
[[ -f "$conf" ]] || { warn "Vhost not found."; press_any; return; }
|
|
|
|
|
|
|
|
|
|
if grep -q "fastcgi_cache WORDPRESS" "$conf"; then
|
|
|
|
|
echo "Cache is currently: ON"
|
|
|
|
|
read -rp "Turn OFF cache? (y/N): " a
|
|
|
|
|
if [[ "$a" =~ ^[Yy]$ ]]; then
|
2025-08-08 20:33:36 +03:00
|
|
|
|
nginx_site_write "$domain" "${WEBROOT}/${domain}" "off"
|
|
|
|
|
ok "Cache disabled."
|
2025-08-08 20:15:53 +03:00
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
echo "Cache is currently: OFF"
|
|
|
|
|
read -rp "Turn ON cache? (y/N): " a
|
|
|
|
|
if [[ "$a" =~ ^[Yy]$ ]]; then
|
2025-08-08 20:33:36 +03:00
|
|
|
|
nginx_site_write "$domain" "${WEBROOT}/${domain}" "on"
|
|
|
|
|
ok "Cache enabled."
|
2025-08-08 20:15:53 +03:00
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xmlrpc_whitelist_menu(){
|
|
|
|
|
clear; echo -e "${c_blue}--- XML-RPC Whitelist (Global) ---${c_nc}\n"
|
|
|
|
|
echo "Current entries:"
|
|
|
|
|
grep -E "^\s*([0-9]{1,3}\.){3}[0-9]{1,3}" "$XMLRPC_WHITELIST_FILE" | awk '{print " - " $1}' || echo " - None"
|
|
|
|
|
echo
|
|
|
|
|
echo "1) Add IP to whitelist (e.g., your Odoo server)"
|
|
|
|
|
echo "2) Remove IP from whitelist"
|
|
|
|
|
echo "3) Back"
|
|
|
|
|
read -rp "Choice: " c
|
|
|
|
|
case "$c" in
|
|
|
|
|
1)
|
|
|
|
|
read -rp "Enter IP to ALLOW: " ip
|
|
|
|
|
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
|
|
|
if grep -q "$ip" "$XMLRPC_WHITELIST_FILE"; then
|
|
|
|
|
warn "IP already present."
|
|
|
|
|
else
|
2025-08-08 20:33:36 +03:00
|
|
|
|
sed -i "/^geo /a\ \ \ \ ${ip} 1;" "$XMLRPC_WHITELIST_FILE"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
nginx -t && systemctl reload nginx
|
|
|
|
|
ok "Added ${ip}."
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
warn "Invalid IP format."
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
2)
|
|
|
|
|
read -rp "Enter IP to REMOVE: " ip
|
|
|
|
|
if grep -q "$ip" "$XMLRPC_WHITELIST_FILE"; then
|
2025-08-08 20:33:36 +03:00
|
|
|
|
sed -i "/${ip}/d" "$XMLRPC_WHITELIST_FILE"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
nginx -t && systemctl reload nginx
|
|
|
|
|
ok "Removed ${ip}."
|
|
|
|
|
else
|
|
|
|
|
warn "IP not found."
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
*);;
|
|
|
|
|
esac
|
|
|
|
|
press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backups_menu(){
|
|
|
|
|
clear; echo -e "${c_blue}--- Backups (Local Only) ---${c_nc}\n"
|
|
|
|
|
echo "1) Backup a single site now"
|
|
|
|
|
echo "2) Backup ALL sites now"
|
|
|
|
|
echo "3) Schedule daily backups at 02:00"
|
|
|
|
|
echo "4) Disable scheduled backups"
|
|
|
|
|
echo "5) Back"
|
|
|
|
|
read -rp "Choice: " c
|
|
|
|
|
case "$c" in
|
2025-08-08 20:33:36 +03:00
|
|
|
|
1) read -rp "Domain: " domain; backup_site "$domain" ;;
|
|
|
|
|
2) backup_all_sites ;;
|
|
|
|
|
3) (crontab -l 2>/dev/null | grep -v "woo.sh backup-all"; echo "0 2 * * * /usr/bin/env bash /root/woo.sh backup-all >> /var/log/woo-toolkit/backup.log 2>&1") | crontab - ; ok "Daily backups scheduled."; press_any ;;
|
|
|
|
|
4) (crontab -l 2>/dev/null | grep -v "woo.sh backup-all") | crontab - ; ok "Scheduled backups disabled."; press_any ;;
|
|
|
|
|
*) ;;
|
2025-08-08 20:15:53 +03:00
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backup_site(){
|
|
|
|
|
local domain="$1"
|
|
|
|
|
local dir="${WEBROOT}/${domain}"
|
|
|
|
|
[[ -d "$dir" ]] || { warn "No site found at ${dir}."; press_any; return; }
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
log "Backing up ${domain}…"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
local out="/root/woo_backups/${domain}"; mkdir -p "$out"
|
|
|
|
|
local ts="$(date +%Y%m%d_%H%M%S)"
|
|
|
|
|
local archive="${out}/full_${ts}.tar.gz"
|
|
|
|
|
local db="$(sudo -u www-data wp config get DB_NAME --path="$dir" --quiet || echo)"
|
|
|
|
|
local tmpdb="/tmp/db_${domain}_${ts}.sql"
|
|
|
|
|
if [[ -n "$db" ]]; then
|
|
|
|
|
sudo -u www-data wp db export "$tmpdb" --path="$dir" || true
|
|
|
|
|
tar -czf "$archive" -C "$dir" . -C /tmp "$(basename "$tmpdb")"
|
|
|
|
|
rm -f "$tmpdb"
|
|
|
|
|
else
|
|
|
|
|
tar -czf "$archive" -C "$dir" .
|
|
|
|
|
fi
|
|
|
|
|
ls -tp "${out}"/full_*.tar.gz 2>/dev/null | tail -n +8 | xargs -r -d $'\n' rm -f --
|
|
|
|
|
ok "Backup stored at: ${archive}"
|
|
|
|
|
press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backup_all_sites(){
|
|
|
|
|
log "--- Backing up all sites ---"
|
|
|
|
|
for s in $(ls -1 "${WEBROOT}" 2>/dev/null | grep -v '^html$' || true); do
|
|
|
|
|
backup_site "$s"
|
|
|
|
|
done
|
|
|
|
|
ok "All backups complete."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
doctor_report(){
|
|
|
|
|
clear; echo -e "${c_blue}--- System Doctor Report ---${c_nc}\n"
|
|
|
|
|
echo "- OS: $(. /etc/os-release; echo "$PRETTY_NAME")"
|
|
|
|
|
echo "- Nginx: $(nginx -v 2>&1)"
|
|
|
|
|
echo "- PHP-FPM: $(php-fpm${PHP_VERSION} -v 2>/dev/null | head -n1 || echo php${PHP_VERSION})"
|
|
|
|
|
echo "- MariaDB: $(mysql --version)"
|
|
|
|
|
echo "- Redis: $(redis-server --version 2>/dev/null | head -n1 || echo 'installed')"
|
|
|
|
|
echo "- RAM total: $(awk '/MemTotal/ {print int($2/1024) "MB"}' /proc/meminfo)"
|
|
|
|
|
echo "- Free disk (/): $(df -h / | awk 'NR==2{print $4 " free"}')"
|
|
|
|
|
echo "- Nginx config test:"; nginx -t || true
|
|
|
|
|
echo "- SSL certs expiring in <14 days:"
|
|
|
|
|
for d in /etc/letsencrypt/live/*; do
|
|
|
|
|
[[ -d "$d" ]] || continue
|
|
|
|
|
local crt="$d/fullchain.pem"; [[ -f "$crt" ]] || continue
|
|
|
|
|
local exp_ts=$(date -d "$(openssl x509 -enddate -noout -in "$crt" | cut -d= -f2)" +%s)
|
|
|
|
|
local now=$(date +%s); local diff=$(( (exp_ts-now)/86400 ))
|
|
|
|
|
(( diff < 14 )) && echo " - $(basename "$d"): ${diff}d"
|
|
|
|
|
done
|
|
|
|
|
echo; press_any
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# --------------------------- First-run Setup --------------------------------
|
2025-08-08 20:15:53 +03:00
|
|
|
|
first_run_setup(){
|
|
|
|
|
preflight
|
|
|
|
|
ensure_swap
|
|
|
|
|
install_stack
|
|
|
|
|
secure_mariadb
|
|
|
|
|
tune_mariadb
|
|
|
|
|
harden_php
|
|
|
|
|
harden_imagick
|
|
|
|
|
harden_firewall_fail2ban
|
|
|
|
|
nginx_global
|
|
|
|
|
systemd_logrotate
|
|
|
|
|
|
2025-08-08 20:33:36 +03:00
|
|
|
|
# Create the 'woo' convenience command
|
|
|
|
|
cat >/usr/local/bin/woo <<'EOF'
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
exec sudo -E bash /root/woo.sh "$@"
|
|
|
|
|
EOF
|
|
|
|
|
chmod +x /usr/local/bin/woo
|
|
|
|
|
|
|
|
|
|
ok "Base server setup complete. Next time, just type: woo"
|
2025-08-08 20:15:53 +03:00
|
|
|
|
press_any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --------------------------- Menu -------------------------------------------
|
|
|
|
|
main_menu(){
|
|
|
|
|
while true; do
|
|
|
|
|
clear
|
|
|
|
|
echo -e "${c_blue}=====================================================${c_nc}"
|
|
|
|
|
echo -e "${c_blue} WOO — WordPress Operations Orchestrator (v12) ${c_nc}"
|
|
|
|
|
echo -e "${c_blue}=====================================================${c_nc}\n"
|
|
|
|
|
echo " 1) Add New Site"
|
|
|
|
|
echo " 2) Remove Site"
|
|
|
|
|
echo " 3) List Sites"
|
|
|
|
|
echo " 4) Manage Cache"
|
|
|
|
|
echo " 5) XML-RPC Whitelist"
|
|
|
|
|
echo " 6) Backups"
|
|
|
|
|
echo " 7) Doctor (Report)"
|
|
|
|
|
echo " 8) Exit"
|
|
|
|
|
echo
|
|
|
|
|
read -rp "Choose an option [1-8]: " ans
|
|
|
|
|
case "$ans" in
|
|
|
|
|
1) create_wp_site ;;
|
|
|
|
|
2) remove_wp_site ;;
|
|
|
|
|
3) list_sites ;;
|
|
|
|
|
4) manage_cache ;;
|
|
|
|
|
5) xmlrpc_whitelist_menu ;;
|
|
|
|
|
6) backups_menu ;;
|
|
|
|
|
7) doctor_report ;;
|
|
|
|
|
8) clear; exit 0 ;;
|
|
|
|
|
*) warn "Invalid option." ; press_any ;;
|
2025-08-08 20:33:36 +03:00
|
|
|
|
case_esac
|
2025-08-08 20:15:53 +03:00
|
|
|
|
done
|
2025-08-06 20:00:38 +03:00
|
|
|
|
}
|
2025-08-06 19:01:20 +03:00
|
|
|
|
|
2025-08-08 20:15:53 +03:00
|
|
|
|
# --------------------------- Entry Point ------------------------------------
|
2025-08-08 20:33:36 +03:00
|
|
|
|
main(){
|
|
|
|
|
# Normalize line endings if needed (harmless if already fine)
|
|
|
|
|
command -v dos2unix >/dev/null 2>&1 || { apt-get update -y && apt-get install -y dos2unix; }
|
|
|
|
|
dos2unix "$0" >/dev/null 2>&1 || true
|
|
|
|
|
perl -i -pe 's/\r$//' "$0" || true
|
|
|
|
|
|
|
|
|
|
# Detect PHP version after packages (or default for paths)
|
|
|
|
|
if [[ -z "$PHP_VERSION" ]]; then
|
|
|
|
|
for v in "${PHP_CANDIDATES[@]}"; do
|
|
|
|
|
[[ -d "/etc/php/${v}" ]] && PHP_VERSION="$v" && break
|
|
|
|
|
done
|
|
|
|
|
[[ -z "$PHP_VERSION" ]] && PHP_VERSION="8.2"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# One-time base setup
|
|
|
|
|
if [[ ! -f /root/.woo-first-run-complete ]]; then
|
|
|
|
|
first_run_setup
|
|
|
|
|
touch /root/.woo-first-run-complete
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Always show the menu
|
|
|
|
|
main_menu
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main "$@"
|