Asegurando un API con mTLS – Parte 2

En la primera parte Parte 1 expusimos la necesidad de asegurar la información entre los dos últimos tramos del tránsito de un API, el Manejador de APIs y el Servidor de APIs. Ahora daré los pasos detallados de cómo implementar esta técnica en un API. Aunque para simplificar el ejemplo usaré una simple aplicación “hello world” de nginx en docker, los principios son aplicables a prácticamente cualquier tecnología.

En este ejemplo, el servidor se llamará server.healthserver.com y el cliente client.healthserver.com, y ambos estarán usando un certificado autogenerado. En la vida real, el servidor del API probablemente use un certificado universalmente válido y el cliente uno autogenerado, pero, si el servidor está protegido detrás de un Manejador de APIs, entonces no tiene motivo crear un certificado universalmente válido para ninguno de los dos lados, puesto que será una conversación netamente privada.

Crea tu propia Autoridad de Certificados (CA)

Genera una clave privada

openssl genrsa -des3 -out myCA.key 2048

Crea la CA

openssl req -x509 -new -nodes -key myCA.key -sha256 -days 1825 -out myCA.pem

Crea un certificado de cliente

Genera una clave privada

openssl genrsa -out client.healthserver.com.key 2048

Genera el csr (Certificate Request)

openssl req -new -key client.healthserver.com.key -out client.healthserver.com.csr

Genera el archivo de extensión. Crea un archivo con el nombre client.healthserver.com.ext y agrégale lo siguiente:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = client.healthserver.com

Genera el certificado

openssl x509 -req -in client.healthserver.com.csr -CA myCA.pem -CAkey myCA.key \
  -CAcreateserial -out client.healthserver.com.crt -days 825 -sha256 -extfile client.healthserver.com.ext

Create un certificado para el servidor (si es necesario)


Genera una clave privada

openssl genrsa -out server.healthserver.com.key 2048

Genera el csr (Certificate Request)

openssl req -new -key server.healthserver.com.key -out server.healthserver.com.csr

Crea el archivo de extensión. Crea un archivo con el nombre server.healthserver.com.ext y agrégale lo siguiente:

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = server.healthserver.com

Genera el certificado

openssl x509 -req -in server.healthserver.com.csr -CA myCA.pem -CAkey myCA.key \
  -CAcreateserial -out server.healthserver.com.crt -days 825 -sha256 -extfile server.healthserver.com.ext

Asegúrate que puedas correr el ejemplo

sudo docker run --rm \
  -p 8080:80 \
  dockerbogo/docker-nginx-hello-world:latest

y puedes inspeccionar los resultados con

curl -v localhost:8080

En este ejemplo, nginx está configurado con el archivo /etc/nginx/conf.d/helloworld.conf de la siguiente manera:

server {
  listen 80;

  root /usr/share/nginx/html;
  try_files /index.html =404;

  expires -1;

  sub_filter_once off;
  sub_filter 'server_hostname' '$hostname';
  sub_filter 'server_address' '$server_addr:$server_port';
  sub_filter 'server_url' '$request_uri';
  sub_filter 'server_date' '$time_local';
  sub_filter 'request_id' '$request_id';
}

Es decir, la aplicación está totalmente abierta y disponible a través de un canal HTTP no cifrado. Hagámosle un pequeño cambio e introduzcamos nuestro certificado del servidor para que la aplicación esté protegida por TLS.

Modificar la aplicación para usar TLS

Como verás, modificar una aplicación o API que usa nginx como proxy es extremadamente fácil. Verás que sólo tendré que hacer unos pequeños cambios a la configuración de la aplicación e inyectar los certificados correspondientes.

Modifica la configuración de la aplicación. Para esto, crea tu propia versión del archivo helloworld.conf:

server {
  listen 443 ssl;
  server_name server.healthserver.com;
  ssl_certificate server.healthserver.com.crt;
  ssl_certificate_key server.healthserver.com.key;

  root /usr/share/nginx/html;
  try_files /index.html =404;

  expires -1;

  sub_filter_once off;
  sub_filter 'server_hostname' '$hostname';
  sub_filter 'server_address' '$server_addr:$server_port';
  sub_filter 'server_url' '$request_uri';
  sub_filter 'server_date' '$time_local';
  sub_filter 'request_id' '$request_id';
}

Y ahora inyecta esta configuración y los certificados correspondientes:

sudo docker run --rm \
 -p 8443:443 \
 -v $(pwd)/helloworld.conf:/etc/nginx/conf.d/helloworld.conf \
 -v $(pwd)/myCA.pem:/etc/ssl/certs/ca-certificates.crt \
 -v $(pwd)/server.healthserver.com.crt:/etc/nginx/server.healthserver.com.crt \
 -v $(pwd)/server.healthserver.com.key:/etc/nginx/server.healthserver.com.key \
 dockerbogo/docker-nginx-hello-world:latest

Ahora, la aplicación ya no se publicará usando HTTP abierto en el puerto 8080, sino con el protocolo HTTPS cifrado y en el puerto 8443. Entonces si vuelves a intentar:

curl -v localhost:8080 

Obtendrás un error, pero si intentas:

curl -vvv -k \
 https://server.healthserver.com:8443 \
--resolve server.healthserver.com:8443:127.0.0.1

Voilá, funciona. Para poder comunicarnos con el servidor ahora, tuvimos que usar dos trucos de curl:

  • -k sirve para informarle a curl que no importa si el certificado del servidor no es válido (puesto que estamos usando un certificado autofirmado)
  • –resolve sirve para informarle a curl que cuando vea la dirección server.healthserver.com:8443 en lugar de ir a DNS para tratar de resolverla, que la resuelva directamente a la dirección dada (127.0.0.1, que es localhost).

Ahora, en lugar de usar -k, porque podríamos estar engañándonos a nosotros mismos puesto que le estamos diciendo a curl que ignore los errores de certificado, es más adecuado proveer a curl con la autoridad correspondiente, es decir, el CA que se usó para general el certificado.

curl -vvv \
 --cacert $(pwd)/myCA.pem \
 https://server.healthserver.com:8443 \
 --resolve server.healthserver.com:8443:127.0.0.1

Perfecto, esto debe funcionar y darnos la seguridad que nuestro website, o API, está protegido por TLS. Si quieres ver la demostración en tu explorador deberás agregar una entrada a tu archivo /etc/hosts con 127.0.0.1 server.healthserver.com y así podrás ver el mismo resultado desde tu explorador.

Ahora protégela con mTLS

De TLS a mTLS es un pequeño, pero importantísimo paso. Con mTLS, tanto tu cliente como tu servidor sabrán a ciencia cierta que su interlocutor ha sido identificado y autorizado.

Para lograr mTLS en esta aplicación, simplemente agrega unas pocas lineas a tu configuración e inyecta los certificados correspondientes.

Modifica la configuración:

server {
  listen 443 ssl;
  server_name server.healthserver.com;
  ssl_certificate server.healthserver.com.crt;
  ssl_certificate_key server.healthserver.com.key;

  ssl_client_certificate ca.pem;
  ssl_verify_depth 2;
  ssl_verify_client on;
  ssl_protocols TLSv1.2 TLSv1.3;

  root /usr/share/nginx/html;
  try_files /index.html =404;

  expires -1;

  sub_filter_once off;
  sub_filter 'server_hostname' '$hostname';
  sub_filter 'server_address' '$server_addr:$server_port';
  sub_filter 'server_url' '$request_uri';
  sub_filter 'server_date' '$time_local';
  sub_filter 'request_id' '$request_id';
}

Inyecta el myCA.pem como el cliente confiable:

sudo docker run --rm \
 -p 8443:443 \
 -v $(pwd)/helloworld.conf:/etc/nginx/conf.d/helloworld.conf \
 -v $(pwd)/myCA.pem:/etc/ssl/certs/ca-certificates.crt \
 -v $(pwd)/server.healthserver.com.crt:/etc/nginx/server.healthserver.com.crt \
 -v $(pwd)/server.healthserver.com.key:/etc/nginx/server.healthserver.com.key \
 -v $(pwd)/myCA.pem:/etc/nginx/ca.pem \
 dockerbogo/docker-nginx-hello-world:latest

Y ahora, cuando intentes repetir el acceso anterior:

curl -vvv \
 --cacert $(pwd)/myCA.pem \
 https://server.healthserver.com:8443 \
 --resolve server.healthserver.com:8443:127.0.0.1

Recibirás el error:

<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>

O si lo intentas en el explorador verás:

400 Bad Request

No required SSL certificate was sent

¿Qué pasó? Tu aplicación o API ahora está protegida con mTLS, y por más que intentes no lograrás conseguir que inicie la conversación a menos que proveas el doble certificado necesario, aún -k dejará de funcionar porque ya no depende del cliente, sino del servidor que exige la doble verificación.

Para demostrar que tu aplicación está bien y que está realmente protegida puedes usar el curl de la siguiente manera:

curl -vvv \
 --cacert $(pwd)/myCA.pem \
 --key $(pwd)/client.healthserver.com.key \
 --cert $(pwd)/client.healthserver.com.crt  \
 https://server.healthserver.com:8443 \
 --resolve server.healthserver.com:8443:127.0.0.1

De allí en adelante, tu aplicación o API estará protegido y sólamente podrá ser accesado por los clientes a quienes les entregues la CA, el certificado y su clave. Ahora, tu Manejador puede estar en la nube y tu servidor en tu dormitorio y puedes descansar confiado que nadie puede ver ni interferir con la información en tránsito entre los dos. Buena suerte mTLSeando.