# WordPress Multisite server configuration # Map for allowed CORS origins map $http_origin $cors_origin { default ""; "~^https?://host\.uk\.com$" $http_origin; "~^https?://social\.host\.uk\.com$" $http_origin; "~^https?://link\.host\.uk\.com$" $http_origin; "~^https?://analytics\.host\.uk\.com$" $http_origin; "~^https?://trust\.host\.uk\.com$" $http_origin; "~^https?://notify\.host\.uk\.com$" $http_origin; "~^https?://localhost(:[0-9]+)?$" $http_origin; "~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin; } server { listen [::]:80 default_server; listen 80 default_server; # Only accept subdomain traffic (*.host.uk.com), not apex domain # The apex domain (host.uk.com) should route to Host Hub (Laravel) server_name ~^(?.+)\.host\.uk\.com$ hestia.host.uk.com *.host.uk.com; # Serve error page for apex domain - this shouldn't hit WordPress # If it does, Coolify routing is misconfigured error_page 503 /wp-content/routing-error.html; if ($host = "host.uk.com") { return 503; } # Reject completely unknown hosts with connection close if ($host !~ "\.host\.uk\.com$") { return 444; } sendfile off; tcp_nodelay on; absolute_redirect off; root /var/www/html; index index.php; client_max_body_size 64M; # Security headers add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; # WordPress multisite rewrite rules location / { try_files $uri $uri/ /index.php?$args; } # REST API with CORS headers for headless operation location /wp-json/ { # Handle preflight OPTIONS requests if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' $cors_origin always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-WP-Nonce' always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Max-Age' 86400 always; add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain'; return 204; } # Add CORS headers to actual requests add_header 'Access-Control-Allow-Origin' $cors_origin always; add_header 'Access-Control-Allow-Credentials' 'true' always; add_header 'Access-Control-Expose-Headers' 'X-WP-Total, X-WP-TotalPages, Link' always; try_files $uri $uri/ /index.php?$args; } # Pass the PHP scripts to PHP-FPM listening on unix socket location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; fastcgi_index index.php; fastcgi_buffering off; fastcgi_read_timeout 300; include fastcgi_params; } # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires max; log_not_found off; access_log off; } location = /favicon.ico { log_not_found off; access_log off; } location = /robots.txt { allow all; log_not_found off; access_log off; } # Block XML-RPC by default, allow with secret token # Usage: /xmlrpc.php?token=YOUR_XMLRPC_TOKEN location = /xmlrpc.php { set $xmlrpc_allowed 0; # Allow if valid token provided (set in environment or change here) if ($arg_token = "xrpc-9f8e7d6c5b4a") { set $xmlrpc_allowed 1; } # Block if no valid token if ($xmlrpc_allowed = 0) { return 403; } # Pass to PHP if allowed try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_index index.php; include fastcgi_params; } # Deny access to hidden files location ~ /\. { deny all; access_log off; log_not_found off; } # Deny access to backup files location ~ ~$ { access_log off; log_not_found off; deny all; } # Allow fpm ping and status from localhost location ~ ^/(fpm-status|fpm-ping)$ { access_log off; allow 127.0.0.1; deny all; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_pass unix:/run/php-fpm.sock; } }