Instalar SSL de Let’s Encrypt en Google appEngine

tl;dr;

Muchas de las tiendas de eCommerce que construimos en akuamedia, están alojadas en la nube, y una gran cantidad de ellas directamente en Google appEngine, la modalidad serverless para mí más interesante de Google:  subes el código y con un pequeño fichero de configuración, controlas las capacidades de elasticidad de las instancias, las rutas, su seguridad, etc. ¿Y qué mejor que usar los certificados SSL de Let’s Encrypt?

La seguridad es lo primero, y la navegación de los clientes así como, y más importante, la introducción de datos en los formularios de la tienda, debe estar protegido de curiosos.  Se hace imprescindible la instalación de un certificado SSL.

Si bien hay decenas de proveedores de certificados, para hacer las pruebas básicas de la web antes de entregarla al cliente, desde la aparición de Let’s Encrypt, nosotros usamos sus certificados para trabajar desde el minuto 1 en un dominio seguro bajo el protocolo https.  ¿Por qué Let’s Encrypt? Obtención e instalación simple, certificados gratuitos, certificados admitidos por el 99.5% de los navegadores y creciendo, sistema integrado de obtención, verificación e instalación en cada vez más proveedores de alojamiento. ¿Algún motivo más?

Así que te voy a mostrar paso a paso como pedimos, gestionamos e instalamos con muy pocos pasos manuales los certificados de Let’s Encrypt en una web alojada en Google AppEngine. Y usaré esta misma web que estás viendo como ejemplo, y que desde ahora, accederás por https. Aunque evidentemente, la mayoría de los pasos son extrapolables a cualquier hosting o alojamiento que tengas, sin necesidad de tener acceso SSH al sistema.

 

Asignar los nombres de dominio

La web alojada en appEngine debe tener ya asignado el dominio definitivo en el que se va a publicar, así que debes introducirlos en la pestaña de “Custom Domains”, verificar la propiedad del dominio, y crear y apuntar las entradas A y AAAA en tu DNS a las IP de Google App Engine.

Custom domains en Google appEngine

 

Preparar la respuesta automática de la Web al challenge de Let’s Encrypt

Para que Let’s Encrypt autorice la expedición de un certificado SSL a un determinado dominio, debes demostrar la propiedad (o al menos la gestión) de dicho dominio. Esto evita que cualquiera puede solicitar y usar un certificado expedido a una web sin ser el propietario o gestionar un dominio, y poder usarlo para hacer Phissing, por ejemplo.

Let’s Encrypt usa el sistema de ACME-challenger para comprobar que tú controlas el dominio. Lo que viene siendo algo como “yo creo una dupla de clave-valor para cada dominio a validar, y espero que en determinada URL del dominio, se me devuelva el valor para cada clave“.

La ruta donde Let’s Encrypt va a pedir esa validación es


http://dominio/.well-known/acme-challenge/clave-de-validación

Obviamente, “dominio” será el dominio que quieras proteger con SSL y “clave-de-validación“, el valor que Let’s Encrypt te va a indicar (no seas impaciente, ahora llego a esa parte…).

Configurar el script de respuesta

Usaremos nuestro fichero de configuración app.yaml para gestionar las peticiones a /.well-know/acme-challenge y contruiremos un script que devuelva el valor correcto para cada clave que nos va a pedir Let’s Encrypt.

En el caso de esta web, construída con WordPress, el script lo haré en PHP y por tanto, el fichero app.yaml queda así:

#letsEncrypt stuff
- url: /.well-known/(.*)
  script: .well-known/acme-challenge/letsEncryptResponse.php

Eso quiere decir que cualquier petición que haya a la web que empiece por /.well-known, será gestionado por el script letsEncriptResponse.php, que está situado en el directorio .well-known/acme-challenge.  De esta forma, el mismo script y la misma configuración responderá a tantas peticiones como sea necesario. Obviamente tú puedes llamar al script como quieras, y situarlo en la ruta que quieras, pero debe estar bien indicado en app.yaml.

No limito las posibles peticiones a una sola clave, porque en mi caso quiero configurar el certificado para joseluisgv.com y para www.joseluisgv.com, y como Let’s Encrypt hace una validación por cada nombre de dominio diferente (y cada subdominio lo considera uno diferente), de esta forma podría incluso tener también bajo el certificado tests.joseluisgv.com o beta.joseluisgv.com.

El script letsEncriptResponse.php, de momento y antes de saber las duplas clave-valor que va a asignar Let’s Encrypt, lo construyo así:

<?php
header('Content-type: text/plain');

$requestedURI = $_SERVER['REQUEST_URI'];
$tokens = explode('/', $requestedURI);
$requestedKey = $tokens[sizeof($tokens)-1];

$keyCodesArray = array(
"clave-1" => "valor-1",
"clave-2" => "valor-2"
);

echo $keyCodesArray[$requestedKey];
die();
?>

El array $keyCodesArray, deberá tener tantas duplas como dominios o subdominios queramos pedir a Let’s Encrypt para la misma web.

En este momento, publicamos nuestra aplicación a appEngine y comprobamos que la respuesta a http://dominio/.well-know/acme-challenge/valor-1 nos devuelve en modo texto “valor-1”. De hecho, debería devolverlo también si accedemos a http://dominio/.well-know/valor-1, porque el script de respuesta será el mismo y la última parte de la URL coincide con una de las claves del array.

Puedes afinar mucho más el script, devolviendo un 404 si la clave no existe, comprobando que la IP de solicitud proviene sólo de Let’s Encrypt, y hacerlo tan complejo y completo como quieras.  Para mis fines, que es la comprobación de la propiedad del dominio, me vale con esto.  Una vez tenga los certificados, dejaré comentadas las lineas de app.yaml y esta ruta ya no será operativa. ¿Para qué complicarse más la vida…  😉 ?

Respuesta script acme-challenge let's encrypt

 

Instalar Certbot en osX (Mac)

certbot es un script que ha construido y publicado la EFF para automatizar todo el proceso de solicitud e instalación de certificados a Let’s Encrypt. En nuestro caso deberemos usarlo en modo “manual”, al no tener acceso por SSH al servidor donde está alojada nuestra aplicación.

Pero primero, en mi caso, necesito instalar certbot en mi máquina (un Mac, con OSx). Así que tiraré de brew (un gestor de paquetes que todo propietario de Mac que le guste cacharrear debería tener instalado, súper fácil de usar e instalar y que te quita de engorros de dependencias).

$> brew update
$> sudo mkdir /etc/letsencrypt
$> sudo mkdir /var/lib/letsencrypt
$> sudo mkdir /var/log/letsencrypt
$> brew install letsencrypt

 

Ejecutar certbot para generar las claves

Y una vez que tenemos certbot (el script en realidad se llama letsencrypt), lo ejecutaremos con los parámetros adecuados para ir introduciendo paso a paso los valores necesarios, y además, obtener las duplas clave-valor que nos indicará para actualizar nuestro script letsEncriptResponse.php.  Lo haremos con “sudo”, porque necesita crearse subcarpetas para el dominio y escribir en la ruta del sistema /etc/letsencrypt/live.

Iremos respondiendo paso a paso a cada pregunta que nos haga

$> sudo letsencrypt -a manual certonly
Password:

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

Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
to cancel):joseluisgv.com,www.joseluisgv.com

Obtaining a new certificate
Performing the following challenges:
http-01 challenge for joseluisgv.com
http-01 challenge for www.joseluisgv.com

-------------------------------------------
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that

Are you OK with your IP being logged?
-------------------------------------------------------------------------------
(Y)es/(N)o: Y

-------------------------------------------------------------------------------
Make sure your web server displays the following content at

http://joseluisgv.com/.well-known/acme-challenge/F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0 before continuing:
F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g

If you don't have HTTP server configured, you can run the following
command on the target server (as root):
mkdir -p /tmp/certbot/public_html/.well-known/acme-challenge
cd /tmp/certbot/public_html
printf "%s" F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g > .well-kno \
wn/acme-challenge/F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0

# run only once per server: $(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
-------------------------------------------------------------------------------
Press Enter to Continue

¡¡OJO!!  ¡¡No pulses Enter aún!!!

En la primera pregunta que hace, he escrito los dominios para los cuales necesito el certificado, separados por coma: joseluisgv.com,www.joseluisgv.com

El script me indica que hará 2 peticiones basadas en el protocolo http v1, una por cada dominio, y que si estoy de acuerdo en que nuestra IP (de la máquina donde estoy ejecutando el script) sea almacenada en un log público. Al responde “Y”, el script letsencrypt me devuelve la primera dupla clave-valor, en este caso me indica que hará la petición:

http://joseluisgv.com/.well-known/acme-challenge/F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0

y que esta debe devolver:


F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g

Así que ya tengo la primera clave y valor.

Actualizo letsEncryptResponse.php y vuelvo a publicar la aplicación en appEngine con este cambio (no puedo esperar a tener la segunda dupla porque si no consigue Let’s Encrypt validar una de ellas, de forma secuencial, cancela el proceso).

$keyCodesArray = array(
    "F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0" => 
        "F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g",
    "clave-2" => "valor-2"
);

Compruebo que la web responde lo esperado, el primer valor para la primera clave:respuesta a primera clave de lets encrypt

Y es ahora cuando pulso ENTER en la linea de comandos que tenía “en espera”…

Me presenta entonces el segundo “challenge” con la siguiente dupla:

Make sure your web server displays the following content at
http://www.joseluisgv.com/.well-known/acme-challenge/dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4 before continuing:

dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g

If you don't have HTTP server configured, you can run the following
command on the target server (as root):

mkdir -p /tmp/certbot/public_html/.well-known/acme-challenge
cd /tmp/certbot/public_html
printf "%s" dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g > .well-known/acme-challenge/dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()" 
-------------------------------------------------------------------------------
Press Enter to Continue

Al igual que antes, el script me dice que va a hacer una petición a:

http://www.joseluisgv.com/.well-known/acme-challenge/dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4

y que espera como respuesta:

dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g

Vuelvo a cambiar letsEncryptResponse.php y vuelvo a publicar la aplicación en appEngine con este cambio

$keyCodesArray = array(
    "F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0" => 
        "F5Xo2zYdeCC0n61aWWMIWnjDGpW5QZkq9f0PSW-apg0.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g",
    "dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4" => 
        "dt_PLi-VWXmFclcZGn6sKUGLA06lCezx0umOcIN1fK4.XP4vUGO7FmRKhHU97IkNUxc7-jn9XDNMuS7QFrBvL2g" );

Y compruebo de nuevo accediendo a la URL correspondiente que para esta segunda clave, me devuelve el valor esperado.

Pulso entonces de nuevo ENTER en la linea de comandos, y Let’s Encrypt hace sus validaciones…y si todo ha ido bien, el script termina y me deja una serie de certificados, claves, etc en la ruta/etc/letsencrypt/live/joseluisgv.com, que además, me recuerda en su texto:

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

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/joseluisgv.com/fullchain.pem. Your cert will
   expire on 2017-07-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 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

certificados de letsEncrypot para joseluisgv.com

 

Transformar la clave privada al formato esperado por Google

Por desgracia, el formato en el que genera la clave privada (privkey.pem) este script, no le gusta a Google AppEngine, así que hay que transformarlo. Pero es tan fácil como ejecutar este comando, que generará un segundo fichero de clave privada, con el nombre privkey_fixed.pem

openssl rsa -inform pem -in /etc/letsencrypt/live/joseluisgv.com/privkey.pem -outform pem > \
/etc/letsencrypt/live/joseluisgv.com/privkey_fixed.pem

 

Instalar el certificado de Let’s Encrypt en Google App Engine

Por fin lo tengo todo preparado en mi equipo, ahora toca subirlo a AppEngine… Pero esto es ya juego de niños.

Accedo a la consola de appEngine, a la pestaña de Certificados SSL en la configuración de la aplicación, pulso el botón “Subir un nuevo certificado”:

Gestionar certificados en appEngine

Y en el formulario que aparece, tengo que subir el fichero “fullchain.pem”cert.pem(1)” (o copiar su contenido en la caja de texto) donde indica “clave pública” (el primer fichero o caja de texto). Y donde indica la “clave privada”, tengo que usar el fichero “privkey_fixed.pem” generado en el paso anterior de transformación.Certificados de Lets Encrypt instalados y validos en Google App Engine

Pulso “SUBIR”, y listo… Google hace sus cositas y me indica que todo correcto, con mi dominio asegurado por el certificado, indicando además la fecha de caducidad del mismo para dentro de 3 meses exactos (la validez de los certificados expedidos por Let’s Encrypt).

Certificados de Lets Encrypt instalados y validos en Google App Engine

 

Redirigir dominio a https y eliminar URL de acme-challenge

Por último, sólo me queda eliminar o comentar de app.yaml las lineas que usa Let’s Encrypt para comprobar que el dominio es mío, y de paso, voy a indicar que las peticiones al dominio, para todos sus tipos de elementos (estáticos y dinámicos, y todas las rutas), debe hacerse por vía segura. Modifico, publico la aplicación, y listo.

#letsEncrypt stuff
# - url: /.well-known/(.*)
# script: .well-known/acme-challenge/letsEncryptResponse.php

- url: /?
script: index.php
secure: always

 

Así que ahora, ya puedes acceder a esta mi casa con todas las garantías… 😉

Eso sí, dentro de menos de 3 meses tendré que renovar los certificados. Quizás de para otro post tipo mini-guía.

 

 

NOTA 1

SSL roto con Chrome en Nexus 6Tras tener publicada esta web bajo https y parecer que todo iba correctamente, Jero (@jerolba en Twitter) y tras esta conversación https://twitter.com/jerolba/status/859110193904459782, me ayudó a descubrir que con el fichero de certificado que estaba publicado (cert.pem) no era suficiente, puesto que en ese fichero no viene toda la cadena de validación de la Entidad Certificadora, y hay algunos navegadores y/o Sistemas Operativos que aún no tienen a Let’s Encrypt en su lista de Entidades Certificadoras y por tanto, aun no confían en estos certificados.

Tocaba investigar…

Así que tras leer algunos artículos al respecto, llegué a la conclusión que en lugar de subir el fichero cert.pem, el fichero que debía haber subido era fullchain.pem

¿Por qué? Pues porque este último (fullchain.pem) va concatenado tanto cert.pem -el certificado en sí mismo- como chain.pem -el certificado intermedio de la propia entidad certificadora que valida al primero-

Así que… ¡¡gracias Jero!!

Ahora sí que sí (o eso espero…) ya está este Blog con https. 😉