Seguridad WordPress

Cada vez más personas utilizan WordPress como gestor de contenidos, ya sea un blog, un sitio web corporativo, o directamente foros, o un e-commerce. Este hecho de que cada vez más personas decidan utilizar WordPress hace que se convierta en un punto de inicio de ataques contra usuarios y sistemas.

Si eres de los que utiliza WordPress, para empezar, tranquilo: WordPress es seguro (las personas no tanto).

Aquí te dejo la presentación de la meetup de WordPress Barcelona en la que hablo de seguridad, presento WPdanger, hablo del próximo Proyecto Tide de WordPress…

Descárgate la presentación en formato PDF (987 KB).

WP-CLI para mantenimiento y seguridad

Si utilizas WordPress y tienes la posibilidad de gestionar el acceso por Shell, sin duda has de tener disponible WP-CLI. Y si utilizas WP-CLI entonces ya sabes que existen una decena de comandos que te facilitan la vida.

En general tengo puesto en mi agenda hacer al menos una revisión semanal de todos los WordPress que tengo, y eso incluye cierto mantenimiento y, de paso, darle una ojeada a los distintos elementos para que, de un vistazo, no se haya colado nada que no tenga que estar…

Esta es una pequeña lista de comandos que uso en este orden para revisar, verificar y actualizar el sistema.

$ reviso la versión de la instalación de WP-CLI
wp cli version
$ compruebo si hay actualizaciones disponibles de WP-CLI
wp cli check-update
$ actualizo WP-CLI a su última versión
wp cli update

$ entro en la carpeta de la instalación que quiero analizar
cd /carpeta/de/instalacion/wordpress/

$ verifico que no haya nada "raro" instalado en el core
wp core verify-checksums
$ listo la configuración del wp-config
wp config get
$ listo la lista de usuarios
wp user list

$ reviso la versión del WordPress
wp core version
$ compruebo si hay una nueva versión del WordPress
wp core check-update
$ actualizo a la última versión del WordPress
wp core update --path="/carpeta/de/instalacion/wordpress/"
$ verifico que no haya nada "raro" instalado en el core
wp core verify-checksums

$ hago una lista de todos los plugins instalados
wp plugin list --path="/carpeta/de/instalacion/wordpress/"
$ listo todos los plugins que tienen actualizaciones
wp plugin update --dry-run --all --path="/carpeta/de/instalacion/wordpress/"
$ actualizo todos los plugins que tienen actualizaciones
wp plugin update --all --path="/carpeta/de/instalacion/wordpress/"

$ hago una lista de todas las plantillas instaladas
wp theme list --path="/carpeta/de/instalacion/wordpress/"
$ listo todas las plantillas que tienen actualizaciones
wp theme update --dry-run --all --path="/carpeta/de/instalacion/wordpress/"
$ actualizo todas las plantillas que tienen actualizaciones
wp theme update --all --path="/carpeta/de/instalacion/wordpress/"

$ hago una lista de todos los idiomas instalados
wp language core list --status=installed
$ listo todos los idiomas que tienen actualizaciones
wp language core update --dry-run --path="/carpeta/de/instalacion/wordpress/"
$ actualizo todos los idiomas que tienen actualizaciones
wp language core update --path="/carpeta/de/instalacion/wordpress/"

$ elimino todos los transients caducados
wp transient delete --expired
$ compruebo el prefijo del WordPress
wp db prefix
$ verifico el tamaño de la base de datos del WordPress
wp db size
$ optimizo la base de datos del WordPress
wp db optimize
$ verifico el tamaño de la base de datos del WordPress
wp db size

Obviamente este listado de comandos tiene algunos “inútiles” pero que son válidos para comprobar por enésima vez que todo es correcto y que nada se descuadra de lo que ha de ser. De la misma forma hay varios comandos que revisa y re compruebas elementos que no son necesarios comprobar. Pero aún así, con este listado, poco a poco, salen las cosas bien.

WordPress SEO: ¿Yoast o más sencillo?

En muchas ocasiones cuando instalamos un WordPress vamos con nuestra lista predefinida de plugins, y habitualmente incorporamos uno de SEO. Estos plugins de SEO en realidad hacen poco, ya que es cierto que WordPress no permite de serie gestionar cómo queremos los títulos, pero él es capaz de generar automáticamente el meta-description si se hace bien.

Es por esto que mucha gente utiliza Yoast como plugin de SEO, que además de llevar muchas herramientas que te dicen cómo escribir (algo que nunca he usado ni usaría), te corrigen (en cierta manera) el formato de título y snippet que se muestra en los resultados de búsqueda.

Hace poco me puse a buscar una solución que fuera extremadamente simple para el tema de eso: título, descripción y palabras clave. Estos tres viejos elementos son en el fondo lo único que necesitaba, el resto ya lo hace bien la plantilla. Y acabé encontrando un plugin que no está en el repositorio oficial pero que está gestionado como tal: WP SEO: A simple, straightforward SEO plugin for WordPress. Just the facts, Jack. Para aquellos que quieran, descarga la versión 0.13.0.

Este plugin lo que hace es añadir estos campos:

Y hace simplemente eso… aunque se pueden usar los distintos códigos de sustitución:

#archive_date#
#author#
#categories#
#date_modified#
#date_published#
#excerpt#
#post_type_plural_name#
#post_type_singular_name#
#search_term#
#site_description#
#site_name#
#tags#
#term_description#
#term_name#
#thumbnail_url#
#title#

Con estos códigos te puedes hacer la configuración personalizada para todos los tipos de página que tengas en tu sitio:

En la última opción, que te permite crear tus propios “metas”, podrías por ejemplo configurar uno para Google Search Console, de forma que el nombre sea google-site-verification y el content, el identificador que Google proporciona.

Lo siguiente es… si hasta ahora he estado utilizando Yoast ¿puedo migrar los datos de Yoast a este sistema? La respuesta es sí, se puede hacer, aunque no tengo ningún plugin ni nada parecido. Aún así, con 2 consultas SQL se hace muy rápido:

UPDATE wp_postmeta SET meta_key = '_meta_title' WHERE meta_key = '_yoast_wpseo_title';
UPDATE wp_postmeta SET meta_key = '_meta_description' WHERE meta_key = '_yoast_wpseo_metadesc';

Y si quieres hacer limpieza una vez desinstalado Yoast, puedes borrar “los restos”:

DELETE FROM wp_postmeta WHERE meta_key LIKE '_yoast%';

A partir de aquí tendrás una forma sencilla de añadir el título, descripción y keyworks que quieras sin necesidad de instalar un plugin pesado como son generalmente los de SEO.

Mejora la página de mantenimiento de WordPress

Cuando actualizamos el núcleo de WordPress, sus plantillas o los plugins, en ese fragmento de tiempo pequeño, suele aparecer un breve mensaje de texto en cualquier pantalla de navegación en la que nos encontramos. Esa pantalla suele ser una pantalla fría, blanca con el mensaje de texto Briefly unavailable for scheduled maintenance. Check back in a minute. Sin duda no es la pantalla más bonita que podemos mostrarle a nuestros usuarios, es útil, pero no tiene gracia.

El mantenimiento de WordPress se gestiona por la creación de un fichero en la carpeta raíz (donde está el index.php principal) llamado .maintenance. El hecho de que exista este fichero con un pequeño codigo hace que todo el sitio quede bloqueado hasta que acaben de actualizarse todos los complementos.

¿Te gustaría darle un formato un poco más elegante a la página? Se puede hacer, creando un fichero en PHP que haga lo mismo que hace el sistema por defecto, pero con cierto contenido. Para ello deberás incluir el fichero maintenance.php dentro de la carpeta /wp-content/.

Ese fichero ha de tener una cabecera que devuelva un código 503 (conforme la página no está accesible temporalmente) y el tiempo en el que se debe volver a revisar. Como habitualmente las actualizaciones de WordPress son rápidas, pondremos que el reintento se haga en 1 minuto.

<?php
wp_load_translations_early();
$protocol = wp_get_server_protocol();
header($protocol.' 503 Service Unavailable', true, 503);
header('Retry-After: 60');
header('Content-Type: text/html; charset=utf-8');
?>

Teniendo esto en cuenta, podemos crear diseños que tengan al menos alguna animación:

<?php
wp_load_translations_early();
$protocol = wp_get_server_protocol();
header($protocol.' 503 Service Unavailable', true, 503);
header('Retry-After: 60');
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
  <title><?php _e('Maintenance'); ?></title>
  <style>.container{width:200px;height:100px;padding-top:100px;margin:0 auto}.ball{width:10px;height:10px;margin:10px auto;border-radius:50px}.ball:nth-child(1){background:#ff005d;-webkit-animation:right 1s infinite ease-in-out;-moz-animation:right 1s infinite ease-in-out;animation:right 1s infinite ease-in-out}.ball:nth-child(2){background:#35ff99;-webkit-animation:left 1.1s infinite ease-in-out;-moz-animation:left 1.1s infinite ease-in-out;animation:left 1.1s infinite ease-in-out}.ball:nth-child(3){background:#008597;-webkit-animation:right 1.05s infinite ease-in-out;-moz-animation:right 1.05s infinite ease-in-out;animation:right 1.05s infinite ease-in-out}.ball:nth-child(4){background:#fc0;-webkit-animation:left 1.15s infinite ease-in-out;-moz-animation:left 1.15s infinite ease-in-out;animation:left 1.15s infinite ease-in-out}.ball:nth-child(5){background:#2d3443;-webkit-animation:right 1.1s infinite ease-in-out;-moz-animation:right 1.1s infinite ease-in-out;animation:right 1.1s infinite ease-in-out}.ball:nth-child(6){background:#ff7c35;-webkit-animation:left 1.05s infinite ease-in-out;-moz-animation:left 1.05s infinite ease-in-out;animation:left 1.05s infinite ease-in-out}.ball:nth-child(7){background:#4d407c;-webkit-animation:right 1s infinite ease-in-out;-moz-animation:right 1s infinite ease-in-out;animation:right 1s infinite ease-in-out}@-webkit-keyframes right{0%,100%{-webkit-transform:translate(-15px)}50%{-webkit-transform:translate(15px)}}@-webkit-keyframes left{0%,100%{-webkit-transform:translate(15px)}50%{-webkit-transform:translate(-15px)}}@-moz-keyframes right{0%,100%{-moz-transform:translate(-15px)}50%{-moz-transform:translate(15px)}}@-moz-keyframes left{0%,100%{-moz-transform:translate(15px)}50%{-moz-transform:translate(-15px)}}@keyframes right{0%,100%{transform:translate(-15px)}50%{transform:translate(15px)}}@keyframes left{0%,100%{transform:translate(15px)}50%{transform:translate(-15px)}}</style>
</head>
<body>
  <center><h1><?php _e('Maintenance'); ?></h1></center>
  <p align="center"><?php _e('Briefly unavailable for scheduled maintenance. Check back in a minute.'); ?></p>
  <div class="container"><div class="ball"></div><div class="ball"></div><div class="ball"></div><div class="ball"></div><div class="ball"></div><div class="ball"></div><div class="ball"></div></div>
  <script>var maintenance_check=function(){var n=new XMLHttpRequest;n.open("HEAD",window.location,!0),n.onload=function(){this.status>=200&&this.status<400?window.location.reload():setTimeout(maintenance_check,5000)},n.onerror=function(){setTimeout(maintenance_check,5000)},n.send()};maintenance_check();</script>
</body>
</html>

Aunque si ese, que es sencillo y con un poco de formato te ha gustado, este tampoco se queda atrás.

<?php
wp_load_translations_early();
$protocol = wp_get_server_protocol();
header($protocol.' 503 Service Unavailable', true, 503);
header('Retry-After: 60');
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
  <title><?php _e('Maintenance'); ?></title>
  <style>body{background-color:#012;background-image:url(https://cssanimation.rocks/demo/starwars/images/bg.jpg);background-size:33%;background-repeat:repeat;min-height:2025px;color:#fff}.system{position:relative;top:0;left:0;width:100%;height:100%;-webkit-transform:scale(.75);transform:scale(.75)}.sun{width:144px;height:144px;border-radius:72px;position:absolute;top:1066.67px;left:50%;margin:-72px;background-image:url(https://sdo.gsfc.nasa.gov/assets/img/latest/latest_256_HMIIF.jpg);background-size:144px;background-repeat:no-repeat}.mer,.mer-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-mer{from{-webkit-transform:rotate(0) translatey(-84px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-84px) rotate(-360deg)}}@-keyframes rot-mer{from{-webkit-transform:rotate(0) translatey(-84px) rotate(0);transform:rotate(0) translatey(-84px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-84px) rotate(-360deg);transform:rotate(360deg) translatey(-84px) rotate(-360deg)}}.mer{width:3.5px;height:3.5px;background-color:#888;margin:-1.75px;-webkit-animation:rot-mer .88s infinite linear;animation:rot-mer .88s infinite linear;z-index:200}.mer-path{width:168px;height:168px;z-index:100;margin:-84px;border:1px solid #444}.ven,.ven-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-ven{from{-webkit-transform:rotate(0) translatey(-90px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-90px) rotate(-360deg)}}@-keyframes rot-ven{from{-webkit-transform:rotate(0) translatey(-90px) rotate(0);transform:rotate(0) translatey(-90px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-90px) rotate(-360deg);transform:rotate(360deg) translatey(-90px) rotate(-360deg)}}.ven{width:5.5px;height:5.5px;background-color:#f5f9be;margin:-2.75px;-webkit-animation:rot-ven 2.25s infinite linear;animation:rot-ven 2.25s infinite linear;z-index:200}.ven-path{width:180px;height:180px;z-index:100;margin:-90px;border:1px solid #444}.ear,.ear-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-ear{from{-webkit-transform:rotate(0) translatey(-102px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-102px) rotate(-360deg)}}@-keyframes rot-ear{from{-webkit-transform:rotate(0) translatey(-102px) rotate(0);transform:rotate(0) translatey(-102px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-102px) rotate(-360deg);transform:rotate(360deg) translatey(-102px) rotate(-360deg)}}.ear{width:7px;height:7px;background-color:#4b94f9;margin:-3.5px;-webkit-animation:rot-ear 3.65s infinite linear;animation:rot-ear 3.65s infinite linear;z-index:200}.ear-path{width:204px;height:204px;z-index:100;margin:-102px;border:1px solid #444}.mar,.mar-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-mar{from{-webkit-transform:rotate(0) translatey(-118px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-118px) rotate(-360deg)}}@-keyframes rot-mar{from{-webkit-transform:rotate(0) translatey(-118px) rotate(0);transform:rotate(0) translatey(-118px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-118px) rotate(-360deg);transform:rotate(360deg) translatey(-118px) rotate(-360deg)}}.mar{width:6px;height:6px;background-color:#dd411a;margin:-3px;-webkit-animation:rot-mar 6.87s infinite linear;animation:rot-mar 6.87s infinite linear;z-index:200;background-image:-webkit-repeating-linear-gradient(top,#fff,#fff 1px,transparent 1px,transparent 5px);background-image:repeating-linear-gradient(to bottom,#fff,#fff 1px,transparent 1px,transparent 5px)}.mar-path{width:236px;height:236px;z-index:100;margin:-118px;border:1px solid #444}.jup,.jup-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-jup{from{-webkit-transform:rotate(0) translatey(-228px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-228px) rotate(-360deg)}}@-keyframes rot-jup{from{-webkit-transform:rotate(0) translatey(-228px) rotate(0);transform:rotate(0) translatey(-228px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-228px) rotate(-360deg);transform:rotate(360deg) translatey(-228px) rotate(-360deg)}}.jup{width:70px;height:70px;background-color:#eaad3b;margin:-35px;-webkit-animation:rot-jup 43.32s infinite linear;animation:rot-jup 43.32s infinite linear;z-index:200;background-image:-webkit-repeating-linear-gradient(84deg,#797663 22px,#e1dcde 16px,#c3a992 30px,#e9ece2 30px);background-image:repeating-linear-gradient(6deg,#797663 22px,#e1dcde 16px,#c3a992 30px,#e9ece2 30px)}.jup-path{width:456px;height:456px;z-index:100;margin:-228px;border:1px solid #444}.sat,.sat-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-sat{from{-webkit-transform:rotate(0) translatey(-362px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-362px) rotate(-360deg)}}@-keyframes rot-sat{from{-webkit-transform:rotate(0) translatey(-362px) rotate(0);transform:rotate(0) translatey(-362px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-362px) rotate(-360deg);transform:rotate(360deg) translatey(-362px) rotate(-360deg)}}.sat{width:58px;height:58px;background-color:#d6cd93;margin:-29px;-webkit-animation:rot-sat 107.59s infinite linear;animation:rot-sat 107.59s infinite linear;z-index:200}.sat-path{width:724px;height:724px;z-index:100;margin:-362px;border:1px solid #444}.ura,.ura-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-ura{from{-webkit-transform:rotate(0) translatey(-648px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-648px) rotate(-360deg)}}@-keyframes rot-ura{from{-webkit-transform:rotate(0) translatey(-648px) rotate(0);transform:rotate(0) translatey(-648px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-648px) rotate(-360deg);transform:rotate(360deg) translatey(-648px) rotate(-360deg)}}.ura{width:26px;height:26px;background-color:#bfeef2;margin:-13px;-webkit-animation:rot-ura 306.87s infinite linear;animation:rot-ura 306.87s infinite linear;z-index:200}.ura-path{width:1296px;height:1296px;z-index:100;margin:-648px;border:1px solid #444}.nep,.nep-path{border-radius:50%;position:absolute;top:1066.67px;left:50%}@-webkit-keyframes rot-nep{from{-webkit-transform:rotate(0) translatey(-972px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-972px) rotate(-360deg)}}@-keyframes rot-nep{from{-webkit-transform:rotate(0) translatey(-972px) rotate(0);transform:rotate(0) translatey(-972px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-972px) rotate(-360deg);transform:rotate(360deg) translatey(-972px) rotate(-360deg)}}.nep{width:24px;height:24px;background-color:#363ed7;margin:-12px;-webkit-animation:rot-nep 601.9s infinite linear;animation:rot-nep 601.9s infinite linear;z-index:200}.nep-path{width:1944px;height:1944px;z-index:100;margin:-972px;border:1px solid #444}.plu,.plu-path{border-radius:50%}@-webkit-keyframes rot-plu{from{-webkit-transform:rotate(0) translatey(-1246px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-1246px) rotate(-360deg)}}@-keyframes rot-plu{from{-webkit-transform:rotate(0) translatey(-1246px) rotate(0);transform:rotate(0) translatey(-1246px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-1246px) rotate(-360deg);transform:rotate(360deg) translatey(-1246px) rotate(-360deg)}}.plu{width:3px;height:3px;background-color:#963;position:absolute;left:50%;margin:-1.5px;-webkit-animation:rot-plu 904.65s infinite linear;animation:rot-plu 904.65s infinite linear;z-index:200}.dem,.jove,.lune,.pho{background-color:#fff;left:50%;position:absolute}.plu-path{width:2492px;height:2492px;z-index:100;position:absolute;left:50%;margin:-1246px;border:1px solid #444}@-webkit-keyframes rot-lune{from{-webkit-transform:rotate(0) translatey(-7px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-7px) rotate(-360deg)}}@-keyframes rot-lune{from{-webkit-transform:rotate(0) translatey(-7px) rotate(0);transform:rotate(0) translatey(-7px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-7px) rotate(-360deg);transform:rotate(360deg) translatey(-7px) rotate(-360deg)}}.lune{width:2px;height:2px;top:50%;margin:-1.5px;-webkit-animation:rot-lune .27s infinite linear;animation:rot-lune .27s infinite linear}.dem,.pho{margin:-1px}@-webkit-keyframes rot-pho{from{-webkit-transform:rotate(0) translatey(-7px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-7px) rotate(-360deg)}}@-keyframes rot-pho{from{-webkit-transform:rotate(0) translatey(-7px) rotate(0);transform:rotate(0) translatey(-7px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-7px) rotate(-360deg);transform:rotate(360deg) translatey(-7px) rotate(-360deg)}}@-webkit-keyframes rot-dem{from{-webkit-transform:rotate(0) translatey(-9px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-9px) rotate(-360deg)}}@-keyframes rot-dem{from{-webkit-transform:rotate(0) translatey(-9px) rotate(0);transform:rotate(0) translatey(-9px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-9px) rotate(-360deg);transform:rotate(360deg) translatey(-9px) rotate(-360deg)}}.dem,.pho{width:1px;height:1px;top:50%}.pho{-webkit-animation:rot-pho .15s infinite linear;animation:rot-pho .15s infinite linear}.dem{-webkit-animation:rot-dem .2s infinite linear;animation:rot-dem .2s infinite linear}.jove{width:2px;height:2px;top:35px}@-webkit-keyframes rot-io{from{-webkit-transform:rotate(0) translatey(-39px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-39px) rotate(-360deg)}}@-keyframes rot-io{from{-webkit-transform:rotate(0) translatey(-39px) rotate(0);transform:rotate(0) translatey(-39px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-39px) rotate(-360deg);transform:rotate(360deg) translatey(-39px) rotate(-360deg)}}@-webkit-keyframes rot-eur{from{-webkit-transform:rotate(0) translatey(-41px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-41px) rotate(-360deg)}}@-keyframes rot-eur{from{-webkit-transform:rotate(0) translatey(-41px) rotate(0);transform:rotate(0) translatey(-41px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-41px) rotate(-360deg);transform:rotate(360deg) translatey(-41px) rotate(-360deg)}}@-webkit-keyframes rot-gan{from{-webkit-transform:rotate(0) translatey(-45px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-45px) rotate(-360deg)}}@-keyframes rot-gan{from{-webkit-transform:rotate(0) translatey(-45px) rotate(0);transform:rotate(0) translatey(-45px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-45px) rotate(-360deg);transform:rotate(360deg) translatey(-45px) rotate(-360deg)}}@-webkit-keyframes rot-cal{from{-webkit-transform:rotate(0) translatey(-53px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-53px) rotate(-360deg)}}@-keyframes rot-cal{from{-webkit-transform:rotate(0) translatey(-53px) rotate(0);transform:rotate(0) translatey(-53px) rotate(0)}to{-webkit-transform:rotate(360deg) translatey(-53px) rotate(-360deg);transform:rotate(360deg) translatey(-53px) rotate(-360deg)}}.io{-webkit-animation:rot-io .2s infinite linear;animation:rot-io .2s infinite linear}.eur{-webkit-animation:rot-eur .35s infinite linear;animation:rot-eur .35s infinite linear}.gan{-webkit-animation:rot-gan .7s infinite linear;animation:rot-gan .7s infinite linear}.cal{-webkit-animation:rot-cal 1.65s infinite linear;animation:rot-cal 1.65s infinite linear}.spot{position:absolute;width:16px;height:12px;border-radius:8px/6px;top:45px;left:50%;background-color:#bc833b;box-shadow:0 0 5px #e1dcde;border:1px solid #e1dcde;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;z-index:300}.nep .spot{background-color:#29319d;border:0;box-shadow:none;top:50%;left:45%;width:10px;height:6px;margin:-2px;border-radius:5px/3px;border-left:solid 1px #7ed6fe}div[class$="-ring"]{border-radius:50%;position:absolute;top:50%;left:50%;opacity:.7;-webkit-transform:rotatex(45deg);transform:rotatex(45deg)}.a-ring{border:5px solid #96866f;width:119px;height:119px;margin:-64.5px}.b-ring{border:10px solid #554c3c;width:104px;height:104px;margin:-62px}.c-ring{border:9px solid #574f4a;width:95px;height:95px;margin:-56.5px}.f-ring{border:2px solid #908e8d;width:133px;height:133px;margin:-68.5px}.e-ring{border:7px solid #908e8d;width:76px;height:76px;margin:-45px;-webkit-transform:rotatex(0) rotatey(89deg)!important;transform:rotatex(0) rotatey(89deg)!important}.plu,.plu-path{top:1354.07px}</style>
</head>
<body>
  <center><h1><?php _e('Maintenance'); ?></h1></center>
  <p align="center"><?php _e('Briefly unavailable for scheduled maintenance. Check back in a minute.'); ?></p>
  <div class="system"><div class="sun"></div><div class="mer-path"></div><div class="mer"></div><div class="ven-path"></div><div class="ven"></div><div class="ear-path"></div><div class="ear"><div class="lune"></div></div><div class="mar-path"></div><div class="mar"><div class="pho"></div><div class="dem"></div></div><div class="jup-path"></div><div class="jup"><div class="spot"></div><div class="jove io"></div><div class="jove eur"></div><div class="jove gan"></div><div class="jove cal"></div></div><div class="sat-path"></div><div class="sat"><div class="f-ring"></div><div class="a-ring"></div><div class="b-ring"></div><div class="c-ring"></div></div><div class="ura-path"></div><div class="ura"><div class="e-ring"></div></div><div class="nep-path"></div><div class="nep"><div class="spot"></div></div><div class="plu-path"></div><div class="plu"></div></div>
  <script>var maintenance_check=function(){var n=new XMLHttpRequest;n.open("HEAD",window.location,!0),n.onload=function(){this.status>=200&&this.status<400?window.location.reload():setTimeout(maintenance_check,5000)},n.onerror=function(){setTimeout(maintenance_check,5000)},n.send()};maintenance_check();</script>
</body>
</html>

Ahora ya tienes unas primeras plantillas para optimizar tu WordPress cuando esté en mantenimiento y tus usuarios no vean un feo texto.

WPdanger, análisis de seguridad para WordPress

Un clásico de los sitios web que usan un CMS de fondo es la falta de actualización de los mismos. Esto hace que si en un par de meses tu sitio está sin actualizar sea objetivo de cualquier ataque, ya que todos los problemas que puedan haber, estarán.

Si a esta preocupación personal le sumamos que en verano suelo hacer “cositas por entretenerme”, salen proyectos como WPdanger, un sitio web en el que pones la dirección de tu sitio en WordPress y te da algunas vulnerabilidades o te puede servir para encontrar problemas que puedas tener y que todos pueden verificar.

Al principio comencé usando WPscan para hacer algunas pruebas, pero aunque en particular funcionaba, a lo grande ya no era tan simple. Así que monté un primer sistema que mostraba los resultados por pantalla web, posteriormente comené a procesarlos y al final ha salido una web en la que analiza más de lo que hacía al principio (que era puramente reactivo) a algo un poco más decente y que da más datos, además de mostrar enlaces a todos aquellos problemas que pueden aparecer.

Pero esto era poco… si cualquiera tiene acceso a este tipo de herramientas ¿cómo evitar que los datos que aparecen sean útiles para prevenir ataques? Obviamente la primera fase era evitar enseñar lo que no se tiene que enseñar, es decir, que la herramienta crea que lo que realmente hay no esté. Así, si un hacker usa estas herramientas, y ve que aparentemente no hay mucho que hacer en el sitio, pase de él. Esto me ha llevado durante algunos días a hacer y hacer análisis hasta poder ir bloqueando aquellos elementos que bloqueasen ataques pero hiciesen que la web siguiera funcionando. Tras varias horas revisando y analizando y probando con mi sitio, llegué a varias conclusiones.

La primera de ellas es que WordPress funciona mejor con nginx que con Apache HTTPD. En principio debería dar igual, pero a la hora de configurar elementos ys eguridad, ya no tanto. Entre uno y otro, me quedo con nginx. Lo segundo es que hay que montar PHP 7, principalmente por temas de rendimiento. A partir de esta combinación, podemos comenzar a trabajar.

La segunda es que hay plugins (muy utilizados) que no piensan en la seguridad, no permiten desactivar determinados elementos y que ponen en riesgo todo t sitio. El principal que me he encontrado es el Yoast SEO. Me gusta, no me importa que tenga código que ponga que lo usas, pero por favor, no indiquemos la versión, o al menos que la versión sea un eleemnto que se pueda desactivar. Como digo, es el plugin más inseguro (desde ese punto de vista) que he encontrado.

La tercera es que el hecho de que WordPress sea tan estándar y tan sencillo, hace que se puedan detectar versiones de plugins y plantillas de forma extremadamente sencilla (y sobre todo, de forma automática). Obviamente hay que encontrar un mix entre funcionalidad y seguridad. Me ha costado, pero he acabado encontrándolo.

El cuarto es que las configuraciones de seguridad que hay por Internet sobre WordPress se quedan extremadamente cortos. Es obvio que hay que informar, siempre lo primero, en que tu WordPress esté actualizado, obvio ye s así, pero por ejemplo, en todos los manuales se habla del famoso “usuario admin” que desde hace muchísimas versiones ya no viene por defecto. Está bien decir que no se use, pero han pasado años de que sea el mayor problema de seguridad de los usuarios. Lo que más me molesta es que no se explique esto, que es un tema histórico y que una instalación nueva no debería preocuparse en exceso por este asunto.

El quinto es que me ha parecido extraño que los grandes proveedores de hosting especializados en WordPress no den soluciones de seguridad sencillas más avanzadas, sobre todo en la parte preventiva.

Obviamente, no hay sitios 100% seguros, ni pretendo que los haya, pero sí que la prevención de la que siempre se habla en seguridad, se aplique. No tiene sentido decir que tienes un Firewall si luego puedo saber la lista de plugins que tienes instalados, o qué plantillas usas, y mucho menos saber qué versiones son y si están al día o no, incluyendo al núcleo.

¿Qué he aprendido con todo esto? A mejorar la ocultaciónd e información de la manera más simple posible que he encontrado, y principalmente poner difícil a los demás saber qué uso o dejo de usar. Un ejemplo, ahora mismo, es que si lanzo un análisis contra mi sitio, hace unos días me decía qué versión de WordPress tenía (exactamente), toda la lista de plugins, con sus versiones y sabiendo sie staban actualizados o no, y lo mismo con las plantillas… Ahora me dice una versión casi exacta de la que tengo en realidad de WordPress, pero ya no me dice qué plugins o plantilla tengo.

Esto no significa que si te miras un poco el código no se pueda saber, pero el hecho de que “de forma automática nos e sepa” me hace sentir un poco mejor, porque mucha gente que quiera hacer maldades lo tendrá ligeramente más difícil o, como mínimo, tendrá que dedicarle tiempo humano (y no tiempo máquina) para hacer maldades (a alguien que, la verdad, dudo que le aporte o vaya a conseguir nada útil).

Para acabar, tengo escrito un ¿folleto? de 15 páginas sobre seguridad de WordPress en el que explico con pelos y señales cómo solventar prácticamente todos estos problemas. La cuestión es si debería publicarlo por completo o parcialmente y plantear como vía de negocio / modelo de vida (y más ahora que no estoy trabajando en ningún proyecto y he vuelto al autonomismo) y de esa forma, ofreciendo uns ervicio barato, ofrecer la posibilidad de montar WordPress de forma segura a quien no sepa qué hacer con su sitio web. Aún así, como siempre, si alguien necesita ayuda con algo, parcialmente como parte de la comunidad, parcialmente como modelo de vida, estoy aquí para ayudar.

Consejos básicos de seguridad para WordPress

WordPress es seguro. Esa es una máxima que normalmente los seres humanos rompemos y convertimos el software en algo menos seguro de lo que debería.

En la presentación que hice ayer tuve la oportunidad de presentar algunas cosas que todo el mundo explica sobre seguridad, otras que se dicen pero no se hacen y otras tantas que son más desconocidas o que deberían hacerse y no se hacen.

Además, como tema máximo, comenté sobre el uso del 2FA (segunda autenticación) que ayuda a bloquear intrusos aunque te roben tus claves.

Aquí os dejo la descarga del PDF de mi parte de la presentación (Consejos de Seguridad para WordPress), en la que hay bastantes códigos para el wp-config.php, además de algunos plugins que te pueden ayudar a proteger un poco más el sistema.

Instalar WordPress desde cero en Ubuntu 16 (para torpes como yo)

Uso WordPress desde 2005 y en todos estos años he pasado por todo tipo de alojamiento web: hosting compartidos, dedicados, VPS, gestionados, sin gestionar… Normalmente depende de la cantidad de sitios que he de gestionar para tomar una decisión u otra.

Ahora que estoy volviendo a hacer mis cosas a mi manera, me ha dado por volver a montarme yo mismo los WordPress, de una forma que sea escalable, o al menos que en un futuro me permita crecer. Y para eso he decidido pasar de un hosting compartidos a un VPS. Esto me ha llevado a algunos problemas a la hora de realizar las migraciones, pero al fin y al cabo, hay que empezar por algún sitio.

Este manual está creado y funcionando en un VPS de 5$/mes de DigitalOcean (los de 512MB de RAM). Obviamente está organizado para poder escalar si es necesario, aunque con una máquina de estas, para un blog “normalito” hay más que suficiente, y 60 dólares al año es un precio razonable para tener tu sitio con un control del 100%.

NOTA 1: En esta documentación las IP que usaré son rangos de IP privada (por supuesto hay que sustituirlas por la IP pública que os de vuestro alojamiento) y como dominio usaré siempre example.com (que por supuesto tenéis que cambiar por vuestro dominio).

NOTA 2: Seguro que hay mil formas de hacer esto que os voy a explicar yo. Esta es una de ellas, que a mi me funciona, mejorable, seguro (y si alguien quiere, que comente y lo mejoramos)

Lo primero que recomiendo hacer es tener la máquina creada, y pedir la IPv6. Puestos ya, configurémoslo todo al 100%. Esto implica que una vez tengamos la IPv4 y la IPv6 lo que hay que hacer es configurar las DNS con las entradas siguientes:

@    A      172.16.0.0
www  A      172.16.0.0
@    AAAA   fd12:3456:789a:1::1
www  AAAA   fd12:3456:789a:1::1

Si habéis elegido DigitalOcean como proveedor, lo primero es instalar su sistema interno de estadísticas (esto se puede obviar en cualquier otro hosting):

$ curl -sSL https://agent.digitalocean.com/install.sh | sh

Lo siguiente es poner en hora la máquina:

$ timedatectl set-timezone UTC
$ timedatectl set-ntp on

Y actualizar el sistema:

$ apt-get -y update
$ apt-get -y upgrade
$ apt-get -y dist-upgrade
$ apt-get -y autoremove

Comencemos instalando algo de software base:

$ apt-get -y install software-properties-common curl vim unzip

Lo primero que vamos a montar es la base de datos. Para ello usaremos MariaDB 10.2

$ apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xF1656F24C74CD1D8
$ add-apt-repository 'deb [arch=amd64,i386,ppc64el] http://ams2.mirrors.digitalocean.com/mariadb/repo/10.2/ubuntu xenial main'
$ apt-get -y install mariadb-server
$ mysql_secure_installation

Lo primero que se hace es descargar e instalar, y posteriormente configurar. Es muy recomendable poner contraseña a la base de datos y eliminar todo aquello que no se vaya a utilizar.

Lo siguiente es la instalación del servidor web. Aunque he utilizado durante muchos años Apache HTTPD, en este caso propongo el uso de nginx.

$ apt-get -y install nginx

Y finalmente vamos a instalar PHP, en este caso la versión 7.

$ apt-get -y install php php-fpm
$ apt-get -y install php-common php-curl php-gd php-iconv php-json php-mbstring php-mcrypt php-mysqli php-xml php-xmlrpc

La instalación la hago en 2 pasos; primero la instalación base, del PHP-FPM y posteriormente algunas bibliotecas añadidas para que WordPress funcione sin problema.

Ahora que tenemos todo instalado, activamos los servicios

$ systemctl enable nginx.service
$ systemctl enable mysql.service
$ systemctl enable php7.0-fpm.service

En este punto podríamos tener un sitio web funcionando, pero hoy en día lo interesante es tener un sitio web segcurizado con certificados SSL/TLS, así que vamos a usar Let’s Encrypt para ello, y que actualice y haga todo sólo. Comenzamos generando una clave segura:

$ openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

E instalamos el software de gestión de los certificados

$ add-apt-repository ppa:certbot/certbot
$ apt-get -y update
$ apt-get -y install python-certbot-nginx

Aunque aún no tenemos ningún certificado, os recomiendo dejar ya el cron puesto para que se autorenueven 8se ejecuta una vez al día a las 0645 UTC; podéis cambiarlo, por supuesto):

$ crontab -e

Contenido a añadir:

45 6 * * * certbot renew --dry-run

Lo siguiente es activar los Firewall (por ahora abrimos el nginx 80/443 y el SSH 22):

$ ufw app list
$ ufw allow 'Nginx Full'
$ ufw allow 'OpenSSH'
$ ufw enable

Vamos a montar un servidor de correo que permita el envío de correo sólo interno, para los correos que se manden desde el propio WordPress. Este sistema estará cerrado a correos externos o cualquier otra cosa.

$ apt -y install mailutils
$ ufw allow 'Postfix'
$ ufw reload

Ahora que está instalado, vamos a configurarlo

$ vim /etc/postfix/main.cf

Contenido a modificar:

inet_interfaces = loopback-only
mydestination = $myhostname, localhost.$mydomain, $mydomain

Iniciaremos el servicio de correo

$ systemctl enable postfix

Ahora configuraremos el sistema para que tenga nuestra cuenta de correo como base:

$ vim /etc/aliases

Contenido a modificar/añadir:

postmaster:    root
root:          mi.correo@example.com

Y para aplicar la configuración, ejecutaremos:

$ newaliases

Para acabar la parte de configuración del servidor, lo que queda por hacer es limpiar la web por defecto:

$ rm /var/www/html/index.*

Y sustituirla por otra cosa que “no moleste mucho”:

$ vim /var/www/html/index.html

Contenido a crear:

<!DOCTYPE html>
<p>Hello World!</p>

En este momento, si visitas la web desde la IP, deberías ver un mensaje por pantalla que diga: Hello World!. A partir de aquí podríamos utilizar este sistema para todas las webs distintas que queramos.

Para comenzar lo que haremos es crear la base de datos:

$ mysql -u root -p

Accederemos con la contraseña que configuramos en su momento, y crearemos la base de datos:

CREATE DATABASE example CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;

Porteriormente crearemos un usuario que tenga permisos para controlar la base de datos:

GRANT ALL ON example.* TO 'exampleuser'@'localhost' IDENTIFIED BY 'examplepassword';

Y antes de salir, lo que haremos es refrescar los permisos:

FLUSH PRIVILEGES;

Recuerda que para salir del servidor de base de datos puedes escribir “quit”.

El siguiente bloque es el de subir el propio WordPress, el software; el objetivo es crear el lugar donde subiremos el software y dejarlo de forma ordenada.

$ mkdir /var/www/example.com/
$ cd /var/www/example.com/
$ wget https://wordpress.org/latest.tar.gz
$ tar xzvf latest.tar.gz
$ rm latest.tar.gz
$ mv ./wordpress/* ./
$ rm -rf ./wordpress/

Y posteriormente configuraremos los permisos de los ficheros y de su sistema:

$ chown -R www-data:www-data ./
$ find /var/www/example.com/ -type d -exec chmod 755 {} \;
$ find /var/www/example.com/ -type f -exec chmod 644 {} \;

El primero de estos comandos lo que hace es dar permisos a los ficheros como el nginx genera; el segundo da permisos a las carpetas y el tercero a los ficheros. En principio con estos permisos se debería poder trabajar sin problema.

Comenzaremos a configurar el WordPress con su fichero de configuración. Aunque el software de instalación permite generarlo, he preferido crear una plantilla base con ciertas configuraciones:

$ cp wp-config-sample.php wp-config.php
$ vim wp-config.php

Contenido:

<?php
define('DB_NAME', 'example');
define('DB_USER', 'exampleuser');
define('DB_PASSWORD', 'examplepassword');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_ci');
// Crear esto con https://api.wordpress.org/secret-key/1.1/salt/
define('AUTH_KEY', 'xxxxx');
define('SECURE_AUTH_KEY', 'xxxxx');
define('LOGGED_IN_KEY', 'xxxxx');
define('NONCE_KEY', 'xxxxx');
define('AUTH_SALT', 'xxxxx');
define('SECURE_AUTH_SALT', 'xxxxx');
define('LOGGED_IN_SALT', 'xxxxx');
define('NONCE_SALT', 'xxxxx');
//
$table_prefix = 'example_';
define('WPLANG', 'es_ES');
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', false);
define('WP_CACHE', true);
define('WP_SITEURL', 'https://www.example.com');
define('WP_HOME', 'https://www.example.com');
define('COOKIE_DOMAIN', 'www.example.com');
define('AUTOSAVE_INTERVAL', 30);
define('EMPTY_TRASH_DAYS', 7);
define('WP_POST_REVISIONS', false);
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('WP_AUTO_UPDATE_CORE', true);
define('IMAGE_EDIT_OVERWRITE', true);
//
if(!defined('ABSPATH')) define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
?>

He puesto como configuración de “CHARSET / COLLATE” la de “utf8mb4” que permitiría una gestión avanzada de idiomas “raros” (no occidentales, como el chino, japonés, cirílico, árabe…) sin problema. Obviamente aquí cada uno que configure los detalles como le parezca mejor. Esto es una plantilla que yo uso.

Ahora que tenemos el fichero, vamos a quitar permisos para que no pueda ser modificado o leído por terceros:

$ chown www-data:www-data wp-config.php
$ chmod 600 wp-config.php

El siguiente bloque es el que va a permitirnos tener sitios webs distintos. La primera vez hemos de reconfigurar el sitio web por defecto, que se supone que sólo será para cuando se acceda a las IP (o cuando haya algo configurado apuntando a esas IP pero no sea algo que tengamos en marcha).

$ cd /etc/nginx/sites-enabled/
$ rm default
$ cd /etc/nginx/sites-available/
$ rm default
$ vim default.conf

Contenido:

server {
  listen 80 default_server;
  #listen 443 ssl http2 default_server;
  server_name _;
  root /var/www/html;
  index index.html index.php;
  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }
  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }
  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
  }
  location ~ /.well-known {
    allow all;
  }
  location ~ /\.ht {
    deny all;
  }
}

Activaremos el sitio web, para que funcione y posteriormente comprobaremos que el nginx dice que está todo correcto:

$ ln -s /etc/nginx/sites-available/default.conf /etc/nginx/sites-enabled/
$ nginx -t

Lo que nos debería mostrar por pantalla un mensaje tal que:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Si todo va correctamente, ahora sí que crearemos un sitio web para el WordPress; lo crearemos en dos fases. La primera simplemente haremos que responda el sitio y que se puedan generar los certificados de seguridad. Posteriormente los activaremos.

$ vim example.com.conf

Contenido:

server {
  listen 80;
    listen [::]:80;
  server_name untor.com www.untor.com;
    root /var/www/untor.com;
  location ~ /.well-known {
    allow all;
  }
}

Activaremos el sitio y comprobaremos que la configuración es correcta y reiniciaremos el servicio:

$ ln -s /etc/nginx/sites-available/untor.com.conf /etc/nginx/sites-enabled/
$ nginx -t
$ systemctl restart nginx.service

Ahora crearemos el certificado y los ficheros necesarios para que vaya el HTTPS:

$ certbot --nginx certonly

Elegiremos las opciones y los certificados que se requieran, y una vez que estén creados, los añadiremos al fichero de configuración. Sustituiremos el que acabamos de crear por uno mucho más detallado:

$ vim example.com.conf

Contenido:

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;
  return 301 https://www.example.com$request_uri;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  #ssl on;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
  ssl_ecdh_curve secp384r1;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;
  ssl_session_timeout 1d;
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  resolver 208.67.222.222 valid=300s;
  resolver_timeout 5s;
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  server_name example.com;
  return 301 https://www.example.com$request_uri;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  #ssl on;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
  ssl_ecdh_curve secp384r1;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;
  ssl_session_timeout 1d;
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  resolver 208.67.222.222 valid=300s;
  resolver_timeout 5s;
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_dhparam /etc/ssl/certs/dhparam.pem;
  server_name www.example.com;
  root /var/www/example.com;
  index index.php;
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }
  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }
  location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
  }
  location ~* \.(bmp|bz2|cur|doc|docx|exe|gif|eot|gz|htc|ico|jpeg|jpg|mid|midi|mp3|mp4|ogg|ogv|otf|png|ppt|pptx|rar|rtf|svg|svgz|tar|tgz|ttf|wav|webm|woff|woff2|xls|xlsx|zip)$ {
    expires max;
    add_header Cache-Control "public";
    log_not_found off;
  }
  location ~* \.(atom|css|js|rss)$ {
    expires 1d;
    add_header Cache-Control "public";
  }
  location ~ /.well-known {
    allow all;
  }
  location ~ /\.ht {
    deny all;
  }
  location ~* wp-config.php {
    deny all;
  }
}

Si todo va bien, ahora deberíamos probar este fichero, y si nos da correcto, reiniciar el servidor web:

$ nginx -t
$ systemctl restart nginx.service

Ahora es el momento de irte a tu navegador web, y visitar https://www.example.com/ (el dominio que hayas configurado), y debería aparecerte el instalador de WordPress que te pedirá el nombre de tu sitio, tu usuario y contraseña, etcétera.

En principio el sistema ya está funcionando correctamente, y permitiría, usando estos últimos bloques, disponer de varios sitios web funcionando en paralelo. pero antes de acabar quiero hacer unos pequeños cambios a la configuración del PHP.

$ cd /etc/php/7.0/fpm/
$ vim php.ini

Sustituir:

error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

Por:

error_reporting = E_ALL

Este cambio no debe afectar en principio a nada, ya que el muestreo de errores seguirá apagado, pero si en un momento determinado se ha de activar, lo mejor es que muestre absolutamente todos los errores habidos y por haber.

Sustituir:

upload_max_filesize = 2M

Por:

upload_max_filesize = 32M

Este cambio es bastante útil para el WordPress, ya que permitirá que se puedan subir ficheros (imágenes, adjuntos, etc…) -los llamados Media- de ese tamaño que pongamos. Por norma general si sólo usas imágenes 2 Megas puede ser una cifra correcta, pero si realmente trabajas con imágenes de calidad, ficheros PDF o similares, seguro que se queda corto, por lo que puedes subirlo a 32 Megas, 128 Megas, e incluso si tienes vídeos y los quieres subir así, a 1 Giga (o más). Tampoco recomiendo pasarse.

Sustituir:

;date.timezone =

Por:

date.timezone = UTC

Y ya mi TOC me lleva a configurar algo que genera a veces problemas en muchos sistemas, que es la hora. Al principio pusimos la máquina en hora UTC, y ahora configuramos el PHP para que siga también esa regla. Finalmente lo único que queda es aplicar los cambios, reiniciando el propio PHP.

$ systemctl restart php7.0-fpm.service

Y hasta aquí el sistema que configura una máquina Ubuntu 16 para uno o más WordPress, con sus certificados HTTPS, con un sistema integrado de caché (activando además la propia de WordPress), con una base de datos de última generación, con un PHP a su última versión… Comod ecía, seguro que hay más trucos, configuraciones y demás, pero esto ha de servir de base para una instalación de uno o varios WordPress, y que funciona.

Configuración Inicial de WordPress

Hoy en la WordPress Barcelona estoy dando una charla sobre la Configuración Inicial de WordPress. Es curioso porque hace 3 años cuando comenzamos las meetups en Barcelona, la primera de ellas (que también di yo) se llamaba La configuración perfecta y básicamente el concepto de la presentación era el mismo. En aquella ocasión había una carga técnica muy elevada que, como feedback de aquella ocasión, en esta he eliminado a la mínima expresión necesaria.

En esta presentación comienzo desde el momento en el que el WordPress ha sido instalado pero aún no has entrado, explicando detalles sbre el acceso HTTPS, actualizaciones, la configuración básica del propio sistema WordPress, los plugins básicos que debería llevar WordPress de srie instalados (no quiero que los lleve, pero sí que tú los instales), y cómo configurar cada uno de ellos de la mejor forma inicial.

Pues, a partir de aquí no me queda nada más que decir que si te interesa, puedes descargarte la presentación Configuración Inicial de WordPress (PDF 5.42 MB).

WordPress y algunos problemas de phishing / spam

Desde hace unos pocos meses cada vez más veo hackeos en los WordPress. No tengo muy claro si es un problema interno del propio WordPress o si es de algún plugin (sí que me he encontrado en algún caso problemas con algunas plantillas que no estaban actualizadas y que usaban software externo para subir ficheros).

La cuestión es que como a veces e difícil darse cuenta de si te han colado algún bicho, me propuse hace un par de días a hacer una pequeña y simple mejora que, en general, debe de funcionar. La idea es sencilla: no dejar funcionar el PHP en las carpetas de subida de ficheros.

En general los usuarios tienen configurada la carpeta /wp-content/uploads/ y todo lo que suele haber aquí son imágenes o ficheros binarios, pero nada ejecutable. Si este es el caso, se puede incluir un fichero .htaccess en esta carpeta con el siguiente contenido:

RewriteEngine off
RemoveHandler .php
RemoveType .php
php_flag engine off

Básicamente aquí se consiguen varias cosas: que no funcione el “mod_rewrite”, que PHP no pueda ejecutar los ficheros PHP y, de paso, directamente apagar el PHP en estas carpetas.

Seguramente este fichero se podrá subir en algunas otras carpetas dentro de toda la estructura, aunque mi recomendación es que sólo se aplique a las carpetas a las que el usuario tiene acceso (¡cuidado! que depende de cómo esta configuración se hereda en subcarpetas) y en las carpetas de caché.

WordPress Child Themes y WPO

Desde hace unas cuantas versiones de WordPress que está disponible la opción de los Child Themes. Este sistema una de las cosas que pide es que, la creación de este sistema tiene como base hacer un @import del CSS padre.

En este sentido, aunque los ficheros PHP sobre escriben los otros, por lo que no hay afectación directa sobre el WPO, sí que hay un detalle que no es del todo correcto, y es precisamente ese, el base para crear un hijo, el de hacer ese import.

Y es que los @import son una de las reglas “prohibidas” en cuanto a WPO. Así que… ¿qué solución tenemos para arreglar esto?. La primera de ellas es hacer un copy & paste de todo el contenido del fichero. ¿Problema? Que si hay una actualización de la plantilla padre la información de la plantilla hijo no se actualizará.

La otra opción, que es algo más elaborada, lo que viene a hacer es lo mismo pero de forma automatizada. Para empezar el fichero no será un CSS al uso, sino que será físicamente un fichero PHP. En esa carpeta tendremos que incluir un .htaccess que haga un “rewrite” del .CSS al .PHP. Con esto tenemos solventado que el sistema vea que eso es una hoja de estilos válida. El fichero .htaccess será algo tal que así:

RewriteEngine On
RewriteRule ^style.css style.php

Ahora lo siguiente es automatizar el fichero .CSS. Para ello debemos hacer un par de temas. Lo primero es pensar que es un fichero programado, de forma que hay que devolver el contenido en el formato correcto. Así, la primera línea del fichero será algo tal que:

header('Content-Type: text/css');

Lo siguiente será incluir el contenido del fichero CSS original. Por ejemplo:

$contenidocss = trim(file_get_contents('../twentyeleven/style.css'));
if($contenidocss) {
  echo $contenidocss;
}

Si lo juntamos todo, nos quedaría algo similar a esto en el fichero style.php:

<?php
header('Content-Type: text/css');
?>
/*
Theme Name:     Twenty Eleven Child
Theme URI:      http://example.com/
Description:    Child theme for the Twenty Eleven theme 
Author:         Your name here
Author URI:     http://example.com/about/
Template:       twentyeleven
Version:        0.1.0
*/
<?php
$contenidocss = trim(file_get_contents('../twentyeleven/style.css'));
if($contenidocss) {
  echo $contenidocss;
}
?>
// a partir de aquí van los cambios y nuestros retoques del CSS
body {
  font-family: Verdana;
}

Con esto conseguiremos que el waterfall del sitio web no se vea bloqueado por culpa de ese @import en el CSS, que además siempre está al principio de las peticiones y no tiene ningún sentido.