Instalar un servidor web LEMP (IV). Certificado SSL/TLS de Let’s Encrypt

Tras instalar el sistema operativo base (Debian 9 Stretch), los servidores (MySQL y nginx) y configurarlos y migrar la instalación de WordPress desde otro servidor, llega el momento de isntalar un certificado SSL/TLS de Let’s Encrypt para proteger la comunicación.

Esta es la cuarta entrada de un conjunto de entradas relacionadas, en las que explico cómo instalar un servidor web LEMP con WordPress y Let’s Encrypt en Debian 9 Stretch.

  1. Instalación y configuración del sistema operativo.
  2. Instalación y configuración del MySQL, nginx y PHP.
  3. Migración de WordPress desde el servidor antiguo al nuevo.
  4. Obtención y configuración de un certificado SSL/TLS de Let’s Encrypt.

Convenciones

# – indica que el comando que viene a continuación tiene que ser ejecutado con permisos de root directamente con el usuario root o mediante el comando sudo.
$ – indica que el comando que viene a continuación puede ser ejecutado por un usuario normal sin privilegios de administración.

¿Qué es Let’ s Encrypt?

Let’s Encrypt es una autoridad de certificación (CA) libre, gratuita y automatizada, operada por el Internet Security Research Group (ISRG), con miembros de entidades tan importantes como Akamai, Mozilla, Cisco, EFF, CoreOS, OVH, Google o la Internet Society. Su financiación se lleva a cabo a través de sponsors, tales como  Akamai, Mozilla, Cisco, EFF, OVH, Google, Internet Society, Automattic, Facebook o Digital Ocean.

Su misión es proporcionar certificados digitales que permitan habilitar HTTPS (SSL/TLS) en sitios web de la forma más sencilla posible.

Instalación

La instalación es sencilla y se encuentra explicada en la web de la EFF.

En el menú que me muestra selecciono el servidor que estoy usando (nginx) y el sistema operativo (Debian 9 stretch) y automáticamente me lleva la web donde explican el proceso de instalación.

Certbot EFF

Para realizar la instalación del cliente tengo que ejecutar

# apt-get update
# apt-get install certbot

Dado que la arquitectura usada (Debian y nginx) no soporta la instalación automática del certificado, lo que voy a hacer es obtener el certificado y posteriormente instalarlo.

Para obtener el certificado ejecuto el siguiente comando, que va a ir mostrándome un menú interactivo por línea de comandos, en el que iré introduciendo la información solicitada, que muestro en negrita. Los dominios están cambiados para adecuarme al ejemplo que estoy siguiendo a lo largo de estos tutoriales, pero la salida es de un caso real con otros dominios.

Hay que tener en cuenta que considero dos dominios diferentes example.com y www.example.com, ya que el segundo caso es un subdominio, ampliamente usado en web.

# certbot certonly
Saving debug log to /var/log/letsencrypt/letsencrypt.log

How would you like to authenticate with the ACME CA?
-------------------------------------------------------------------------------
1: Place files in webroot directory (webroot)
2: Spin up a temporary webserver (standalone)
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel):correo@electronico.com

-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf. You must agree
in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A
Please enter in your domain name(s) (comma and/or space separated) (Enter 'c'
to cancel): www.example.com example.com subdominio.example.com 
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.example.com
http-01 challenge for example.com
http-01 challenge for subdominio.example.com 

Select the webroot for www.example.com:
-------------------------------------------------------------------------------
1: Enter a new webroot
-------------------------------------------------------------------------------
Press 1 [enter] to confirm the selection (press 'c' to cancel): 1
Input the webroot for www.example.com: (Enter 'c' to cancel):/var/www/example_com/wordpress

Select the webroot for example.com:
-------------------------------------------------------------------------------
1: Enter a new webroot
2: /var/www/example_com/wordpress
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2

Select the webroot for subdominio.example.com:
-------------------------------------------------------------------------------
1: Enter a new webroot
2: /var/www/example_com/wordpress
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
Input the webroot for subdominio.example.com: (Enter 'c' to cancel):/var/www/subdominio_example_com/wordpress

Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0000_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0000_csr-certbot.pem

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/example.com/fullchain.pem. Your cert will
expire on 2017-10-30. To obtain a new or tweaked version of this
certificate in the future, simply run certbot again. To
non-interactively renew *all* of your certificates, run "certbot
renew"
- If you lose your account credentials, you can recover through
e-mails sent to correo@electronico.com.
- Your account credentials have been saved in your Certbot
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Certbot so
making regular backups of this folder is ideal.
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

Ya tengo el certificado creado y algún archivo más, que comentaré a posteriori.

Estos archivos se almacenan en “/etc/letsencrypt/archive/example.com” y se referencian a través de los archivos existentes en “/etc/letsencrypt/live/example.com”, que no son más que enlaces simbólicos a los archivos que se encuentran en “/etc/letsencrypt/archive/example.com”. Esto se hace así porque cada vez que se renueva el certificado se cambia el enlace simbólico para que apunte al nuevo certificado y así no tener que andar cambiando referencias en los archivos de configuración, como explicaré a continuación.

Una vez que tengo el certificado generado en mi servidor, lo siguiente que voy a hacer es configurar nginx para que use el certificado.

Configuración de nginx

Ahora tengo que indicarle a nginx que redireccione todas las peticiones HTTP a HTTPS y que utilice los certificados creados anteriormente. Además habilito HTTP/2, que ya se encuentra soportado por la mayoría de navegadores. En este enlace puedes ver las principales diferencias con HTTP/1.x.

Lo voy a hacer para example.com, donde también incluyo el subdominio www.example.com. De la misma forma tendría que hacerlo para subdominio.example.com, pero como es similar ya no lo detallo.

Edito el archivo /etc/nginx/sites-available/example_com, añadiendo lo indicado en negrita.

server {
    listen 80;
    listen [::]:80;
    root /var/www/example_com/wordpress;
    index index.php index.html index.htm;
    server_name example.com www.example.com;
    location ~ /\.well-known/acme-challenge/ {
        allow all;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

El primer elemento lo que hace es permitir la respuesta HTTP a todo lo que se encuentre en el directorio “.well-known/acme-challenge/”, usado por Let’s Encrypt para crear y renovar los certificados, ya que para verificar que en nuestro servidor está configurado ese dominio y las DNS apuntan a ese servidor, lo que hace Let’s Encrypt es generar un archivo en el directorio “.well-known/acme-challenge/” (referenciado respecto a la raíz del directorio donde está instalado el sitio web, indicado en el archivo de configuración de nginx por la variable “root”) y tratar de acceder externamente navegando a ese archivo.

Todas las peticiones que se soliciten en esa dirección no serán redireccionadas a HTTPS.

location ~ /\.well-known/acme-challenge/ {
    allow all;
}

El segundo elemento redirecciona las peticiones HTTP a HTTPS mediante una redirección 301 (elemento movido permanentemente).

location / {
    return 301 https://$host$request_uri;
}

En el mismo archivo de configuración añado la configuración para las peticiones HTTPS, que explico a continuación.

server {
    listen 443 ssl http2;
    root /var/www/example_com/wordpress;
    index index.php index.html index.htm;
    server_name example.com www.example.com;
    location / {
        try_files $uri $uri/ /index.php?q=$uri&$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.0-fpm-example_com.sock;
    }
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers On;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    ssl_stapling on;
    ssl_stapling_verify on;
    add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
}

Escucho el puerto 443, con SSL habilitado y HTTP/2.

listen 443 ssl http2;

Indico el directorio raíz donde se encuentra el sistema web.

root /var/www/example_com/wordpress;

Defino los elementos que serán usados como index.

index index.php index.html index.htm;

Indico los dominios y/o subdominios que atenderé.

server_name example.com www.example.com;

Compruebo la existencia de archivos en el orden indicado y usa la primera ocurrencia para devolver el archivo.

location / { 
    try_files $uri $uri/ /index.php?q=$uri&$args; 
}

Cargo un código externo de configuración, mediante el include, y paso la llamada al servicio FastCGI, encargado de procesar el código.

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.0-fpm-example_com.sock;
}

Indico los protocolos SSL que se pueden usar

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

Indico los cifrados habilitados.

ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';

Especifico que el cifrado de servidor debe prevalecer sobre el cifrado de cliente cuando se utilizan los protocolos SSLv3 y TLS.

ssl_prefer_server_ciphers On;

Indico la ruta de los archivos del certificado (ssl_certificate), la clave secreta (ssl_certificate_key) y el certificado de la autoridad de certificación (CA) (ssl_trusted_certificate)

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; 
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; 
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

Habilito las respuestas OSCP.

ssl_stapling on;

Habilito la verificación de las respuestas OSCP.

ssl_stapling_verify on;

Habilito HTTP Strict Transport Security

add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";

Guardo el archivo.

Compruebo que la configuración es correcta.

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

Reinicio el servicio

# systemctl restart nginx.service

Ahora mismo la web ya está accesible mediante HTTPS.

Para comprobar el certificado puedes ejecutar el test disponible en este enlace o en este enlace.

Actualizo http por https en MySQL

Solo queda por actualizar las referencias internas que pueda tener WordPress a http://, debido a que WordPress guarda las referencias absolutas a las URL.

Para ello abro una consola MySQL

# mysql -uroot -p
Enter password:

Selecciono la base de datos que quiero usar.

mysql> use example_com;

Ejecuto los comandos de actualización de las referencias, tanto para el dominio principal como para el subdominio www.

UPDATE wp_options SET option_value = replace(option_value, 'http://example.com', 'https://example.com') WHERE option_name = 'home' OR option_name = 'site url';
UPDATE wp_options SET option_value = replace(option_value, 'http://www.example.com', 'https://www.example.com') WHERE option_name = 'home' OR option_name = 'site url'; 

UPDATE wp_posts SET guid = replace(guid, 'http://example.com','https://example.com');
UPDATE wp_posts SET guid = replace(guid, 'http://www.example.com','https://www.example.com'); 
UPDATE wp_posts SET post_content = replace(post_content, 'http://example.com', 'https://example.com');
UPDATE wp_posts SET post_content = replace(post_content, 'http://www.example.com', 'https://www.example.com'); 

UPDATE wp_postmeta SET meta_value = replace(meta_value,'http://example.com','https://example.com');
UPDATE wp_postmeta SET meta_value = replace(meta_value,'http://www.example.com','https://www.example.com');

Renovación automática

Solo me falta por configurar la renovación automática del certificado, ya que su validez es de 3 meses. Para ello lo que hago es introducir en el cron una tarea que trate de renovar el certificado dos veces por día.

A priori parece que con hacerlo mensualmente llegaría, pero la recomendación de Let’s Encrypt es la siguiente:

Note:

if you’re setting up a cron or systemd job, we recommend running it twice per day (it won’t do anything until your certificates are due for renewal or revoked, but running it regularly would give your site a chance of staying online in case a Let’s Encrypt-initiated revocation happened for some reason). Please select a random minute within the hour for your renewal tasks.

Para probar la renovación automática ejecuto el siguiente comando, con el parámetro “dry-run”, que simula la renovación.

# sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/example.com.conf
-------------------------------------------------------------------------------
Cert not due for renewal, but simulating renewal for dry run
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for example.com
http-01 challenge for www.example.com
http-01 challenge for subdominio.example.com
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0002_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0002_csr-certbot.pem
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates below have not been saved.)

Congratulations, all renewals succeeded. The following certs have been renewed:
/etc/letsencrypt/live/example.com/fullchain.pem (success)
** DRY RUN: simulating 'certbot renew' close to cert expiry
** (The test certificates above have not been saved.)

IMPORTANT NOTES:
- Your account credentials have been saved in your Certbot
configuration directory at /etc/letsencrypt. You should make a
secure backup of this folder now. This configuration directory will
also contain certificates and private keys obtained by Certbot so
making regular backups of this folder is ideal.

Lo siguiente que hago es probar el comando, pero añadiéndole un “post hook“, que permite ejecutar un comando por shell tras el intento de renovación del certificado. Esto lo hago para que nginx cargue los nuevos certificados, ya que podría pasar que los tuviera renovados y que, al no recargar la configuración, siguiera usando los antiguos y estos caducaran.

# certbot renew --post-hook "systemctl restart nginx.service"

Su salida es

Saving debug log to /var/log/letsencrypt/letsencrypt.log

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/example.com.conf
-------------------------------------------------------------------------------
Cert not yet due for renewal

The following certs are not due for renewal yet:
/etc/letsencrypt/live/example.com/fullchain.pem (skipped)
No renewals were attempted.
No hooks were run.

El programa trató de renovar el certificado, pero como no está próximo a caducar no hizo nada. Tampoco se reinicia nginx, a pesar de que en la documentación indican que el post-hook se ejecuta siempre:

–pre-hook and –post-hook hooks run before and after every renewal attempt. If you want your hook to run only after a successful renewal, use –renew-hook in a command like this.

Busco la ubicación del programa certbot, ya que en el cron usaré rutas absolutas

# which certbot
/usr/bin/certbot

Su ubicación es

/usr/bin/certbot

Hago lo mismo para systemctl

# which systemctl
/bin/systemctl

Su ubicación es

/bin/systemctl

Edito el cron

# sudo crontab -e

Y añado la siguiente línea, que se ejecuta a las 2:15 y a las 14:15 horas (2 veces al día) y será la encargada de renovar los certificados existentes en la máquina.

15 2,14 * * * /usr/bin/certbot renew --quiet --post-hook "/bin/systemctl restart nginx.service"

Con esto ya tendría programada la renovación automática del certificado.

Más información

Leave a Reply

Your email address will not be published. Required fields are marked *