Ninguna IA ha estado involucrada en la creación de este articulo.
En este articulo veremos:
Como lograr desbloquear remotamente nuestro NixOS que use LUKS como medida de cifrado completo. Esto será posible ya que, durante el Boot, nuestro OS solicitará a un "dispositivo remoto autorizado", por ejemplo nuestro celular, la clave LUKS. Como intermediario de comunicación usaremos Ntfy y todo el intercambio de información estará cifrado con múltiples capas de seguridad. Finalmente, y como algo opcional, abordaremos la integración de AirVPN (OpenVPN) como una VPN que se usara durante el Boot para asegurar la comunicación. También abordaremos detalles respecto a posibles DNS leak del proceso.
Esta solución nace desde la imposibilidad, en circunstancias normales, de desbloquear un servidor NixOS que tenga LUKS de alguna forma que no requiera la presencia física de un humano con dicha maquina. En mi caso tengo un HomeLab que, físicamente, se encuentra en un lugar remoto y donde no tengo acceso habitual. Luego de cortes energéticos o reinicios de emergencia, era para mi un fastidio tener que ir físicamente a introducir la clave LUKS para que mi servidor iniciara nuevamente a su funcionamiento habitual. Buscando soluciones a esto, solo encontré el proyecto Clevis-Tang, un servidor de autentificación LUKS, que podría ayudar con mi problema. Pero sus características técnicas no se adecuaron a mis necesidades; requiriendo modificaciones profundas del proyecto para poder adaptarlo a mis circunstancias. Tales como: una funcionalidad asíncrona del sistema, la capacidad manual de introducir la clave LUKS, la autorización por parte de dispositivos específicos y la funcionalidad de usar un celular como "terminal remoto". Con ello, nace esta idea.
La propuesta que presento es estable y puede ser implementada con confianza. La lógica de seguridad que usaremos es resiliente a diversos vectores de ataque, tales como: Men-in-the-middle, fuerza bruta, intervención/secuestro/inyección de paquetes de datos y suplantación de identidad. A pesar de esto, es aun una obra en progreso y muchas mejoras pueden ser realizadas.
Este articulo también es una invitación a, exactamente, mejorar esta propuesta y añadir nuevas funcionalidades.
A continuación podrán encontrar dos videos tutoriales relacionados a este articulo. El primero es una introducción y el segundo toda la guiá de implementación. Los códigos que usamos (aunque son pocos) los pueden encontrar aquí: -GitHub-.
Video Introducción:
Video Implementación:
Requisitos
1- El requisito mas importante es que nuestro NixOS tenga conectividad a internet durante Stage1 del proceso de Boot. Claramente, si nuestro sistema no tiene red, sera imposible realizar un proceso de descifrado remoto. En este articulo pueden encontrar una guiá completa de como lograr esto.
1.1- Opcionalmente: tener habilitado en la Stage1, del proceso de Boot, un servidor SSH. Lo anterior, para facilitar el debugeo ante posibles errores en el proceso de implementación. En el articulo donde explico como tener internet en Stage1, también explico como habilitar un servidor SSH para estos propósitos. Nuevamente, esto es solo opcional y algo útil de tener habilitado en caso de encontrarnos con errores inesperados durante el Boot.

Red y SSH en Stage1 (boot - initrd) de NixOS - Parte 1
Federico Jensen ・ Apr 4
2- Estar dispuestos en usar Ntfy como nuestro "intermediario" para enviar y recibir mensajes entre el NixOS y el "dispositivo remoto" desde donde desbloquearemos la unidad LUKS. Ntfy ofrece canales de comunicación con funcionalidades pub/sub gratuitos y de alta disponibilidad. El gran "pero" es que los canales son públicos. A pesar de ello, el sistema propuesto usa diferentes métodos criptográficos donde esto no afecta la seguridad de la solución. Esta guiá usa exclusivamente Ntfy, aunque los invito a tomar la iniciativa y hacer implementación para otros intermediarios de comunicación como: Plugins para Discord, sistemas basados en Email, conexiones directas/privadas entre NixOS y otro servidor... la imaginación es el limite.
3- En caso de seguir la parte final de este articulo: "Implementación de una VPN para el proceso de desbloqueo remoto (opcional)", estar dispuestos a pagar por los servicios de AirVPN como proveedor de VPN. Como alternativa, también pueden usar cualquier servidor/proveedor de VPN que sea compatible con el cliente de OpenVPN. Vuelvo a repetir, esto es opcional... yo uso AirVPN en este articulo porque consdiero que es el mejor servicio de VPN actualmente.
Por ultimo, hacer un recordatorio que lo expuesto en este articulo es focalizado en NixOS. Aunque no me cabe duda que puede servir de inspiración para realizar una implementación similar en otras distribuciones de Linux.
Certificados CA TLS
Como parte del proceso para habilitar una forma de desbloqueo remoto de LUKS, nuestro NixOS deberá comunicase con los servidores de Ntfy mediante HTTPS. Lamentablemente durante el Boot del sistema operativo, por defecto, no se encuentran disponibles los Certificados CA TLS para resolver el protocolo HTTPS. Nuestra primera configuración sera añadir estos certificados al proceso de Boot.
Para esto, en nuestro archivo de configuración /etc/nixos/hardware-configuration.nix
añadiremos unas declaraciones. Específicamente en la sección boot.initrd.systemd.contents
. Esta delcaracion nos permite listar ficheros que deben ser considerados para ser "montados" en el Ram-Disk/initrd (sistema de archivos temporal, montado en RAM, usado durante el proceso de Boot de Linux).
boot = {
...
initrd = {
...
systemd = {
...
contents = {
"/etc/ssl/certs/ca-certificates.crt".source = builtins.toFile "ca-certificates.crt" (builtins.readFile "/etc/ssl/certs/ca-certificates.crt");
"/etc/ssl/certs/ca-bundle.crt".source = builtins.toFile "ca-bundle.crt" (builtins.readFile "/etc/ssl/certs/ca-bundle.crt");
};
};
};
};
Nota 1: Es buen momento para verificar que boot.initrd.systemd.enable tenga un valor de true. O sea, que estemos usando SystemD para administrar los servicios del proceso de Boot. De todos modos, esto ya deberia estar activado si cumpimos con el requisito de tener internet durante Stage1, ya que es necesario para configurar las interfaces de red correctamente.
Nota 2: La sintaxis que usamos dentro de contents
para "copiar" los ficheros .crt
al listado de los que deben estar presentes en el proceso de Boot puede parecer algo confuso. El uso de builtins.toFile
y builtins.readFile
como métodos auxiliares (parte del lenguaje de Nix se debe a que los .crt
realmente son symlinks. Para que contents
pueda copiar el contenido de los .crt
correctamente debemos usar estos métodos auxiliares para leer el contenido, traspasarlos a un archivo temporal y luego copiar el contenido de dicho archivo a los que deberán estar disponibles durante el Boot.
Clave criptográfica RSA
Ahora debemos generar un par de claves publico-privada RSA. Estas servirán para que solo un "dispositivo remoto" autorizado pueda "darle sentido" a los mensajes que están involucrados en descifrar la unidad LUKS de NixOS y que transitaran por Ntfy. La parte privada, de la clave, quedará en el dispositivo remoto y la parte publica la dejaremos en nuestro NixOS.
Para generar las claves podemos usar OpenSSL. Es MUY importante que la calve RSA generada tenga una longitud de 4096bits. Para generar la clave privada podemos usar el comando:
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:4096
Luego, para generar la parte publica, de dicha clave privada, podemos usar el comando:
openssl rsa -in private_key.pem -pubout -out public_key.pem
En caso de usar Windows, deberán instalar primero OpenSSL. La forma mas fácil es ejecutando un Power Shell en modo administrador y luego usando el comando
choco install openssl
. Luego de reiniciar la terminal, ya podrán usar los comandos anteriores en Windows para generar la clave.
La clave privada la dejaremos en el equipo remoto... mas delante la utilizaremos. Ahora nos debemos enfocar en la clave publica. Esta la deberemos dejar en /etc/nixos/
de nuestro NixOS. En mi caso he llamado al archivo que contiene la clave publica public_key.pem
.
Nota 1: La implementación que propongo en este articulo esta preparada para admitir solo un par de claves. Por tanto, si deseamos tener múltiples dispositivos, cada uno con claves distintas (lo recomendado), deberemos entra a programa dicha funcionalidad.
Nota 2: La necesidad de tener que usar una clave de 4096bits se debe a que, internamente, la implementación que configuremos usara otras claves criptográficas en "modo onion" (capas). Y por longitud de mensajes, debemos hacer que estas sean de 4096bits, en caso contrario las otras "capas" del cifrado superaran el "espacio" permitido.
Con nuestra clave publica ya en /etc/nixos/public_key.pem
, ahora deberemos disponibilizar ese .pem
en el proceso de Boot (similar a como disponibilizamos los .crt
anteriormente).
Para esto en nuestro hardware-configuration.nix
usaremos la declaración boot.initrd.secrets
de esta manera:
boot = {
...
initrd = {
...
secrets = {
...
"/etc/nixos/public_key.pem" = "/etc/nixos/public_key.pem";
};
};
};
Nota: La razón de usar boot.initrd.secrets
para esto y no boot.initrd.systemd.contents
se debe a que, dada las buenas practicas de NixOS, se recomienda el uso de secrets
para... eso... claves; y contents
para ficheros de otra índole.
Creando nuestro canal en Ntfy
Crear un canal de Ntfy es extremadamente sencillo. Simplemente debemos ir a la pagina web de Ntfy (https://ntfy.sh) y luego añadir en la URL un "/"
continuado de una cadena alfanumérica de caracteres ("-"
y "_"
tambien estan permitidos). Esta "cadena" sera la ID del canal. La idea es que esta "cadena" sea algo aleatorio. Por ejemplo: "UGaCGId_S4w-7rYxtg"
. Ahora cualquiera que acceda a https://ntfy.sh/UGaCGId_S4w-7rYxtg
podrá usar dicho canal para enviar o recibir notificaciones (sistema pub/sub).
Lo ideal es usar un genrador aleatorio para crear la ID que usaremos para el canal. La longitud máxima de caracteres para la ID es de 64... así que recomiendo usar dicha cantidad. Recordemos guardar la ID que usaremos, ya que la vamos a necesitar en futuras configuraciones.
Quiero aprovechar nuevamente de recordar, que aunque el canal de Ntfy es publico, o sea, cualquiera que tenga la ID puede escribir y leer mensajes en dicho canal; nuestra implementación es segura. Esto ya que los mensajes que intercambiaremos entre NixOS y el "dispositivo remoto" estarán cifrados con múltiples capas de seguridad. Estos resguardos nos protegen diversos ataques como: intercepción de paquetes, reutilización de paquetes, análisis de patrones, fuerza bruta, man-in-the-middle y/o suplantación de identidad entre otros posibles vectores. En el Anexo de este articulo pueden leer un poco mas sobre estas medidas de seguridad.
Donde sucede la magia: CustomDecrypt.sh
El Script CustomDecrypt.sh es el cual realiza toda la "magia". Este Script se iniciara durante Stage1 del proceso de Boot y sera el encargado de todo el proceso de comunicación con Ntfy y posterior desbloqueo de la unidad LUKS. Este Script en su interior realiza varias otras funciones, como:
- Comprobar el estado de las unidades de almacenamiento y la precedencia de particiones aseguradas con LUKS.
- Usar la clave publica (correspondiente a la clave privada del "dispositivo remoto") para asegurar los mensajes.
- La generación de claves RSA efímeras axilares para lograr una comunicación de con cifrado de múltiples capas.
- Enviar la solicitud de la clave LUKS a Ntfy (para que el "dispositivo remoto" reciba una notificación de la necesidad de digitar la clave LUKS).
- Gestionar la "escucha" de respuestas desde Ntfy de mensajes.
- Considerar varios aspectos de integridad y seguridad del proceso de comunicación.
- Desbloquear las unidades LUKS una vez se tenga una clave valida.
- Soportar algunos procesos extra que deben ser considerados en caso de estar usando OpenVPN.
- Considerar algunos escenarios limites en el cual se puede encontrar el proceso de Boot y/o el Script que podrían generar problemas en la secuencia de inicio y/o seguridad.
Como pueden ver... son bastantes cosas...
Como sea, el contenido de CustomDecrypt.sh lo pueden encontrar aquí. Quiero destacar que el Script es una "obra en progreso", por tanto, no pretende ser perfecto. Poco a poco lo he tenido que ir modificando para ir considerando nuevos escenarios. Los invito a seguir contribuyendo.
Una vez descarguen el archivo, lo deben dejar en /etc/nixos/
. Tener cuidado en que el nombre del fichero debe ser CustomDecrypt.sh para que funcione correctamente con lo que realizaremos a continuación.
Ahora, como ya hemos realizado anteriormente, debemos disponibilizar CustomDecrypt.sh para que este presente durante el proceso de Boot. Para esto en boot.initrd.systemd.contents
debemos agregar:
...
contents = {
...
"/etc/nixos/CustomDecrypt.sh".source = "${/etc/nixos/CustomDecrypt.sh}";
};
Nota: Podrán notar que en este caso la sintaxis es distinta a la usada con los .crt
. Esto se debe a que CustomDecrypt.sh es, efectivamente, un fichero. Mientras que los .crt
son symlinks.
Con el archivo ya disponible en Boot, ahora crearemos un servicio para que ejecute este Script en el momento exacto. Para esto usaremos la declaración boot.initrd.systemd.services
.
boot = {
...
initrd = {
...
systemd = {
...
services = {
...
CustomDecrypt = {
enable = true;
wants = [ "initrd-nixos-copy-secrets.service" ];
after = [ "initrd-nixos-copy-secrets.service" ];
before = [ "systemd-ask-password-console.service" ];
wantedBy = [ "systemd-ask-password-console.service" ];
unitConfig = {
Description = "CustomDecrypt";
DefaultDependencies = false;
};
serviceConfig = {
Type = "forking";
ExecStart="${pkgs.bash}/bin/sh -c 'sh /etc/nixos/CustomDecrypt.sh \"https://ntfy.sh/UGaCGId_S4w-7rYxtg\" \"/etc/nixos/public_key.pem\" &'";
TimeoutStopSec = "3700s";
};
};
};
};
};
};
Es importante notar que en ExecStart
pasamos dos parámetros a CustomDecrypt.sh. El primer parámetro es el canal de Ntfy que usaremos, tengan cuidado de usar https y de indicar correctamente la ID que corresponda a su canal. El segundo parámetro es el path a la clave publica .pem
(par correspondiente a la clave privada que dejamos en nuestro "dispositivo remoto").
Nota 1: Usamos wants
y after
para ejecutar CustomDecrypt.sh luego que el proceso de copia de secretos de NixOS esta listo. Y usamos before
y wantedBy
para iniciar el Script antes de que NixOS pregunte, por el terminal, por la clave LUKS.
Nota 2: El servicio se ejecuta como forking. De esta manera CustomDecrypt.sh quedara en background mientras maneja las comunicaciones de enviado/recepción de mensajes con Ntfy y todos los otros procesos involucrados en el descifrado de LUKS.
Nota 3: El TimeoutStopSec
de 3700s indica el tiempo máximo que CustomDecrypt.sh esperara una respuesta remota con la clave LUKS (es un poco mas que una hora). Luego de este tiempo el servicio de CustomDecrypt.sh terminara, haciendo imposible procesar respuesta remotas con la clave. Esto podrá parecer extraño e incomodo desde el punto de vista del usuario, pero luego veremos que tiene sentido... paciencia.
CustomDecrypt.sh recurre al uso de varios comandos como: ip
, curl
y sed
(entre otros) para su funcionamiento. Normalmente estos comandos están integrados en el OS, pero resulta que en el proceso de Boot no. Esto se debe a que el proceso de Boot trata de cargar las mínimas dependencias necesarias para poder funcionar.
Por tanto, deberemos indicar en nuestro hardware-configuration.nix
que deseamos usar una serie de paquetes "extras" durante el Boot. Esto lo podemos hacer usando la declaración boot.initrd.systemd.initrdBin
. Incluiremos, usando la nomenclatura de NixOS para paquetes, los siguientes paquetes:
-
libuuid
: Para el uso delsblk
y poder listar unidades de almacenamiento. -
gnugrep
: Para el uso degrep
. -
cryptsetup
: Para poder manejar particiones que tengan LUKS. -
openssl
: Para crear y manipular claves RSA. -
curl
: Para comunicarnos con HTTPS con Ntfy. -
iproute2
: Para el uso del comandoip
y validar algunas cosas relacionas a la red. -
gnused
: Para el uso desed
.
Para esto añadimos:
boot = {
...
initrd = {
...
systemd = {
...
initrdBin = [ pkgs.libuuid pkgs.gnugrep pkgs.cryptsetup pkgs.openssl pkgs.curl pkgs.iproute2 pkgs.gnused ];
};
};
};
Nota: Si tienen la necesidad de añadir mas paquetes, simplemente los listan en este arreglo.
Con todo esto estamos listos para crear una nueva generación de NixOS:
sudo nixos-rebuild switch
Y luego reiniciamos. Si todo va bien, deberíamos ver que durante el proceso de Boot, nos llega a nuestro canal de Ntfy un mensaje.
Preparando el dispositivo remoto
Ahora que tenemos listo nuestro NixOS, nos toca ocuparnos del "dispositivo remoto", o sea, el dispositivo desde el cual digitaremos la clave LUKS.
En teoría nuestro "dispositivo remoto" puede ser cualquier cosa que tenga la capacidad de leer/enviar mensajes con Ntfy usando HTTPS y que pueda manipular claves de tipo RSA. O sea, este "dispositivo remoto" podría ser una aplicación de celular, un servidor con una rutina CRON o una plataforma web que hemos programado para este uso especifico... la imaginación es el limite.
En este articulo, nuestro "dispositivo remoto" sera un Windows donde ejecutaremos un Script Python. El Script lo pueden encontrar aquí. Es algo un poco "burdo" y "aburrido"... pero mas que suficiente en términos de funcionalidad practica.
De antemano, me quiero disculpar con lo básico y poco profesional del Script Python. Se que se puede ser mejorado mucho, pero la verdad, es que no he puesto mucho esfuerzo en esta parte del proyecto. Lo importante, es que funciona.
El Script para funcionar necesita del paquete requests y pycryptodome, los cules pueden instalar con pip install requests pycryptodome
. También este Script necesita de dos configuraciones:
- En la variable
TOPIC
, dejar la ID del canal de Ntfy que estemos usando. - En la variable
PRIVATE_KEY_PATH
el Path a la clave privada que hemos generado al inicio del guía.
Hora de probar todo:
Usaremos Python para ejecutar el Script. Mientras tengamos activo el Script de Python, vamos a reiniciar NixOS. Durante el proceso de Boot se enviara un mensaje de NixOS a Ntfy, el Script de Python va a reaccionar a dicho mensaje. Luego veremos que el Script de Python nos solicitará introducir la clave LUKS. Luego de digitarla y apretar enter, un nuevo mensaje sera enviado desde el Script de Python a Ntfy. CustomDecrypt.sh, que corre en NixOS, reaccionara a dicha respuesta y la analizará. Si todo va bien, luego de unos 10 a 15 segundos la unidad LUKS terminara de descifrarse y veremos como el proceso de Boot continuara y terminara de forma normal.
Y con eso ya estamos listos !
El Script de Python no necesariamente lo tenemos que tener previamente activo antes que NixOS se reinicie. Si deseamos, podemos primero reiniciar NixOS y luego podemos iniciar el Script de Python. El Script debería poder "recuperar" el ultimo mensaje enviado a Ntfy (el enviado por NixOS), para usarlo en el proceso de solicitar la calve LUKS.
Recordemos que el TimeoutStopSec
de CustomDecrypt.sh es de 3700 segundos (un poquito mas de 1 hora), por tanto ese sera el plazo de tiempo que tenemos para digitar la clave LUKS desde el momento en que se reinicia NixOS.
Móvil Android como "dispositivo remoto"
Es fácil tener como "dispositivo remoto" nuestro celular Android. Solo debemos instalar la aplicación Pydroid 3. Esta app nos permite ejecutar scripts Python en el celular. Basta que copiemos la clave privada .pem
y el Script .py
a nuestro celular. Y luego usando Pydroid 3 ejecutar el Script. El uso del Script sera exactamente el mismo que en Windows. Recuerda instalar las dependencias de requests
y pycryptodome
en Pydroid 3.
Claramente seria mas elegante con una aplicación dedicada y nativa de celular... en vez de usar un Script de Python. Pero eso ya se los dejo a ustedes para que lo creen.
También pueden instalar la aplicación móvil oficial de Ntfy. Luego de instalarla pueden "subscribirse" al canal de comunicaciones usando su ID. De esta manera recibirán notificaciones en su celular cuando mensajes lleguen al canal. Esta es una buena manera de enterarse de forma oportuna si nuestro NixOS esta solicitando la clave LUKS. Luego podremos usar el Script de Python desde Pydroid 3 para digitar la clave LUKS, o acercarnos a nuestro computador de escritorio y usar el Script desde ahí.
En caso de iOS supongo que existe algo similar a Pydroid 3... pero la verdad no he probado.
Implementado un Watchdog en Stage1 del Boot
Hay un aspecto que aun debemos considerar en nuestra implementacion. Y es el uso de un Watchdog que se encargue de reiniciar el sistema operativo, de forma completa, en caso que dentro un periodo de tiempo determinado, no se desbloquee la unidad LUKS de forma exitosa. Esto lo hacemos por varias razones:
- Siempre esta la posibilidad que durante el Boot algo falle. Por ejemplo que, luego de un corte energético, la arquitectura de red aun no esta totalmente restablecida. O que existe alguna falla en el arranque de Stage1 relacionada al hardware. Un reinicio siempre es la mejor opción para "purgar" y cargar todo limpiamente.
- Nuestro CustomDecrypt.sh solo acepta claves LUKS durante 3700 segundos (
TimeoutStopSec
) desde el momento que iniciar. Si ese tiempo ya ha transcurrido, no hay forma de reiniciarCustomDrcrypt.sh
. Nuestro equipo quedara sin poder terminar el Boot. Reiniciar es la única opción. - Esto también ayuda a que CustomDecrypt.sh genere nuevamente las claves RSA efímeras (de uso internas). Esto ayuda que los mensajes enviados por Ntfy siempre estén en un constante ciclo de "renovación criptográfica", haciendo que no exista tiempo suficiente para hacer ataques mas sofisticados. Mejor dicho, es buena practica permitir al sistema de CustomDecrypt.sh renovar sus claves efímeras de forma habitual... como por ejemplo cada 1 hora.
- CustomDecrypt.sh solo acepta 1 vez una contraseña remota LUKS. Si esta es digitada de forma incorrecta, CustomDecrypt.sh no acepta reintentos. Y estos es apropósito. Es para evitar ataques de fuerza bruta desde el dispositivo remoto. Con el reinicio permitimos que CustomDecrypt.sh solicite nuevamente la clave, dandonos la opcion de tener "reintentos" de manera controlada y segura.
Para implementar nuestro Watchdog sera tan simple como agregar un nuevo servicio a nuestro hardware-configuration.nix
. En la sección boot.initrd.systemd.services
agregamos, junto a nuestro Script de CustomDecrypt, el nuevo servicio de CustomWatchdog:
services = {
...
CustomWatchdog = {
enable = true;
after = [ "timer.target" ];
requiredBy = [ "initrd.target" ];
unitConfig = {
Description = "CustomWatchdog";
DefaultDependencies = false;
};
serviceConfig = {
Type = "forking";
ExecStart="${pkgs.bash}/bin/sh -c 'sleep 3600 && reboot -f &'";
TimeoutStopSec = "3700s";
};
};
};
Lo mas importante en este servicio es el valor de 3600 segundos (1 hora) que tiene asignado el comando sleep
(localizado en la instrucción de ejecución ExecStart
). Este sera el tiempo que CustomWatchdog esperara antes de reiniciar el sistema operativo.
Nota 1: Usamos after
y requiredBy
para ejecutar CustomWatchdog lo antes posible en la cadena de procesos de Stage1 del Boot.
Nota 2: CustomWatchdog se ejecuta en modo forking, ya que estará constantemente en background esperando ejecutar el reinicio (en caso que el proceso de Boot no logre superar la etapa de LUKS correctamente).
Nota 3: Usamos reboot
con la flag -f
(force) para asegurarnos que el sistema se reinicie, a pesar de que otros procesos lo bloqueen.
Nota 4: El TimeoutStopSec
es muy importante. Pueden notar que tiene un valor un poco mayor al usado en el sleep
. Este TimeoutStopSec
es esencial para que SystemD no "crea" que CustomWatchdog esta "pegado" y termine el proceso antes de tiempo. Esto puede suceder, porque en algunas ocasiones, SystemD no sabe interpretar correctamente los sleep
prolongados y "cree" que el servicio esta fallando por alguna razón y que esta "pegado" sin hacer nada.
En caso que deseemos ajustar los tiempos, deberemos ser cuidadosos. El
TimeoutStopSec
de CustomDecrypt y de CustomWatchdog siempre deben tener un valor ligeramente mayor al usado en elsleep
de CustomWatchdog. Esto es importante para no entrar en condiciones de carrera. Por tanto, si queremos por ejemplo, reiniciar nuestro equipo cada 6 horas, deberemos dejar en elsleep
de CustomWatchdog con un valor de 21600 (segundos en 6 horas); y enTimeoutStopSec
de CustomDecrypt como CustomWatchdog, un valor un poco mayor, por ejemplo "21700s".
Con esto listo, podemos crear una nueva generación de NixOS sudo nixos-rebuild switch
y luego reiniciar nuestro sistema. Es un poco difícil "probar" si nuestro Watchdog funciona... ya que deberemos esperar 1 hora. Pero si quieren pueden reducir el valor de los TimeoutStopSec
y sleep
a algo como 10 minutos para hacer una prueba completa.
Conclusión, primera parte, sistema de desbloqueo remoto
Listo, ya tenemos nuestro sistema de desbloqueo remoto LUKS completo, usando Ntfy como intermediario para los mensajes. Yupi!
Ahora inicia la segunda parte, totalmente opcional, de este articulo. Y esto es la implementación de OpenVPN, con el proveedor AirVPN, durante el proceso de Boot. Esto nos ayudara a anonimizar el trafico de red entre NixOS y Ntfy. Posteriormente vamos a abordar algunos temas relacionados a DNS leak para dejar todo perfectamente ajustado y anónimo.
Opcional: Implementación de VPN, con OpenVPN, en Stage1 del proceso de Boot
La razón por la cual yo deseo implementar una VPN durante el proceso de Boot, es que mi HomeLab se ejecuta siempre usando una VPN. Por tanto, es contraproducente, que durante el proceso de Boot me comunique con internet (en este caso con Ntfy) y que dicha conexión no pase por un túnel. Con esta implementación logro, realmente, que desde el inicio de OS, todo el trafico pase por la VPN... incluso durante el Boot.
Como he comentado en un inicio, yo usaré AirVPN como proveedor de VPN (de pago). Es mi servicio de VPN favorito (y por mucho). La política y respaldo que tiene la empresa de AirVPN es muy difícil de cuestionar. Y técnicamente son geniales. Si... soy un poco de "Fanboy". En un internet lleno de VPNs que son estafas y "trampas de tontos"... creadas específicamente para obtener dinero; AirVPN aparece como una opción "real" de lo que debe ser una VPN. Bueno, como sea. El punto es que usaré AirVPN, pero ustedes pueden usar cualquier proveedor que les permita usar OpenVPN como cliente (o sea, que el proveedor les de la posibilidad de genera un archivo
.ovpn
de conexión). Incluso pueden crear su propio servidor VPN usando algún servicio cloud u otro servidor que tengan disponible.
Creando el archivo .ovpn en AirVPN
Lo primero que haremos es crear nuestro .ovpn
. Este es el archivo que tiene las credenciales para que el cliente OpenVPN pueda conectarse al servidor VPN.
Lo primero sera entrar a la web de AirVPN y crear un "Device
" en el panel de "Client Area". Los Device
permiten diferenciar distintos equipos que usen la misma cuenta de AirVPN. Por ejemplo, yo tengo un Device para mi celular, otro para mi notebook y ahora voy a crear uno para este proyecto. Yo le he puesto como nombre "NIX".
Con el Device ya creado, ahora vamos al panel de "Config Generator
", que encontraran también en la Client Area.
En Config Generator vamos a activar la opción Advanced. Luego vamos a seleccionar el OS, o sea, Linux. Posteriormente el protocolo de conexión que usaremos. Yo generalmente uso "OpenVPN TCP 443". Y en la sección Advance vamos a marcar "Resolved hosts". Luego bajaremos y vamos a seleccionar el Device al cual vamos asignar este .ovpn
, en mi caso "NIX". Finalmente vamos a seleccionar el servidor al cual nos conectaremos. Yo generalmente uso "Global". Al final de la pagina pueden darle a "Generate" para iniciar la descarga de su .ovpn
.
Nota: La opción Resolved hosts nos sirve para que el .ovpn
generado tenga definido el punto de acceso del servidor VPN con una dirección IP y no con un hostname. Esto lo necesitamos ya que durante Boot veremos que existen algunos problemas en la resolución de hostnames de manera consistente. Por tanto es mejor usar directamente la IP del servidor VPN.
Luego deberán copiar el .ovpn
y dejarlo en /etc/nixos/
de su NixOS. Yo he usado como nombre para el archivo airvpn.ovpn
.
Antes de continuar, no olvidemos que debemos disponibilizar dicho archivo para que este presente durante el Boot. Para esto en boot.initrd.secrets
de nuestro hardware-configuration.nix
añadimos:
secrets = {
...
"/etc/nixos/airvpn.ovpn" = "/etc/nixos/airvpn.ovpn";
};
Configurando OpenVPN
Ahora vamos a indicar al sistema que permita el uso del paquete de OpenVPN en el Boot. Para esto vamos a añadir al arreglo boot.initrd.systemd.initBin
el paquete pkgs.openvpn
, o sea, nuestro initBin
quedara así:
initrdBin = [ pkgs.openvpn pkgs.libuuid pkgs.gnugrep pkgs.cryptsetup pkgs.openssl pkgs.curl pkgs.iproute2 pkgs.gnused ];
Posteriormente deberemos habilitar unos "Módulos de Kernel" al proceso de Boot, para que el sistema tenga la capacidad de crear adaptadores de red de tipo túnel. En el arreglo de la declaración boot.initrd.availableKernelModules
añadiremos tun
y tap
. Deberá quedar así:
boot.initrd.availableKernelModules = ["tun" "tap" ...<otros módulos>... ];
Lo único que nos falta es crear el servicio que se encargará de iniciar OpenVPN en el momento correcto. Para esto añadiremos un servicio mas a boot.initrd.systemd.services
.
services = {
...
Openvpn = {
enable = true;
wants = [ "network.target" "initrd-nixos-copy-secrets.service" ];
after = [ "network.target" "initrd-nixos-copy-secrets.service" ];
before = [ "CustomDecrypt.service" ];
wantedBy = [ "CustomDecrypt.service" ];
unitConfig = {
Description = "OpenVPN";
DefaultDependencies = false;
};
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.bash}/bin/sh -c 'openvpn --daemon --config /etc/nixos/airvpn.ovpn'";
};
};
};
Es muy importante prestar atención en el nombre del servicio: "Openvpn", ya que influye en un comportamiento de CustomDecrypt.sh
que explicare mas adelante.
Nota 1: Usamos wants
y after
para iniciar Openvpn luego de tener el sistema de network básico listo y luego de tener los secretos disponibles. Usamos before
y wantedBy
para indicar que Openvpn inicie antes que el servicio de CustomDecrypt
.
Nota 2: El Script inicia en modo forking, ya que el cliente de OpenVPN operara como un deamon durante el proceso de Boot.
Nota 3: En el comando de ejecución de ExecStart
pasamos en --config
el path al archivo de configuración .ovpn
que hemos generado en AirVPN.
Con esto listo ya se iniciará de forma automática OpenVPN durante el Boot y se creará un túnel a los servidores de AirVPN.
Aprovecho de mencionar que CustomDecrypt.sh
en su interior tiene una rutina donde, si detecta que un servicio llamado "Openvpn" (ojo, con "O" mayúscula, igual que el nombre usado en boot.initrd.systemd.services.Openvpn
) activo, procurara que el túnel esta activo antes de proseguir con la comunicación con Ntfy. Este es un resguardo para que no se genere comunicación https con el servidor antes que el VPN este listo. En caso que el servicio de Openvpn lo llamarán de forma distinta, pueden cambiar el nombre en la variable OPENVPN_SERVICE_NAME
localizada al inicio de CustomDecrypt.sh
.
Una feature que me gustaría añadir en un futuro es la capacidad de tener distintos
.ovpn
(apuntando a distintos servidores). Y si uno falla, OpenVPN pase a usar otro .ovpn.
Arreglando bug que provoca DNS leak
El talón de Aquiles de los VPN es cuando nuestro OS no resuelve los hostname (nombres de dominio) usando un DNS "seguro". Esto se denomina DNS leak, donde el ISP puede analizar nuestro trafico usando la resolución de DNS. En este segmento nos aseguraremos que los DNS se usen de forma apropiada y no provoquen este problema que potencialmente des-anonimice nuestro trafico.
Generalmente cuando usamos una VPN, se usa un servidor DNS interno a dicha VPN para resolver hostname. Ese es el caso típico también con el servicio de AirVPN... ellos tienen servidores DNS que podemos usar una vez tengamos el túnel activo. Esta configuración de "selección de DNS" casi casi casi siempre es automática. Pero por alguna razón... en NixOS... específicamente en el sistema de red usado en Boot... no sucede.
Esto quiere decir, que aunque estemos conectados al VPN y tengamos el túnel activo, no siempre se usará el DNS de AirVPN. Resulta que si tenemos un DNS configurado a nivel interfaz física, este valor "sobrescribirá" al indicado por OpenVPN al configurar el túnel. Esto, en palabras simples, nos generará un catastrófico DNS leak.
Por suerte solucionarlo es sencillo. Deberemos ir a nuestro hardware-configuration.nix
a la declaración boot.initrd.systemd.network
y luego identificar la interfaz que usamos para conectarnos a internet. En mi caso es la llamada enp0s3
.
Ahora, la forma de solucionar el problema depende de como se configura su interfaz:
- La interfaz es configurada de forma estática (sin DHCP):
En este caso deberían ver algo como:
enp0s3 = {
enable = true;
name = "enp0s3";
address = [ "192.168.4.152/24" ];
gateway = [ "192.168.4.1" ];
dns = [ "8.8.8.8" ];
};
La solución consiste en remover la definición de DNS. O sea, dejarlo así:
enp0s3 = {
enable = true;
name = "enp0s3";
address = [ "192.168.4.152/24" ];
gateway = [ "192.168.4.1" ];
};
- La interfaz es configurada usando DHCP:
En este caso deberían ver algo como:
enp0s3 = {
enable = true;
name = "enp0s3";
DHCP = "yes";
};
La solución consiste en añadir las siguientes declaraciones:
enp0s3 = {
enable = true;
name = "enp0s3";
DHCP = "yes";
dhcpV4Config = {
UseDNS = false;
};
dhcpV6Config = {
UseDNS = false;
};
};
Con esto le indicamos a DHCP que solo configure de forma automática el
address
ygateway
de la interfaz y que no defina ningún DNS.
Independiente del modo en que resuelvan la configuración esto solucionara el echo que, una vez conectados al VPN, se use efectivamente el DNS del VPN.
Otros problemas que pueden causar DNS leak
Resulta que existe otro "problema", que puede surgir, dada la solución que implementamos en el paso anterior. El escenario de este "problema" es el siguiente:
- Nuestras interfaces no tienen asignado un DNS (ni manualmente ni por DHCP).
- Por alguna razón la conexión a la VPN falla de forma catastrófica.
- Por otra razón, totalmente anómala, nuestro sistema operativo intenta resolver un hostname.
¿Que pasará? Pues resulta que el servicio que se encarga de administrar la resolución de DNS del sistema operativo, llamado resolved, usara unos DNS denominados "FallbackDNS". Este es uno (o varios) DNS de emergencia que son usados en caso que no se puedan localizar a nivel interfaz (ni a nivel VPN). Si esto llega a suceder, puede que nuestro sistema intente resolver el hostname de "ntfy.sh" usando estos DNS de emergencia provocando un DNS leak.
Para solucionar esto podemos añadir una definición en configuration.nix
. Específicamente en services.resolved
.
services.resolved = {
fallbackDns = [];
};
Con esto, ya no existirán fallbackDns
que puedan ser usados. Nuestro sistema operativo quedara sin opciones de resolver hostname sin querer.
Esto claramente provoca un "restricción colateral". Si no estamos conectados a la VPN, no podemos resolver hostname. Pero dentro del contexto de la solución de "desbloqueo remoto", no debería haber ninguna razón lógica por la cual desearíamos hacer esto sin antes estar conectados a la VPN.
Ultimo ajuste a resolveD
Finalmente, ya que estamos en configuration.nix
podemos aprovechar de hacer una ultima cosa en services.resolved
:
services.resolved = {
fallbackDns = [];
extraConfig = ''
MulticastDNS=no
LLMNR=no
DNSSEC=yes
'';
};
Estas 3 opciones nos ayudan a terminar de configurar unos detalles finales. MulticastDNS
y LLMNR
son unos protocolos para que el sistema operativo encuentre dispositivos en la red local (como impresoras o carpetas compartidas). Personalmente no me gusta que mi sistema emita paquetes "porque si" a la red (aunque sea local). Con eso desactivamos esos protocolos. DNSSEC
nos ayuda a que la resolución de DNS se realice con los parámetros de seguridad de DNSSEC. El servidor DNS privado de AirVPN soporta DNSSEC, así que de esta manera nos aseguramos que las consultas sean realizadas de manera segura.
Nota: Me gustaría mencionar que CustomDecrypt.sh
, en su interior, tiene una parte de código que se encarga de sobre escribir, automáticamente, el valor de fallbackDns
en caso que OpenVPN se este usando. El valor de fallbackDns
queda configurado con la misma IP que la gateway del túnel generado con AirVPN. Esta es la IP que AirVPN recomienda usar en caso de querer configurar manualmente el uso del DNS privado de ellos. El motivo de reescribir el valor de fallbackDns
es simplemente para asegurarnos que resolved
use correctamente el DNS. Es una prevención extra, a lo que ya es una serie de bugs y comportamientos particulares. Si no desean que CustomDecrypt.sh
toque el fallbackDns
pueden desactivar esta configuración pasando el valor de la variable FORCE_FALLBACK_DNS_VPN
(localizada al inicio de CustomDecrypt.sh
) a 0
. Esto puede que sea necesario en caso que usen otro proveedor de VPN (donde la configuración manual para el uso del DNS privado sea distinta).
Conclusión
Y con esto ya tenemos un sistema que nos permite desbloquear de forma remota nuestra unidad LUKS, durante el Boot, de forma segura y anomia. Usando como intermediario Ntfy hemos logrado crear un sistema de notificaciones y mensajes asíncronos, así que podemos usar un celular como nuestro dispositivo remoto autorizado. Ademas, la implementación de seguridad interna que usa CustomDecrypt.sh
para gestionar los mensajes, nos protegen de múltiples vectores de ataque, haciendo que el uso de canales públicos de comunicación no sea un problema.
No olviden, en su Stage1, desactivar el servidor SSH... ya no lo van a necesitar. También recomiendo ejecutar
sudo nix-collect-garbage -d
para limpiar el cache :)
Nuevamente los invito a seguir mejorando esta propuesta. Creo que una gran contribución seria hacer que el sistema funcione de forma modular, permitiendo cambiar que el canal de comunicación sea Discord, Slack, un sistema basado en intercambio de emails firmados o incluso un comunicación directa entre servidores.
En los próximos días subiré un articulo donde explicaré, detalladamente, la lógica detrás de las decisiones de seguridad que usa CustomDecrypt.sh
. Puede ser una lectura interesante si te apasiona la seguridad.
Un saludo a todos!