En la Parte 1 dejé Umami instalado con Docker, publicado con Nginx Proxy Manager (NPM) y Cloudflare, e integrado con Hugo y PaperMod. En esta segunda parte cuento lo que no salió a la primera y, sobre todo, lo que descubrí al revisar mi propia infraestructura.

Problemas que encontré y cómo los resolví

1. El script no llegaba: la Content Security Policy

Todo parecía correcto: el script estaba en el HTML y respondía 200 desde la VM. Pero el panel mostraba todo en cero.

Mi primera sospecha fue un bloqueador de anuncios. Estaba equivocado: en ventana de incógnito pasaba lo mismo. La pista real estaba en la consola de DevTools (Chrome):

Loading the script … violates the following Content Security Policy directive: “script-src ‘self’ ‘unsafe-inline’ …”

Mi blog enviaba una cabecera CSP restrictiva que solo permitía scripts propios. Provenía de NPM (pestaña Advanced del Proxy Host). La solución fue añadir mi dominio de Umami en dos directivas:

  • script-src: para poder cargar el script.
  • connect-src: para que el script pueda enviar los datos a /api/send.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com https://stats.tudominio.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://cloudflareinsights.com https://stats.tudominio.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;

Recargué con Cmd+Shift+R y las visitas empezaron a aparecer en Realtime.

Lección: cuando un script no carga, la pestaña Console de DevTools suele decirlo explícitamente. Y si tienes CSP, cada servicio nuevo que integres necesita su excepción. Aunque los bloqueadores de anuncios también pueden bloquear el script de Umami, en mi caso no era el problema.

2. Un puerto del blog expuesto sin querer

Al revisar la Security List de Oracle Cloud (las reglas de firewall de la subred) para decidir qué abrir, noté que el puerto 8081 estaba abierto a 0.0.0.0/0. Era el puerto del contenedor nginx que sirve el blog. Es decir, cualquiera podía entrar directamente por http://IP:8081, saltándose Cloudflare, NPM y el HTTPS.

Lo corregí en tres pasos:

  1. Confirmé que NPM apuntaba a 172.17.0.1:8081.
  2. Recreé el contenedor publicando el puerto solo en el bridge de Docker (-p 172.17.0.1:8081:80).
  3. Eliminé la regla del 8081 en la Security List.

Docker gestiona sus propias reglas de red y puede saltarse las reglas de firewalls del sistema operativo como ufw o firewalld. Por eso no conviene asumir que el firewall del host, por sí solo, impedirá el acceso a un puerto publicado. Tener dos barreras (bind limitado a la interfaz del bridge + Security List) marca la diferencia.

3. SSH y el panel de NPM abiertos a todo Internet

En la misma revisión vi que el puerto 22 (SSH) y el 81 (administración de NPM) estaban abiertos a 0.0.0.0/0. Los restringí a mi IP fija (TU.IP.FIJA/32).

Antes de hacerlo conviene tener un acceso alternativo por si tu IP cambia: en mi caso, una VPN propia (WireGuard) y la consola serial de Oracle Cloud. Vale la pena probarlos antes de necesitarlos.

Endurecimiento de seguridad

Instalar un servicio nuevo fue el pretexto para auditar lo que ya tenía expuesto. Resumen de cómo quedó la Security List:

PuertoUsoOrigen permitido
22SSHSolo mi IP fija
81Panel de NPMSolo mi IP fija
80 / 443WebInternet
51820/UDPVPNInternet
3000 y 8081Umami y blogCerrados (solo bridge de Docker)

Además:

  • Contraseña propia y 2FA en el usuario admin de Umami. Cambiar la contraseña por defecto es lo primero que hay que hacer.
  • Actualizaciones con criterio: docker compose pull && docker compose up -d funciona, pero conviene leer antes las notas de versión, sobre todo si usas la etiqueta latest. Fijar una versión concreta hace las actualizaciones más predecibles.

Mejora pendiente: una regla WAF en Cloudflare para que el panel de Umami solo sea accesible desde mi IP, dejando públicas únicamente las rutas del script y /api/send. Para que esa regla sirva, el servidor de origen debe aceptar solo tráfico de Cloudflare; si no, se puede llegar al panel directamente por la IP. Por ahora la dejé fuera y compenso con contraseña fuerte y 2FA.

Resultados y siguientes pasos

Ya puedo ver qué posts se leen más, de dónde llegan los lectores, países y dispositivos.

Lo que viene:

  • Enlaces con UTM al compartir en LinkedIn (?utm_source=linkedin&utm_medium=social) para medir cuánto tráfico trae cada canal.
  • Eventos personalizados, por ejemplo clics en enlaces a repositorios.
  • Consultar las estadísticas con IA (MCP): Umami v3.4 incorporó soporte para el Model Context Protocol, también para instalaciones autoalojadas. En self-hosted viene desactivado por defecto: se habilita con la variable MCP_ENABLED=1, se crea una API key en Settings → API keys y el cliente se conecta al endpoint /mcp de tu instancia. Las herramientas del MCP oficial son de solo lectura. Es lo que quiero probar a continuación; y como ese endpoint quedaría publicado en el subdominio, lo evaluaré con el mismo criterio de seguridad de este artículo.

Conclusiones

¿Vale la pena?

  • Sí, si quieres entender qué contenido funciona y conservar tus datos, y ya manejas Docker.
  • No necesariamente, si solo quieres saber cuántas visitas tienes: Cloudflare ya alcanza y no tienes nada que mantener.

Lecciones aprendidas:

  1. Revisa la consola del navegador antes de sospechar de extensiones.
  2. Una CSP estricta es buena práctica, pero cada integración nueva necesita su excepción.
  3. Instalar un servicio nuevo es buen momento para auditar lo que ya tenías expuesto: yo encontré tres puertos abiertos de más.
  4. Publica los puertos solo donde hacen falta, y no dependas de una sola barrera.