Buenas, en este post, vamos a hablar de cómo desplegar Nginx con una configuración segura y optimizada para un backend de WordPress con PHP-FPM.
Lo primero, es tener clara la arquitectura que vamos a utilizar. En este caso será: Nginx –> WordPress (PHP-FPM) –> MySQL
Todo el stack, lo desplegaremos con docker y docker-compose por simpleza y rapidez. Además de esto, usaremos traefik como proxy web y hacer el offloading de SSL. No vamos a entrar como instalar y configurar docker, docker-compose o traefik por que ya está hecho en posts anteriores.
Nginx
Para empezar, vamos a crear el docker-compose.yaml con el servicio de Nginx. Vamos a añadirle; healthchecks, limites de recursos y que los parámetros vayan por variables. De este modo, se puede reutilizar para diferentes sites sin tener que cambiar nada de la configuración.
...
web:
image: nginx
restart: always
container_name: ${SITE_NAME}-web
environment:
- TZ=${TZ}
volumes:
- wordpress:/var/www/html
- ./etc/nginx/conf.d:/etc/nginx/conf.d:ro
- ./etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /var/log/nginx/${SITE_NAME}:/var/log/nginx
networks:
- net
- traefik
depends_on:
- php-fpm
healthcheck:
test: pidof nginx
deploy:
resources:
limits:
cpus: '0.2'
memory: '500m'
labels:
- "traefik.backend=${SITE_NAME}"
- "traefik.frontend.rule=Host:${SITE_URL}"
- "traefik.docker.network=traefik"
- "traefik.port=80"
- "traefik.frontend.passHostHeader=true"
- "docker.group=${SITE_NAME}"
...
Esto sería en cuanto al docker-compose.yaml. También hay que crear los ficheros de configuración. En este caso, como están distribuidos es de la siguiente manera.
./etc/
|--> nginx/
|--> nginx.conf
|--> conf.d/
|--> headers.conf
|--> default.conf
|--> cache.conf
De esta forma, la configuración de Nginx, irá sobre el fichero nginx.conf y toda la configuración extra, irá sobre conf.d/. Vamos a explicar un poco las partes mas significativas de las configuraciones que vamos a utilizar.
/etc/nginx/nginx.conf
https://github.com/ichasco/Docker-Wordpress/blob/master/etc/nginx/nginx.conf
Deshabilitar las cabeceras inválidas y la versión de nginx
ignore_invalid_headers on;
server_tokens off;
Evitar ataques de buffer overflow
client_body_buffer_size 100K;
client_header_buffer_size 1k;
client_max_body_size 100k;
large_client_header_buffers 2 1k;
Controlar Time-outs
client_body_timeout 15;
client_header_timeout 15;
send_timeout 15;
keepalive_timeout 5 5;
OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;
/etc/nginx/conf.d/cache.conf
https://github.com/ichasco/Docker-Wordpress/blob/master/etc/nginx/conf.d/cache.conf
Vamos a añadir una cache para el FastCGI que cachee todas la peticiones y solo pase al back una para actualizar la cache. De este modo reducimos mucho la carga del back optimizando las peticiones e incrementando el rendimiento.
add_header X-FastCGI-Cache $upstream_cache_status;
fastcgi_buffering on;
fastcgi_cache cache;
fastcgi_ignore_headers Cache-Control Expires;
fastcgi_cache_lock on;
fastcgi_cache_min_uses 1;
fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
fastcgi_cache_valid 200 301 302 30m;
/etc/nginx/conf.d/headers.conf
https://github.com/ichasco/Docker-Wordpress/blob/master/etc/nginx/conf.d/headers.conf
En el apartado de headers, vamos añadir todas la headers recomendadas para evitar posibles ataques.
## Headers ##
fastcgi_hide_header X-Powered-By;
fastcgi_hide_header X-Pingback;
fastcgi_hide_header Link;
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy 'same-origin';
add_header X-Permitted-Cross-Domain-Policies none;
add_header Expect-CT "enforce, max-age=300";
# Esta Cabecera hay que adaptarla al contenido de la web #
add_header Content-Security-Policy "default-src 'self'; connect-src *; font-src *; frame-src *; img-src * data:; media-src *; object-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline';";
/etc/nginx/conf.d/default.conf
https://github.com/ichasco/Docker-Wordpress/blob/master/etc/nginx/conf.d/default.conf
Por último, hay que crear la configuración del site.
Para empezar, añadir el path de la cache y la key que vamos a utilizar
fastcgi_cache_path /tmp/cache keys_zone=cache:256m levels=1:2 inactive=600s max_size=100m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
Ahora, añadir el upstream. Este será el nombre del servicio de wordpress que vamos a crear luego. En este caso, este servicio se llamará php-fpm y el puerto correspondiente.
upstream php {
server php-fpm:9000;
}
A continuación, vamos a crear los location de / y de todos los archivos con extensión .php que vayan por el proxypass al servicio de php-fpm y añadiremos la caché.
location / {
try_files $uri $uri/ /index.php?$query_string;
# Add Headers conf #
include /etc/nginx/conf.d/headers.conf;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Upstream
fastcgi_pass php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
fastcgi_read_timeout 600;
# Add Headers conf #
include /etc/nginx/conf.d/headers.conf;
# Add Cache Settings #
include /etc/nginx/conf.d/cache.conf;
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/*|/tag/.*/feed/*|index.php|/.*sitemap.*\.(xml|xsl)") {
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;
}
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
}
Configurar para que el navegador cachee también los archivos multimedia
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 7d;
access_log off;
add_header Cache-Control "public";
}
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff|woff2)$ {
add_header Access-Control-Allow-Origin "*";
expires 7d;
access_log off;
}
Denegar acceso a todos los archivos ocultos
location ~ /\. {
access_log off;
log_not_found off;
deny all;
}
Configurar la seguridad de WordPress a nivel de Servidor Web.
¡Esto puede dar problemas dependiendo de la configuración y de los plugins que se tenga!
# WordPress: deny wp-content, wp-includes php files
location ~* ^/(?:wp-content|wp-includes)/.*\.php$ {
deny all;
}
# WordPress: deny wp-content/uploads nasty stuff
location ~* ^/wp-content/uploads/.*\.(?:s?html?|php|js|swf)$ {
deny all;
}
# WordPress: deny wp-content/plugins nasty stuff
location ~* ^/wp-content/plugins/.*\.(?!css(\.map)?|js(\.map)?|ttf|ttc|otf|eot|woff|woff2|svgz?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv|pdf|docx?|xlsx?|pptx?) {
deny all;
}
# WordPress: deny scripts and styles concat
location ~* \/wp-admin\/load-(?:scripts|styles)\.php {
deny all;
}
# WordPress: deny general stuff
location ~* ^/(?:xmlrpc\.php|wp-links-opml\.php|wp-config\.php|wp-config-sample\.php|wp-comments-post\.php|readme\.html|license\.txt)$ {
deny all;
}
Bloquear Spammers
if ( $http_referer ~* (babes|forsale|girl|jewelry|love|nudit|organic|poker|porn|sex|teen) ){
return 403;
}
Con estas configuraciones, tendríamos el Nginx configurado para que rinda al máximo de una forma segura.
PHP-FPM
El servicio de PHP-FPM, va a incluir el WordPress. Para este, utilizaremos la imagen oficial de WordPress con php-fpm.
Para empezar, vamos a editar el docker-compose.yaml y añadir el servicio.
...
php-fpm:
image: wordpress:php7.4-fpm
container_name: ${SITE_NAME}-php
restart: always
environment:
- WORDPRESS_DB_USER=${MYSQL_USER}
- WORDPRESS_DB_PASSWORD=${MYSQL_PASSWORD}
- WORDPRESS_DB_HOST=mysql
- WORDPRESS_DB_NAME=${MYSQL_DATABASE}
- TZ=${TZ}
volumes:
- wordpress:/var/www/html
- ./etc/php/conf.d/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
networks:
- net
depends_on:
- mysql
healthcheck:
test: pidof php-fpm
deploy:
resources:
limits:
cpus: '0.2'
memory: '500m'
labels:
- "traefik.enable=false"
- "docker.group=${SITE_NAME}"
...
Y en este caso, solo vamos a pasar un fichero, que es el que limita las subidas de archivos.
/etc/php/conf.d/uploads.ini
https://github.com/ichasco/Docker-Wordpress/blob/master/etc/php/conf.d/uploads.ini
file_uploads = On
memory_limit = 64M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 600
MariaDB
Y ya por último, nos quedaría configurar la base de datos. Para esto, volvemos a editar el docker-compose.yaml y añadimos el servicio de mariadb. En este caso, se opta por mariadb por rendimiento superior a mysql.
...
mysql:
image: mariadb:10.3
restart: always
container_name: ${SITE_NAME}-db
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_ALLOW_EMPTY_PASSWORD=no
- TZ=${TZ}
volumes:
- db:/var/lib/mysql
networks:
- net
healthcheck:
test: mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD && test '0' -eq $$(ps aux | awk '{print $$11}' | grep -c -e '^mysql$$')
deploy:
resources:
limits:
cpus: '0.2'
memory: '500m'
labels:
- "traefik.enable=false"
- "docker.group=${SITE_NAME}"
...
Este servicio no requiere archivos de configuración ni nada por el estilo. Ahora nos quedaría configurar en el docker-compose.yaml los volúmenes y las redes que vamos a utilizar.
volumes:
db:
wordpress:
networks:
net:
traefik:
external:
name: traefik
Y por último, hay que configurar las variables que queramos en el archivo .env
## GENERAL ##
TZ=Europe/Madrid
## WEB ##
SITE_NAME=example
SITE_URL=www.example.com
## MYSQL ##
MYSQL_ROOT_PASSWORD=root_password
MYSQL_USER=wp_usr
MYSQL_PASSWORD=wp_password
MYSQL_DATABASE=wp_db
Y una vez hecho todo esto, ya podemos levantar todo el stack de WordPress
docker-compose --compatibility up -d
Todos los archivos, se pueden encontrar en el repositorio de Github
Y ahora que estaría desplegado todo, podemos comprobar el estado de la web en: https://observatory.mozilla.org
Estos son los consejos para mejorar un despliegue de Worpdress bajo Nginx y PHP-FPM. Si se os ocurre algún consejo mas, siempre es bienvenido 😉
Also published on Medium.