Administrar los registros DNS de tu dominio personal o homelab puede volverse un poco tedioso si tienes que entrar constantemente al panel oficial de Cloudflare. Además, el panel oficial te muestra todos tus registros críticos (como los TXT, MX, DKIM, DMARC), lo que aumenta el riesgo de cometer algún error y borrar algo importante por accidente.
Adicionalmente, existía el reto de mantener actualizados los registros tipo A que apuntan a los servicios de mi homelab, ya que mi ISP cambia mi dirección IP pública cada cierto tiempo. Durante años, dependí del cliente de DDNS integrado en mi firewall Sophos para realizar esta tarea. Sin embargo, hace poco dejó de funcionar porque Sophos requiere usar la Global API Key de Cloudflare (junto con el correo de la cuenta) para autenticarse. Cloudflare ya considera este método obsoleto y recomienda migrar a los API Tokens. Tener que darle a un equipo de terceros tu llave global (que básicamente tiene acceso a toda tu cuenta de Cloudflare) ya no es una buena práctica de seguridad.

Para solucionar tanto la exposición de registros críticos como el fallo de la actualización de IP, decidí construir mi propio Cloudflare DNS GUI: una pequeña aplicación web autoalojada en Docker (NodeJS + Vanilla JS) que usa la API de Cloudflare para gestionar únicamente los registros que de verdad uso en mi día a día (A y CNAME).
Más allá de la funcionalidad, quise enfocarme en la seguridad. Quería construir esta herramienta aplicando el concepto de Zero Trust.
A continuación, detallo algunas de las configuraciones de seguridad que le apliqué al proyecto.
1. Zero Trust: Autenticación MFA por Acción (Step-up Authentication)
Lo normal en muchas plataformas es pedirte el código de 6 dígitos (Google Authenticator o Authy) al iniciar sesión y después confiar ciegamente en tu sesión. Incluso Cloudflare lo hace de esa manera.

Para este panel, implementé MFA basado en acciones (Step-up Authentication). Si alguien lograra robar mi cookie de sesión, o si simplemente dejo la computadora desbloqueada, la persona podría ver el panel, pero no hacer ningún cambio.
Cada vez que intento editar, borrar o cambiar el Proxy de un registro, el backend rechaza la petición a menos que adjunte el código de 6 dígitos actual generado por mi teléfono. Así me aseguro de que nadie pueda modificar los registros sin tener mi teléfono a la mano.

2. Principio de Mínimo Privilegio (PoLP) en la Interfaz
Un clic por error en un registro TXT o MX te puede arruinar la configuración del correo del dominio entero. Aplicando el principio de mínimo privilegio, hice que el backend simplemente filtre y oculte todo lo que no sea un registro tipo A o CNAME.
La aplicación solo funciona con estos dos tipos de registros. Si por alguna razón el panel se viera comprometido, el impacto sería menor porque los registros más importantes ni siquiera son accesibles desde ahí.
3. Cierre de Sesión Estricto (Hard TTL)
Muchos portales mantienen tu sesión viva mientras sigas moviendo el mouse. Para evitar dejar ventanas abiertas, le puse un tiempo de expiración (TTL) estricto de 5 minutos:
- En el Frontend: Un contador visual muestra el tiempo restante y cierra la sesión forzosamente al llegar a cero, sin importar lo que estés haciendo.
- En el Backend: La cookie
auth_tokencaduca en el servidor exactamente a los 5 minutos. - No hay opción de extender el tiempo; la idea es entrar, hacer el cambio de DNS y salir inmediatamente.

4. Rate Limiting y Protección MFA
Aunque el servicio corre en mi red privada y no está expuesto a internet, protegí el login contra ataques de fuerza bruta usando express-rate-limit. El servidor bloquea automáticamente la IP si hay 10 intentos fallidos de login.
Además, agregué una función para que si alguien (ya autenticado) falla el código OTP 3 veces seguidas al querer hacer un cambio, la sesión se cierre por completo por precaución.
5. Configuración del Contenedor Docker (No-Root)
Un fallo de seguridad común es correr contenedores de Docker como root. Para evitar esto, configuré el Dockerfile para que use un usuario sin privilegios llamado node (UID 1000).
# Crear el directorio y transferir permisos
RUN mkdir -p /app/data && chown -R node:node /app
# Transición al usuario sin privilegios
USER node
# Los archivos copiados también deben pertenecer al usuario 'node'
COPY --chown=node:node server.js ./
COPY --chown=node:node public ./public
En el host, los permisos de la carpeta donde se guarda la base de datos de sesiones (/app/data) están asignados estrictamente al UID 1000. De esta manera, si llegara a existir una vulnerabilidad en NodeJS y alguien logra acceder al contenedor, se quedará atrapado con un usuario que no tiene permisos en el sistema.
6. Motor DDNS Integrado
Para no volver a depender de Sophos, integré un script de actualización dinámica (DDNS) en el mismo panel de NodeJS. El funcionamiento es muy sencillo:
Si quiero que un subdominio apunte siempre a la IP pública de mi casa, solo marco la casilla de Monitoring (lo cual me pide confirmar con mi código MFA).

El servidor revisa periódicamente mi dirección IP pública. Si nota que cambió, actualiza automáticamente el registro en Cloudflare usando un API Token que solo tiene permisos para editar esa zona DNS específica.
Cualquier registro que se esté monitoreando se resalta en verde en la interfaz, para poder identificar rápido qué subdominios están vinculados a la red de mi casa.

En la parte inferior agregué un área de Logs del Sistema, donde puedo ver un historial de cuándo cambió mi IP y si la actualización de DNS fue exitosa.

7. Instalación Rápida (Quick Start)
He publicado la imagen lista para producción en Docker Hub. Puedes encontrarla aquí: mxlit/cloudflare-dns-gui en Docker Hub.
Opción 1: Docker Compose (Recomendado para Producción)
El método más seguro para evitar que tus contraseñas queden guardadas en el historial de tu terminal es usar Docker Compose con un archivo .env.
Crea un archivo llamado
.envcon tus secretos:API_TOKEN=tu_api_token_restringido_de_cloudflare PASSWORD=tu_contraseña_maestra_seguraCrea un archivo
docker-compose.yml:services: cloudflare-dns-gui: image: mxlit/cloudflare-dns-gui:latest container_name: cloudflare-dns-gui ports: - "3000:3000" env_file: - .env volumes: - ./data:/app/data restart: unless-stoppedInícialo:
docker compose up -d
Opción 2: Docker Run
Si prefieres un solo comando (Advertencia: tus secretos quedarán guardados en el historial de tu terminal), puedes pasarlos mediante flags -e:
docker run -d \
--name cloudflare-dns-gui \
-p 3000:3000 \
-e API_TOKEN="tu_api_token_restringido_de_cloudflare" \
-e PASSWORD="tu_contraseña_maestra_segura" \
-v /ruta/a/tus/datos:/app/data \
--restart unless-stopped \
mxlit/cloudflare-dns-gui:latest
Variables de Entorno
API_TOKEN: Créalo en Cloudflare. Solo necesita permisos deZone.DNS(Edición) para tus zonas específicas.PASSWORD: La contraseña maestra para iniciar sesión en la interfaz web.
Volúmenes
/app/data: Almacena tumonitored.json(objetivos DDNS) ymfa.json(tu secreto OTP). Asegúrate de que el directorio host pertenezca al UID1000(chown -R 1000:1000 /ruta/a/tus/datos).
Configuración de MFA
- Inicia sesión en el panel con tu
PASSWORD. - En tu primer inicio de sesión, se mostrará un código QR.
- Escanéalo con tu aplicación de Autenticador (Google Authenticator o Authy).
- Ingresa el código para verificar. A partir de entonces, todas las acciones destructivas requerirán un token.
Conclusión
Desarrollar herramientas para nuestro Homelab no significa que debamos descuidar la seguridad. Aplicando MFA por acción, tokens de API con permisos reducidos y asegurando bien los contenedores, podemos tener paneles muy seguros para nuestra red.
¿Qué medidas de seguridad o buenas prácticas aplicas tú en tus servicios autoalojados?