Fecha: 2019-11-25 Tiempo de lectura: 10 minutos Categoría: Operaciones Tags: CA / ssl / https / certificado / openssl
Es muy habitual tener varios entornos en donde ejecutar nuestras aplicaciones; algunos son entornos productivos o copias exactas, pero muchos otros son entornos de desarrollo y de pruebas que solo son accedidos por una minoría, normalmente de nuestra misma empresa. Y si usan certificados SSL válidos, el coste se dispara.
En estos casos podemos recurrir a generar certificados autofirmados, en los que solemos confiar cuando el navegador nos los presenta. Sin embargo, la arquitectura basada en microservicios nos plantea nuevos desafíos, que convierten esta acción de confianza en un problema:
Podemos simplificar todos ellos de forma fácil si generamos nuestros certificados usando uno nuestro intermedio; de esta forma podemos generar los certificados finales de forma rápida y automatizada. Esto simplifica las relaciones de confianza, que quedan reducidas a una sola: confiar en el certificado intermedio.
Trabajando de esta forma, los certificados finales serán confiables sí también lo es el certificado intermedio. De esto último se encargará una sola excepción manual. Así tendremos a nuestra disposición una autoridad certificadora (CA) de “estar por casa”, simple, sencilla y efectiva. Y lo mejor: solo necesitamos instalar un solo paquete, que seguramente ya tenemos instalado: openssl.
Una CA no es otra cosa que una metodología de trabajo. La idea es que es una fábrica para firmar certificados, basándonos en un certificado master. A su vez, este certificado puede estar firmado por otro, y así sucesivamente.
NOTA: Para simplificar, vamos a asumir que solo tenemos un certificado master, que vamos a tratar como nuestro certificado raíz o intermedio.
El primer paso para crear un certificado es generar una clave. Esta clave es privada, y no debería ser accesible a nadie ajeno a nuestros intereses.
gerard@umbra:~/services/ca$ openssl genrsa -out ca.key 2048
Generating RSA private key, 2048 bit long modulus
...................................................+++++
.+++++
e is 65537 (0x010001)
gerard@umbra:~/services/ca$
TRUCO: Es interesante añadir el flag -des3 para que la clave esté cifrada
con una contraseña. No lo he puesto para que la operación de firma no me la pida
y se pueda automatizar el proceso en un futuro.
Teniendo la clave, la podemos usar para generar un certificado autofirmado, que va a ser nuestro certificado raíz. Este certificado es público, y lo deberemos distribuir entre todos aquellos clientes que tengan que confiar en él.
gerard@umbra:~/services/ca$ openssl req -sha256 -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=LinuxSysadmin CA"
gerard@umbra:~/services/ca$
NOTA: El campo CN solo sirve para que el navegador lo ponga en la lista de
autoridades conocidas, y es el texto que va a aparecer en el nombre. Realmente se
puede poner lo que nos apetezca, y nada va a cambiar.
En este punto tenemos dos ficheros: un ca.key y un ca.crt.
Muchos navegadores modernos exigen como medida extra de seguridad que el dominio
de un sitio aparezca en dos lugares del certificado final: el campo CN y el
campo subjectAltName. Para ello necesitamos firmar los certificados con cierto
fichero de opciones que es siempre el mismo, excepto el dominio; vamos a utilizar
una especie de plantilla, que dejamos aquí para el futuro para que la operación
de firma la utilice:
gerard@umbra:~/services/ca$ cat v3.ext.tpl
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = %%DOMAIN%%
gerard@umbra:~/services/ca$
Con esto tenemos nuestra CA funcional, y no tendremos que tocarla hasta que tengamos que cambiar el certificado raíz o la clave, ya sea porque han sido comprometidos o porque ha caducado el certificado a los 10 años indicados.
Esta operación se va a tener que hacer para cada certificado generado.
TRUCO: Para facilitar el copy-paste de comandos, todos ellos van a utilizar una variable de entorno para indicar el dominio, que es fácil de cambiar y sirve en varios puntos de los diferentes comandos usados.
gerard@umbra:~/services/ca$ export DOMAIN=web.local
gerard@umbra:~/services/ca$
Para crear un certificado nuevo necesitamos una clave nueva. Esta clave se genera una sola vez y se puede reutilizar hasta que decidamos revocarla por razones de fuerza mayor, sean de seguridad o de pérdida de la misma. Así pues, si ya la tenemos, podemos saltar este paso.
gerard@umbra:~/services/ca$ openssl genrsa -out ${DOMAIN}.key 2048
Generating RSA private key, 2048 bit long modulus
.....................................................................+++++
.................................+++++
e is 65537 (0x010001)
gerard@umbra:~/services/ca$
Teniendo la clave, necesitamos hacer una petición de firma (CSR). Esta será firmada por el certificado raíz para generar el certificado final, y nuevamente podemos reciclar el fichero tanto como queramos, incluso irlo firmando de nuevo cuando el certificado generado caduque, sin cambiar el CSR.
gerard@umbra:~/services/ca$ openssl req -new -sha256 -out ${DOMAIN}.csr -key ${DOMAIN}.key -subj "/CN=${DOMAIN}"
gerard@umbra:~/services/ca$
TRUCO: El campo CN debe coincidir con el nombre de dominio, o será rechazado por
cualquiera que intente verificar el certificado mostrado, sea un navegador o una librería.
El firmado es el proceso en el que un CSR se convierte en un certificado correcto.
La firma es una operación caduca, que dura según se lo indiquemos en el parámetro
-days; transcurrido ese periodo, la validación fallará siempre, hasta que firmemos
otra vez el CSR (o una nuevo), creando un nuevo certificado en el proceso.
gerard@umbra:~/services/ca$ sed "s/%%DOMAIN%%/${DOMAIN}/" v3.ext.tpl > v3.ext
gerard@umbra:~/services/ca$ openssl x509 -sha256 -CA ca.crt -CAkey ca.key -req -in ${DOMAIN}.csr -days 365 -CAcreateserial -out ${DOMAIN}.crt -extfile v3.ext
Signature ok
subject=CN = web.local
Getting CA Private Key
gerard@umbra:~/services/ca$ rm v3.ext
gerard@umbra:~/services/ca$
TRUCO: Fijáos en el uso de sed para crear el fichero v3.ext a partir de
la plantilla que creamos en v3.ext.tpl. Luego lo usamos y lo limpiamos.
En este punto tenemos 3 ficheros: web.local.key, web.local.csr y web.local.crt.
Los conservaremos todos porque la clave y el CSR nos pueden servir en un futuro,
y la clave y el certificado se necesitan para su uso en los servicios SSL. No hace
falta ser muy conservador tampoco; los podemos volver a crear cuando queramos.
Todos los servicios que necesiten certificados, necesitan también la clave. Hay algunas variaciones en el formato de los ficheros de certificados; indico como van en los dos servicios SSL más usados en este blog:
Una configuración de nginx para un sitio estático HTTPS podría ser la siguiente:
server {
server_name web.local;
listen 443 ssl;
ssl_certificate /run/secrets/web.local.crt;
ssl_certificate_key /run/secrets/web.local.key;
root /srv/www;
index index.html;
error_page 404 /404.html;
location /404.html {
internal;
}
}
NOTA: Para los que no lo sospechen, la configuración anterior se utiliza en un contenedor docker usando secretos y configuraciones.
Si hacemos una petición al dominio anterior, veremos que falla: el certificado
web.local falla la verificación sin más motivos ni errores que el fallo del
certificado issuer, que es el intermedio, del que no confía.
gerard@umbra:~/services/webserver$ curl https://web.local/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl performs SSL certificate verification by default, using a "bundle"
of Certificate Authority (CA) public keys (CA certs). If the default
bundle file isn't adequate, you can specify an alternate file
using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
the bundle, the certificate verification probably failed due to a
problem with the certificate (it might be expired, or the name might
not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
the -k (or --insecure) option.
gerard@umbra:~/services/webserver$
Vamos a ver algunos detalles, ignorando la verificación:
gerard@umbra:~/services/webserver$ curl -svk https://web.local/ 2>&1 | egrep "Host|CN=|h1"
* subject: CN=web.local
* issuer: CN=LinuxSysadmin CA
> Host: web.local
<h1>Hello world!</h1>
gerard@umbra:~/services/webserver$
Podemos comprobar que estamos solicitando el Host: web.local, y se nos presenta
el certificado de CN=web.local, que está firmado por el issuer, que es
el certificado CN=LinuxSysadmin CA (en el que no confiamos todavía). Por lo
demás, todo parece correcto.
Ahora nos urge indicar al cliente HTTPS indicar que debe confiar en el certificado
intermedio, que el el que llamamos ca.crt, y que deberemos distribuir adecuadamente.
Las peticiones curl aceptan un parámetro indicando un certificado de confianza.
Podemos poner directamente el de web.local o el intermedio, que es el objetivo:
gerard@umbra:~/services$ curl -v --cacert ca/ca.crt https://web.local/
...
* Server certificate:
* subject: CN=web.local
...
* subjectAltName: host "web.local" matched cert's "web.local"
* issuer: CN=LinuxSysadmin CA
* SSL certificate verify ok.
...
<h1>Hello world!</h1>
...
gerard@umbra:~/services$
Si estamos protegiendo por HTTPS un servicio REST, la idea es que el consumidor sea el que confíe en el certificado de la CA. Esto es dependiente de cada librería, aunque voy a poner un ejemplo con python-requests que es la que utilizo casi siempre, por su excelente documentación y facilidad de uso.
gerard@umbra:~/services$ python3
...
>>> import requests
>>>
Si el certificado no está aceptado, obtenemos una excepción:
>>> r = requests.get('https://web.local/')
Traceback (most recent call last):
...
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:720)
...
requests.exceptions.SSLError: HTTPSConnectionPool(host='web.local', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:720)'),))
>>>
Podemos optar por ignorar el certificado completamente, pero no se recomienda:
>>> r = requests.get('https://web.local/', verify=False)
/home/gerard/services/env/lib/python3.5/site-packages/urllib3/connectionpool.py:1004: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
InsecureRequestWarning,
>>> r.text
'<h1>Hello world!</h1>\n'
>>>
Si embargo, podemos indicar el certificado final o el intermedio en el parámetro
verify, lo que causa plena confianza con el certificado de la CA. Nuevamente
indico que necesitaremos tener el certificado (que no la clave), en un fichero local.
>>> r = requests.get('https://web.local/', verify='ca/ca.crt')
>>> r.text
'<h1>Hello world!</h1>\n'
>>>
Los navegadores tienen una forma peculiar de aceptar nuevas autoridades certificadoras.
Cada uno es un mundo, pero por lo general suelen tener un apartado de configuración,
en donde podemos importar certificados (en nuestro caso, el ca.crt).
En este ordenador, tengo chromium, y llego a esta configuración si voy a la URL
chrome://settings/certificates. Basta con ir a la pestaña “Authorities” y darle
al botón de “Import”. Tras importar el certificado, aparece en la lista, en donde
lo podéis ver, examinar o eliminar cuando os convenga.
NOTA: El navegador guarda el certificado, con lo que no necesitamos repetir este paso nunca más, a menos que cambiemos el certificado o lo hayamos borrado del navegador en una acción manual (o reinstalemos el navegador).
De ahora en adelante (y hasta la eliminación), los certificados firmados por nuestra CA, van a ser aceptados como seguros, sin ningún tipo de problema por parte de este navegador concreto. Para el resto de navegadores, buscad en la web.
Nuestros certificados van a caducar pasado el tiempo de vigencia. Si se han seguido los comandos indicados, el certificado de la CA va a caducar en 10 años (y va a haber que redistribuirlo o importarlo en el navegador), y los certificados finales van a caducar en 1 año. Eso significa que vamos a tener que volver a recrear el certificado de la CA y refirmar un CSR para cada dominio (que puede ser el mismo) cada cierto tiempo.
Por supuesto, si añadimos más dominios a nuestro servidor web, proxy o balanceador, vamos a tener que generar nuevos certificados, con sus claves y CSRs. Eso no entraña ningúna dificultad y, como confiamos en el certificado de la CA que los firma, no va a haber que añadir más excepciones al navegador ni a nuestro código consumidor.
Eso convierte en el paso intermedio de crear una CA en una herramienta cómoda; añade un poco de complejidad a nuestro algoritmo de generación de certificados, pero a la larga nos libera de muchos pasos relacionados con la confianza de los certificados. Si tenemos una estrategia centralizada de distribución del certificado de la CA, los usuarios de nuestra organización ni siquiera se van a enterar del engaño…