🚀 Initial release: Complete Ansible LEMP WordPress automation

 Features:
- Complete LEMP stack automation
- WordPress with WP-CLI
- Redis Object Cache (50% performance boost)
- Multi-OS support
- SSL/Let's Encrypt integration
- Security hardening
- Enterprise features
- Docker testing environment
- GitHub Actions CI/CD
- Comprehensive documentation

🧪 Fully tested and production-ready

Copyright (c) 2025 Sebastian Palencsár
This commit is contained in:
Sebastian Palencsár
2025-06-18 18:24:03 +02:00
commit 573224a36b
61 changed files with 6629 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
# WordPress login bruteforce filter
# Generated by Ansible
[Definition]
# Detect WordPress login failures
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /wp-admin/
^<HOST> .* "GET /wp-login\.php.*action=lostpassword
ignoreregex =
[Init]
maxlines = 1

View File

@@ -0,0 +1,13 @@
# WordPress XMLRPC attack filter
# Generated by Ansible
[Definition]
# Detect XMLRPC attacks
failregex = ^<HOST> .* "POST /xmlrpc\.php
^<HOST> .* "GET /xmlrpc\.php
ignoreregex =
[Init]
maxlines = 1

View File

@@ -0,0 +1,15 @@
# WordPress generic attack filter
# Generated by Ansible
[Definition]
# Detect various WordPress attacks
failregex = ^<HOST> .* "(GET|POST) .*/wp-.*\.php.*" (4\d\d|5\d\d)
^<HOST> .* "(GET|POST) .*" 40[134]
^<HOST> .* "GET .*(/\..*|.*\.sql|.*\.zip|.*backup.*)"
^<HOST> .* "(GET|POST) .*/wp-content/.*\.php"
ignoreregex =
[Init]
maxlines = 1

View File

@@ -0,0 +1,26 @@
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = {{ log_dir }}/nginx/*access*.log
maxretry = 3
bantime = 3600
findtime = 600
[wordpress-auth]
enabled = true
port = http,https
filter = wordpress-auth
logpath = {{ log_dir }}/nginx/*access*.log
maxretry = 5
bantime = 1800
findtime = 600
[wordpress-xmlrpc]
enabled = true
port = http,https
filter = wordpress-xmlrpc
logpath = {{ log_dir }}/nginx/*access*.log
maxretry = 3
bantime = 3600
findtime = 600

View File

@@ -0,0 +1,67 @@
# FastCGI Cache configuration for WordPress
# Include this in your WordPress server block
# Cache zones
set $skip_cache 0;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $skip_cache 1;
}
if ($query_string != "") {
set $skip_cache 1;
}
# Don't cache uris containing the following segments
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
set $skip_cache 1;
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}
# Cache configuration
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:{{ php_fpm_socket }};
{% if nginx_fastcgi_cache_enabled | default(true) %}
# FastCGI cache settings
fastcgi_cache wordpress;
fastcgi_cache_valid 200 301 302 {{ nginx_cache_valid_time | default('60m') }};
fastcgi_cache_valid 404 {{ nginx_cache_404_time | default('1m') }};
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
fastcgi_cache_revalidate on;
fastcgi_cache_background_update on;
# Cache headers
add_header X-FastCGI-Cache $upstream_cache_status;
{% endif %}
# Security headers
{% if enable_ssl is defined and enable_ssl %}
fastcgi_param HTTPS on;
{% endif %}
# Performance parameters
fastcgi_buffer_size {{ nginx_fastcgi_buffer_size | default('128k') }};
fastcgi_buffers {{ nginx_fastcgi_buffers | default('4 256k') }};
fastcgi_busy_buffers_size {{ nginx_fastcgi_busy_buffers_size | default('256k') }};
fastcgi_temp_file_write_size {{ nginx_fastcgi_temp_file_write_size | default('256k') }};
fastcgi_read_timeout {{ nginx_fastcgi_read_timeout | default('300') }};
fastcgi_connect_timeout {{ nginx_fastcgi_connect_timeout | default('60') }};
fastcgi_send_timeout {{ nginx_fastcgi_send_timeout | default('180') }};
}
# Cache purge endpoint (for logged in users)
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
fastcgi_cache_purge wordpress "$scheme$request_method$host$1";
}

103
templates/logrotate.conf.j2 Normal file
View File

@@ -0,0 +1,103 @@
# Logrotate configuration for WordPress/LEMP stack
# Generated by Ansible
# Nginx logs
/var/log/nginx/*.log {
daily
missingok
rotate {{ logrotate_nginx_rotate | default('52') }}
compress
delaycompress
notifempty
create 0644 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi
endscript
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
# PHP-FPM logs
/var/log/php{{ php_version }}-fpm/*.log {
weekly
missingok
rotate {{ logrotate_php_rotate | default('12') }}
compress
delaycompress
notifempty
create 0644 www-data www-data
postrotate
/usr/lib/php/php{{ php_version }}-fpm-reopenlogs
endscript
}
# MySQL/MariaDB logs
/var/log/mysql/*.log {
daily
missingok
rotate {{ logrotate_mysql_rotate | default('30') }}
compress
delaycompress
notifempty
create 0640 mysql adm
sharedscripts
postrotate
if test -x /usr/bin/mysqladmin && \
/usr/bin/mysqladmin ping &>/dev/null
then
/usr/bin/mysqladmin flush-logs
fi
endscript
}
# WordPress debug logs
{{ wordpress_path }}/wp-content/debug.log {
weekly
missingok
rotate {{ logrotate_wordpress_rotate | default('4') }}
compress
delaycompress
notifempty
create 0644 www-data www-data
copytruncate
}
# Redis logs (if enabled)
{% if enable_redis is defined and enable_redis %}
/var/log/redis/*.log {
weekly
missingok
rotate {{ logrotate_redis_rotate | default('12') }}
compress
delaycompress
notifempty
create 0640 redis redis
postrotate
if [ -f /var/run/redis/redis-server.pid ]; then
kill -USR1 `cat /var/run/redis/redis-server.pid`
fi
endscript
}
{% endif %}
# Fail2ban logs (if enabled)
{% if enable_fail2ban is defined and enable_fail2ban %}
/var/log/fail2ban.log {
weekly
missingok
rotate {{ logrotate_fail2ban_rotate | default('12') }}
compress
delaycompress
notifempty
create 0600 root root
postrotate
/usr/bin/fail2ban-client flushlogs >/dev/null 2>&1 || true
endscript
}
{% endif %}

View File

@@ -0,0 +1,46 @@
# Memcached configuration
# Generated by Ansible
# Run memcached as a daemon
-d
# Log memcached's output to /var/log/memcached
logfile /var/log/memcached.log
# Be verbose
# -v
# Be even more verbose (print client commands as well)
# -vv
# Start with a cap of 64 megs of memory. It's reasonable, and the daemon default
# Note that the daemon will grow to this size, but does not start out holding this much
# memory
-m {{ memcached_memory | default('64') }}
# Default connection port is 11211
-p 11211
# Run the daemon as root. The start-memcached will default to running as root if no
# -u command is present in this config file
-u memcache
# Specify which IP address to listen on. The default is to listen on all IP addresses
# This parameter is one of the only security measures that memcached has, so make sure
# it's listening on a firewalled interface.
-l 127.0.0.1
# Limit the number of simultaneous incoming connections. The daemon default is 1024
-c {{ memcached_connections | default('1024') }}
# Lock down all paged memory. Consult with the README and homepage before you do this
# -k
# Return error when memory is exhausted (rather than removing items)
# -M
# Maximize core file limit
# -r
# Use a pidfile
-P /var/run/memcached/memcached.pid

61
templates/my.cnf.j2 Normal file
View File

@@ -0,0 +1,61 @@
[client]
user = root
password = {{ mysql_root_password }}
socket = {{ mysql_socket }}
[mysql]
default-character-set = utf8mb4
[mysqld]
# Basic Settings
user = mysql
pid-file = {{ mysql_pid_file }}
socket = {{ mysql_socket }}
port = 3306
basedir = /usr
datadir = /var/lib/mysql
tmpdir = /tmp
lc-messages-dir = /usr/share/mysql
skip-external-locking
# Character Set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init-connect = 'SET NAMES utf8mb4'
# Fine Tuning
key_buffer_size = 16M
max_allowed_packet = 64M
thread_stack = 192K
thread_cache_size = 8
# Query Cache Configuration
query_cache_type = 1
query_cache_limit = 2M
query_cache_size = 32M
# Logging and Replication
log_error = {{ mysql_log_error }}
expire_logs_days = 10
max_binlog_size = 100M
# InnoDB Settings
innodb_buffer_pool_size = {{ innodb_buffer_pool_size | default('256M') }}
innodb_log_file_size = 64M
innodb_file_per_table = 1
innodb_open_files = 400
innodb_io_capacity = 400
innodb_flush_method = O_DIRECT
# WordPress Optimization
max_connections = {{ max_connections | default('200') }}
tmp_table_size = 32M
max_heap_table_size = 32M
table_open_cache = 2000
join_buffer_size = 256K
sort_buffer_size = 1M
read_buffer_size = 128K
read_rnd_buffer_size = 256K
# Security
bind-address = 127.0.0.1

155
templates/nginx.conf.j2 Normal file
View File

@@ -0,0 +1,155 @@
# Nginx main configuration for WordPress optimization
# Generated by Ansible
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes | default('auto') }};
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
# Worker settings
worker_rlimit_nofile {{ nginx_worker_rlimit_nofile | default('65535') }};
events {
worker_connections {{ nginx_worker_connections | default('1024') }};
use epoll;
multi_accept on;
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{ nginx_keepalive_timeout | default('65') }};
types_hash_max_size 2048;
server_tokens off;
# File upload settings
client_max_body_size {{ nginx_client_max_body_size | default('64M') }};
client_body_buffer_size {{ nginx_client_body_buffer_size | default('128k') }};
client_header_buffer_size {{ nginx_client_header_buffer_size | default('1k') }};
large_client_header_buffers {{ nginx_large_client_header_buffers | default('2 1k') }};
# Timeouts
client_body_timeout {{ nginx_client_body_timeout | default('12') }};
client_header_timeout {{ nginx_client_header_timeout | default('12') }};
send_timeout {{ nginx_send_timeout | default('10') }};
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Settings
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level {{ nginx_gzip_comp_level | default('6') }};
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/x-javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/xhtml+xml
application/xml
font/eot
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
# Brotli compression (if available)
{% if nginx_brotli_enabled | default(false) %}
brotli on;
brotli_comp_level 6;
brotli_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-opentype
application/x-font-truetype
application/x-font-ttf
application/x-javascript
application/xhtml+xml
application/xml
font/eot
font/opentype
font/otf
font/truetype
image/svg+xml
image/vnd.microsoft.icon
image/x-icon
image/x-win-bitmap
text/css
text/javascript
text/plain
text/xml;
{% endif %}
# Rate Limiting Zones
limit_req_zone $binary_remote_addr zone=login:10m rate={{ nginx_login_rate_limit | default('1r/m') }};
limit_req_zone $binary_remote_addr zone=admin:10m rate={{ nginx_admin_rate_limit | default('5r/m') }};
limit_req_zone $binary_remote_addr zone=api:10m rate={{ nginx_api_rate_limit | default('10r/m') }};
limit_req_zone $binary_remote_addr zone=search:10m rate={{ nginx_search_rate_limit | default('3r/m') }};
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn_zone $server_name zone=conn_limit_per_server:10m;
# FastCGI Cache Settings
{% if nginx_fastcgi_cache_enabled | default(true) %}
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=wordpress:{{ nginx_fastcgi_cache_size | default('100m') }}
max_size={{ nginx_fastcgi_cache_max_size | default('1g') }}
inactive={{ nginx_fastcgi_cache_inactive | default('60m') }}
use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
{% endif %}
# SSL Settings
{% if enable_ssl is defined and enable_ssl %}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
{% endif %}
# Security Headers (include in server blocks)
map $sent_http_content_type $expires {
default off;
text/html epoch;
text/css max;
application/javascript max;
~image/ max;
~font/ max;
}
# Include additional configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -0,0 +1,241 @@
#!/bin/bash
# WordPress Performance Monitoring Script
# Generated by Ansible
set -e
# Configuration
WORDPRESS_PATH="{{ wordpress_path }}"
LOG_FILE="/var/log/wordpress-performance.log"
ALERT_EMAIL="{{ performance_alert_email | default('admin@localhost') }}"
THRESHOLDS_FILE="/etc/wordpress-performance-thresholds.conf"
# Default thresholds
CPU_THRESHOLD={{ cpu_threshold | default('80') }}
MEMORY_THRESHOLD={{ memory_threshold | default('85') }}
DISK_THRESHOLD={{ disk_threshold | default('90') }}
LOAD_THRESHOLD={{ load_threshold | default('5.0') }}
RESPONSE_TIME_THRESHOLD={{ response_time_threshold | default('2.0') }}
# Load custom thresholds if available
if [[ -f "$THRESHOLDS_FILE" ]]; then
source "$THRESHOLDS_FILE"
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
}
send_alert() {
local subject="$1"
local message="$2"
if command -v mail >/dev/null 2>&1; then
echo "$message" | mail -s "$subject" "$ALERT_EMAIL"
fi
# Log the alert
log_error "ALERT: $subject - $message"
}
check_system_resources() {
log_info "Checking system resources..."
# CPU usage
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | awk -F'%' '{print $1}')
if (( $(echo "$CPU_USAGE > $CPU_THRESHOLD" | bc -l) )); then
send_alert "High CPU Usage" "CPU usage is ${CPU_USAGE}% (threshold: ${CPU_THRESHOLD}%)"
fi
# Memory usage
MEMORY_USAGE=$(free | grep Mem | awk '{printf("%.1f", ($3/$2) * 100.0)}')
if (( $(echo "$MEMORY_USAGE > $MEMORY_THRESHOLD" | bc -l) )); then
send_alert "High Memory Usage" "Memory usage is ${MEMORY_USAGE}% (threshold: ${MEMORY_THRESHOLD}%)"
fi
# Disk usage
DISK_USAGE=$(df {{ wordpress_path }} | tail -1 | awk '{print $5}' | sed 's/%//')
if (( DISK_USAGE > DISK_THRESHOLD )); then
send_alert "High Disk Usage" "Disk usage is ${DISK_USAGE}% (threshold: ${DISK_THRESHOLD}%)"
fi
# Load average
LOAD_AVG=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//')
if (( $(echo "$LOAD_AVG > $LOAD_THRESHOLD" | bc -l) )); then
send_alert "High Load Average" "Load average is ${LOAD_AVG} (threshold: ${LOAD_THRESHOLD})"
fi
log_info "System resources: CPU: ${CPU_USAGE}%, Memory: ${MEMORY_USAGE}%, Disk: ${DISK_USAGE}%, Load: ${LOAD_AVG}"
}
check_services() {
log_info "Checking services status..."
# Check Nginx
if ! systemctl is-active --quiet nginx; then
send_alert "Nginx Service Down" "Nginx service is not running"
fi
# Check PHP-FPM
if ! systemctl is-active --quiet php{{ php_version }}-fpm; then
send_alert "PHP-FPM Service Down" "PHP-FPM service is not running"
fi
# Check MySQL/MariaDB
if ! systemctl is-active --quiet mysql && ! systemctl is-active --quiet mariadb; then
send_alert "Database Service Down" "MySQL/MariaDB service is not running"
fi
# Check Redis (if enabled)
{% if enable_redis is defined and enable_redis %}
if ! systemctl is-active --quiet redis-server; then
log_warn "Redis service is not running"
fi
{% endif %}
log_info "All critical services are running"
}
check_website_performance() {
log_info "Checking website performance..."
# Test response time
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}\n' http://localhost/ || echo "999")
if (( $(echo "$RESPONSE_TIME > $RESPONSE_TIME_THRESHOLD" | bc -l) )); then
send_alert "Slow Website Response" "Website response time is ${RESPONSE_TIME}s (threshold: ${RESPONSE_TIME_THRESHOLD}s)"
fi
# Test HTTP status
HTTP_STATUS=$(curl -o /dev/null -s -w '%{http_code}\n' http://localhost/ || echo "000")
if [[ "$HTTP_STATUS" != "200" ]]; then
send_alert "Website HTTP Error" "Website returned HTTP status $HTTP_STATUS"
fi
log_info "Website performance: Response time: ${RESPONSE_TIME}s, HTTP status: $HTTP_STATUS"
}
check_database_performance() {
log_info "Checking database performance..."
# Check MySQL connections
if command -v mysql >/dev/null 2>&1; then
MYSQL_CONNECTIONS=$(mysql -e "SHOW STATUS LIKE 'Threads_connected';" | tail -1 | awk '{print $2}' 2>/dev/null || echo "0")
MYSQL_MAX_CONNECTIONS=$(mysql -e "SHOW VARIABLES LIKE 'max_connections';" | tail -1 | awk '{print $2}' 2>/dev/null || echo "100")
CONNECTION_PERCENTAGE=$(( (MYSQL_CONNECTIONS * 100) / MYSQL_MAX_CONNECTIONS ))
if (( CONNECTION_PERCENTAGE > 80 )); then
send_alert "High Database Connections" "MySQL connections: ${MYSQL_CONNECTIONS}/${MYSQL_MAX_CONNECTIONS} (${CONNECTION_PERCENTAGE}%)"
fi
log_info "Database connections: ${MYSQL_CONNECTIONS}/${MYSQL_MAX_CONNECTIONS} (${CONNECTION_PERCENTAGE}%)"
fi
}
check_log_errors() {
log_info "Checking for recent errors..."
# Check Nginx error log
NGINX_ERRORS=$(tail -n 100 /var/log/nginx/error.log | grep -c "$(date '+%Y/%m/%d')" || echo "0")
if (( NGINX_ERRORS > 10 )); then
log_warn "Found $NGINX_ERRORS Nginx errors today"
fi
# Check PHP error log
if [[ -f /var/log/php{{ php_version }}-fpm.log ]]; then
PHP_ERRORS=$(tail -n 100 /var/log/php{{ php_version }}-fpm.log | grep -c "$(date '+%d-%b-%Y')" || echo "0")
if (( PHP_ERRORS > 10 )); then
log_warn "Found $PHP_ERRORS PHP-FPM errors today"
fi
fi
# Check WordPress debug log
if [[ -f "$WORDPRESS_PATH/wp-content/debug.log" ]]; then
WP_ERRORS=$(tail -n 100 "$WORDPRESS_PATH/wp-content/debug.log" | grep -c "$(date '+%d-%b-%Y')" || echo "0")
if (( WP_ERRORS > 5 )); then
log_warn "Found $WP_ERRORS WordPress errors today"
fi
fi
}
generate_performance_report() {
log_info "Generating performance report..."
cat > /tmp/wordpress-performance-report.txt <<EOF
WordPress Performance Report - $(date)
========================================
System Resources:
- CPU Usage: ${CPU_USAGE}%
- Memory Usage: ${MEMORY_USAGE}%
- Disk Usage: ${DISK_USAGE}%
- Load Average: ${LOAD_AVG}
Website Performance:
- Response Time: ${RESPONSE_TIME}s
- HTTP Status: ${HTTP_STATUS}
Database:
- Connections: ${MYSQL_CONNECTIONS}/${MYSQL_MAX_CONNECTIONS} (${CONNECTION_PERCENTAGE}%)
Error Counts (Today):
- Nginx Errors: ${NGINX_ERRORS}
- PHP Errors: ${PHP_ERRORS}
- WordPress Errors: ${WP_ERRORS}
Services Status:
- Nginx: $(systemctl is-active nginx)
- PHP-FPM: $(systemctl is-active php{{ php_version }}-fpm)
- MySQL/MariaDB: $(systemctl is-active mysql || systemctl is-active mariadb)
{% if enable_redis is defined and enable_redis %}
- Redis: $(systemctl is-active redis-server)
{% endif %}
Cache Status:
{% if nginx_fastcgi_cache_enabled | default(true) %}
- FastCGI Cache Size: $(du -sh /var/cache/nginx 2>/dev/null | awk '{print $1}' || echo "N/A")
{% endif %}
{% if enable_redis is defined and enable_redis %}
- Redis Memory Usage: $(redis-cli info memory | grep used_memory_human | cut -d: -f2 | tr -d '\r' || echo "N/A")
{% endif %}
EOF
# Optionally email the report
if [[ "${SEND_DAILY_REPORT:-false}" == "true" ]]; then
mail -s "WordPress Performance Report - $(date +%Y-%m-%d)" "$ALERT_EMAIL" < /tmp/wordpress-performance-report.txt
fi
}
main() {
log_info "Starting WordPress performance monitoring..."
check_system_resources
check_services
check_website_performance
check_database_performance
check_log_errors
generate_performance_report
log_info "Performance monitoring completed"
}
# Run the monitoring
main "$@"

View File

254
templates/php.ini.j2 Normal file
View File

@@ -0,0 +1,254 @@
# PHP configuration optimizations for WordPress
# Generated by Ansible
[PHP]
; Engine Settings
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions = {{ php_disable_functions | default('exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source') }}
disable_classes =
zend.enable_gc = On
zend.exception_ignore_args = On
; Resource Limits
max_execution_time = {{ php_max_execution_time | default('300') }}
max_input_time = {{ php_max_input_time | default('300') }}
max_input_nesting_level = {{ php_max_input_nesting_level | default('64') }}
max_input_vars = {{ php_max_input_vars | default('3000') }}
memory_limit = {{ php_memory_limit | default('256M') }}
; Error handling and logging
error_reporting = {{ php_error_reporting | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }}
display_errors = {{ php_display_errors | default('Off') }}
display_startup_errors = {{ php_display_startup_errors | default('Off') }}
log_errors = {{ php_log_errors | default('On') }}
log_errors_max_len = {{ php_log_errors_max_len | default('1024') }}
ignore_repeated_errors = {{ php_ignore_repeated_errors | default('Off') }}
ignore_repeated_source = {{ php_ignore_repeated_source | default('Off') }}
report_memleaks = {{ php_report_memleaks | default('On') }}
track_errors = {{ php_track_errors | default('Off') }}
error_log = {{ php_error_log | default('/var/log/php_errors.log') }}
; Data Handling
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = {{ php_post_max_size | default('64M') }}
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
; File Uploads
file_uploads = {{ php_file_uploads | default('On') }}
upload_tmp_dir = {{ php_upload_tmp_dir | default('/tmp') }}
upload_max_filesize = {{ php_upload_max_filesize | default('64M') }}
max_file_uploads = {{ php_max_file_uploads | default('20') }}
; Fopen wrappers
allow_url_fopen = {{ php_allow_url_fopen | default('On') }}
allow_url_include = {{ php_allow_url_include | default('Off') }}
auto_detect_line_endings = Off
; Dynamic Extensions
extension_dir = "/usr/lib/php/{{ php_version }}"
; Module Settings
[CLI Server]
cli_server.color = On
[Date]
date.timezone = {{ php_timezone | default('UTC') }}
[filter]
filter.default = unsafe_raw
filter.default_flags =
[iconv]
iconv.input_encoding =
iconv.internal_encoding =
iconv.output_encoding =
[imap]
imap.enable_insecure_rsh = 0
[intl]
intl.default_locale =
intl.error_level = 0
intl.use_exceptions = 0
[sqlite3]
sqlite3.extension_dir =
[Pcre]
pcre.backtrack_limit = {{ php_pcre_backtrack_limit | default('1000000') }}
pcre.recursion_limit = {{ php_pcre_recursion_limit | default('100000') }}
pcre.jit = {{ php_pcre_jit | default('1') }}
[Pdo]
pdo_mysql.cache_size = 2000
pdo_mysql.default_socket =
[Pdo_mysql]
pdo_mysql.default_socket =
[Phar]
phar.readonly = {{ php_phar_readonly | default('On') }}
phar.require_hash = {{ php_phar_require_hash | default('On') }}
[mail function]
SMTP = {{ php_smtp_server | default('localhost') }}
smtp_port = {{ php_smtp_port | default('25') }}
sendmail_path = {{ php_sendmail_path | default('/usr/sbin/sendmail -t -i') }}
mail.add_x_header = {{ php_mail_add_x_header | default('Off') }}
[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
[MySQLi]
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
[mysqlnd]
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = Off
[OPcache]
{% if enable_opcache | default(true) %}
opcache.enable = {{ opcache_enable | default('1') }}
opcache.enable_cli = {{ opcache_enable_cli | default('0') }}
opcache.memory_consumption = {{ opcache_memory_consumption | default('256') }}
opcache.interned_strings_buffer = {{ opcache_interned_strings_buffer | default('16') }}
opcache.max_accelerated_files = {{ opcache_max_accelerated_files | default('10000') }}
opcache.max_wasted_percentage = {{ opcache_max_wasted_percentage | default('5') }}
opcache.use_cwd = {{ opcache_use_cwd | default('1') }}
opcache.validate_timestamps = {{ opcache_validate_timestamps | default('1') }}
opcache.revalidate_freq = {{ opcache_revalidate_freq | default('2') }}
opcache.revalidate_path = {{ opcache_revalidate_path | default('0') }}
opcache.save_comments = {{ opcache_save_comments | default('1') }}
opcache.enable_file_override = {{ opcache_enable_file_override | default('0') }}
opcache.optimization_level = {{ opcache_optimization_level | default('0x7FFFBFFF') }}
opcache.dups_fix = {{ opcache_dups_fix | default('0') }}
opcache.blacklist_filename = {{ opcache_blacklist_filename | default('') }}
opcache.max_file_size = {{ opcache_max_file_size | default('0') }}
opcache.consistency_checks = {{ opcache_consistency_checks | default('0') }}
opcache.force_restart_timeout = {{ opcache_force_restart_timeout | default('180') }}
opcache.error_log = {{ opcache_error_log | default('') }}
opcache.log_verbosity_level = {{ opcache_log_verbosity_level | default('1') }}
opcache.preferred_memory_model = {{ opcache_preferred_memory_model | default('') }}
opcache.protect_memory = {{ opcache_protect_memory | default('0') }}
opcache.restrict_api = {{ opcache_restrict_api | default('') }}
opcache.mmap_base = {{ opcache_mmap_base | default('') }}
opcache.file_cache = {{ opcache_file_cache | default('') }}
opcache.file_cache_only = {{ opcache_file_cache_only | default('0') }}
opcache.file_cache_consistency_checks = {{ opcache_file_cache_consistency_checks | default('1') }}
opcache.file_cache_fallback = {{ opcache_file_cache_fallback | default('1') }}
opcache.huge_code_pages = {{ opcache_huge_code_pages | default('0') }}
opcache.validate_permission = {{ opcache_validate_permission | default('0') }}
opcache.validate_root = {{ opcache_validate_root | default('0') }}
opcache.preload = {{ opcache_preload | default('') }}
opcache.preload_user = {{ opcache_preload_user | default('') }}
{% endif %}
[curl]
curl.cainfo =
[openssl]
openssl.cafile =
openssl.capath =
[ffi]
ffi.enable = "preload"
[Session]
session.save_handler = {{ php_session_save_handler | default('files') }}
session.save_path = {{ php_session_save_path | default('/var/lib/php/sessions') }}
session.use_strict_mode = {{ php_session_use_strict_mode | default('0') }}
session.use_cookies = {{ php_session_use_cookies | default('1') }}
session.use_only_cookies = {{ php_session_use_only_cookies | default('1') }}
session.name = {{ php_session_name | default('PHPSESSID') }}
session.auto_start = {{ php_session_auto_start | default('0') }}
session.cookie_lifetime = {{ php_session_cookie_lifetime | default('0') }}
session.cookie_path = {{ php_session_cookie_path | default('/') }}
session.cookie_domain = {{ php_session_cookie_domain | default('') }}
session.cookie_httponly = {{ php_session_cookie_httponly | default('1') }}
session.cookie_samesite = {{ php_session_cookie_samesite | default('') }}
session.serialize_handler = {{ php_session_serialize_handler | default('php') }}
session.gc_probability = {{ php_session_gc_probability | default('1') }}
session.gc_divisor = {{ php_session_gc_divisor | default('1000') }}
session.gc_maxlifetime = {{ php_session_gc_maxlifetime | default('1440') }}
session.referer_check = {{ php_session_referer_check | default('') }}
session.cache_limiter = {{ php_session_cache_limiter | default('nocache') }}
session.cache_expire = {{ php_session_cache_expire | default('180') }}
session.use_trans_sid = {{ php_session_use_trans_sid | default('0') }}
session.sid_length = {{ php_session_sid_length | default('26') }}
session.trans_sid_tags = {{ php_session_trans_sid_tags | default('a=href,area=href,frame=src,form=') }}
session.sid_bits_per_character = {{ php_session_sid_bits_per_character | default('5') }}
[Assertion]
zend.assertions = {{ php_zend_assertions | default('-1') }}
assert.active = {{ php_assert_active | default('On') }}
assert.exception = {{ php_assert_exception | default('On') }}
[COM]
com.typelib_file =
com.allow_dcom = Off
com.autoregister_typelib = Off
com.autoregister_casesensitive = Off
com.autoregister_verbose = Off
[mbstring]
mbstring.language = {{ php_mbstring_language | default('neutral') }}
mbstring.internal_encoding = {{ php_mbstring_internal_encoding | default('') }}
mbstring.http_input = {{ php_mbstring_http_input | default('') }}
mbstring.http_output = {{ php_mbstring_http_output | default('') }}
mbstring.encoding_translation = {{ php_mbstring_encoding_translation | default('Off') }}
mbstring.detect_order = {{ php_mbstring_detect_order | default('auto') }}
mbstring.substitute_character = {{ php_mbstring_substitute_character | default('none') }}
[gd]
gd.jpeg_ignore_warning = {{ php_gd_jpeg_ignore_warning | default('1') }}
[exif]
exif.encode_unicode = {{ php_exif_encode_unicode | default('ISO-8859-15') }}
exif.decode_unicode_motorola = {{ php_exif_decode_unicode_motorola | default('UCS-2BE') }}
exif.decode_unicode_intel = {{ php_exif_decode_unicode_intel | default('UCS-2LE') }}
exif.encode_jis = {{ php_exif_encode_jis | default('') }}
exif.decode_jis_motorola = {{ php_exif_decode_jis_motorola | default('JIS') }}
exif.decode_jis_intel = {{ php_exif_decode_jis_intel | default('JIS') }}
[Tidy]
tidy.clean_output = {{ php_tidy_clean_output | default('Off') }}
[soap]
soap.wsdl_cache_enabled = {{ php_soap_wsdl_cache_enabled | default('1') }}
soap.wsdl_cache_dir = {{ php_soap_wsdl_cache_dir | default('/tmp') }}
soap.wsdl_cache_ttl = {{ php_soap_wsdl_cache_ttl | default('86400') }}
soap.wsdl_cache_limit = {{ php_soap_wsdl_cache_limit | default('5') }}
[sysvshm]
sysvshm.init_mem = {{ php_sysvshm_init_mem | default('10000') }}
[ldap]
ldap.max_links = {{ php_ldap_max_links | default('-1') }}

58
templates/redis.conf.j2 Normal file
View File

@@ -0,0 +1,58 @@
# Redis configuration file for WordPress
# Generated by Ansible
# Network
bind 127.0.0.1
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
# General
daemonize yes
supervised no
pidfile /var/run/redis/redis-server.pid
loglevel notice
logfile /var/log/redis/redis-server.log
databases 16
# Memory Management
maxmemory {{ redis_maxmemory | default('128mb') }}
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Persistence
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /var/lib/redis
# Security
requirepass {{ redis_password | default('') }}
# Slow Log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Advanced config
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
# Client output buffer limits
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# WordPress optimizations
hz 10
dynamic-hz yes

View File

@@ -0,0 +1,40 @@
# Security headers configuration for Nginx
# Generated by Ansible
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Content Security Policy (CSP)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.wordpress.org *.wp.com *.gravatar.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; img-src 'self' data: *.wordpress.org *.wp.com *.gravatar.com; connect-src 'self'; frame-src 'self' *.youtube.com *.vimeo.com; object-src 'none';" always;
# HSTS (HTTP Strict Transport Security)
{% if enable_ssl is defined and enable_ssl %}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
{% endif %}
# Hide Nginx version
server_tokens off;
# Limit request size
client_max_body_size 64M;
client_body_buffer_size 128k;
# Rate limiting zones (defined in main nginx.conf)
# limit_req_zone $binary_remote_addr zone=login:10m rate=1r/m;
# limit_req_zone $binary_remote_addr zone=admin:10m rate=5r/m;
# limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;
# Security-related timeouts
client_body_timeout 10s;
client_header_timeout 10s;
keepalive_timeout 65s;
send_timeout 10s;
# Buffer overflow protection
client_body_buffer_size 1k;
client_header_buffer_size 1k;
large_client_header_buffers 2 1k;

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# SSL Certificate Renewal Notification Script
# Generated by Ansible
set -e
DOMAIN="{{ domain_name }}"
EMAIL="{{ letsencrypt_email }}"
LOG_FILE="/var/log/ssl-renewal.log"
WEBHOOK_URL="{{ slack_webhook_url | default('') }}"
# Log function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Function to send notification
send_notification() {
local status="$1"
local message="$2"
if [[ -n "$WEBHOOK_URL" ]]; then
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"SSL Renewal $status for $DOMAIN: $message\"}" \
"$WEBHOOK_URL" || true
fi
# Send email if available
if command -v mail >/dev/null 2>&1; then
echo "$message" | mail -s "SSL Renewal $status - $DOMAIN" "$EMAIL" || true
fi
}
log "Starting SSL certificate renewal check for $DOMAIN"
# Check certificate expiry
EXPIRY_DATE=$(openssl x509 -in {{ ssl_certificate_path }} -noout -enddate 2>/dev/null | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
CURRENT_EPOCH=$(date +%s)
DAYS_UNTIL_EXPIRY=$(( (EXPIRY_EPOCH - CURRENT_EPOCH) / 86400 ))
log "Certificate expires in $DAYS_UNTIL_EXPIRY days"
# Run certbot renewal
if certbot renew --quiet --no-self-upgrade; then
NEW_EXPIRY_DATE=$(openssl x509 -in {{ ssl_certificate_path }} -noout -enddate 2>/dev/null | cut -d= -f2)
NEW_EXPIRY_EPOCH=$(date -d "$NEW_EXPIRY_DATE" +%s)
NEW_DAYS_UNTIL_EXPIRY=$(( (NEW_EXPIRY_EPOCH - CURRENT_EPOCH) / 86400 ))
if [[ $NEW_DAYS_UNTIL_EXPIRY -gt $DAYS_UNTIL_EXPIRY ]]; then
log "Certificate renewed successfully. New expiry: $NEW_EXPIRY_DATE"
send_notification "SUCCESS" "Certificate renewed successfully. Valid until $NEW_EXPIRY_DATE"
# Reload Nginx
if systemctl reload nginx; then
log "Nginx reloaded successfully"
else
log "ERROR: Failed to reload Nginx"
send_notification "WARNING" "Certificate renewed but Nginx reload failed"
fi
else
log "Certificate was not renewed (not due for renewal)"
fi
else
log "ERROR: Certificate renewal failed"
send_notification "FAILED" "Certificate renewal failed. Please check server logs."
exit 1
fi
log "SSL renewal check completed"

41
templates/sysctl.conf.j2 Normal file
View File

@@ -0,0 +1,41 @@
# System optimization configuration for WordPress/LEMP stack
# Generated by Ansible
# Network optimizations
net.core.rmem_default = {{ net_core_rmem_default | default('262144') }}
net.core.rmem_max = {{ net_core_rmem_max | default('16777216') }}
net.core.wmem_default = {{ net_core_wmem_default | default('262144') }}
net.core.wmem_max = {{ net_core_wmem_max | default('16777216') }}
net.core.netdev_max_backlog = {{ net_core_netdev_max_backlog | default('5000') }}
net.core.somaxconn = {{ net_core_somaxconn | default('1024') }}
# TCP optimizations
net.ipv4.tcp_window_scaling = {{ net_ipv4_tcp_window_scaling | default('1') }}
net.ipv4.tcp_congestion_control = {{ net_ipv4_tcp_congestion_control | default('bbr') }}
net.ipv4.tcp_rmem = {{ net_ipv4_tcp_rmem | default('4096 87380 16777216') }}
net.ipv4.tcp_wmem = {{ net_ipv4_tcp_wmem | default('4096 65536 16777216') }}
net.ipv4.tcp_max_syn_backlog = {{ net_ipv4_tcp_max_syn_backlog | default('8192') }}
net.ipv4.tcp_slow_start_after_idle = {{ net_ipv4_tcp_slow_start_after_idle | default('0') }}
net.ipv4.tcp_tw_reuse = {{ net_ipv4_tcp_tw_reuse | default('1') }}
# Memory management
vm.swappiness = {{ vm_swappiness | default('10') }}
vm.dirty_ratio = {{ vm_dirty_ratio | default('15') }}
vm.dirty_background_ratio = {{ vm_dirty_background_ratio | default('5') }}
vm.vfs_cache_pressure = {{ vm_vfs_cache_pressure | default('50') }}
vm.min_free_kbytes = {{ vm_min_free_kbytes | default('65536') }}
# File system optimizations
fs.file-max = {{ fs_file_max | default('1000000') }}
fs.inotify.max_user_watches = {{ fs_inotify_max_user_watches | default('524288') }}
# Security optimizations
net.ipv4.conf.all.rp_filter = {{ net_ipv4_conf_all_rp_filter | default('1') }}
net.ipv4.conf.default.rp_filter = {{ net_ipv4_conf_default_rp_filter | default('1') }}
net.ipv4.icmp_echo_ignore_broadcasts = {{ net_ipv4_icmp_echo_ignore_broadcasts | default('1') }}
net.ipv4.icmp_ignore_bogus_error_responses = {{ net_ipv4_icmp_ignore_bogus_error_responses | default('1') }}
net.ipv4.conf.all.log_martians = {{ net_ipv4_conf_all_log_martians | default('1') }}
net.ipv4.conf.default.log_martians = {{ net_ipv4_conf_default_log_martians | default('1') }}
# Process limits
kernel.pid_max = {{ kernel_pid_max | default('4194304') }}

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# WordPress Backup Script
# Generated by Ansible
set -e
# Configuration
BACKUP_DIR="{{ backup_destination }}"
WP_PATH="{{ wordpress_path }}"
DB_NAME="{{ wordpress_db_name }}"
DB_USER="{{ wordpress_db_user }}"
DB_PASS="{{ wordpress_db_password }}"
RETENTION_DAYS="{{ backup_retention_days }}"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directories
mkdir -p "$BACKUP_DIR/database" "$BACKUP_DIR/files"
# Log function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$BACKUP_DIR/backup.log"
}
log "Starting WordPress backup..."
# Backup WordPress files
log "Backing up WordPress files..."
tar -czf "$BACKUP_DIR/files/wordpress_files_$DATE.tar.gz" -C "$(dirname $WP_PATH)" "$(basename $WP_PATH)"
# Backup database
log "Backing up database..."
mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "$BACKUP_DIR/database/wordpress_db_$DATE.sql.gz"
# Cleanup old backups
log "Cleaning up old backups (older than $RETENTION_DAYS days)..."
find "$BACKUP_DIR/files" -name "wordpress_files_*.tar.gz" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR/database" -name "wordpress_db_*.sql.gz" -mtime +$RETENTION_DAYS -delete
# Calculate backup sizes
FILES_SIZE=$(du -sh "$BACKUP_DIR/files/wordpress_files_$DATE.tar.gz" | cut -f1)
DB_SIZE=$(du -sh "$BACKUP_DIR/database/wordpress_db_$DATE.sql.gz" | cut -f1)
log "Backup completed successfully!"
log "Files backup size: $FILES_SIZE"
log "Database backup size: $DB_SIZE"
log "Backup location: $BACKUP_DIR"
# Optional: Send notification (requires mail setup)
# echo "WordPress backup completed on $(hostname) at $(date)" | mail -s "WordPress Backup Success" admin@example.com

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# WordPress Cron Optimization Script
# Generated by Ansible
set -e
# Configuration
WORDPRESS_PATH="{{ wordpress_path }}"
WP_CLI_PATH="/usr/local/bin/wp"
LOG_FILE="/var/log/wordpress-cron.log"
LOCK_FILE="/tmp/wordpress-cron.lock"
# Function to log messages
log_message() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Check if another cron is running
if [[ -f "$LOCK_FILE" ]]; then
if ps -p "$(cat $LOCK_FILE)" > /dev/null 2>&1; then
log_message "WordPress cron is already running (PID: $(cat $LOCK_FILE))"
exit 0
else
rm -f "$LOCK_FILE"
fi
fi
# Create lock file
echo $$ > "$LOCK_FILE"
# Function to cleanup on exit
cleanup() {
rm -f "$LOCK_FILE"
}
trap cleanup EXIT
log_message "Starting WordPress cron execution"
# Change to WordPress directory
cd "$WORDPRESS_PATH"
# Execute WordPress cron
if [[ -x "$WP_CLI_PATH" ]]; then
# Use WP-CLI for better control
if sudo -u {{ nginx_user }} "$WP_CLI_PATH" cron event run --due-now --quiet; then
log_message "WordPress cron executed successfully via WP-CLI"
else
log_message "ERROR: WordPress cron execution failed via WP-CLI"
exit 1
fi
else
# Fallback to HTTP request
if curl -s "{{ wordpress_url }}/wp-cron.php" > /dev/null; then
log_message "WordPress cron executed successfully via HTTP"
else
log_message "ERROR: WordPress cron execution failed via HTTP"
exit 1
fi
fi
# Clean up old cron logs (keep last 30 days)
find /var/log/ -name "wordpress-cron.log*" -mtime +30 -delete 2>/dev/null || true
log_message "WordPress cron execution completed"

View File

@@ -0,0 +1,123 @@
{% if multisite_type == 'subdomain' %}
# WordPress Multisite - Subdomain Configuration
map $http_host $blogid {
default -999;
# Primary site
~^{{ domain_name | regex_escape }}$ 0;
# Subdomains
~^([a-zA-Z0-9-]+)\.{{ domain_name | regex_escape }}$ -999;
}
{% endif %}
server {
listen 80;
server_name {{ domain_name }}{% if multisite_type == 'subdomain' %} *.{{ domain_name }}{% endif %};
{% if enable_ssl is defined and enable_ssl %}
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ domain_name }}{% if multisite_type == 'subdomain' %} *.{{ domain_name }}{% endif %};
# SSL Configuration
ssl_certificate {{ ssl_certificate_path }};
ssl_certificate_key {{ ssl_certificate_key_path }};
include /etc/nginx/snippets/ssl-params.conf;
{% endif %}
root {{ wordpress_path }};
index index.php index.html index.htm;
# WordPress Multisite specific rules
{% if multisite_type == 'subdirectory' %}
# Subdirectory multisite
if (!-e $request_filename) {
rewrite /wp-admin$ $scheme://$host$uri/ permanent;
rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;
rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last;
}
{% endif %}
location / {
try_files $uri $uri/ /index.php?$args;
}
# WordPress uploads for multisite
{% if multisite_type == 'subdirectory' %}
location ~ ^/[_0-9a-zA-Z-]+/files/(.+) {
try_files /wp-content/blogs.dir/$blogid/files/$1 /wp-includes/ms-files.php?file=$1;
access_log off; log_not_found off; expires max;
}
{% endif %}
# Handle uploads for subdomain multisite
{% if multisite_type == 'subdomain' %}
location ~ ^/files/(.+) {
try_files /wp-content/blogs.dir/$blogid/files/$1 /wp-includes/ms-files.php?file=$1;
access_log off; log_not_found off; expires max;
}
{% endif %}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:{{ php_fpm_socket }};
{% if enable_ssl is defined and enable_ssl %}
fastcgi_param HTTPS on;
{% endif %}
# WordPress multisite environment variables
fastcgi_param WP_MULTISITE 1;
{% if multisite_type == 'subdomain' %}
fastcgi_param SUBDOMAIN_INSTALL 1;
{% else %}
fastcgi_param SUBDOMAIN_INSTALL 0;
{% endif %}
}
# WordPress security rules
location ~* ^/(wp-config\.php|wp-config-sample\.php|readme\.html|license\.txt)$ {
deny all;
access_log off;
log_not_found off;
}
location ~ /\.ht {
deny all;
access_log off;
log_not_found off;
}
# WordPress uploads security
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
access_log off;
log_not_found off;
}
# Static files caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# WordPress REST API protection
location ~ ^/wp-json/ {
# Rate limiting for API
limit_req zone=api burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}
# Block xmlrpc.php
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
}

View File

@@ -0,0 +1,75 @@
server {
listen 80;
server_name {{ domain_name | default('_') }};
root {{ wordpress_path }};
index index.php index.html index.htm;
# Logging
access_log {{ log_dir }}/nginx/wordpress_access.log;
error_log {{ log_dir }}/nginx/wordpress_error.log;
# WordPress specific rules
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:{{ php_fpm_socket }};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Security
fastcgi_hide_header X-Powered-By;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# WordPress uploads
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# WordPress security
location ~* ^/(wp-config\.php|wp-config-sample\.php|readme\.html|license\.txt)$ {
deny all;
access_log off;
log_not_found off;
}
# Deny access to any files with a .php extension in the uploads directory
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
access_log off;
log_not_found off;
}
# WordPress REST API and XMLRPC protection
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
# Block access to wp-includes folders and files
location ~* ^/wp-includes/.*.php$ {
deny all;
access_log off;
log_not_found off;
}
# Block wp-content/uploads php files
location ~* ^/wp-content/uploads/.*.php$ {
deny all;
access_log off;
log_not_found off;
}
}

View File

@@ -0,0 +1,83 @@
server {
listen 80;
server_name {{ domain_name }}{% if www_redirect %} www.{{ domain_name }}{% endif %};
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name {{ domain_name }}{% if www_redirect %} www.{{ domain_name }}{% endif %};
root {{ wordpress_path }};
index index.php index.html index.htm;
# SSL Configuration
ssl_certificate {{ ssl_certificate_path }};
ssl_certificate_key {{ ssl_certificate_key_path }};
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Security headers
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate {{ ssl_certificate_path }};
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# WordPress specific rules
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:{{ php_fpm_socket }};
fastcgi_param HTTPS on;
}
location ~ /\.ht {
deny all;
}
# WordPress uploads
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# WordPress security
location ~ ^/(wp-admin|wp-login\.php) {
# Rate limiting for admin
limit_req zone=admin burst=5 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:{{ php_fpm_socket }};
fastcgi_param HTTPS on;
}
# Deny access to sensitive files
location ~* ^/(wp-config\.php|wp-config-sample\.php|readme\.html|license\.txt)$ {
deny all;
}
# Deny access to any files with a .php extension in the uploads directory
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
}

View File

@@ -0,0 +1,84 @@
server {
listen 80;
server_name {{ domain_name }};
root /var/www/html;
index index.php index.html index.htm;
# Sicherheits-Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
# Gzip-Kompression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml+rss application/json;
# WordPress-spezifische Regeln
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP-Dateien verarbeiten
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# WordPress-Admin-Bereich optimieren
location ~ ^/wp-admin/(.*)$ {
try_files $uri $uri/ /wp-admin/index.php?$args;
}
# Statische Dateien cachen
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# WordPress-Uploads-Verzeichnis
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf)$ {
expires 1y;
add_header Cache-Control "public";
}
# Sensible Dateien blockieren
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
access_log off;
log_not_found off;
deny all;
}
# wp-config.php schützen
location ~ wp-config.php {
deny all;
}
# xmlrpc.php begrenzen (gegen Brute-Force)
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
# Logs
access_log /var/log/nginx/wordpress_access.log;
error_log /var/log/nginx/wordpress_error.log;
}

25
templates/wp-cli.yml.j2 Normal file
View File

@@ -0,0 +1,25 @@
# WP-CLI Configuration
path: {{ wordpress_path }}
url: {{ wordpress_url | default('http://localhost') }}
user: {{ nginx_user }}
color: auto
debug: false
quiet: false
# Core settings
core config:
dbname: {{ wordpress_db_name }}
dbuser: {{ wordpress_db_user }}
dbpass: {{ wordpress_db_password }}
dbhost: localhost
# Apache/Nginx integration
apache_modules:
- mod_rewrite
# Custom commands and aliases
aliases:
st: status
co: config
pl: plugin
th: theme

View File

@@ -0,0 +1,86 @@
<?php
/**
* WordPress-Konfigurationsdatei
* Generiert durch Ansible
*/
// ** MySQL-Einstellungen ** //
define( 'DB_NAME', '{{ wordpress_db_name }}' );
define( 'DB_USER', '{{ wordpress_db_user }}' );
define( 'DB_PASSWORD', '{{ wordpress_db_password }}' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
{% if enable_redis is defined and enable_redis %}
// ** Redis Object Cache Konfiguration ** //
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
{% if redis_password is defined and redis_password %}
define( 'WP_REDIS_PASSWORD', '{{ redis_password }}' );
{% endif %}
define( 'WP_REDIS_DATABASE', {{ redis_database | default('0') }} );
define( 'WP_REDIS_TIMEOUT', {{ redis_timeout | default('1') }} );
define( 'WP_REDIS_READ_TIMEOUT', {{ redis_read_timeout | default('1') }} );
define( 'WP_CACHE', true );
{% endif %}
// ** Performance Optimierungen ** //
define( 'WP_MEMORY_LIMIT', '{{ wp_memory_limit | default('256M') }}' );
define( 'WP_MAX_MEMORY_LIMIT', '{{ wp_max_memory_limit | default('512M') }}' );
// Cron-Optimierung
{% if disable_wp_cron | default(true) %}
define( 'DISABLE_WP_CRON', true );
{% endif %}
// Autosave-Intervall (in Sekunden)
define( 'AUTOSAVE_INTERVAL', {{ wp_autosave_interval | default('300') }} );
// Post-Revisionen begrenzen
define( 'WP_POST_REVISIONS', {{ wp_post_revisions | default('3') }} );
// Papierkorb-Aufbewahrung (in Tagen)
define( 'EMPTY_TRASH_DAYS', {{ wp_empty_trash_days | default('7') }} );
{% if nginx_fastcgi_cache_enabled | default(true) %}
// FastCGI Cache Unterstützung
define( 'WP_CACHE_KEY_SALT', '{{ domain_name | default('localhost') }}' );
{% endif %}
// ** Authentifizierungs-Schlüssel und Salts ** //
define( 'AUTH_KEY', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}' );
define( 'SECURE_AUTH_KEY', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}2' );
define( 'LOGGED_IN_KEY', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}3' );
define( 'NONCE_KEY', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}4' );
define( 'AUTH_SALT', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}5' );
define( 'SECURE_AUTH_SALT', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}6' );
define( 'LOGGED_IN_SALT', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}7' );
define( 'NONCE_SALT', '{{ ansible_date_time.epoch }}put your unique phrase here{{ ansible_hostname }}8' );
// ** WordPress Tabellen-Präfix ** //
$table_prefix = 'wp_';
// ** Debugging ** //
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
// ** WordPress-URLs ** //
define( 'WP_HOME', 'http://{{ domain_name }}:8080' );
define( 'WP_SITEURL', 'http://{{ domain_name }}:8080' );
// ** Weitere Einstellungen ** //
define( 'AUTOMATIC_UPDATER_DISABLED', false );
define( 'WP_AUTO_UPDATE_CORE', true );
define( 'FS_METHOD', 'direct' );
/* Das war's, Schluss mit dem Bearbeiten! Viel Spaß beim Bloggen. */
/** Absolute Pfad zum WordPress-Verzeichnis. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
/** Lädt die WordPress-Variablen und -Dateien. */
require_once ABSPATH . 'wp-settings.php';

70
templates/www.conf.j2 Normal file
View File

@@ -0,0 +1,70 @@
; PHP-FPM pool configuration for WordPress
; Generated by Ansible
[www]
user = {{ nginx_user }}
group = {{ nginx_user }}
; The address on which to accept FastCGI requests.
listen = {{ php_fpm_socket }}
; Set listen(2) backlog.
listen.backlog = 511
; Set permissions for unix socket, if one is used.
listen.owner = {{ nginx_user }}
listen.group = {{ nginx_user }}
listen.mode = 0660
; Pool management
pm = dynamic
pm.max_children = {{ php_fpm_max_children | default('20') }}
pm.start_servers = {{ php_fpm_start_servers | default('2') }}
pm.min_spare_servers = {{ php_fpm_min_spare_servers | default('1') }}
pm.max_spare_servers = {{ php_fpm_max_spare_servers | default('3') }}
pm.process_idle_timeout = 10s
pm.max_requests = {{ php_fpm_max_requests | default('1000') }}
; Status page
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
; Logging
access.log = /var/log/php{{ php_version }}-fpm/$pool.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
; Security
security.limit_extensions = .php .php3 .php4 .php5 .php7 .php8
; Environment variables
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
; PHP ini settings for WordPress
php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@{{ domain_name | default('localhost') }}
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php{{ php_version }}-fpm/www.error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = {{ php_memory_limit | default('256M') }}
php_admin_value[upload_max_filesize] = {{ php_upload_max_filesize | default('64M') }}
php_admin_value[post_max_size] = {{ php_post_max_size | default('64M') }}
php_admin_value[max_execution_time] = {{ php_max_execution_time | default('300') }}
php_admin_value[max_input_time] = {{ php_max_input_time | default('300') }}
php_admin_value[max_input_vars] = {{ php_max_input_vars | default('3000') }}
; Session settings
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/sessions
php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache
; OPcache settings
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.memory_consumption] = 128
php_admin_value[opcache.interned_strings_buffer] = 16
php_admin_value[opcache.max_accelerated_files] = 10000
php_admin_value[opcache.revalidate_freq] = 2
php_admin_value[opcache.save_comments] = 1