<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>blog.fidelramos.net - Software</title><link href="https://blog.fidelramos.net/es/" rel="alternate"/><link href="https://blog.fidelramos.net/feeds/software.atom.xml" rel="self"/><id>https://blog.fidelramos.net/es/</id><updated>2026-06-25T18:45:17.708732+00:00</updated><entry><title>Aventuras en vibe coding: Venice.ai KDE widget</title><link href="https://blog.fidelramos.net/es/software/vibe-coding-escapades-venice-kde-widget" rel="alternate"/><published>2026-06-25T00:00:00+00:00</published><updated>2026-06-25T18:45:17.708732+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-06-25:/es/software/vibe-coding-escapades-venice-kde-widget</id><summary type="html">&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Vengo usando Venice.ai (&lt;a href="https://venice.ai/chat?ref=XPrCO2"&gt;enlace con referencia si te interesa apuntarte&lt;/a&gt;) desde hace tiempo para todas mis necesidades de inteligencia artificial.
Me encanta porque a diferencia de la mayoría de plataformas de IA, es &lt;strong&gt;privado&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando usas modelos propios de Venice (modelos libres que ejecutan en sus propios …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Vengo usando Venice.ai (&lt;a href="https://venice.ai/chat?ref=XPrCO2"&gt;enlace con referencia si te interesa apuntarte&lt;/a&gt;) desde hace tiempo para todas mis necesidades de inteligencia artificial.
Me encanta porque a diferencia de la mayoría de plataformas de IA, es &lt;strong&gt;privado&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando usas modelos propios de Venice (modelos libres que ejecutan en sus propios servidores) no guardan registro de los &lt;em&gt;prompts&lt;/em&gt;, o sea que es un uso privado. La mayoría de proveedores guarda registros de todo, y peor aún son los que te identifican de forma personal, digamos con tu cuenta de Google, entonces pueden asociar todas tus conversaciones para hacer perfiles con un nivel de detalle muy peligroso.&lt;/li&gt;
&lt;li&gt;Al usar modelos externos (GPT, Claude, Gemini, etc.) Venice actúa como proxy anonimizador, o sea que el modelo final no sabe quién está realizando la petición. Obviamente reciben el &lt;em&gt;prompt&lt;/em&gt; completo y lo registrarán, pero mientras no dés información personal en él pues no deberían poder identificarte.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Otra cosa que me gusta es que tienen un sistema de &lt;a href="https://venice.ai/token"&gt;tokens&lt;/a&gt; con el que disfruto de cuenta Pro (con 100 VVV), y también tengo créditos a gastar cada día (1 DIEM da $1 en créditos cada día, para siempre).&lt;/p&gt;
&lt;p&gt;El problema entonces era que a veces estaba programando en Opencode y de repente veía el mensaje de que me había quedado sin créditos.
Quería un &lt;em&gt;widget&lt;/em&gt; en mi escritorio (&lt;a href="https://kde.org/"&gt;KDE&lt;/a&gt;) para saber de un vistazo cuánto me queda por gastar en el día.
En otro tiempo habría sido un proyecto enterrado en el baúl de los recuerdos, pero tras descubrir &lt;a href="https://github.com/Zetaphor/whatcable-linux"&gt;whatcable-linux&lt;/a&gt; cobré conciencia de que crear estos &lt;em&gt;widgets&lt;/em&gt; estaba al alcance de mi mano.&lt;/p&gt;
&lt;p&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/vibe_coding_escapades_3/retrosun.webp"&gt;&lt;figcaption&gt;Venice.ai KDE widget sobre un sol retro&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 id="ejecucion"&gt;Ejecución&lt;/h2&gt;
&lt;p&gt;Empecé un proyecto nuevo con Deepseek V4 Pro en Opencode.
Como ya comenté en &lt;a href="https://blog.fidelramos.net/es/software/vibe-coding-escapades-pelican-copy-code"&gt;el artículo anterior&lt;/a&gt; había salido hace poco y prometía resultados similares a Claude Opus a una fracción de precio, así que quería probarlo con proyectos reales.&lt;/p&gt;
&lt;p&gt;Lo primero fue generar un &lt;a href="https://github.com/haplo/venice-kde-widget/blob/main/AGENTS.md"&gt;&lt;em&gt;AGENTS.md&lt;/em&gt;&lt;/a&gt; con el objetivo y estructura del proyecto.
Después le pedí documentar la &lt;a href="https://docs.venice.ai/api-reference/endpoint/billing/balance"&gt;API de Venice que íbamos a usar&lt;/a&gt;.
Hasta ahí todo bien.&lt;/p&gt;
&lt;p&gt;Lo siguiente era generar un &lt;em&gt;widget&lt;/em&gt; estático que mostrase la información que necesitaba, pero sin interactuar con ninguna API por el momento.
Aquí Deepseek falló estrepitosamente: los ficheros &lt;a href="https://en.wikipedia.org/wiki/QML"&gt;QML&lt;/a&gt; (el lenguaje propio de la librería Qt que utiliza KDE) que generaba ni siquiera compilaban, el &lt;em&gt;widget&lt;/em&gt; no llegaba a cargar.
Tras varios intentos tuve que desistir y cambiar a Claude Opus 4.7.&lt;/p&gt;
&lt;p&gt;Opus descubrió que el problema era que &lt;strong&gt;el &lt;em&gt;widget&lt;em&gt; estaba siendo &lt;/em&gt;cacheado&lt;/em&gt;&lt;/strong&gt; por KDE, de forma que aunque instalase una versión nueva seguía cargándose el antiguo.
Probablemente Deepseek había arreglado el problema pero yo seguía viendo la versión antigua &lt;em&gt;cacheada&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Opus encontró la forma de reiniciar Plasma para que cargase la nueva versión del &lt;em&gt;widget&lt;/em&gt;, y con eso ya compilaba y se visualizaba.
A partir de ahí el proceso fue mucho más iterativo.&lt;/p&gt;
&lt;p&gt;Siguiente paso: conectar con la API real.
Para simplificar pedí que leyese el &lt;em&gt;token&lt;/em&gt; de una variable de entorno, Opus &lt;a href="https://github.com/haplo/venice-kde-widget/commit/e489c9831acf427d432aff5b6c0f722be348786e"&gt;lo consiguió a la primera&lt;/a&gt;.
¡Ya veía mis datos reales!
Como proyecto personal podía haber parado aquí, ya era útil y cubría lo que necesitaba, pero quería pulirlo un poco y publirlo por si otros quisieran usarlo.&lt;/p&gt;
&lt;p&gt;Lo siguiente fue almacenar el &lt;em&gt;token&lt;/em&gt; de forma segura, que no estuviese en texto plano en ninguna parte.
Le pedí a Opus que explorase opciones:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/KDE/kwallet"&gt;kwallet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man.archlinux.org/man/core/libsecret/secret-tool.1.en"&gt;secret-tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.passwordstore.org/"&gt;pass&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Decidí avanzar con &lt;em&gt;secret-tool&lt;/em&gt; porque aunque KWallet sea el sistema nativo de KDE,
hace tiempo que se integra con libsecret (y por tanto con secret-tool).
Opus lo tuvo &lt;a href="https://github.com/haplo/venice-kde-widget/commit/7bf3fe4d8c584282afde0b1bb14d615e683db0dc"&gt;en un intento&lt;/a&gt; (más &lt;a href="https://github.com/haplo/venice-kde-widget/commit/c289380250685326ce2f3171f7009ed339ac487d"&gt;un bug al arrancar&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;El resto fue iterar en la interfaz de usuario, ajustando colores y posiciones un poco, e introducir algunas opciones de configuración, nada importante que destacar.&lt;/p&gt;
&lt;h2 id="resultado"&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Tras un fin de semana entero jugueteando, el resultado fue
&lt;a href="https://github.com/haplo/venice-kde-widget/"&gt;venice-kde-widget&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/vibe_coding_escapades_3/screenshot.webp"&gt;&lt;figcaption&gt;Venice.ai KDE widget&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Lo uso todos los días y ha estado funcionando a las mil maravillas. :)&lt;/p&gt;
&lt;h2 id="lecciones"&gt;Lecciones&lt;/h2&gt;
&lt;p&gt;El problema inicial de que el &lt;em&gt;widget&lt;/em&gt; era &lt;em&gt;cacheado&lt;/em&gt; por KDE Plasma me hizo darme cuenta de que muchos modelos necesitan instrucciones más específicas que otros, son menos creativos.
Si les hubiese pedido que pensasen opciones alternativas a por qué seguía fallando quizá lo habrían encontrado, pero yo mismo no pensé en la posibilidad de que estuviese &lt;em&gt;cacheado&lt;/em&gt;.
Opus fue el único suficientemente proactivo como para pensar "vamos a confirmar si el código que se está realmente ejecutando es el que tenemos en el repositorio".
Fue más listo que yo, en definitivas cuentas.&lt;/p&gt;
&lt;p&gt;Esa proactividad es algo sobre lo que &lt;a href="https://simonw.substack.com/p/claude-fable-is-relentlessly-proactive"&gt;Simon Willison escribía al respecto de Fable 5&lt;/a&gt;.
Esta experiencia es indicativa que es algo que aplica a otros modelos, o sea Claude Opus 4.7 fue más proactivo que Deepseek V4 Pro.
Me entra curiosidad de pensar si &lt;strong&gt;la proactividad puede ser algo medible entre modelos&lt;/strong&gt;, y si hay estudios al respecto.&lt;/p&gt;
&lt;p&gt;Otra lección es que para un proyecto con una tecnología tan minoritaria (&lt;em&gt;widgets&lt;/em&gt; de KDE en QML) la mayoría de modelos no van a dar grandes resultados, les debe &lt;strong&gt;faltar entrenamiento&lt;/strong&gt;.
Quizás los resultados habrían sido mejores si les hubiese indicado que mirasen el código de otros &lt;em&gt;widgets&lt;/em&gt; para que les sirviese de &lt;strong&gt;referencia&lt;/strong&gt;.
Opus acabó haciendo eso para algunos problemas que no conseguía resolver: miró al resto de widgets instalados en el sistema para analizar su código.
Probablemente habría sido una buena idea haber &lt;strong&gt;buscado widgets similares&lt;/strong&gt; y haber pedido un &lt;strong&gt;análisis antes de implementar&lt;/strong&gt;.
O mejor aún afinar un modelo programador con muchos ejemplos de &lt;em&gt;widgets&lt;/em&gt; KDE, pero eso obviamente sería exagerado para un proyecto como este.&lt;/p&gt;
&lt;p&gt;Los resultados en este proyecto, más los de &lt;a href="https://blog.fidelramos.net/es/software/vibe-coding-escapades-pelican-copy-code"&gt;pelican-copy-code en el artículo anterior&lt;/a&gt;, me han llevado a &lt;strong&gt;reducir el uso de Deepseek V4 Pro&lt;/strong&gt;; es más barato pero no me compensa si me lleva a callejones sin salida con código que no funciona y hay que arreglar.&lt;/p&gt;
&lt;p&gt;Seguí usando &lt;strong&gt;Kimi 2.6&lt;/strong&gt; como modelo por defecto, sobre todo por ser privado en Venice.ai.
Eso cambió con &lt;strong&gt;GLM 5.2&lt;/strong&gt;, pero esa será otra historia.&lt;/p&gt;</content><category term="Software"/><category term="ai"/><category term="kde"/><category term="programming"/></entry><entry><title>Tailscale autoalojado, Parte 4: Servicios web sólo en Tailscale</title><link href="https://blog.fidelramos.net/es/software/tailscale-4-web-services" rel="alternate"/><published>2026-06-04T00:00:00+00:00</published><updated>2026-06-04T14:53:22.938833+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-06-04:/es/software/tailscale-4-web-services</id><summary type="html">&lt;p&gt;Desde hace años tengo un &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;servidor casero&lt;/a&gt; con varios servicios autoalojados (Immich, Jellyfin, Nextcloud...) detrás de una única instancia de Caddy utilizando &lt;code&gt;caddy-docker-proxy&lt;/code&gt; (documentado &lt;a href="https://blog.fidelramos.net/es/software/switch-nginx-caddy-docker-compose"&gt;en esta entrada&lt;/a&gt;).
Algunos de estos servicios tienen usos públicos útiles, como por ejemplo compartir una carpeta en Nextcloud o un álbum en Immich; o conectarse …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Desde hace años tengo un &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;servidor casero&lt;/a&gt; con varios servicios autoalojados (Immich, Jellyfin, Nextcloud...) detrás de una única instancia de Caddy utilizando &lt;code&gt;caddy-docker-proxy&lt;/code&gt; (documentado &lt;a href="https://blog.fidelramos.net/es/software/switch-nginx-caddy-docker-compose"&gt;en esta entrada&lt;/a&gt;).
Algunos de estos servicios tienen usos públicos útiles, como por ejemplo compartir una carpeta en Nextcloud o un álbum en Immich; o conectarse a Jellyfin a través de un dispositivo externo (como un Smart TV).
No obstante, la mayoría de mis interacciones son a través de dispositivos personales, así que tenerlos expuestos a la Internet pública supone un &lt;strong&gt;riesgo de seguridad&lt;/strong&gt; considerable, dado que son puntos de entrada a mis sistemas internos.&lt;/p&gt;
&lt;p&gt;Ahora que tengo &lt;a href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients"&gt;Tailscale funcionando&lt;/a&gt;, la solución es hacer que el mayor número posible de servicios sean accesibles únicamente a través de la tailnet.
Para lograrlo, ejecutaré &lt;strong&gt;dos instancias de Caddy en paralelo&lt;/strong&gt;: una vinculada a la interfaz pública, para los servicios accesibles por Internet; otra vinculada a la interfaz de Tailscale, para los servicios exclusivos de la tailnet.&lt;/p&gt;
&lt;p&gt;La solución con &lt;code&gt;caddy-docker-proxy&lt;/code&gt; no fue obvia, pero tras superar varios escollos conseguí que funcionara sin recurrir a ningún truco sucio.
En esta entrada detallo la configuración y las lecciones aprendidas.&lt;/p&gt;
&lt;h2 id="la-arquitectura"&gt;La arquitectura&lt;/h2&gt;
&lt;p&gt;Dos instancias independientes de Caddy en Docker, ambas utilizando &lt;code&gt;caddy-docker-proxy&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;caddy-public&lt;/code&gt;&lt;/strong&gt;: vinculada a la IP del servidor en la LAN, accesible desde Internet por el reenvío de puertos del router en los puertos 80 y 443. Gestiona los servicios públicos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;caddy-tailscale&lt;/code&gt;&lt;/strong&gt;: vinculado a la IP Tailscale del host (&lt;code&gt;100.x.y.z&lt;/code&gt;), accesible únicamente desde dispositivos de mi tailnet. Se encarga de los servicios privados.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cada Caddy supervisa las etiquetas de Docker con un prefijo diferente, por lo que los servicios se incorporan a uno u otro (o a ambos, en raras ocasiones) utilizando el prefijo de etiqueta adecuado y uniéndose a la red de Docker correspondiente.&lt;/p&gt;
&lt;p&gt;Los servicios privados utilizan &lt;a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge"&gt;DNS-01&lt;/a&gt; (mediante Gandi, mi proveedor de alojamiento DNS) para obtener los certificados, dado que no son accesibles por Internet.
Los servicios públicos siguen utilizando certificados individuales normales a través de HTTP-01.&lt;/p&gt;
&lt;p&gt;Aquí dejo un pequeño diagrama general:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Internet ──► router:443 ──► host:LAN_IP:443 ──► caddy-public    ──► headscale
Tailnet  ──► host:TAILSCALE_IP:443          ──► caddy-tailscale ──► immich, jellyfin, nextcloud, etc.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="por-que-dos-caddys-y-no-uno-solo-con-una-lista-de-direcciones-ip-permitidas"&gt;¿Por qué dos Caddys y no uno solo con una lista de direcciones IP permitidas?&lt;/h2&gt;
&lt;p&gt;Lo pensé.
Un único Caddy con un filtro &lt;a href="https://caddyserver.com/docs/modules/http.matchers.remote_ip"&gt;&lt;code&gt;remote_ip&lt;/code&gt;&lt;/a&gt; que restringiera ciertos nombres de dominio a &lt;code&gt;100.64.0.0/10&lt;/code&gt; también funcionaría.
&lt;strong&gt;Pero&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El puerto 443 seguiría siendo accesible desde Internet para esos dominios. Tendría que confiar en el filtrado de capa 7 para mantener en raya a atacantes de servicios que no deberían estar expuestos a la WAN en absoluto.&lt;/li&gt;
&lt;li&gt;Los servicios privados necesitan certificados DNS-01 (porque Let's Encrypt no puede acceder a una dirección &lt;code&gt;100.x&lt;/code&gt; para HTTP-01), mientras que los servicios públicos no.&lt;/li&gt;
&lt;li&gt;Se te olvida incluir una etiqueta en Docker y de repente un servicio interno queda expuesto a la Internet pública.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dos Caddys independientes me proporcionan aislamiento a nivel de red: los servicios privados, literalmente, no están a la escucha en un &lt;em&gt;socket&lt;/em&gt; accesible desde la WAN.
Esa es una garantía mucho más sólida que "recibiendo peticiones pero rechazándolas".&lt;/p&gt;
&lt;h2 id="la-configuracion-de-docker-compose"&gt;La configuración de Docker Compose&lt;/h2&gt;
&lt;p&gt;Esta es la estructura básica (con algunas variables de entorno para mayor claridad):&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy-public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;dockerfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Dockerfile.caddy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy-docker-proxy-gandi:local&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy-public&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;unless-stopped&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;CADDY_INGRESS_NETWORKS=caddy_public&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${LAN_IP:?LAN_IP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set}:80:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${LAN_IP:?LAN_IP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set}:443:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy.email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_EMAIL}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_FILES_PATH}/public:/data&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_public&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.1.1.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;9.9.9.9&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy-tailscale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;.&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;dockerfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Dockerfile.caddy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy-docker-proxy-gandi:local&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy-tailscale&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;unless-stopped&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;CADDY_INGRESS_NETWORKS=caddy_tailscale&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;CADDY_DOCKER_LABEL_PREFIX=caddy_ts&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;GANDI_BEARER_TOKEN=${GANDI_BEARER_TOKEN}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${TAILSCALE_IP:?TAILSCALE_IP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set}:80:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${TAILSCALE_IP:?TAILSCALE_IP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set}:443:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy_ts.email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_EMAIL}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy_ts.acme_dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"gandi&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{env.GANDI_BEARER_TOKEN}"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_FILES_PATH}/tailscale:/data&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_tailscale&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.1.1.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;9.9.9.9&lt;/span&gt;

&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_public&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy_tailscale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_tailscale&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Algunos puntos que vale la pena destacar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Espacios de nombres independientes para las etiquetas.&lt;/strong&gt; &lt;code&gt;CADDY_DOCKER_LABEL_PREFIX=caddy_ts&lt;/code&gt; indica al Caddy de tailnet que lea las etiquetas que empiecen por &lt;code&gt;caddy_ts&lt;/code&gt;. El Caddy público utiliza el valor predeterminado &lt;code&gt;caddy&lt;/code&gt;. Los servicios se incorporan a uno u otro Caddy mediante el prefijo, sin posibilidad de contaminación cruzada.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;CADDY_INGRESS_NETWORKS&lt;/code&gt;&lt;/strong&gt;: Cada Caddy está configurado para detectar únicamente los contenedores conectados a su propia red. Sin esto, cada Caddy intentaría actuar como proxy de todos los contenedores etiquetados que pudiera ver a través del socket de Docker, y entrarían en conflicto.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Volúmenes &lt;code&gt;/data&lt;/code&gt; separados.&lt;/strong&gt; Cada Caddy tiene su propio almacenamiento de certificados. Podrían compartir un mismo volumen, pero separarlos es más limpio y elimina un caso extremo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Escucha en IP específicas, no en &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/strong&gt; Esto me costó un buen rato depurando (lo explico más abajo).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Las guardas &lt;code&gt;:?&lt;/code&gt; en las variables de entorno.&lt;/strong&gt; &lt;em&gt;Compose&lt;/em&gt; usa silenciosamente &lt;code&gt;0.0.0.0&lt;/code&gt; si la parte de la IP del host de una especificación de puerto está vacía. La sintaxis &lt;code&gt;:?&lt;/code&gt; hace que, en cambio, falle de forma evidente y se pueda corregir. Después de tropezar dos veces con la misma piedra, pienso usar estas guardas en cualquier servicio que necesite estar vinculado a una IP específica.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="imagen-de-caddy-con-el-complemento-dns-de-gandi"&gt;Imagen de Caddy con el complemento DNS de Gandi&lt;/h2&gt;
&lt;p&gt;Utilizo Gandi como proveedor de DNS y, por suerte, Caddy cuenta con &lt;a href="https://github.com/caddy-dns/gandi"&gt;un complemento para ello&lt;/a&gt;.
Sin embargo, la imagen &lt;code&gt;lucaslorentz/caddy-docker-proxy&lt;/code&gt; no lo incluye, por lo que hay que compilarlos; no es posible añadirlos en tiempo de ejecución.
He creado una pequeña imagen personalizada mediante un archivo &lt;code&gt;Dockerfile.caddy&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CADDY_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;caddy:${CADDY_VERSION}-builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xcaddy&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
--with&lt;span class="w"&gt; &lt;/span&gt;github.com/lucaslorentz/caddy-docker-proxy/v2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
--with&lt;span class="w"&gt; &lt;/span&gt;github.com/caddy-dns/gandi

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;caddy:${CADDY_VERSION}-alpine&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--from&lt;span class="o"&gt;=&lt;/span&gt;builder&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/caddy&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/caddy

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"caddy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker-proxy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Gracias a &lt;code&gt;dockerfile: Dockerfile.caddy&lt;/code&gt; en &lt;em&gt;compose.yml&lt;/em&gt;, con &lt;code&gt;docker compose build caddy-tailscale&lt;/code&gt; se realiza la compilación de la imagen.&lt;/p&gt;
&lt;p&gt;Este patrón es habitual y &lt;a href="https://hub.docker.com/_/caddy#adding-custom-caddy-modules"&gt;está documentado para crear imágenes personalizadas de Caddy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Un detalle clave es el &lt;code&gt;CMD&lt;/code&gt; final. &lt;code&gt;caddy-docker-proxy&lt;/code&gt; se ejecuta en un modo diferente al del servidor Caddy normal, y hay que anularlo explícitamente.&lt;/p&gt;
&lt;p&gt;En Gandi creé un token de acceso personal con permiso para "Gestionar configuraciones técnicas de dominios".
Lo incluí en &lt;code&gt;.env&lt;/code&gt; como &lt;code&gt;GANDI_BEARER_TOKEN&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="traslado-de-servicios-a-tailscale-caddy"&gt;Traslado de servicios a Tailscale Caddy&lt;/h2&gt;
&lt;p&gt;Ahora que DNS-01 debería funcionar, es el momento de trasladar mis servicios web a Tailscale.&lt;/p&gt;
&lt;p&gt;Los cambios son sencillos: basta con ponerlos en la red &lt;code&gt;caddy_tailscale&lt;/code&gt;, cambiar las etiquetas &lt;code&gt;caddy&lt;/code&gt; por &lt;code&gt;caddy_ts&lt;/code&gt; y ajustar los nombres de dominio a &lt;code&gt;ts.fidelramos.net&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Por ejemplo, mi servicio Immich (muestro solo las partes relevantes):&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;immich-server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;caddy_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;immich.ts.fidelramos.net&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;caddy_ts.reverse_proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"{{upstreams&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2283}}"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy_tailscale&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;immich&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Qué maravilla, ¿no te parece?&lt;/p&gt;
&lt;h2 id="configuracion-del-dns"&gt;Configuración del DNS&lt;/h2&gt;
&lt;p&gt;No hay registros DNS públicos.
En su lugar, utilizo la opción &lt;code&gt;customDNS&lt;/code&gt; de Blocky (véase la &lt;a href="https://blog.fidelramos.net/es/software/tailscale-2-ad-blocking-dns"&gt;Parte 2&lt;/a&gt;) para crear registros DNS que solo serán visibles para mis dispositivos de la red tailnet.&lt;/p&gt;
&lt;p&gt;En la configuración de Blocky:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;customDNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;customTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1h&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;filterUnmappedTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ts.fidelramos.net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tail&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# permite "X.ts.fidelramos.net" como alias de "X.tail"&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Nombres descriptivos para los servicios en tailnet&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;immich.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;jellyfin.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;nextcloud.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Esto me obliga a gestionar manualmente las entradas y las direcciones IP, pero al tratarse de una modesta red doméstica, los servicios cambian con poca frecuencia y es una solución razonable.&lt;/p&gt;
&lt;h2 id="verificacion"&gt;Verificación&lt;/h2&gt;
&lt;p&gt;Tras ejecutar &lt;code&gt;docker-compose up -d&lt;/code&gt;, compruebo los registros de los contenedores para asegurarme de que todo va bien.
Las dos instancias de Caddy se inician correctamente.
&lt;code&gt;caddy-tailscale&lt;/code&gt; emite nuevos certificados mediante el desafío DNS-01.&lt;/p&gt;
&lt;p&gt;En el servidor compruebo que cada instancia de Caddy escucha en su IP asignada:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;ss&lt;span class="w"&gt; &lt;/span&gt;-tlnp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;“:80&lt;span class="p"&gt;|&lt;/span&gt;:443”
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.168.1.50:80&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.168.1.50:443&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1:80&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1:443&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;El router solo reenvía a la IP de la LAN, por lo que el Caddy de la &lt;em&gt;tailnet&lt;/em&gt; es invisible para Internet.&lt;/p&gt;
&lt;p&gt;Se puede acceder a los servicios públicos en &lt;code&gt;https://&amp;lt;service&amp;gt;.fidelramos.net&lt;/code&gt; desde cualquier lugar.
Solo se puede acceder a los servicios privados en &lt;code&gt;https://&amp;lt;service&amp;gt;.ts.fidelramos.net&lt;/code&gt; desde dispositivos de mi tailnet.&lt;/p&gt;
&lt;h2 id="los-problemas-con-los-que-me-tope-para-que-tu-no-tengas-que-hacerlo"&gt;Los problemas con los que me topé para que tú no tengas que hacerlo&lt;/h2&gt;
&lt;p&gt;Esta es la parte que me hubiera gustado que alguien me hubiera explicado, ya que me llevó bastante tiempo resolverla.&lt;/p&gt;
&lt;h3 id="compose-recurre-automaticamente-a-0000"&gt;Compose recurre automáticamente a &lt;code&gt;0.0.0.0&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Si escribes &lt;code&gt;${TAILSCALE_IP}:80:80&lt;/code&gt; y &lt;code&gt;TAILSCALE_IP&lt;/code&gt; está vacío, Docker vincula &lt;code&gt;0.0.0.0:80&lt;/code&gt;.
Entonces, ambos Caddys compiten por el mismo puerto y uno falla al iniciarse con el mensaje "el puerto ya está asignado".&lt;/p&gt;
&lt;p&gt;Utilicé &lt;code&gt;${TAILSCALE_IP:?...}&lt;/code&gt; para que fallara de forma evidente si la variable de entorno no está definida.&lt;/p&gt;
&lt;p&gt;Resolver esto fue bastante rápido, pero luego me topé con el siguiente problema...&lt;/p&gt;
&lt;h3 id="los-contenedores-fallidos-dejan-zombis"&gt;Los contenedores fallidos dejan zombis&lt;/h3&gt;
&lt;p&gt;Cuando un contenedor no consigue vincular sus puertos, Docker lo deja en un extraño estado intermedio: &lt;code&gt;NetworkMode&lt;/code&gt; está configurado correctamente, pero el contenedor en realidad no está conectado a ninguna red.
&lt;code&gt;docker inspect&lt;/code&gt; muestra &lt;code&gt;"Networks": {}&lt;/code&gt;, y &lt;code&gt;ip addr&lt;/code&gt; dentro del contenedor solo muestra &lt;code&gt;lo&lt;/code&gt;.
Un &lt;code&gt;docker compose up -d&lt;/code&gt; posterior detecta que el contenedor "existe" y simplemente lo inicia sin arreglar nada.
El resultado es un contenedor en ejecución que no tiene conectividad alguna.&lt;/p&gt;
&lt;p&gt;La solución fue empezar de cero, con &lt;code&gt;docker compose down&lt;/code&gt;, y luego &lt;code&gt;docker compose up -d&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="la-conexion-de-000080-entra-en-conflicto-con-100xyz80"&gt;La conexión de &lt;code&gt;0.0.0.0:80&lt;/code&gt; entra en conflicto con &lt;code&gt;100.x.y.z:80&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Aunque ambas IP sean "válidas", Docker trata &lt;code&gt;0.0.0.0&lt;/code&gt; como un superconjunto. Tenía &lt;code&gt;caddy-public&lt;/code&gt; vinculado a &lt;code&gt;0.0.0.0:80&lt;/code&gt; y &lt;code&gt;caddy-tailscale&lt;/code&gt; vinculado a &lt;code&gt;100.64.0.1:80&lt;/code&gt;, y se producían fallos intermitentes en el inicio dependiendo del orden.&lt;/p&gt;
&lt;p&gt;La solución es fijar &lt;strong&gt;ambos&lt;/strong&gt; a direcciones IP específicas (la IP de LAN para el público y la IP de Tailscale para tailnet).&lt;/p&gt;
&lt;p&gt;Sin solapamientos, sin conflictos.&lt;/p&gt;
&lt;h3 id="caddy-docker-proxy-necesita-la-red-del-servicio"&gt;&lt;code&gt;caddy-docker-proxy&lt;/code&gt; necesita la red del servicio&lt;/h3&gt;
&lt;p&gt;Tanto el filtro &lt;code&gt;CADDY_INGRESS_NETWORKS&lt;/code&gt; de Caddy como el enrutamiento propiamente dicho requieren que el servicio al que se aplica el proxy se encuentre en la misma red de Docker que Caddy.
Un servicio puede estar en redes adicionales (para su propia base de datos, etc.), pero necesita al menos la red de entrada de Caddy, o será invisible.&lt;/p&gt;
&lt;h2 id="lo-volveria-a-hacer"&gt;¿Lo volvería a hacer?&lt;/h2&gt;
&lt;p&gt;Sí.
La configuración inicial fue sorprendentemente complicada porque se acumularon múltiples fallos, pero la arquitectura resultante es limpia y fácil de ampliar.&lt;/p&gt;
&lt;p&gt;Cada nuevo servicio que añado pasa por la misma decisión sencillísima:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Público: etiquetas &lt;code&gt;caddy.*&lt;/code&gt;, red &lt;code&gt;caddy_public&lt;/code&gt;, registro DNS en Gandi.&lt;/li&gt;
&lt;li&gt;Privado: etiquetas &lt;code&gt;caddy_ts.*&lt;/code&gt;, red &lt;code&gt;caddy_tailscale&lt;/code&gt;, registro DNS en Blocky.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No hay reglas de cortafuegos que mantener sincronizadas, ni middleware de autenticación por servicio, sin ese "solo lo expondré temporalmente" que acaba convirtiéndose en permanente.&lt;/p&gt;
&lt;p&gt;La breve lección que se desprende de todo este ejercicio: Tailscale es excelente para hacer que los servicios "solo para mí" sean accesibles desde mis dispositivos, pero integrarlo con un proxy inverso basado en Docker presenta más dificultades que cualquiera de las dos tecnologías por separado.
Vincular los listeners a direcciones IP específicas, generar un error claro cuando faltan variables de entorno y asignar a los contenedores resolutores DNS explícitos son los tres hábitos que me habrían ahorrado la mayor parte de los problemas.&lt;/p&gt;
&lt;h2 id="mejoras-futuras-acceso-limitado-para-otras-personas"&gt;Mejoras futuras: acceso limitado para otras personas&lt;/h2&gt;
&lt;p&gt;Con esta configuración, he perdido la posibilidad de compartir un álbum de Immich con mi familia o amigos, o de compartir una carpeta de Nextcloud enviando un enlace.
Se trata de casos de uso realmente útiles que me gustaría recuperar de alguna manera.&lt;/p&gt;
&lt;p&gt;En cuanto a mi familia, estudiaré la posibilidad de darles acceso a mi tailnet utilizando listas de control de acceso (ACL) para restringir el acceso de sus dispositivos únicamente a los servicios que necesiten.&lt;/p&gt;
&lt;p&gt;Para compartir archivos, estoy investigando opciones como &lt;a href="https://www.filestash.app/"&gt;Filestash&lt;/a&gt; o &lt;a href="https://filebrowserquantum.com/"&gt;FileBrowser Quantum&lt;/a&gt;, que son servicios diseñados específicamente para este caso de uso concreto.
Lo que quiero es que la interfaz de usuario de administración completa solo sea accesible a través de la red de acceso restringido, y que sus puntos finales de uso compartido público sean enrutables por Internet.&lt;/p&gt;
&lt;p&gt;Sea cual sea la solución a la que llegue, probablemente escribiré entradas de blog al respecto.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="howto"/><category term="linux"/><category term="security"/><category term="tailscale"/><category term="vpn"/><category term="web"/></entry><entry><title>Aventuras en vibe coding: pelican-copy-code</title><link href="https://blog.fidelramos.net/es/software/vibe-coding-escapades-pelican-copy-code" rel="alternate"/><published>2026-05-26T00:00:00+00:00</published><updated>2026-06-25T18:10:12.070668+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-26:/es/software/vibe-coding-escapades-pelican-copy-code</id><summary type="html">&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Me di cuenta que este blog usa muchísimos bloques de código, pero no es fácil copiarlos porque falta un botón de &lt;em&gt;Copiar&lt;/em&gt; en cada bloque.&lt;/p&gt;
&lt;p&gt;Busqué un plugin para &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt; que lo hiciese, convencido de que habría alguno ya hecho, ¡pero me sorprendí al no encontrar nada!&lt;/p&gt;
&lt;p&gt;Me …&lt;/p&gt;</summary><content type="html">&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Me di cuenta que este blog usa muchísimos bloques de código, pero no es fácil copiarlos porque falta un botón de &lt;em&gt;Copiar&lt;/em&gt; en cada bloque.&lt;/p&gt;
&lt;p&gt;Busqué un plugin para &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt; que lo hiciese, convencido de que habría alguno ya hecho, ¡pero me sorprendí al no encontrar nada!&lt;/p&gt;
&lt;p&gt;Me pareció un proyecto ideal para resolver rápidamente con IA.
Podía solventar mi problema y crear un proyecto útil para la comunidad.&lt;/p&gt;
&lt;h2 id="ejecucion"&gt;Ejecución&lt;/h2&gt;
&lt;p&gt;Mi objetivo era crear un nuevo proyecto publicable a PyPI, con estos requisitos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compatible con Pelican estable.&lt;/li&gt;
&lt;li&gt;Instalable con &lt;code&gt;pip&lt;/code&gt; y con autodescubrimiento. O sea debe ser un plugin Pelican de nueva generación, no necesita añadirse en la configuración de Pelican (&lt;code&gt;pelicanconf.py&lt;/code&gt;), se instala con &lt;code&gt;pip&lt;/code&gt; y a correr.&lt;/li&gt;
&lt;li&gt;Ficheros estáticos de Javascript y CSS, sin &lt;code&gt;npm&lt;/code&gt; ni similar.&lt;/li&gt;
&lt;li&gt;Sin dependencias externas de Javascript ni CSS, sólo APIs estándar.&lt;/li&gt;
&lt;li&gt;No debe necesitar cambios en las plantillas. Una vez activado el plugin inserta el botón automáticamente en todos los bloques.&lt;/li&gt;
&lt;li&gt;Sin manipulación del DOM, el botón se inserta al generar el sitio Pelican.&lt;/li&gt;
&lt;li&gt;Configurable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usé OpenCode (aislado, &lt;a href="https://blog.fidelramos.net/es/software/how-i-sandbox-ai-agents"&gt;lo describo en este otro artículo&lt;/a&gt;), inicialmente con Kimi 2.6, después con Deepseek V4 Pro, y más adelante con Claude Opus 4.7.
Comentaré los detalles en la sección de &lt;em&gt;Lecciones&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;El desarrollo fue a ratos en una tarde.
Mientras hacía otras cosas iba al ordenador, le pedía el siguiente cambio en el modo de planificación, luego volvía, revisaba rápidamente y le daba el visto bueno para implementar.&lt;/p&gt;
&lt;p&gt;Los cambios los iba probando en local en este blog.
Simplemente instalé el plugin con &lt;code&gt;uv&lt;/code&gt; en modo editable: &lt;code&gt;uv add --editable ~/Code/pelican-copy-code&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Es un proyecto muy pequeño así que no me molesté en generar ni mantener tests, &lt;em&gt;linting&lt;/em&gt;, ni acciones de Github.&lt;/p&gt;
&lt;h2 id="resultados"&gt;Resultados&lt;/h2&gt;
&lt;p&gt;El plugin es &lt;code&gt;pelican-copy-code&lt;/code&gt;, publicado en &lt;a href="https://github.com/haplo/pelican-copy-code"&gt;Github&lt;/a&gt; y &lt;a href="https://pypi.org/project/pelican-copy-code/"&gt;PyPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Es instalable directamente como se pretendía, está a un &lt;code&gt;pip install pelican-copy-code&lt;/code&gt; de distancia.&lt;/p&gt;
&lt;p&gt;Mejor que una captura de pantalla dejo este bloque de código con el que experimentarlo en vivo, sólo pasa el cursor por encima y haz click en "Copiar":&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;swallow_airspeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;laden&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;laden&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"African or European?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BlackKnight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flesh_wounds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"'Tis but a scratch!"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;is_defeated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flesh_wounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swallow_airspeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;laden&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"coconut"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="lecciones"&gt;Lecciones&lt;/h2&gt;
&lt;p&gt;Inicialmente usé Kimi 2.6 porque es mi modelo por defecto en &lt;a href="https://venice.ai/"&gt;Venice.ai&lt;/a&gt;.
Es barato, rápido y privado (Venice.ai no guarda registros de las conversaciones en sus propios modelos).&lt;/p&gt;
&lt;p&gt;Probé a cambiar a Deepseek V4 Pro porque era novedoso y decían que algo mejor para programar, comparable a Claude Opus 4.5.
No noté demasiado cambio, para un proyecto tan pequeño probablemente cualquiera de los dos habría dado buenos resultados.&lt;/p&gt;
&lt;p&gt;Sí es verdad que Kimi 2.6 cometió algunos errores en su implementación inicial, como por ejemplo algunos de los ajustes de configuración que le pedí incluir no tenían efecto, o sea el código no los usaba nunca.
Fue algo fácil de corregir en sesiones separadas, pero me hizo abrir los ojos sobre lo importante que es la fase de pruebas.&lt;/p&gt;
&lt;p&gt;También acabé haciendo algunas refactorizaciones a mano que probablemente me podría haber evitado si hubiese sido más específico en las instrucciones en &lt;code&gt;AGENTS.md&lt;/code&gt;.
Por ejemplo mantener las variables de configuración en orden alfabético, debería haber sido un requisito de estilo en &lt;code&gt;AGENTS.md&lt;/code&gt;.
También el agente puso algunas variables de configuración fuera del diccionario.
Un error de estilo que un humano probablemente no habría cometido.&lt;/p&gt;
&lt;p&gt;Cuando llegó la hora de probar el plugin por primera vez resultó que no funcionaba, daba un error por mal uso de &lt;a href="https://github.com/haplo/pelican-copy-code/blob/f0a625b85caa3ff134ab85bfdf006abad4bd505c/pelican/plugins/copy_code/__init__.py#L8"&gt;las señales y los generadores&lt;/a&gt;.
Se lo puse a Deepseek V4 Pro pero no lo consiguó arreglar.
Ahí fue cuando cambié a Claude Opus 4.7, que al primer intento encontró el problema y lo arregló correctamente.
Cobró sentido los consejos que había leído por ahí de usar a Opus como planificador y a otro modelo más barato para ejecutar los cambios.
Se nota que es el modelo más avanzado, pero también mucho más caro que Kimi o Deepseek.&lt;/p&gt;
&lt;p&gt;Cuando implementé el &lt;a href="https://github.com/haplo/pelican-copy-code/commit/b76cff671550231135ced60dbd598993a160ebcf"&gt;soporte de traducciones&lt;/a&gt; lo hice con Deepseek V4 Pro, y funcionó bien.
Sin embargo duplicó código (las cadenas "Copy" y "Copied!" estaban duplicadas en dos ficheros distintos).
Le pedí refactorizar para eliminar la duplicación, pero la propuesta no era nada buena, eliminaba la duplicación pero añadía más código con mal estilo.
Una vez más cambié a Opus 4.7 que planteó una solución mucho mejor que realmente simplificaba el código.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="ai"/><category term="blog"/><category term="pelican"/><category term="programming"/><category term="python"/></entry><entry><title>Aventuras en vibe coding</title><link href="https://blog.fidelramos.net/es/software/vibe-coding-escapades" rel="alternate"/><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T17:54:36.939648+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-24:/es/software/vibe-coding-escapades</id><summary type="html">&lt;p&gt;Voy a empezar una nueva serie de artículos sobre cómo estoy utilizando la IA para programar software o resolver problemas técnicos.&lt;/p&gt;
&lt;p&gt;Los artículos tendrán esta estructura:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Problema&lt;/li&gt;
&lt;li&gt;Ejecución&lt;/li&gt;
&lt;li&gt;Resultado&lt;/li&gt;
&lt;li&gt;Lecciones y descubrimientos&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En este artículo no quiero entrar en consideraciones éticas, medioambientales o político-económicas de las IA, pero sí en …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Voy a empezar una nueva serie de artículos sobre cómo estoy utilizando la IA para programar software o resolver problemas técnicos.&lt;/p&gt;
&lt;p&gt;Los artículos tendrán esta estructura:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Problema&lt;/li&gt;
&lt;li&gt;Ejecución&lt;/li&gt;
&lt;li&gt;Resultado&lt;/li&gt;
&lt;li&gt;Lecciones y descubrimientos&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En este artículo no quiero entrar en consideraciones éticas, medioambientales o político-económicas de las IA, pero sí en mi &lt;strong&gt;experiencia como desarrollador&lt;/strong&gt;.
Las opiniones y estudios sobre el impacto de las IA en la productividad de los desarrolladores de software son dispares.
Algunos dicen que puede multiplicar la productividad, otros que aunque dé la sensación de mejorar, en realidad disminuye.
Tras estos meses de experiencia personal me inclino a pensar que son &lt;a href="https://www.joshwcomeau.com/email/wham-launch-005-elephant-2-p/"&gt;especialmente efectivas para quienes tienen profundos conocimientos técnicos&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tuve mi primer ordenador a los 5 años (¡casi 40 años ya!), con el que empecé a aprender informática y a programar.
Uso GNU/Linux desde 1998.
Completé mi educación superior en Ingeniería Informática.
He trabajado 15 años como ingeniero de software.
Y con las IA me siento &lt;strong&gt;10 veces más productivo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Ojo que digo "me siento", porque no tengo métricas objetivas para decir cuánto más productivo realmente soy.
Los estudios apuntan a que &lt;a href="https://www.rudikershaw.com/articles/how-much-faster"&gt;la mejora es de alrededor del 20%&lt;/a&gt;, pero las estadísticas suelen calcularse sobre proyectos grandes, que no es mi caso.
Trabajando en proyectos nuevos o pequeños creo que las IAs son muy eficientes.
En mi caso es indudable que estos proyectos no serían una realidad si no tuviese la ayuda de las IA.&lt;/p&gt;
&lt;p&gt;Mi principal problema en los últimos años ha sido el tiempo.&lt;br/&gt;
Tiempo para aprender algo nuevo.&lt;br/&gt;
Tiempo para dedicarle horas a una única tarea.&lt;br/&gt;
Hay demasiadas cosas que hacer y estoy demasiado ocupado con la vida para hacerlo todo.
Creo que esta es una experiencia común especialmente entre padres.&lt;/p&gt;
&lt;p&gt;Por eso mis ideas se quedaban años como &lt;em&gt;TODO&lt;/em&gt; en mis notas.
Ahora estoy entrando en una rutina en la que echo un vistazo a esa lista, elijo una y la ejecuto satisfactoriamente en minutos u horas, a ratos sueltos durante un fin de semana.
&lt;strong&gt;Esto es lo que pienso documentar en esta serie de artículos&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Dicho todo esto, no quiero ignorar &lt;a href="https://tomwojcik.com/posts/2026-02-15/finding-the-right-amount-of-ai/"&gt;las voces&lt;/a&gt; que avisan de los &lt;a href="https://www.media.mit.edu/publications/your-brain-on-chatgpt/"&gt;efectos negativos&lt;/a&gt; del uso (o &lt;a href="https://www.reddit.com/r/vibecoding/comments/1qz8opi/does_anyone_get_burnout_from_vibecoding_ive_been/"&gt;abuso&lt;/a&gt;) de las IA.
Personalmente espero que no me afecte negativamente, porque hago un uso relativamente reducido, pero procuraré ser introspectivo sobre mi estado mental.&lt;/p&gt;
&lt;p&gt;También leo a muchos programadores que odian la IA, hasta el punto de abandonar sus carreras, porque lo que disfrutan es el escribir el código ellos mismos.
Siento su dolor cuando tienen trabajos en los que les obligan a usar IA y están presionados a producir más y más rápido.
Su día a día ha cambiado rápidamente y cuando antes pasaban la mayor parte del tiempo escribiendo código, ahora eso se ha invertido y tienen que dedicarse a generar, revisar, corregir y ajustar, lo cuál es mucho menos divertido y mentalmente estresante y agotador.&lt;/p&gt;
&lt;p&gt;En mi caso escribir el código es secundario, me importa más el resultado final que el proceso.
Por eso estoy feliz de tomar los atajos que me brinda la IA, mientras pueda mantener el control y tener un resultado más rápido con la calidad que busco.&lt;/p&gt;</content><category term="Software"/><category term="ai"/><category term="programming"/></entry><entry><title>Tailscale autoalojado, Parte 3: Syncthing sobre Tailscale</title><link href="https://blog.fidelramos.net/es/software/tailscale-3-syncthing" rel="alternate"/><published>2026-05-18T00:00:00+00:00</published><updated>2026-06-04T11:32:59.035876+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-18:/es/software/tailscale-3-syncthing</id><summary type="html">&lt;p&gt;Llevo años usando &lt;a href="https://syncthing.net/"&gt;Syncthing&lt;/a&gt; para sincronizar archivos entre mi portátil, ordenador de sobremesa, teléfono, tablet y servidor. Es un programa fabuloso y ha sido muy fiable, con solo algún conflicto ocasional, pero nunca he perdido ningún dato.
Al contrario, el control de versiones de Syncthing me ha salvado el pellejo …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Llevo años usando &lt;a href="https://syncthing.net/"&gt;Syncthing&lt;/a&gt; para sincronizar archivos entre mi portátil, ordenador de sobremesa, teléfono, tablet y servidor. Es un programa fabuloso y ha sido muy fiable, con solo algún conflicto ocasional, pero nunca he perdido ningún dato.
Al contrario, el control de versiones de Syncthing me ha salvado el pellejo más de una vez.&lt;/p&gt;
&lt;p&gt;Syncthing tiene sus propias soluciones para &lt;a href="https://docs.syncthing.net/specs/globaldisco-v3.html"&gt;descubrimiento&lt;/a&gt; y &lt;a href="https://docs.syncthing.net/specs/relay-v1.html"&gt;NAT traversal&lt;/a&gt;, para que los dispositivos que están detrás de un cortafuegos puedan verse entre sí y seguir conectándose.
Sin embargo, eso depende de servidores externos para gestionar las conexiones, y podrían guardar metadatos. Incluso si no lo hacen, pueden degradar el rendimiento.&lt;/p&gt;
&lt;p&gt;Ahora que tengo una &lt;a href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients"&gt;tailnet interconectando todos mis dispositivos&lt;/a&gt;, no hay razón para seguir lidiando con nada de eso.
Cada dispositivo ya tiene una dirección estable accesible desde cualquier otro dispositivo.
Syncthing puede simplemente usarla, solo necesita algo de configuración.&lt;/p&gt;
&lt;p&gt;Este artículo explica la reconfiguración de Syncthing para que use la tailnet en exclusiva, desactivando toda la maquinaria que ya no necesita.&lt;/p&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#objetivos"&gt;Objetivos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#por-que-hacer-esto"&gt;Por qué hacer esto&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#reconfiguracion-de-syncthing"&gt;Reconfiguración de Syncthing&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#acerca-de-las-direcciones-de-escucha"&gt;Acerca de las direcciones de escucha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#configurar-pares-por-direccion-de-tailnet"&gt;Configurar pares por dirección de tailnet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cortafuegos"&gt;Cortafuegos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#router"&gt;Router&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#verificacion"&gt;Verificación&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#dificultades-encontradas"&gt;Dificultades encontradas&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#sintaxis-de-la-direccion-de-escucha-tailscale0"&gt;Sintaxis de la dirección de escucha %tailscale0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#los-pares-no-se-encuentran-despues-de-deshabilitar-el-descubrimiento"&gt;Los pares no se encuentran después de deshabilitar el descubrimiento&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#magicdns-falla-al-inicio-en-un-cliente-lento"&gt;MagicDNS falla al inicio en un cliente lento&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#syncthing-usando-contenedores"&gt;Syncthing usando contenedores&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cache-de-anuncios-de-descubrimiento"&gt;Caché de anuncios de descubrimiento&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#siguientes-pasos"&gt;Siguientes pasos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="objetivos"&gt;Objetivos&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Los pares de Syncthing se comunican entre sí exclusivamente a través de la tailnet.&lt;/li&gt;
&lt;li&gt;No depender de los servidores de descubrimiento global de Syncthing.&lt;/li&gt;
&lt;li&gt;No depender de la red de retransmisión de Syncthing.&lt;/li&gt;
&lt;li&gt;No hay necesidad de NAT traversal, lo gestiona Tailscale.&lt;/li&gt;
&lt;li&gt;No hay puertos abiertos en el router, no hay necesidad de UPnP.&lt;/li&gt;
&lt;li&gt;Todo sigue funcionando igual cuando estoy fuera de casa.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="por-que-hacer-esto"&gt;Por qué hacer esto&lt;/h2&gt;
&lt;p&gt;Hay varias pequeñas victorias que van sumando:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Privacidad.&lt;/strong&gt; El descubrimiento global de Syncthing se comunica con un servidor central para anunciar &lt;code&gt;(ID de dispositivo → IP pública)&lt;/code&gt;. Con la tailnet, esa difusión es inútil y se puede desactivar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fiabilidad.&lt;/strong&gt; Los relés son lentos y ocasionalmente inestables. Las conexiones directas a la tailnet son rápidas y consistentes, y el DERP de Tailscale ya proporciona un respaldo en la capa de red si no se puede establecer una ruta directa.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplicidad.&lt;/strong&gt; La dirección de cada par se convierte en una IP de tailnet fija (o nombre MagicDNS). Dejo de ver problemas de conexión entre dispositivos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad.&lt;/strong&gt; Syncthing escucha en TCP/UDP 22000 y UDP 21027 por defecto. Si esos puertos están expuestos en la WAN, son un servicio expuesto a Internet. Desactivar todo excepto la ruta de la tailnet cierra esa puerta, reduciendo la superficie de ataque.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reconfiguracion-de-syncthing"&gt;Reconfiguración de Syncthing&lt;/h2&gt;
&lt;p&gt;Syncthing viene con un montón de características para que funcione a través de Internet sin configuración:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuración&lt;/th&gt;
&lt;th&gt;Predeterminado&lt;/th&gt;
&lt;th&gt;Propósito&lt;/th&gt;
&lt;th&gt;En tailnet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sync Protocol Listen Addresses&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dónde aceptar conexiones entrantes&lt;/td&gt;
&lt;td&gt;La IP tailnet del dispositivo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permitir NAT traversal&lt;/td&gt;
&lt;td&gt;Habilitado&lt;/td&gt;
&lt;td&gt;UPnP / NAT-PMP to open ports in router&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Deshabilitado&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descubrimiento global&lt;/td&gt;
&lt;td&gt;Habilitado&lt;/td&gt;
&lt;td&gt;Announce device ID → IP on public discovery servers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Deshabilitado&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descubrimiento local&lt;/td&gt;
&lt;td&gt;Habilitado&lt;/td&gt;
&lt;td&gt;LAN multicast to find peers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Deshabilitado&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Habilitar retransmisión&lt;/td&gt;
&lt;td&gt;Habilitado&lt;/td&gt;
&lt;td&gt;Fallback traffic via public relay servers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Deshabilitado&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;El descubrimiento global y local, el NAT traversal y la retransmisión son innecesarios entre pares que están ya conectados por Tailscale.&lt;/p&gt;
&lt;p&gt;Cambié la configuración en cada ordenador a través de la interfaz web de Syncthing (&lt;strong&gt;Acciones → Configuración → Conexiones&lt;/strong&gt;):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/tailscale_part_3/syncthing_settings_web.png" title="Haz clic para ver en pantalla completa"&gt; &lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/tailscale_part_3/syncthing_settings_web.png" title="Haz clic para ver en pantalla completa"/&gt;&lt;figcaption&gt;Configuración web de Syncthing&lt;/figcaption&gt;&lt;/figure&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Y en los dispositivos Android en la configuración de la aplicación Syncthing (dentro de &lt;strong&gt;Opciones de Syncthing&lt;/strong&gt;):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/tailscale_part_3/syncthing_settings_android.png" title="Haz clic para ver en pantalla completa"&gt; &lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/tailscale_part_3/syncthing_settings_android.png" title="Haz clic para ver en pantalla completa"/&gt;&lt;figcaption&gt;Configuración de Syncthing en Android&lt;/figcaption&gt;&lt;/figure&gt; &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Es posible que necesites reiniciar Syncthing después de guardar.&lt;/p&gt;
&lt;h3 id="acerca-de-las-direcciones-de-escucha"&gt;Acerca de las direcciones de escucha&lt;/h3&gt;
&lt;p&gt;Lamentablemente, Syncthing no admite vincularse a una interfaz específica, es decir, esto no funcionará:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tcp://%tailscale0:22000, quic://%tailscale0:22000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Hay que vincular a la IP de la tailnet:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tcp://100.64.0.1:22000, quic://100.64.0.1:22000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Si el dispositivo se vuelve a registrar en la tailnet y cambia su IP se romperá esta configuración, pero eso será lo suficientemente raro en mi caso como para que sea un compromiso razonable.&lt;/p&gt;
&lt;p&gt;Una alternativa menos segura sería dejar esta configuración como &lt;code&gt;default&lt;/code&gt;, lo que se vinculará a todas las interfaces, y luego confiar en el cortafuegos del dispositivo para bloquear las conexiones que no sean a través de la tailnet.&lt;/p&gt;
&lt;h3 id="configurar-pares-por-direccion-de-tailnet"&gt;Configurar pares por dirección de tailnet&lt;/h3&gt;
&lt;p&gt;Con el descubrimiento global desactivado, los pares ya no pueden encontrarse automáticamente.
La solución es actualizar la configuración de cada dispositivo para que apunte explícitamente a la dirección de tailnet del par:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dispositivo → Editar → Direcciones:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tcp://homeserver.ts.fidelramos.net:22000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Los nombres de host de MagicDNS funcionan bien aquí porque Syncthing resuelve la dirección en cada intento de conexión, no solo al inicio (a diferencia de las direcciones de escucha).
Si MagicDNS es inestable en un cliente en particular, se puede usar su IP de tailnet (p.ej. &lt;code&gt;tcp://100.64.0.1:22000&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Hago este cambio entre cada par de dispositivos, en ambas direcciones.&lt;/p&gt;
&lt;h3 id="cortafuegos"&gt;Cortafuegos&lt;/h3&gt;
&lt;p&gt;Dado que Syncthing solo debe ser accesible a través de la tailnet, elimino las reglas existentes que permitían las conexiones de Syncthing en mi red pública.&lt;/p&gt;
&lt;p&gt;En mi caso, uso firewalld, eliminé las reglas de las zonas &lt;code&gt;public&lt;/code&gt; y &lt;code&gt;home&lt;/code&gt; y dejé que &lt;code&gt;trusted&lt;/code&gt; (que está asociada a la interfaz &lt;code&gt;tailscale0&lt;/code&gt;) aceptara todo automáticamente:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cerrar los puertos de Syncthing previamente permitidos&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;/tcp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;/udp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;21027&lt;/span&gt;/udp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;home&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;/tcp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;home&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;/udp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--permanent&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;home&lt;span class="w"&gt; &lt;/span&gt;--remove-port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;21027&lt;/span&gt;/udp
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Para comprobar:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;--list-all&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;firewall-cmd&lt;span class="w"&gt; &lt;/span&gt;--zone&lt;span class="o"&gt;=&lt;/span&gt;home&lt;span class="w"&gt; &lt;/span&gt;--list-all
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Ninguna zona debería listar el puerto 22000 en ningún lugar.&lt;/p&gt;
&lt;p&gt;La zona &lt;code&gt;trusted&lt;/code&gt; no necesita nada listado porque es permisiva por defecto.&lt;/p&gt;
&lt;p&gt;Para las implementaciones de Syncthing basadas en Docker, vale la pena mencionar que una configuración como &lt;code&gt;ports: ["22000:22000"]&lt;/code&gt; se salta firewalld.
O bien se vincula a la IP de la tailnet (&lt;code&gt;ports: ["100.64.0.1:22000:22000/tcp"]&lt;/code&gt;) o se utiliza la red del host.
En este caso las instalaciones nativas son más sencillas.&lt;/p&gt;
&lt;h3 id="router"&gt;Router&lt;/h3&gt;
&lt;p&gt;Solía tener el puerto TCP/UDP 22000 reenviado de mi router a mi servidor doméstico, y otros puertos para otros dispositivos en mi red doméstica. Ya no hace falta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Elimino el puerto 22000 (TCP y UDP) de las reglas de reenvío de puertos del router.&lt;/li&gt;
&lt;li&gt;Elimino reglas de cortafuegos en el router.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Menos puertos expuestos a Internet es &lt;strong&gt;bien&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="verificacion"&gt;Verificación&lt;/h2&gt;
&lt;p&gt;La interfaz web de Syncthing es mi primera comprobación. En cada dispositivo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El par se muestra como &lt;strong&gt;Conectado&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El detalle de la conexión muestra algo como &lt;code&gt;tcp://100.64.0.2:22000&lt;/code&gt;, es decir, una dirección de tailnet, no una pública.&lt;/li&gt;
&lt;li&gt;Sin indicador de "relé".&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Desde la línea de comandos en el servidor doméstico:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;ss&lt;span class="w"&gt; &lt;/span&gt;-tulpn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'22000|21027'&lt;/span&gt;
udp&lt;span class="w"&gt;   &lt;/span&gt;UNCONN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.4:22000&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*&lt;span class="w"&gt;    &lt;/span&gt;users:&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"syncthing"&lt;/span&gt;,pid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1526&lt;/span&gt;,fd&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;113&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
tcp&lt;span class="w"&gt;   &lt;/span&gt;LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.4:22000&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*&lt;span class="w"&gt;    &lt;/span&gt;users:&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"syncthing"&lt;/span&gt;,pid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1526&lt;/span&gt;,fd&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;110&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Syncthing sigue escuchando en el puerto 22000, pero en la IP de la tailnet.&lt;/p&gt;
&lt;p&gt;Así está bien, firewalld asegura que solo la tailnet pueda alcanzarlo.&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# La conexión a la IP pública falla&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;nc&lt;span class="w"&gt; &lt;/span&gt;-zv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;83&lt;/span&gt;.241.182.82&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;
nc:&lt;span class="w"&gt; &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;83&lt;/span&gt;.241.182.82&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;tcp&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;failed:&lt;span class="w"&gt; &lt;/span&gt;Connection&lt;span class="w"&gt; &lt;/span&gt;refused

&lt;span class="c1"&gt;# La conexión a la IP de la LAN falla&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;nc&lt;span class="w"&gt; &lt;/span&gt;-zv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;192&lt;/span&gt;.168.1.10&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;
nc:&lt;span class="w"&gt; &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;83&lt;/span&gt;.241.182.82&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;tcp&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;failed:&lt;span class="w"&gt; &lt;/span&gt;Connection&lt;span class="w"&gt; &lt;/span&gt;refused

&lt;span class="c1"&gt;# La conexión a la IP de la tailnet funciona&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;nc&lt;span class="w"&gt; &lt;/span&gt;-zv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;
Connection&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;tcp/snapenetio&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;succeeded!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Con Tailscale de nuevo activado, la misma comprobación a través de la dirección de tailnet debería tener éxito.&lt;/p&gt;
&lt;h2 id="dificultades-encontradas"&gt;Dificultades encontradas&lt;/h2&gt;
&lt;h3 id="sintaxis-de-la-direccion-de-escucha-tailscale0"&gt;Sintaxis de la dirección de escucha &lt;code&gt;%tailscale0&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Como mencionaba anteriormente, no intentes vincular Syncthing a un nombre de interfaz.
Usa la IP de la tailnet del dispositivo, o déjalo como &lt;code&gt;default&lt;/code&gt; (pero configura bien el cortafuegos).&lt;/p&gt;
&lt;h3 id="los-pares-no-se-encuentran-despues-de-deshabilitar-el-descubrimiento"&gt;Los pares no se encuentran después de deshabilitar el descubrimiento&lt;/h3&gt;
&lt;p&gt;Obvio en retrospectiva, pero me pilló una vez: si la dirección de un par todavía está configurada como &lt;code&gt;dynamic&lt;/code&gt; con todo el descubrimiento desactivado, no tiene forma de encontrar a nadie.
Establece direcciones explícitas para cada par.&lt;/p&gt;
&lt;h3 id="magicdns-falla-al-inicio-en-un-cliente-lento"&gt;MagicDNS falla al inicio en un cliente lento&lt;/h3&gt;
&lt;p&gt;Si Syncthing intenta alcanzar, por ejemplo, &lt;code&gt;homeserver.ts.fidelramos.net&lt;/code&gt; antes de que Tailscale esté activo en un dispositivo (por ejemplo, en un teléfono que se reinició), la conexión falla con un error de DNS. Lo intenta de nuevo unos segundos más tarde y tiene éxito. No hay nada que arreglar, es solo ruido en los registros.&lt;/p&gt;
&lt;p&gt;Usar direcciones IP en lugar de nombres MagicDNS evita el reintento pero pierde la legibilidad y la resiliencia si un dispositivo se vuelve a registrar en Headscale.&lt;/p&gt;
&lt;h3 id="syncthing-usando-contenedores"&gt;Syncthing usando contenedores&lt;/h3&gt;
&lt;p&gt;Si ejecutas Syncthing en Docker, la sección &lt;code&gt;ports:&lt;/code&gt; de tu archivo compose no debe exponer el puerto 22000 a la WAN. Mi recomendación: ejecútalo en el host en su lugar, o usa &lt;code&gt;network_mode: host&lt;/code&gt;, o vincúlalo explícitamente a la IP de la tailnet. La publicación de puertos de Docker por defecto es &lt;code&gt;0.0.0.0&lt;/code&gt; y omite firewalld.&lt;/p&gt;
&lt;h3 id="cache-de-anuncios-de-descubrimiento"&gt;Caché de anuncios de descubrimiento&lt;/h3&gt;
&lt;p&gt;Después de desactivar el descubrimiento global, otros usuarios de Syncthing que habían descubierto previamente mi dispositivo a través del servidor de descubrimiento público seguirían teniendo la IP pública obsoleta en caché durante un tiempo. Esto no es una fuga, es solo que intentarían conectarse a la IP antigua y fallarían. Puramente cosmético, y desaparece por sí solo en uno o dos días. No hay nada que hacer a menos que estés compartiendo carpetas con personas fuera de tu tailnet (y en ese caso, no deberías haber desactivado el descubrimiento global en primer lugar).&lt;/p&gt;
&lt;h2 id="siguientes-pasos"&gt;Siguientes pasos&lt;/h2&gt;
&lt;p&gt;Syncthing ahora se ejecuta completamente dentro de la tailnet, sin exposición a la WAN y sin depender de la infraestructura pública de Syncthing.
He notado que los dispositivos se conectan más rápido, pero no he realizado ninguna prueba de rendimiento en las velocidades de transferencia.&lt;/p&gt;
&lt;p&gt;En la Parte 4 haré lo mismo con mis servicios web: moveré la mayoría de mis aplicaciones de un dominio público &lt;code&gt;&amp;lt;app&amp;gt;.fidelramos.net&lt;/code&gt; a &lt;code&gt;&amp;lt;app&amp;gt;.ts.fidelramos.net&lt;/code&gt; solo para tailnet, utilizando una solución con doble &lt;code&gt;caddy-docker-proxy&lt;/code&gt; de forma que sigan funcionando sus certificados automáticos.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="howto"/><category term="linux"/><category term="security"/><category term="syncthing"/><category term="tailscale"/><category term="vpn"/></entry><entry><title>Cómo aislo mis agentes IA</title><link href="https://blog.fidelramos.net/es/software/how-i-sandbox-ai-agents" rel="alternate"/><published>2026-05-14T00:00:00+00:00</published><updated>2026-05-25T13:17:07.448793+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-14:/es/software/how-i-sandbox-ai-agents</id><summary type="html">&lt;p&gt;He estado usando &lt;a href="https://opencode.ai/"&gt;Opencode&lt;/a&gt; como mi agente de programación por IA.
No tengo una larga lista de razones por las que lo elegí entre las muchas alternativas existentes, algo impropio de mí.
Solo quería empezar con un sistema de agentes que cumpliera mis requisitos principales:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debe ser completamente de código …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;He estado usando &lt;a href="https://opencode.ai/"&gt;Opencode&lt;/a&gt; como mi agente de programación por IA.
No tengo una larga lista de razones por las que lo elegí entre las muchas alternativas existentes, algo impropio de mí.
Solo quería empezar con un sistema de agentes que cumpliera mis requisitos principales:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debe ser completamente de código abierto.&lt;/li&gt;
&lt;li&gt;Popular, lo que significa que probablemente durará. No quiero perder el tiempo aprendiendo a usar un programa solo para que desaparezca en unos meses.&lt;/li&gt;
&lt;li&gt;Extensible y configurable.&lt;/li&gt;
&lt;li&gt;Que soporte muchos proveedores de IA, incluida mi elección actual, Venice.ai (¡aquí hay un &lt;a href="https://venice.ai/chat?ref=XPrCO2"&gt;enlace con referencia&lt;/a&gt; para los interesados!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Una de las cosas que no me gusta de Opencode es que no usa sandboxing, y eso no es un descuido sino una decisión consciente de diseño.
Sí, Opencode pregunta antes de que un agente intente acceder a un archivo fuera del proyecto, pero el software de Opencode en sí no está restringido y tiene acceso al sistema completo.&lt;/p&gt;
&lt;p&gt;Mi solución es simple: envuelvo Opencode con &lt;a href="https://github.com/netblue30/firejail"&gt;Firejail&lt;/a&gt;, que ya estoy usando para la mayoría de los otros programas en mis ordenadores.
De esta manera, restrinjo Opencode para que no tenga acceso a lo que no debe (¡como mis claves SSH!) a nivel del &lt;em&gt;kernel&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/how_i_sandbox_my_ai_agents/opencode_ssh_keys.webp" title="Cliquea para pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/how_i_sandbox_my_ai_agents/opencode_ssh_keys.webp" title="Cliquea para pantalla completa"/&gt;&lt;figcaption&gt;Opencode aislado no tiene acecso a mis claves SSH&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="configuracion-de-firejail"&gt;Configuración de Firejail&lt;/h2&gt;
&lt;p&gt;Firejail &lt;a href="https://github.com/netblue30/firejail/pull/7135"&gt;ya integró un perfil de Opencode&lt;/a&gt; pero no está en una versión estable en el momento de escribir estas palabras.
Por ahora, uso mi propio perfil de firejail en &lt;em&gt;~/.config/firejail/opencode.profile&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Firejail profile for opencode&lt;/span&gt;
&lt;span class="c1"&gt;# Description: An open source AI coding agent.&lt;/span&gt;
&lt;span class="c1"&gt;# This file is overwritten after every install/update&lt;/span&gt;
&lt;span class="n"&gt;quiet&lt;/span&gt;
&lt;span class="c1"&gt;# Persistent local customizations&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;
&lt;span class="c1"&gt;# Persistent global definitions&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;globals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;

&lt;span class="c1"&gt;# allow executables in HOME, see disable-exec.inc&lt;/span&gt;
&lt;span class="n"&gt;ignore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;noexec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# blacklisted by disable-programs.inc&lt;/span&gt;
&lt;span class="n"&gt;noblacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;noblacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;noblacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;

&lt;span class="c1"&gt;# Allows files commonly used by IDEs&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;allow&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;devel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;

&lt;span class="c1"&gt;# Disable Wayland&lt;/span&gt;
&lt;span class="n"&gt;blacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNUSER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;wayland&lt;/span&gt;&lt;span class="o"&gt;-*&lt;/span&gt;
&lt;span class="c1"&gt;# Disable RUNUSER (cli only; supersedes Disable Wayland)&lt;/span&gt;
&lt;span class="n"&gt;blacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNUSER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# Remove the next blacklist if your system has no /usr/libexec dir,&lt;/span&gt;
&lt;span class="c1"&gt;# otherwise try to add it.&lt;/span&gt;
&lt;span class="n"&gt;blacklist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;libexec&lt;/span&gt;

&lt;span class="c1"&gt;# disable-*.inc includes&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x11&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;
&lt;span class="n"&gt;include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;xdg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inc&lt;/span&gt;

&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;whitelist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;whitelist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;
&lt;span class="n"&gt;whitelist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;/.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;opencode&lt;/span&gt;

&lt;span class="c1"&gt;# Commands that reduce access to resources.&lt;/span&gt;
&lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;span class="c1"&gt;##caps.keep CAPS&lt;/span&gt;
&lt;span class="c1"&gt;##hostname NAME&lt;/span&gt;
&lt;span class="c1"&gt;# CLI only&lt;/span&gt;
&lt;span class="n"&gt;ipc&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;
&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="n"&gt;no3d&lt;/span&gt;
&lt;span class="n"&gt;nodvd&lt;/span&gt;
&lt;span class="n"&gt;nogroups&lt;/span&gt;
&lt;span class="n"&gt;noinput&lt;/span&gt;
&lt;span class="n"&gt;nonewprivs&lt;/span&gt;
&lt;span class="n"&gt;noprinters&lt;/span&gt;
&lt;span class="n"&gt;noroot&lt;/span&gt;
&lt;span class="n"&gt;nosound&lt;/span&gt;
&lt;span class="n"&gt;notv&lt;/span&gt;
&lt;span class="n"&gt;nou2f&lt;/span&gt;
&lt;span class="n"&gt;novideo&lt;/span&gt;
&lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;inet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;inet6&lt;/span&gt;
&lt;span class="n"&gt;seccomp&lt;/span&gt;
&lt;span class="c1"&gt;#tracelog&lt;/span&gt;

&lt;span class="n"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alternatives&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ca&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;certificates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;dconf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;so&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ld&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;so&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pki&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;resolv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;
&lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;

&lt;span class="n"&gt;dbus&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;none&lt;/span&gt;
&lt;span class="n"&gt;dbus&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;none&lt;/span&gt;

&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NO_BROWSER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;restrict&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;namespaces&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Este perfil utiliza un modelo de lista blanca (&lt;em&gt;whitelist&lt;/em&gt;), por lo que Opencode solo tendrá acceso a esos directorios específicos y ni siquiera verá el resto.
Eso significa que al invocar Opencode necesito incluir en la lista blanca el directorio del proyecto en el que quiero que trabaje.
Lo hago así:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;~/Code/someproject$&lt;span class="w"&gt; &lt;/span&gt;firejail&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="o"&gt;=&lt;/span&gt;opencode&lt;span class="w"&gt; &lt;/span&gt;--whitelist&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/usr/bin/opencode
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Como escribir todo eso se volvería rápidamente aburrido, tengo &lt;code&gt;opencode&lt;/code&gt; configurado como un &lt;code&gt;abbr&lt;/code&gt;, por lo que se expandirá automáticamente a lo anterior.&lt;/p&gt;
&lt;p&gt;Un &lt;code&gt;abbr&lt;/code&gt; es una &lt;a href="https://fishshell.com/docs/current/cmds/abbr.html"&gt;construcción de fish shell&lt;/a&gt; que funciona como un alias, pero es editable al expandirse.
Esto es muy útil en caso de que quiera editar el comando, como en este caso podría necesitar incluir en la lista blanca alguna ruta adicional.&lt;/p&gt;
&lt;h2 id="resultados-y-proximos-pasos"&gt;Resultados y próximos pasos&lt;/h2&gt;
&lt;p&gt;He estado usando esta configuración durante un mes y ha sido sólida una vez que ajusté la configuración del perfil de Firejail.
Ahora puedo usar agentes con la seguridad de que no exfiltrarán datos privados, o que un error en Opencode podría afectar a mis archivos o mi sistema.&lt;/p&gt;
&lt;p&gt;A continuación, quiero extender esta configuración para poder usar Opencode con un modelo LLM local a través de llama.cpp, pero sin acceso a Internet.
Quiero que esto funcione en proyectos altamente personales donde no confiaría ni siquiera en los modelos privados de Venice.ai, como la configuración de &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;mi homelab&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;También he desarrollado mis propios agentes de investigación en Opencode, para gestionar proyectos de investigación de larga duración como un conjunto de archivos Markdown que sirven como memoria.
Utiliza una extensión del perfil de Firejail, además de configuración adicional de Opencode y prompts para el agente investigador y los subagentes buscadores.
Sigo refinándolo, pero ya está demostrando ser útil, así que probablemente terminaré escribiendo una publicación al respecto.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="ai"/><category term="firejail"/><category term="linux"/><category term="security"/></entry><entry><title>Tailscale autoalojado, Parte 2: DNS con bloqueo de publicidad</title><link href="https://blog.fidelramos.net/es/software/tailscale-2-ad-blocking-dns" rel="alternate"/><published>2026-05-11T00:00:00+00:00</published><updated>2026-05-22T21:01:21.181918+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-11:/es/software/tailscale-2-ad-blocking-dns</id><summary type="html">&lt;p&gt;En la &lt;a href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients"&gt;Parte 1&lt;/a&gt; configuré Headscale e hice que todos los clientes de &lt;em&gt;tailnet&lt;/em&gt; usaran Cloudflare DNS.
Funcionó, pero podía ser mejor: cada dispositivo que se conecta a mi &lt;em&gt;tailnet&lt;/em&gt; obtiene los servidores DNS que yo configure, así que bien podría ejecutar mi propio servidor DNS dentro de la red …&lt;/p&gt;</summary><content type="html">&lt;p&gt;En la &lt;a href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients"&gt;Parte 1&lt;/a&gt; configuré Headscale e hice que todos los clientes de &lt;em&gt;tailnet&lt;/em&gt; usaran Cloudflare DNS.
Funcionó, pero podía ser mejor: cada dispositivo que se conecta a mi &lt;em&gt;tailnet&lt;/em&gt; obtiene los servidores DNS que yo configure, así que bien podría ejecutar mi propio servidor DNS dentro de la red privada y conseguir bloqueo de anuncios, bloqueo de rastreadores y resolución de nombres interna esté donde esté, de forma transparente y cifrada.&lt;/p&gt;
&lt;p&gt;En este artículo cambiaré Cloudflare por &lt;a href="https://0xerr0r.github.io/blocky/"&gt;Blocky&lt;/a&gt;, un proxy DNS ligero con soporte incorporado para listas de bloqueo.&lt;/p&gt;
&lt;p&gt;Elegí Blocky en lugar de &lt;a href="https://pi-hole.net/"&gt;Pi-hole&lt;/a&gt; o &lt;a href="https://github.com/AdguardTeam/Adguardhome"&gt;AdGuard Home&lt;/a&gt; porque es más simple, se configura en un único archivo YAML.
No intenta ser un producto web completo, solo un servidor DNS que hace su trabajo y no molesta.&lt;/p&gt;
&lt;p&gt;Podrías reemplazar fácilmente Blocky con algún otro servidor o proxy DNS de tu elección manteniendo las partes de Headscale y Tailscale.&lt;/p&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#objetivos"&gt;Objetivos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#por-que-no-usar-solo-el-dns-privado-de-android"&gt;¿Por qué no usar solo el DNS privado de Android?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#arquitectura"&gt;Arquitectura&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#blocky-en-docker-compose"&gt;Blocky en Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#configuracion-de-blocky"&gt;Configuración de Blocky&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#verificacion-desde-el-host"&gt;Verificación desde el host:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cambios-en-headscale"&gt;Cambios en Headscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#por-que-es-mejor-que-dot-en-cada-dispositivo"&gt;Por qué es mejor que DoT en cada dispositivo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#inconvenientes-encontrados"&gt;Inconvenientes encontrados&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#android-no-se-puede-conectar-con-el-dns-privado-configurado"&gt;Android "no se puede conectar" con el DNS privado configurado&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#la-resolucion-de-dns-se-rompe-en-el-servidor"&gt;La resolución de DNS se rompe en el servidor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#los-clientes-no-usan-el-nuevo-dns"&gt;Los clientes no usan el nuevo DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#hay-que-deshabilitar-magicdns"&gt;Hay que deshabilitar MagicDNS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#que-sigue"&gt;Qué sigue&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="objetivos"&gt;Objetivos&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Bloquear anuncios y rastreadores a nivel de DNS en cada dispositivo con Tailscale.&lt;/li&gt;
&lt;li&gt;Usar DNS cifrado hacia los DNS globales (&lt;em&gt;DoT&lt;/em&gt; ascendente), aunque el tráfico entre mis dispositivos y Blocky ya esté protegido por WireGuard. Eso evitará el espionaje de mi ISP.&lt;/li&gt;
&lt;li&gt;Tener resolución de nombres interna, que, por ejemplo, &lt;code&gt;homeserver.ts.fidelramos.net&lt;/code&gt;  siga funcionando.&lt;/li&gt;
&lt;li&gt;Sin configuración de DNS a nivel de dispositivo. Headscale envía la configuración y los clientes la usan automáticamente.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="por-que-no-usar-solo-el-dns-privado-de-android"&gt;¿Por qué no usar solo el DNS privado de Android?&lt;/h2&gt;
&lt;p&gt;Android tiene una función incorporada de "DNS privado" que hace &lt;a href="https://es.wikipedia.org/wiki/DNS_mediante_TLS"&gt;DoT&lt;/a&gt; a cualquier servicio de resolución público.
Anteriormente estaba usando un servidor DNS privado de AdGuard (&lt;code&gt;xxxxxxxx.d.adguard-dns.com&lt;/code&gt;).
Intenté usarlo junto con Tailscale, pero no funciona: tan pronto como Tailscale envía cualquier configuración de DNS al cliente, Android enruta el DNS a través del túnel a través del resolvedor interno de Tailscale (&lt;code&gt;100.100.100.100&lt;/code&gt;), que no habla DoT.
La sonda de DNS privado de Android falla con "no se puede conectar" y la resolución de DNS se rompe en todo el dispositivo.&lt;/p&gt;
&lt;p&gt;Puedes deshabilitar "Usar DNS de Tailscale" en la aplicación para recuperar el DNS privado, pero entonces pierdes &lt;em&gt;MagicDNS&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Forzado a elegir, fui por el otro camino: ejecutar mi propio DNS en la &lt;em&gt;tailnet&lt;/em&gt; y hacer que Headscale lo envíe a todos los clientes.&lt;/p&gt;
&lt;h2 id="arquitectura"&gt;Arquitectura&lt;/h2&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;  Dispositivo en la tailnet
        │
        │ DNS sin cifrar (dentro de túnel WireGuard)
        ▼
  Blocky  ──▶  listas de bloqueo (local)
        │
        │ DNS-over-TLS (DoT) a DNS global
        ▼
  Cloudflare / Quad9 / Google DNS / etc.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Los clientes se comunican por DNS sin cifrar con la IP de &lt;em&gt;tailnet&lt;/em&gt; de Blocky. No hace falta cifrado porque la ruta completa va por un túnel WireGuard.&lt;/li&gt;
&lt;li&gt;Blocky realiza el filtrado contra las listas de bloqueo que mantiene localmente.&lt;/li&gt;
&lt;li&gt;Todo lo que no está bloqueado se resuelve por servicios DNS globales que Blocky contacta a través de &lt;em&gt;DoT&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Blocky asociará algunos dominios locales de la &lt;em&gt;tailnet&lt;/em&gt; a IPs internas.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="blocky-en-docker-compose"&gt;Blocky en Docker Compose&lt;/h2&gt;
&lt;p&gt;Ejecuto Blocky en el mismo proyecto &lt;em&gt;Docker Compose&lt;/em&gt; donde están todos los otros servicios de mi servidor doméstico.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;blocky&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;spx01/blocky:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;blocky&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;network_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;host&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;unless-stopped&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${BLOCKY_ROOT}/config.yml:/app/config.yml:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/log/blocky:/logs:rw&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;TZ=${TZ}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CMD"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/app/blocky"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"healthcheck"&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1m&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Puntos a destacar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;network_mode: host.&lt;/strong&gt; Esto es un poco perezoso por mi parte, pero abrir el puerto Docker solo a la IP de &lt;em&gt;tailnet&lt;/em&gt; es más complicado de lo que parece. Blocky se configurará en la siguiente sección para que solo escuche en la interfaz de Tailscale, por lo que no creo que sea un gran riesgo de seguridad. Confío en que Blocky no vaya a escuchar en otras interfaces o puertos del sistema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El archivo de configuración es de solo lectura&lt;/strong&gt; dentro del contenedor. Es más seguro y me recuerda que el origen es mi configuración versionada en disco, y no lo que está en el contenedor.&lt;/li&gt;
&lt;li&gt;Tuve que hacer &lt;code&gt;chown 100 /var/log/blocky&lt;/code&gt;, ya que ese es el UID que usa el contenedor de blocky. Antes de hacerlo los registros de Docker de Blocky mostraban errores de permiso de escritura.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="configuracion-de-blocky"&gt;Configuración de Blocky&lt;/h2&gt;
&lt;p&gt;Esta es mi configuración con los nombres internos redactados. Consulta la &lt;a href="https://0xerr0r.github.io/blocky/latest/"&gt;documentación de Blocky&lt;/a&gt; para todos los detalles.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/var/opt/blocky/config/config.yml&lt;/code&gt;&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# =================================================================&lt;/span&gt;
&lt;span class="c1"&gt;# Blocky config — Tailscale/headscale global resolver&lt;/span&gt;
&lt;span class="c1"&gt;# =================================================================&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Upstream resolvers (encrypted via DoT)&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;upstreams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;blocking&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tcp-tls:dns.quad9.net:853&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tcp-tls:one.one.one.one:853&lt;/span&gt;

&lt;span class="c1"&gt;# Explicit IPs for upstream resolution at startup&lt;/span&gt;
&lt;span class="c1"&gt;# (avoids chicken-and-egg when Blocky is itself the system resolver)&lt;/span&gt;
&lt;span class="nt"&gt;bootstrapDns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tcp-tls:1.1.1.1:853&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.1.1.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.0.0.1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tcp-tls:9.9.9.9:853&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;9.9.9.9&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;149.112.112.112&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Client identification (static, since no DHCP on tailnet)&lt;/span&gt;
&lt;span class="c1"&gt;# Add a new entry here whenever you add a tailscale node&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;clientLookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;homeserver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;laptop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.4&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Custom local DNS records for your tailnet nodes&lt;/span&gt;
&lt;span class="c1"&gt;# Add a new entry here whenever you add a tailscale node&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;customDNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;customTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1h&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;filterUnmappedTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ts.fidelramos.net&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tail&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# allows 'X.ts.fidelramos.net' as shortcut for 'X.tail'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;homeserver.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;laptop.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.2&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;phone.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;desktop.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.4&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Friendly names for services on tailnet (for Part 4)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;immich.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;jellyfin.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;nextcloud.tail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Blocking&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;denylists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://adaway.org/hosts.txt&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://v.firebog.net/hosts/AdguardDNS.txt&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://v.firebog.net/hosts/Easyprivacy.txt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;malware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://urlhaus.abuse.ch/downloads/hostfile/&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://v.firebog.net/hosts/Prigent-Malware.txt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;tracking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://v.firebog.net/hosts/Easyprivacy.txt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;smart-tv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://perflyst.github.io/PiHoleBlocklist/SmartTV.txt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;kids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://blocklistproject.github.io/Lists/porn.txt&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://blocklistproject.github.io/Lists/gambling.txt&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;allowlists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;# Common false positives&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;clients4.google.com&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;clients2.google.com&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;googleads.g.doubleclick.net&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;clientGroupsBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ads&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;malware&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;tracking&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Per-client overrides by name (from clientLookup above)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# phone:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - ads&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - malware&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - tracking&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - smart-tv&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# kids-tablet:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - ads&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - malware&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;#   - kids&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;refreshPeriod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;24h&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;60s&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;cooldown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;blocking&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;maxErrorsPerSource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;blockType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;zeroIp&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;blockTTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1m&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Caching&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;caching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;minTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5m&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;maxTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30m&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;maxItemsCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;prefetching&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;prefetchExpires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2h&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;prefetchThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;cacheTimeNegative&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30m&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Ports &amp;amp; binding&lt;/span&gt;
&lt;span class="c1"&gt;# Bind ONLY to tailnet IP so Blocky isn't exposed on LAN/WAN&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1:53&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1:853&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# optional DoT for tailnet clients&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;127.0.0.1:4000&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# metrics / API (local only)&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Logging &amp;amp; metrics&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="nt"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;info&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;text&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;privacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;queryLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;csv-client&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/logs&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;logRetentionDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;7&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;creationAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;creationCooldown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2s&lt;/span&gt;

&lt;span class="nt"&gt;prometheus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/metrics&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Misc&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Block leaks of private-use TLDs to public resolvers&lt;/span&gt;
&lt;span class="nt"&gt;specialUseDomains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;rfc6762-appendixG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;A grandes rasgos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DNS global sobre DoT&lt;/strong&gt;: Cloudflare y Quad9 como servicios de resolución global, en paralelo, usando DNS-over-TLS para que el tráfico proveniente de Blocky esté cifrado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entradas DNS personalizadas&lt;/strong&gt;: algunos nombres internos que quiero resolver en toda la &lt;em&gt;tailnet&lt;/em&gt; sin tocar los &lt;code&gt;extra_records&lt;/code&gt; de Headscale (esto lo cubriré en la Parte 4).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloqueo&lt;/strong&gt;: una mezcla de listas generales y algunas listas de bloqueo específicas que me interesan. Iré ajustándolas, pero por ahora son un buen punto de partida.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caché&lt;/strong&gt;: habilitado, con valores predeterminados sensatos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Métricas de Prometheus&lt;/strong&gt;: desactivadas por ahora, no tengo un recolector de métricas en este momento, lo haré en mi &lt;a href="https://blog.fidelramos.net/es/software/self-hosted-home-1"&gt;nuevo servidor doméstico&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Arrancarlo:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;-d
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;blocky
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="verificacion-desde-el-host"&gt;Verificación desde el host:&lt;/h2&gt;
&lt;p&gt;La resolución por DNS funciona:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;@100.64.0.1&lt;span class="w"&gt; &lt;/span&gt;example.com
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;DiG&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;.20.22&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;@100.64.0.1&lt;span class="w"&gt; &lt;/span&gt;example.com
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;server&lt;span class="w"&gt; &lt;/span&gt;found&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;global&lt;span class="w"&gt; &lt;/span&gt;options:&lt;span class="w"&gt; &lt;/span&gt;+cmd
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Got&lt;span class="w"&gt; &lt;/span&gt;answer:
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt;&amp;gt;HEADER&lt;span class="s"&gt;&amp;lt;&amp;lt;- opco&lt;/span&gt;de:&lt;span class="w"&gt; &lt;/span&gt;QUERY,&lt;span class="w"&gt; &lt;/span&gt;status:&lt;span class="w"&gt; &lt;/span&gt;NOERROR,&lt;span class="w"&gt; &lt;/span&gt;id:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;57184&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;flags:&lt;span class="w"&gt; &lt;/span&gt;qr&lt;span class="w"&gt; &lt;/span&gt;rd&lt;span class="w"&gt; &lt;/span&gt;ra&lt;span class="w"&gt; &lt;/span&gt;ad&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;QUERY:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;ANSWER:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;AUTHORITY:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;ADDITIONAL:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;OPT&lt;span class="w"&gt; &lt;/span&gt;PSEUDOSECTION:
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;EDNS:&lt;span class="w"&gt; &lt;/span&gt;version:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;flags:&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;udp:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;512&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;QUESTION&lt;span class="w"&gt; &lt;/span&gt;SECTION:
&lt;span class="p"&gt;;&lt;/span&gt;example.com.&lt;span class="w"&gt;                   &lt;/span&gt;IN&lt;span class="w"&gt;      &lt;/span&gt;A

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ANSWER&lt;span class="w"&gt; &lt;/span&gt;SECTION:
example.com.&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;IN&lt;span class="w"&gt;      &lt;/span&gt;A&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="m"&gt;172&lt;/span&gt;.66.147.243
example.com.&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;IN&lt;span class="w"&gt;      &lt;/span&gt;A&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="m"&gt;104&lt;/span&gt;.20.23.154

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Query&lt;span class="w"&gt; &lt;/span&gt;time:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;msec
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;SERVER:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1#53&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;UDP&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;WHEN:&lt;span class="w"&gt; &lt;/span&gt;Sun&lt;span class="w"&gt; &lt;/span&gt;May&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;:30:57&lt;span class="w"&gt; &lt;/span&gt;WEST&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;MSG&lt;span class="w"&gt; &lt;/span&gt;SIZE&lt;span class="w"&gt;  &lt;/span&gt;rcvd:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;72&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Dominio bloqueado (devuelve 0.0.0.0):&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;@100.64.0.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;doubleclick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DiG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.20.22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;@100.64.0.1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;doubleclick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;global&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Got&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HEADER&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;opcode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;QUERY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NOERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45469&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;qr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;QUERY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ANSWER&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AUTHORITY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ADDITIONAL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;QUESTION&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SECTION&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;doubleclick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="n"&gt;IN&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ANSWER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SECTION&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;doubleclick&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;IN&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="mf"&gt;0.0.0.0&lt;/span&gt;

&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;msec&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SERVER&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.64.0.1&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;100.64.0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UDP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WHEN&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;May&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WEST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;
&lt;span class="p"&gt;;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MSG&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SIZE&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;rcvd&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Blocky solo se enlaza a la IP de &lt;em&gt;tailnet&lt;/em&gt;, no hay puerto DNS expuesto a Internet:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;ss&lt;span class="w"&gt; &lt;/span&gt;-ulnp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;:53
UNCONN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1:53&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*&lt;span class="w"&gt;    &lt;/span&gt;users:&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"blocky"&lt;/span&gt;,pid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2984612&lt;/span&gt;,fd&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;ss&lt;span class="w"&gt; &lt;/span&gt;-tlnp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;':53|:853'&lt;/span&gt;
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1:53&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*&lt;span class="w"&gt;    &lt;/span&gt;users:&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"blocky"&lt;/span&gt;,pid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2984612&lt;/span&gt;,fd&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;16&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
LISTEN&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.64.0.1:853&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:*&lt;span class="w"&gt;    &lt;/span&gt;users:&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;"blocky"&lt;/span&gt;,pid&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2984612&lt;/span&gt;,fd&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="cambios-en-headscale"&gt;Cambios en Headscale&lt;/h2&gt;
&lt;p&gt;Ahora, la magia de Tailscale: le digo a Headscale que envíe la IP de &lt;em&gt;tailnet&lt;/em&gt; de Blocky como DNS global a cada cliente.
Edita &lt;code&gt;/var/opt/headscale/config/config.yaml&lt;/code&gt;, su bloque &lt;code&gt;dns&lt;/code&gt; cambia de la siguiente manera:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;magic_dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;override_local_dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# que los clientes usen nuestro DNS, reemplazando su DNS del sistema&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;base_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ts.fidelramos.net&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nameservers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.1&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;# Blocky en la tailnet&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;search_domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Reinicia Headscale:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;headscale
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Cada cliente tiene que obtener la nueva configuración:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo tailscale up --login-server=https://headscale.fidelramos.net --accept-dns=true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Android:&lt;/strong&gt; desconecta y vuelve a conectar Tailscale. También &lt;strong&gt;deshabilita el DNS privado de Android&lt;/strong&gt; (Ajustes → Red → DNS privado → Desactivado), ya que entraría en conflicto.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Verifica desde un cliente Linux:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;resolvectl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-A2&lt;span class="w"&gt; &lt;/span&gt;tailscale0
&lt;span class="c1"&gt;# Debería mostrar 100.64.0.1 como el servidor DNS en tailscale0&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;dig&lt;span class="w"&gt; &lt;/span&gt;example.com
&lt;span class="c1"&gt;# Debería resolver a través de Blocky&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="por-que-es-mejor-que-dot-en-cada-dispositivo"&gt;Por qué es mejor que DoT en cada dispositivo&lt;/h2&gt;
&lt;p&gt;El cifrado es o igual o mejor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dispositivo → Blocky&lt;/strong&gt;: DNS sin cifrar, pero dentro de un túnel WireGuard. El ISP ve un flujo UDP al servidor doméstico, pero nada más.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blocky → DNS global&lt;/strong&gt;: &lt;em&gt;DNS-over-TLS&lt;/em&gt; cifrado. El DNS global ve la consulta del servidor, pero no qué dispositivo original la hizo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Y hay beneficios que no tendría con sólo &lt;em&gt;DoT&lt;/em&gt; en cada dispositivo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Autoconfiguración&lt;/strong&gt;, sólo con conectarse por Tailscale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Listas de bloqueo centralizadas&lt;/strong&gt;. Cambio un archivo y todos los dispositivos se benefician.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registros de consultas por cliente&lt;/strong&gt; en un solo lugar. Blocky puede registrar consultas por IP de cliente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Entradas DNS internas&lt;/strong&gt;, las usaré en la Parte 4.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Funciona idénticamente en todas las plataformas&lt;/strong&gt;, sin el baile del DNS privado de Android.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No es necesario cambiar la configuración del router&lt;/strong&gt;. Mi router actual no me permite cambiar los DNS, así que esta es una forma ingeniosa de configurar el DNS en todos mis dispositivos sin tener que ir uno por uno.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="inconvenientes-encontrados"&gt;Inconvenientes encontrados&lt;/h2&gt;
&lt;h3 id="android-no-se-puede-conectar-con-el-dns-privado-configurado"&gt;Android "no se puede conectar" con el DNS privado configurado&lt;/h3&gt;
&lt;p&gt;Ya lo mencioné en la Parte 1, pero vale la pena repetirlo: con el DNS de Tailscale activo, la configuración de DNS privado de Android debe estar &lt;strong&gt;Desactivada&lt;/strong&gt; o &lt;strong&gt;Automática&lt;/strong&gt;. Configurar un nombre de host DoT estricto rompe el DNS por completo en el dispositivo mientras Tailscale está conectado.&lt;/p&gt;
&lt;h3 id="la-resolucion-de-dns-se-rompe-en-el-servidor"&gt;La resolución de DNS se rompe en el servidor&lt;/h3&gt;
&lt;p&gt;Si el servidor usa &lt;code&gt;127.0.0.53&lt;/code&gt; (&lt;em&gt;systemd-resolved&lt;/em&gt;) y luego le pides que use Blocky para todo, y Blocky está en un contenedor que depende de DNS para iniciarse... puedes meterte en un callejón sin salida.&lt;/p&gt;
&lt;p&gt;Al menos dos formas de evitarlo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deja el &lt;code&gt;/etc/resolv.conf&lt;/code&gt; del host apuntando a un servicio de resolución externo (por ejemplo, Cloudflare) y solo envía Blocky a los &lt;em&gt;clientes de tailnet&lt;/em&gt;. Esto es lo que yo hago: el propio host no usa Blocky, solo los dispositivos que se conectan a través de Tailscale.&lt;/li&gt;
&lt;li&gt;O, fija la imagen de Blocky para que siempre esté disponible localmente y deja que Docker la inicie antes de que cualquier otra cosa necesite DNS.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="los-clientes-no-usan-el-nuevo-dns"&gt;Los clientes no usan el nuevo DNS&lt;/h3&gt;
&lt;p&gt;Headscale envía la configuración de DNS en cada actualización del mapa, pero algunos clientes (especialmente Linux con NetworkManager) necesitan un empujón.
Ejecutar &lt;code&gt;tailscale up&lt;/code&gt; de nuevo con &lt;code&gt;--accept-dns=true&lt;/code&gt; fuerza una resincronización.
En Android, apagar y encender suele ser suficiente.&lt;/p&gt;
&lt;h3 id="hay-que-deshabilitar-magicdns"&gt;Hay que deshabilitar MagicDNS&lt;/h3&gt;
&lt;p&gt;Si &lt;code&gt;magic_dns&lt;/code&gt; se hubiera dejado habilitado en la configuración de Headscale, habría entrado en conflicto con Blocky DNS, ya que ambos intentarían usar el mismo puerto 53.&lt;/p&gt;
&lt;p&gt;Decidí deshabilitar MagicDNS y mantener &lt;code&gt;mapping&lt;/code&gt; en la configuración de Blocky de dominios e IPs.
Es un proceso manual, pero mi &lt;em&gt;tailnet&lt;/em&gt; es pequeña y no cambiará mucho, así que no me importa si eso implica una configuración más sencilla.&lt;/p&gt;
&lt;p&gt;Una solución alternativa sería ejecutar el &lt;em&gt;MagicDNS&lt;/em&gt; de Headscale en un puerto diferente y luego hacer que Blocky lo use.&lt;/p&gt;
&lt;h2 id="que-sigue"&gt;Qué sigue&lt;/h2&gt;
&lt;p&gt;Con Blocky así configurado, cada dispositivo en mi &lt;em&gt;tailnet&lt;/em&gt; tiene bloqueo de anuncios y DNS filtrado, a través de cualquier conexión a Internet en cualquier parte del mundo.&lt;/p&gt;
&lt;p&gt;En la Parte 3 reconfiguraré Syncthing para usar exclusivamente la &lt;em&gt;tailnet&lt;/em&gt;, deshabilitando todas sus funciones de descubrimiento y retransmisión orientadas a la WAN ahora que son innecesarias.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="dns"/><category term="howto"/><category term="linux"/><category term="security"/><category term="tailscale"/><category term="vpn"/></entry><entry><title>Tailscale autoalojado, Parte 1: Headscale y clientes</title><link href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients" rel="alternate"/><published>2026-05-05T00:00:00+00:00</published><updated>2026-05-11T17:18:45.792540+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-05-05:/es/software/tailscale-1-headscale-and-clients</id><summary type="html">&lt;p&gt;Hace bastante que oigo a mucha gente hablar maravillas de &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; como solución para interconectar dispositivos, o en otras palabras, para crear tu propia VPN en malla (&lt;em&gt;mesh&lt;/em&gt;). Sobre el papel pinta fenomenal: fácil de configurar, rápido y ligero, basado en un protocolo abierto (&lt;a href="https://www.wireguard.com/"&gt;WireGuard&lt;/a&gt;), funciona en todas partes, resuelve …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hace bastante que oigo a mucha gente hablar maravillas de &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; como solución para interconectar dispositivos, o en otras palabras, para crear tu propia VPN en malla (&lt;em&gt;mesh&lt;/em&gt;). Sobre el papel pinta fenomenal: fácil de configurar, rápido y ligero, basado en un protocolo abierto (&lt;a href="https://www.wireguard.com/"&gt;WireGuard&lt;/a&gt;), funciona en todas partes, resuelve el problema de NAT... Sin embargo, me echaba para atrás el entregar mis datos a una empresa, aunque solo fueran metadatos, y que requirieran información personal para registrarse. ¿No hay una solución soberana? ¡Pues sí, la hay! Y te contaré paso a paso cómo configurarla.&lt;/p&gt;
&lt;p&gt;El artículo oficial sobre &lt;a href="https://tailscale.com/blog/how-tailscale-works"&gt;cómo funciona Tailscale&lt;/a&gt; me pareció muy bien explicado y aclaratorio. Si no conoces conceptos como plano de datos, plano de control, las diferencias entre redes &lt;em&gt;hub-and-spoke&lt;/em&gt; y de malla, o NAT traversal, te recomiendo que le eches un vistazo. Después de todo, nuestro objetivo será autoalojar nuestro propio plano de control.&lt;/p&gt;
&lt;p&gt;Esta es la primera entrada de una serie sobre cómo configurar una VPN de malla compatible con Tailscale autoalojada en mi servidor doméstico, sin dependencias en la nube y sin ninguna cuenta en servicios de terceros.&lt;/p&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#objetivos"&gt;Objetivos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#por-que-no-usar-simplemente-el-servicio-alojado-de-tailscale"&gt;¿Por qué no usar simplemente el servicio alojado de Tailscale?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#arquitectura"&gt;Arquitectura&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#requisitos-previos"&gt;Requisitos previos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#headscale-en-docker-compose"&gt;Headscale en Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#configuracion-de-headscale"&gt;Configuración de Headscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#registros-dns"&gt;Registros DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#primera-ejecucion"&gt;Primera ejecución&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#creacion-de-un-usuario-y-claves-de-preautenticacion"&gt;Creación de un usuario y claves de preautenticación&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#tailscale-en-linux"&gt;Tailscale en Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#tailscale-en-android"&gt;Tailscale en Android&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#otros-clientes"&gt;Otros clientes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#dificultades-encontradas"&gt;Dificultades encontradas&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#clave-incorrecta"&gt;Clave incorrecta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#el-dispositivo-android-aparece-con-nombre-invalid-en-headscale"&gt;El dispositivo Android aparece con nombre invalid-... en headscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#android-no-conectado-sin-error"&gt;Android "No conectado" sin error&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#el-dns-privado-de-android-interfiere-con-el-arranque-de-tailscale"&gt;El DNS privado de Android interfiere con el arranque de Tailscale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#magicdns-no-resuelve-en-un-dispositivo-especifico"&gt;MagicDNS no resuelve en un dispositivo específico&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#desfase-horario"&gt;Desfase horario&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#siguientes-pasos"&gt;Siguientes pasos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="objetivos"&gt;Objetivos&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Establecer una &lt;strong&gt;VPN de malla&lt;/strong&gt; entre mis dispositivos (teléfono, tableta, portátil, ordenador de escritorio, servidor doméstico) para poder acceder a cualquiera de elos desde cualquier otro desde cualquier lugar de forma segura.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sin necesitar cuenta de terceros&lt;/strong&gt; para iniciar sesión. Nada de Google, Apple, GitHub o Microsoft.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sin servidores de terceros&lt;/strong&gt; involucrados en la coordinación. Mis dispositivos hablan con mi servidor, y punto.&lt;/li&gt;
&lt;li&gt;Funciona en Linux y Android (las plataformas que uso), utilizando clientes de código abierto. Vale la pena señalar que otros clientes son de código cerrado, pero no me importa porque no uso esas plataformas.&lt;/li&gt;
&lt;li&gt;Fácil de mantener actualizado, tanto clientes como servidor.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="por-que-no-usar-simplemente-el-servicio-alojado-de-tailscale"&gt;¿Por qué no usar simplemente el servicio alojado de Tailscale?&lt;/h2&gt;
&lt;p&gt;Tailscale es un gran producto y quiero ser justo al respecto: el plano de datos está cifrado de extremo a extremo con &lt;a href="https://www.wireguard.com/"&gt;WireGuard&lt;/a&gt;, por lo que Tailscale Inc. no ve el contenido del tráfico.&lt;/p&gt;
&lt;p&gt;Pero:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sí ven metadatos.&lt;/strong&gt; Cada cliente contacta con el plano de control para obtener el mapa de red. Eso significa que Tailscale vería la IP pública de cada dispositivo que poseo, cada vez que se conectara, desde dondequiera que estuviera. Eso es un montón de historial de ubicaciones en los registros de una empresa que preferiría evitar si puedo. Y sí que puedo, así que lo haré.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La identidad depende de un tercero.&lt;/strong&gt; La versión gratuita de Tailscale requiere iniciar sesión con Google, GitHub, Microsoft, Apple o un proveedor OIDC similar. Si alguno de ellos suspende mi cuenta, perdería el acceso a mi propia red. Amén de la pérdida de privacidad al saber que uso el servicio.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Es un servicio fuera de mi control.&lt;/strong&gt; Si Tailscale sufre una caída, cambia los precios, modifica su producto o es adquirido, mi instalación se vería afectada.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="arquitectura"&gt;Arquitectura&lt;/h2&gt;
&lt;p&gt;El núcleo de la arquitectura se basa en &lt;a href="https://headscale.net/"&gt;Headscale&lt;/a&gt;, una reimplementación de código abierto del servidor de coordinación de Tailscale.
Elimina las tres preocupaciones anteriores.
Los clientes oficiales de Tailscale funcionan con él sin cambios.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/tailscale_part_1/architecture.svg" title="Haz clic para ver en pantalla completa"&gt; &lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/tailscale_part_1/architecture.svg" title="Haz clic para ver en pantalla completa"/&gt;&lt;figcaption&gt;Arquitectura de una Tailnet con un Headscale autoalojado&lt;/figcaption&gt;&lt;/figure&gt; &lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Headscale&lt;/strong&gt; se ejecuta en Docker Compose en mi servidor doméstico, detrás de Caddy para HTTPS.&lt;/li&gt;
&lt;li&gt;Los &lt;strong&gt;clientes de Tailscale&lt;/strong&gt; se ejecutan en cada dispositivo, configurados para usar mi instancia de Headscale como servidor de control. Cada cliente informa al servidor sobre su situación de red y, a cambio, obtiene conexiones a los otros nodos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS&lt;/strong&gt; en esta primera parte utiliza Cloudflare. En la segunda parte lo reemplazaré por un servidor DNS Blocky autoalojado con bloqueo de anuncios y rastreadores que usarán automáticamente todos los clientes de mi Tailnet.&lt;/li&gt;
&lt;li&gt;Los clientes establecen túneles WireGuard directos entre pares; los &lt;a href="https://tailscale.com/docs/reference/derp-servers"&gt;relés DERP&lt;/a&gt; públicos de Tailscale se utilizan solo como respaldo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="requisitos-previos"&gt;Requisitos previos&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Un dominio bajo mi control (&lt;code&gt;fidelramos.net&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Una forma de que mi servidor doméstico sea accesible desde Internet en los puertos 80 y 443. Utilizo DNS dinámico porque mi ISP me da una IP pública dinámica. Mi servidor actualiza periódicamente un registro A que apunta &lt;code&gt;headscale.fidelramos.net&lt;/code&gt; a mi IP actual. Si tienes una IP estática, pues mejor que mejor.&lt;/li&gt;
&lt;li&gt;El router reenvía los puertos TCP 80 y 443, y el puerto UDP 41641 a mi servidor doméstico. Mi servidor está en la DMZ del router, así que esto es automático.&lt;/li&gt;
&lt;li&gt;Mi servidor, ejecutando Debian con Docker y Docker Compose instalados y configurados.&lt;/li&gt;
&lt;li&gt;Caddy con caddy-docker-proxy como proxy inverso. Ya escribí sobre esa configuración &lt;a href="https://blog.fidelramos.net/es/software/switch-nginx-caddy-docker-compose"&gt;en esta entrada anterior&lt;/a&gt;. Es la forma más sencilla que he encontrado para tener un proxy inverso habilitado para HTTPS. Realmente me encanta.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="headscale-en-docker-compose"&gt;Headscale en Docker Compose&lt;/h2&gt;
&lt;p&gt;Tengo un proyecto &lt;em&gt;compose&lt;/em&gt; con otros servicios, incluido un servicio &lt;code&gt;caddy&lt;/code&gt; con &lt;code&gt;caddy-docker-proxy&lt;/code&gt; como se describe en la entrada ya mencionada.&lt;/p&gt;
&lt;p&gt;Añadí un servicio &lt;code&gt;headscale&lt;/code&gt; a &lt;code&gt;compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;headscale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;headscale/headscale:stable&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;headscale&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;unless-stopped&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;serve&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${HEADSCALE_ROOT}/config:/etc/headscale&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${HEADSCALE_ROOT}/data:/var/lib/headscale&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;caddy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;headscale.fidelramos.net&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy.reverse_proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"{{upstreams&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8080}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Algunos puntos a destacar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;image: headscale/headscale:stable&lt;/code&gt; sigue la última versión estable. Lo combino con &lt;a href="https://getwud.github.io/wud/"&gt;WUD&lt;/a&gt; para actualizaciones controladas (escribiré una futura entrada al respecto). Por supuesto, puedes hacer un &lt;code&gt;docker compose pull&lt;/code&gt; manual.&lt;/li&gt;
&lt;li&gt;No hay sección &lt;code&gt;ports:&lt;/code&gt;. Headscale solo se alcanza internamente a través de Caddy.&lt;/li&gt;
&lt;li&gt;Establezco mi &lt;code&gt;HEADSCALE_ROOT&lt;/code&gt; en &lt;code&gt;/var/opt/headscale&lt;/code&gt;, pero puedes usar el directorio que tenga sentido para ti.&lt;/li&gt;
&lt;li&gt;Las dos etiquetas son todo lo que &lt;code&gt;caddy-docker-proxy&lt;/code&gt; necesita para enrutar &lt;code&gt;headscale.fidelramos.net&lt;/code&gt; al puerto 8080 del contenedor.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="configuracion-de-headscale"&gt;Configuración de Headscale&lt;/h2&gt;
&lt;p&gt;Lo puse en &lt;code&gt;/var/opt/headscale/config/config.yaml&lt;/code&gt;, pero puedes ponerlo donde te funcione, solo ajusta el mapeo de volumen en la configuración de compose.&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;server_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://headscale.fidelramos.net&lt;/span&gt;
&lt;span class="nt"&gt;listen_addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.0.0.0:8080&lt;/span&gt;
&lt;span class="nt"&gt;metrics_listen_addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.0.0.0:9090&lt;/span&gt;
&lt;span class="nt"&gt;grpc_listen_addr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.0.0.0:50443&lt;/span&gt;
&lt;span class="nt"&gt;grpc_allow_insecure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;noise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;private_key_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/lib/headscale/noise_private.key&lt;/span&gt;

&lt;span class="nt"&gt;prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;v4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;100.64.0.0/10&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;v6&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;fd7a:115c:a1e0::/48&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;allocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;sequential&lt;/span&gt;

&lt;span class="nt"&gt;derp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://controlplane.tailscale.com/derpmap/default&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;auto_update_enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;update_frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;24h&lt;/span&gt;

&lt;span class="nt"&gt;disable_check_updates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;ephemeral_node_inactivity_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;30m&lt;/span&gt;

&lt;span class="nt"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;sqlite&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;sqlite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/lib/headscale/db.sqlite&lt;/span&gt;

&lt;span class="nt"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;info&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;text&lt;/span&gt;

&lt;span class="nt"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;magic_dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;override_local_dns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;base_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ts.fidelramos.net&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nameservers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.1.1.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.0.0.1&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;search_domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[]&lt;/span&gt;

&lt;span class="nt"&gt;unix_socket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/headscale/headscale.sock&lt;/span&gt;
&lt;span class="nt"&gt;unix_socket_permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"0770"&lt;/span&gt;

&lt;span class="nt"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;file&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Estos son los puntos más importantes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server_url&lt;/code&gt; es la URL HTTPS pública para el plano de control. Debe ser accesible por los clientes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;base_domain&lt;/code&gt; es el sufijo utilizado por MagicDNS para los nombres de host de tailnet (por ejemplo, &lt;code&gt;homeserver.ts.fidelramos.net&lt;/code&gt;). &lt;strong&gt;No debe ser igual ni padre de&lt;/strong&gt; el dominio &lt;code&gt;server_url&lt;/code&gt;, si no los clientes se confunden.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dns.nameservers.global&lt;/code&gt; son los servidores DNS que se envían a cada cliente. En esta configuración uso Cloudflare; en la Parte 2 lo cambiaré para usar una instancia de Blocky.&lt;/li&gt;
&lt;li&gt;DERP está deshabilitado para el servidor y utiliza el mapa DERP público de Tailscale como relés de respaldo para los clientes. Usar sus DERP no filtra tráfico (es WireGuard retransmitido, siempre cifrado de extremo a extremo) pero es la única pieza no autoalojada, es un compromiso razonable para mi caso.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="registros-dns"&gt;Registros DNS&lt;/h2&gt;
&lt;p&gt;Solo uno: un registro A para &lt;code&gt;headscale.fidelramos.net&lt;/code&gt; que apunta a la IP pública de mi servidor doméstico (a través de DDNS).
Ahí se conectan los clientes y es el dominio que Let's Encrypt verificará con HTTP-01 a través de Caddy.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ts.fidelramos.net&lt;/code&gt; (el &lt;code&gt;base_domain&lt;/code&gt;) &lt;strong&gt;no&lt;/strong&gt; obtiene un registro DNS público.
Se resuelve completamente dentro de la tailnet por MagicDNS de Tailscale.
Esto significa que solo mis clientes de Tailscale podrán acceder a estos dominios, y no se publicarán en ningún lugar.&lt;/p&gt;
&lt;h2 id="primera-ejecucion"&gt;Primera ejecución&lt;/h2&gt;
&lt;p&gt;En el proyecto Docker compose:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;-d
sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;headscale
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Una vez que los registros muestren que la API está escuchando y Caddy ha obtenido un certificado, confirmo que el plano de control es accesible:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;curl&lt;span class="w"&gt; &lt;/span&gt;https://headscale.fidelramos.net/health
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Debería devolver &lt;code&gt;{"status":"pass"}&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="creacion-de-un-usuario-y-claves-de-preautenticacion"&gt;Creación de un usuario y claves de preautenticación&lt;/h2&gt;
&lt;p&gt;Headscale utiliza &lt;strong&gt;usuarios&lt;/strong&gt; como espacios de nombres que poseen dispositivos.
Creo uno para mí:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;users&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;fidel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Luego genero una clave de preautenticación cada vez que necesito registrar un nuevo dispositivo:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;preauthkeys&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--expiration&lt;span class="w"&gt; &lt;/span&gt;1h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;La clave solo es válida por una hora.
Suficiente para registrar un dispositivo y no tener que preocuparme por si se filtra.&lt;/p&gt;
&lt;h2 id="tailscale-en-linux"&gt;Tailscale en Linux&lt;/h2&gt;
&lt;p&gt;Debian:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;CODENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;lsb_release&lt;span class="w"&gt; &lt;/span&gt;--codename&lt;span class="w"&gt; &lt;/span&gt;--short&lt;span class="k"&gt;)&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-fsSL&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.tailscale.com/stable/debian/&lt;span class="nv"&gt;$CODENAME&lt;/span&gt;.noarmor.gpg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/usr/share/keyrings/tailscale-archive-keyring.gpg&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null
curl&lt;span class="w"&gt; &lt;/span&gt;-fsSL&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.tailscale.com/stable/debian/&lt;span class="nv"&gt;$CODENAME&lt;/span&gt;.tailscale-keyring.list&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/apt/sources.list.d/tailscale.list
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;tailscale
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;tailscaled
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Fedora:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;config-manager&lt;span class="w"&gt; &lt;/span&gt;addrepo&lt;span class="w"&gt; &lt;/span&gt;--from-repofile&lt;span class="o"&gt;=&lt;/span&gt;https://pkgs.tailscale.com/stable/fedora/tailscale.repo
sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;tailscale
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;tailscaled
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Arch Linux y derivados:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pacman&lt;span class="w"&gt; &lt;/span&gt;-S&lt;span class="w"&gt; &lt;/span&gt;tailscale
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;tailscaled
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Para todos los sistemas, una vez que Tailscale esté habilitado, registro el dispositivo con Headscale usando una clave de preautenticación:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tailscale&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--login-server&lt;span class="o"&gt;=&lt;/span&gt;https://headscale.fidelramos.net&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--auth-key&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;preauth-key&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--hostname&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;hostname&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Para verificar:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# on client&lt;/span&gt;
tailscale&lt;span class="w"&gt; &lt;/span&gt;status
&lt;span class="c1"&gt;# on server&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;nodes&lt;span class="w"&gt; &lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;El nodo debería aparecer en ambas listas, con una dirección &lt;code&gt;100.64.0.x&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="tailscale-en-android"&gt;Tailscale en Android&lt;/h2&gt;
&lt;p&gt;La aplicación de Android se comunica con Tailscale público por defecto y no hay una configuración obvia para cambiarlo.
Considero esta ofuscación un patrón oscuro, pero comprensible ya que quieren tener un negocio exitoso.
Aún así fue exasperante tener que buscar cómo hacerlo a pesar de saber que la opción estaba ahí escondida en algún lugar.&lt;/p&gt;
&lt;p&gt;El truco es la opción de &lt;strong&gt;servidor alternativo&lt;/strong&gt;, que está oculta:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Instala la aplicación Tailscale desde F-Droid, la Play Store o, mi forma elegida, a través de &lt;a href="https://obtainium.imranr.dev/"&gt;Obtanium&lt;/a&gt; usando &lt;a href="https://pkgs.tailscale.com/stable/tailscale-android-universal-latest.apk"&gt;https://pkgs.tailscale.com/stable/tailscale-android-universal-latest.apk&lt;/a&gt; como fuente.&lt;/li&gt;
&lt;li&gt;Inicia la aplicación y cancela el intento de iniciar sesión en Tailscale.&lt;/li&gt;
&lt;li&gt;Una vez que llegues a la pantalla con el botón Conectar, toca el engranaje para el menú de configuración, y después dale a &lt;strong&gt;Cuentas&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;En el menú superior derecho de esa pantalla, elige &lt;strong&gt;Usar un servidor alternativo&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Introduce tu servidor Headscale (en mi caso &lt;code&gt;https://headscale.fidelramos.net&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Inicia sesión. Se abrirá una página en el navegador con instrucciones sobre qué ejecutar en el servidor Headscale para registrar este cliente.&lt;/li&gt;
&lt;li&gt;Como se indica, ejecuta el comando en el servidor headscale:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;headscale&lt;span class="w"&gt; &lt;/span&gt;nodes&lt;span class="w"&gt; &lt;/span&gt;register&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\ &lt;/span&gt;--key&lt;span class="w"&gt; &lt;/span&gt;nodekey:abcdef...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Si quieres una guía visual, encontré este vídeo con instrucciones paso a paso:&lt;/p&gt;
&lt;div align="center" class="youtube"&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" frameborder="0" height="500" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ofVyohBLuPg?si=h4qve8iXcedCvquO&amp;amp;start=1161" title="YouTube video player" width="800"&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Después del registro, activa el interruptor de conexión en la aplicación.
&lt;code&gt;tailscale status&lt;/code&gt; en un dispositivo Linux en la tailnet debería mostrar el teléfono, y debería aparecer en la lista de nodos de Headscale como se mencionó en la sección del cliente Linux.&lt;/p&gt;
&lt;p&gt;Mencionaré una cosa específica de Android: Tailscale se añade como un servicio VPN, y al menos en mis dispositivos se marcó como &lt;em&gt;siempre activo&lt;/em&gt; y también el &lt;em&gt;bloquear conexiones sin VPN&lt;/em&gt;.
Recomiendo dejar &lt;em&gt;siempre activo&lt;/em&gt; habilitado, pero &lt;strong&gt;deshabilitar el bloqueo de conexiones sin VPN&lt;/strong&gt;.
Como Tailscale es una VPN solo entre tus dispositivos, si se cae no pierdes anonimato como con una VPN de reenvío de tráfico, simplemente pierdes el acceso a los pares de la VPN.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/tailscale_part_1/android_vpn_settings.png" title="Haz clic para ver en pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/tailscale_part_1/android_vpn_settings.png" title="Haz clic para ver en pantalla completa"/&gt;&lt;figcaption&gt;Configuración de VPN de Android&lt;/figcaption&gt;&lt;/figure&gt; &lt;/a&gt;&lt;/p&gt;
&lt;h2 id="otros-clientes"&gt;Otros clientes&lt;/h2&gt;
&lt;p&gt;Solo uso Linux y Android, así que eso es lo que he cubierto y probado.
Para otras plataformas, los clientes de Tailscale funcionan de la misma manera, simplemente apúntalos al servidor alternativo y regístrate.
El proyecto Headscale mantiene &lt;a href="https://headscale.net/stable/usage/connect/"&gt;instrucciones actualizadas para macOS, iOS y Windows&lt;/a&gt; que no duplicaré aquí.&lt;/p&gt;
&lt;h2 id="dificultades-encontradas"&gt;Dificultades encontradas&lt;/h2&gt;
&lt;p&gt;Recopilo aquí algunos problemas que encontré en el camino, por si le puedo ahorrar sufrimiento a alguien.&lt;/p&gt;
&lt;h3 id="clave-incorrecta"&gt;Clave incorrecta&lt;/h3&gt;
&lt;p&gt;Si pasas la clave incorrecta a &lt;code&gt;headscale nodes register&lt;/code&gt;, obtendrías el siguiente error:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cannot register node: node not found in registration cache&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;El valor esperado es la cadena &lt;code&gt;nodekey:...&lt;/code&gt; de la URL del navegador del cliente, &lt;strong&gt;no&lt;/strong&gt; una clave de preautenticación.
Las claves de preautenticación se utilizan en el lado del cliente con &lt;code&gt;tailscale up --auth-key=...&lt;/code&gt;, no en el servidor.&lt;/p&gt;
&lt;p&gt;La caché de registro está en memoria y caduca después de ~15 minutos, así que si te tomas un descanso para comer entre el inicio de sesión del cliente y el comando del servidor, vuelve a activar el inicio de sesión en el cliente para generar una nueva.&lt;/p&gt;
&lt;h3 id="el-dispositivo-android-aparece-con-nombre-invalid-en-headscale"&gt;El dispositivo Android aparece con nombre &lt;code&gt;invalid-...&lt;/code&gt; en headscale&lt;/h3&gt;
&lt;p&gt;Headscale rechaza los nombres que no son etiquetas DNS válidas (espacios, caracteres &lt;em&gt;unicode&lt;/em&gt;, etc.).
Android envía el nombre de dispositivo legible por humanos, lo que a menudo falla esta verificación (por ejemplo, "Pixel 8 Pro" en mi caso).
Headscale renombra el nodo automáticamente a &lt;code&gt;invalid-&amp;lt;hash&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Puedes renombrarlo en el servidor Headscale después del registro:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bash sudo docker compose exec headscale headscale nodes rename -i &amp;lt;node-id&amp;gt; pixel8pro&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Puramente cosmético, pero MagicDNS no funcionará para el nombre original hasta que hagas esto.&lt;/p&gt;
&lt;h3 id="android-no-conectado-sin-error"&gt;Android "No conectado" sin error&lt;/h3&gt;
&lt;p&gt;Si la aplicación muestra la cuenta de Headscale pero se queda en "No conectado" cuando activas la conexión, algunas cosas que debes verificar en orden:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Comprueba &lt;code&gt;sudo docker compose logs -f headscale&lt;/code&gt; mientras activas. Deberías ver al cliente accediendo a &lt;code&gt;/ts2021&lt;/code&gt;. Si no aparece nada, no está llegando al servidor. Podría ser un problema de TLS (comprueba los registros de caddy), DNS, firewall...&lt;/li&gt;
&lt;li&gt;Desde el navegador del teléfono, &lt;code&gt;https://headscale.fidelramos.net/health&lt;/code&gt; debe tener éxito.&lt;/li&gt;
&lt;li&gt;Comprueba que DERP se cargó: &lt;code&gt;sudo docker compose exec headscale headscale debug derp&lt;/code&gt; debería listar las regiones. Si está vacío, el contenedor no puede alcanzar &lt;code&gt;controlplane.tailscale.com&lt;/code&gt; para el mapa, primero corrige el DNS del contenedor o su salida a Internet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS privado de Android&lt;/strong&gt;: esto merece una sección propia.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="el-dns-privado-de-android-interfiere-con-el-arranque-de-tailscale"&gt;El DNS privado de Android interfiere con el arranque de Tailscale&lt;/h3&gt;
&lt;p&gt;Estaba usando AdGuard DNS como DNS privado personalizado, pero se rompió cuando Tailscale se conectó, dejándome sin una configuración DNS válida, por lo que la mayoría de las conexiones a Internet no funcionaban.&lt;/p&gt;
&lt;p&gt;Vi dos errores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;En la configuración de red de Android, &lt;em&gt;DNS privado&lt;/em&gt; decía &lt;em&gt;no se pudo conectar&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Poco después de configurar el DNS privado personalizado, saltaba una notificación: &lt;em&gt;La red no tiene acceso a Internet. No se puede acceder al servidor DNS privado.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Afortunadamente, pude sortear este problema autoalojando mi propio DNS de bloqueo de anuncios y haciendo que todos mis clientes de Tailscale lo usaran.
Esto acabó siendo una bendición, ya que ganaba control total sobre el DNS y ya no tendría que pagar por AdGuard DNS.&lt;/p&gt;
&lt;p&gt;Esta solución se presentará en la próxima Parte 2 de esta serie de artículos.&lt;/p&gt;
&lt;h3 id="magicdns-no-resuelve-en-un-dispositivo-especifico"&gt;MagicDNS no resuelve en un dispositivo específico&lt;/h3&gt;
&lt;p&gt;Después de cambiar la configuración de DNS de Headscale, los clientes necesitan obtener nuevas configuraciones.&lt;/p&gt;
&lt;p&gt;En Linux, &lt;code&gt;sudo tailscale up --login-server=... --accept-dns=true&lt;/code&gt; resincroniza.&lt;/p&gt;
&lt;p&gt;En Android, activa y desactiva el interruptor de la VPN.&lt;/p&gt;
&lt;h3 id="desfase-horario"&gt;Desfase horario&lt;/h3&gt;
&lt;p&gt;El intercambio de ruido de Tailscale falla silenciosamente si hay una deriva de reloj significativa.
Si nada más explica un problema de conexión, comprueba que NTP (sincronización de tiempo) esté funcionando en ambos extremos.&lt;/p&gt;
&lt;p&gt;En Linux configura &lt;em&gt;systemd-timesyncd&lt;/em&gt;, &lt;em&gt;ntpd&lt;/em&gt;, &lt;em&gt;chrony&lt;/em&gt; o similar.&lt;/p&gt;
&lt;p&gt;En Android, pon la configuración de fecha/hora en "automático" en la configuración del sistema.&lt;/p&gt;
&lt;h2 id="siguientes-pasos"&gt;Siguientes pasos&lt;/h2&gt;
&lt;p&gt;Esta configuración ya es funcional: mis dispositivos pueden comunicarse entre sí a través de mi propio servidor de control, sin Tailscale ni cuentas o servicios de terceros.
Puedo acceder desde cualquier parte a aplicaciones que antes solo eran accesibles a través de la LAN.&lt;/p&gt;
&lt;p&gt;En la Parte 2, cambiaré el DNS de Cloudflare configurado actualmente por una instancia de Blocky autoalojada para obtener bloqueo de anuncios y rastreadores y conseguir resolución de nombres internos en cada cliente de la tailnet.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="howto"/><category term="linux"/><category term="seguridad"/><category term="tailscale"/><category term="vpn"/></entry><entry><title>Pre-estableciendo conexiones maestras de SSH para Ansible, o cómo usar OnlyKey con Ansible sin morir en el intento</title><link href="https://blog.fidelramos.net/es/software/ansible-ssh-master-connections" rel="alternate"/><published>2026-04-07T00:00:00+00:00</published><updated>2026-06-11T17:11:22.929016+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-04-07:/es/software/ansible-ssh-master-connections</id><summary type="html">&lt;p&gt;Uso una &lt;a href="https://onlykey.io/"&gt;OnlyKey&lt;/a&gt; como dispositivo de seguridad de hardware para varios propósitos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Las contraseñas comunes se escriben automáticamente con solo pulsar un botón.&lt;/li&gt;
&lt;li&gt;Como segundo factor en mi gestor de contraseñas (KeepassXC), usando HMAC aplicado a la contraseña.&lt;/li&gt;
&lt;li&gt;Conexiones SSH.&lt;/li&gt;
&lt;li&gt;Cifrado y firma GPG.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/ansible_and_onlykey/onlykey.webp"/&gt;&lt;figcaption&gt;OnlyKey&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Las conexiones SSH se manejan …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Uso una &lt;a href="https://onlykey.io/"&gt;OnlyKey&lt;/a&gt; como dispositivo de seguridad de hardware para varios propósitos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Las contraseñas comunes se escriben automáticamente con solo pulsar un botón.&lt;/li&gt;
&lt;li&gt;Como segundo factor en mi gestor de contraseñas (KeepassXC), usando HMAC aplicado a la contraseña.&lt;/li&gt;
&lt;li&gt;Conexiones SSH.&lt;/li&gt;
&lt;li&gt;Cifrado y firma GPG.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/ansible_and_onlykey/onlykey.webp"/&gt;&lt;figcaption&gt;OnlyKey&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Las conexiones SSH se manejan a través del &lt;a href="https://docs.onlykey.io/onlykey-agent.html"&gt;onlykey-agent&lt;/a&gt;, y funciona bastante bien.
Sin embargo, con Ansible se convierte en una pesadilla, ya que pide docenas y docenas de pulsaciones de teclas:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/ansible_and_onlykey/ansible_onlykey.webp" title="Haz clic para pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/ansible_and_onlykey/ansible_onlykey.webp" title="Haz clic para pantalla completa"/&gt;&lt;figcaption&gt;Parte de una sesión de Ansible con onlykey-agent&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Este dolor de cabeza me estaba haciendo usar Ansible cada vez menos para mis automatizaciones, así que decidí encontrar una solución.
El objetivo es necesitar solo &lt;strong&gt;una pulsación de OnlyKey por cada servidor&lt;/strong&gt;, con un comportamiento bien definido y razonablemente seguro.&lt;/p&gt;
&lt;p&gt;Me alegra decir que he logrado una solución razonable, que documento aquí con la esperanza de que sea útil para otras personas.
Podría ayudar no solo a los usuarios de OnlyKey, sino a cualquiera con una &lt;strong&gt;configuración de SSH que requiera intervención manual&lt;/strong&gt; en el momento de la conexión, y para la que por alguna razón la reutilización de conexiones SSH de Ansible no esté funcionando.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id="el-problema"&gt;El problema&lt;/h2&gt;
&lt;p&gt;Ansible a menudo abre más de una sesión SSH por servidor durante su ejecución.
Incluso con una buena configuración, es posible que sigas viendo conexiones repetidas para tareas, recopilación de información (&lt;em&gt;fact gathering&lt;/em&gt;), escalada de privilegios y otras operaciones.&lt;/p&gt;
&lt;p&gt;Cuando se usan claves SSH normales, todo esto es casi invisible.
Se pierde rendimiento por las conexiones extras a la red y el cifrado adicional, pero funcionaría.&lt;/p&gt;
&lt;p&gt;Con &lt;code&gt;onlykey-agent&lt;/code&gt;, cada nueva autenticación SSH requiere confirmación física.
Eso hace que todas las conexiones repetidas se convierte rápidamente en un auténtico coñazo.&lt;/p&gt;
&lt;h2 id="lo-que-observe"&gt;Lo que observé&lt;/h2&gt;
&lt;p&gt;La solución a alto nivel es obvia: &lt;strong&gt;reutilizar las conexiones SSH&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Y la cuestión es que &lt;a href="https://www.lisenet.com/2022/speed-up-ansible-ssh-with-multiplexing/"&gt;Ansible soporta la reutilización de conexiones&lt;/a&gt; mediante la multiplexación de conexiones de OpenSSH.&lt;/p&gt;
&lt;p&gt;De forma resumida, OpenSSH puede establecer una conexión como maestra, dejar un &lt;em&gt;socket&lt;/em&gt; abierto y hacer que las nuevas conexiones al mismo servidor/usuario/puerto lo reutilicen automáticamente.&lt;/p&gt;
&lt;p&gt;Sin embargo, cuando intenté configurarlo con Ansible no funcionó, seguía recibiendo las mismas peticiones en la OnlyKey.&lt;/p&gt;
&lt;p&gt;Cuando miraba el directorio de los &lt;em&gt;sockets&lt;/em&gt; veía cómo se creaban, sólo para desaparecer momentos después.&lt;/p&gt;
&lt;p&gt;Entonces probé la multiplexación SSH &lt;strong&gt;fuera de Ansible&lt;/strong&gt;, y funcionó.&lt;/p&gt;
&lt;p&gt;Configuré &lt;code&gt;ControlMaster&lt;/code&gt; en &lt;code&gt;~/.ssh/config&lt;/code&gt;, y una vez que me conecté a un servidor con &lt;code&gt;onlykey-agent&lt;/code&gt; pude abrir una nueva conexión &lt;strong&gt;sin otra petición de la OnlyKey&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Sin embargo, cuando cerraba la conexión, el socket desaparecía.&lt;/p&gt;
&lt;p&gt;Supongo que Ansible está cerrando las conexiones por alguna razón, pero no he descubierto exactamente por qué ni cómo.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id="la-solucion"&gt;La solución&lt;/h2&gt;
&lt;p&gt;Aprovechando que las conexiones abiertas mantenían el socket abierto, planteé una solución que funcionase de forma consistente:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configurar la multiplexación SSH normalmente.&lt;/li&gt;
&lt;li&gt;Configurar Ansible para que use los mismos &lt;em&gt;sockets&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Ejecutar Ansible dentro de un &lt;em&gt;script&lt;/em&gt; que:&lt;ol&gt;
&lt;li&gt;Lea la lista de servidores del inventario de Ansible.&lt;/li&gt;
&lt;li&gt;Establezca una conexión maestra de SSH para cada servidor.&lt;/li&gt;
&lt;li&gt;Mantenga vivo cada maestro con un &lt;code&gt;sleep&lt;/code&gt; remoto.&lt;/li&gt;
&lt;li&gt;Ejecute &lt;code&gt;ansible-playbook&lt;/code&gt; con todos los argumentos originales sin cambios.&lt;/li&gt;
&lt;li&gt;Termine las conexiones maestras al completar.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="configuracion-de-ssh"&gt;Configuración de SSH&lt;/h2&gt;
&lt;p&gt;Primero, asegúrate de que tu configuración de SSH habilite la multiplexación y use un &lt;em&gt;control path&lt;/em&gt; estable.&lt;/p&gt;
&lt;p&gt;Añade esto a &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Host *
    ControlMaster auto
    ControlPersist 10m
    ControlPath ~/.ssh/sockets/%h-%r-%p
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Luego crea el directorio para los sockets:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/sockets
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/sockets
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;También me gusta configurar usuarios y puertos en la configuración de SSH, por ejemplo:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Host blog.fidelramos.net
    User fidel
    Port 22022
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="configuracion-de-ansible"&gt;Configuración de Ansible&lt;/h2&gt;
&lt;p&gt;Usa un &lt;code&gt;ansible.cfg&lt;/code&gt; como este:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[defaults]&lt;/span&gt;
&lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;ssh&lt;/span&gt;

&lt;span class="k"&gt;[ssh_connection]&lt;/span&gt;
&lt;span class="na"&gt;ssh_args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;-o ControlMaster=auto -o ServerAliveInterval=30 -o ServerAliveCountMax=3&lt;/span&gt;
&lt;span class="na"&gt;control_path_dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;~/.ssh/sockets&lt;/span&gt;
&lt;span class="na"&gt;control_path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;%(directory)s/%%h-%%r-%%p&lt;/span&gt;
&lt;span class="na"&gt;pipelining&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;transport = ssh&lt;/code&gt; fuerza el uso de OpenSSH en lugar de Paramiko, que no permite multiplexación SSH.
  Este debería ser el comportamiento predeterminado en las versiones recientes de Ansible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;control_path_dir&lt;/code&gt; y &lt;code&gt;control_path&lt;/code&gt; hacen que Ansible use el mismo patrón de nomenclatura de sockets que SSH, para que reutilice los que el &lt;em&gt;script&lt;/em&gt; ha establecido previamente.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pipelining = True&lt;/code&gt; reduce parte de la sobrecarga de SSH y la rotación de archivos temporales remotos. Esta es una buena idea incluso sin multiplexación para reducir el número de conexiones y acelerar la ejecución de Ansible.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="el-script"&gt;El script&lt;/h2&gt;
&lt;p&gt;Aquí está el &lt;em&gt;script&lt;/em&gt; completo.&lt;/p&gt;
&lt;p&gt;Intencionadamente &lt;strong&gt;no&lt;/strong&gt; intenta gestionar por sí mismo los nombres de usuario o puertos por servidor.
Esos son aplicados por la configuración de SSH, y Ansible también los reutiliza.&lt;/p&gt;
&lt;p&gt;El &lt;em&gt;script&lt;/em&gt; pasa todos los argumentos a &lt;code&gt;ansible-playbook&lt;/code&gt; exactamente como se proporcionan.
Mira los ejemplos de uso más abajo.&lt;/p&gt;
&lt;p&gt;También &lt;a href="https://gist.github.com/haplo/5a6a1194b6f84ec3dad2429d1b3af58b"&gt;publiqué el &lt;em&gt;script&lt;/em&gt; como un Gist&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="ansible-ssh-masterssh"&gt;&lt;code&gt;ansible-ssh-masters.sh&lt;/code&gt;&lt;/h3&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c1"&gt;# ansible-ssh-masters.sh&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Establish one SSH master connection per host, keep it alive with a&lt;/span&gt;
&lt;span class="c1"&gt;# remote sleep, run ansible-playbook with all original arguments, then&lt;/span&gt;
&lt;span class="c1"&gt;# clean up only the masters this script started.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Useful when SSH authentication has a per-connection human cost:&lt;/span&gt;
&lt;span class="c1"&gt;# hardware security keys, passphrase prompts, FIDO2/U2F, etc.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Usage (timeout in seconds):&lt;/span&gt;
&lt;span class="c1"&gt;#   ANSIBLE_SSH_MASTERS_TIMEOUT=3600 ./ansible-ssh-masters.sh playbook.yml [args...]&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-euo&lt;span class="w"&gt; &lt;/span&gt;pipefail

&lt;span class="nv"&gt;TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANSIBLE_SSH_MASTERS_TIMEOUT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;3600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SOCKET_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.ssh/sockets"&lt;/span&gt;

&lt;span class="nv"&gt;PIDS&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;STARTED_HOSTS&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;INVENTORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"inventory"&lt;/span&gt;

&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;32m'&lt;/span&gt;
&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;33m'&lt;/span&gt;
&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0;31m'&lt;/span&gt;
&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'\033[0m'&lt;/span&gt;

log&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GREEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[+]&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
warn&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;YELLOW&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[!]&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
err&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RED&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;[-]&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

parse_inventory_arg&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i&amp;lt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i++&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-i&lt;span class="p"&gt;|&lt;/span&gt;--inventory&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing value after &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nv"&gt;INVENTORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;--inventory&lt;span class="o"&gt;=&lt;/span&gt;*&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nv"&gt;INVENTORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARGS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]#--inventory=&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

cleanup&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Closing master connections..."&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i&lt;span class="w"&gt; &lt;/span&gt;pid&lt;span class="w"&gt; &lt;/span&gt;host

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!PIDS[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;STARTED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nb"&gt;wait&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Closed master for &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (pid &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failed to kill master for &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (pid &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;warn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Master for &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was already gone (pid &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Done."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;trap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cleanup&lt;span class="w"&gt; &lt;/span&gt;EXIT&lt;span class="w"&gt; &lt;/span&gt;INT&lt;span class="w"&gt; &lt;/span&gt;TERM

parse_inventory_arg

mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOCKET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOCKET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Remove stale sockets (no process behind them)&lt;/span&gt;
find&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SOCKET_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-maxdepth&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;s&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;-exec&lt;span class="w"&gt; &lt;/span&gt;fuser&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-delete&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;ansible-inventory&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INVENTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--graph&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Inventory could not be parsed: &lt;/span&gt;&lt;span class="nv"&gt;$INVENTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

mapfile&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;HOSTS&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ansible-inventory&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INVENTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--graph&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
&lt;span class="s1"&gt;        /@/ { next }&lt;/span&gt;
&lt;span class="s1"&gt;        {&lt;/span&gt;
&lt;span class="s1"&gt;            line = $0&lt;/span&gt;
&lt;span class="s1"&gt;            gsub(/^[[:space:]|]+/, "", line)&lt;/span&gt;
&lt;span class="s1"&gt;            sub(/^--/, "", line)&lt;/span&gt;
&lt;span class="s1"&gt;            if (line != "") {&lt;/span&gt;
&lt;span class="s1"&gt;                print line&lt;/span&gt;
&lt;span class="s1"&gt;            }&lt;/span&gt;
&lt;span class="s1"&gt;        }&lt;/span&gt;
&lt;span class="s1"&gt;    '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-u
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No hosts found in inventory: &lt;/span&gt;&lt;span class="nv"&gt;$INVENTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Found &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hosts in inventory: &lt;/span&gt;&lt;span class="nv"&gt;$INVENTORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Per-host connection lifetime: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds"&lt;/span&gt;
log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Establishing master connections (authentication may be required)..."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;

&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;host&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ControlPath=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOCKET_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/%h-%r-%p"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: reusing existing master"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;warn&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: connecting..."&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ControlMaster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ControlPath=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOCKET_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/%h-%r-%p"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveInterval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ServerAliveCountMax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ForwardAgent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ForwardX11&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;ClearAllForwardings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sleep &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;_&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;seq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ControlPath=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOCKET_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/%h-%r-%p"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ControlPath=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOCKET_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/%h-%r-%p"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: master established (pid &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;PIDS&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;STARTED_HOSTS&lt;/span&gt;&lt;span class="o"&gt;+=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: failed to establish master"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-0&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nb"&gt;wait&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;err&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;HOSTS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: failed to stop failed SSH process (pid &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;
log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Running: ansible-playbook &lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;+e
ansible-playbook&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ANSIBLE_EXIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ANSIBLE_EXIT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;ansible-ssh-masters.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h3 id="captura-de-pantalla"&gt;Captura de pantalla&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/ansible_and_onlykey/ansible_ssh_masters.webp" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/ansible_and_onlykey/ansible_ssh_masters.webp" title="Haz clic para ver a pantalla completa"/&gt;&lt;figcaption&gt;ansible-ssh-masters en acción&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="uso"&gt;Uso&lt;/h3&gt;
&lt;p&gt;Simplemente reemplaza &lt;code&gt;ansible-playbook&lt;/code&gt; por &lt;code&gt;ansible-ssh-masters.sh&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;./ansible-ssh-masters.sh&lt;span class="w"&gt; &lt;/span&gt;playbook.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;En mi caso, antepongo &lt;code&gt;onlykey-agent&lt;/code&gt; con la clave correcta:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;onlykey-agent&lt;span class="w"&gt; &lt;/span&gt;fidelramos.net&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;./ansible-ssh-masters.sh&lt;span class="w"&gt; &lt;/span&gt;playbook.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;También puedes iniciar una &lt;a href="https://docs.onlykey.io/onlykey-agent.html#start-multiple-ssh-sessions-from-a-sub-shell"&gt;subshell de &lt;code&gt;onlykey-agent&lt;/code&gt;&lt;/a&gt; y luego ejecutar &lt;code&gt;ansible-ssh-masters.sh&lt;/code&gt; dentro:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;onlykey-agent&lt;span class="w"&gt; &lt;/span&gt;fidelramos.net&lt;span class="w"&gt; &lt;/span&gt;-s
./ansible-ssh-masters.sh&lt;span class="w"&gt; &lt;/span&gt;playbook.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Puedes pasar cualquier número de argumentos, o usar otras fuentes de inventario; se pasan tal cual a &lt;code&gt;ansible-playbook&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;./ansible-ssh-masters.sh&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;production.ini&lt;span class="w"&gt; &lt;/span&gt;playbook.yml&lt;span class="w"&gt; &lt;/span&gt;--limit&lt;span class="w"&gt; &lt;/span&gt;web&lt;span class="w"&gt; &lt;/span&gt;--tags&lt;span class="w"&gt; &lt;/span&gt;deploy&lt;span class="w"&gt; &lt;/span&gt;-v
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Para cambiar el tiempo máximo que pueden vivir las sesiones maestras, establece el tiempo de espera en &lt;strong&gt;segundos&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;ANSIBLE_ONLYKEY_TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;7200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./ansible-ssh-masters.sh&lt;span class="w"&gt; &lt;/span&gt;long-playbook.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h3 id="puntos-clave-de-diseno-del-script"&gt;Puntos clave de diseño del script&lt;/h3&gt;
&lt;p&gt;Voy a ahondar en algunas decisiones deliberadas de diseño.&lt;/p&gt;
&lt;h4 id="1-cerrar-conexiones-al-finalizar-o-salir"&gt;1. Cerrar conexiones al finalizar o salir&lt;/h4&gt;
&lt;p&gt;Con el uso de &lt;code&gt;trap&lt;/code&gt;, el &lt;em&gt;script&lt;/em&gt; se asegura de que las conexiones se cierren tan pronto como ya no sean necesarias, o también si el &lt;em&gt;script&lt;/em&gt; se interrumpe por alguna razón (Ctrl-C, proceso terminado o shell cerrada, por ejemplo).&lt;/p&gt;
&lt;h4 id="2-limpia-lo-que-inicio"&gt;2. Limpia lo que inició&lt;/h4&gt;
&lt;p&gt;El &lt;em&gt;script&lt;/em&gt; solo mata los procesos maestros de SSH que creó.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No&lt;/strong&gt; limpia todo el directorio de sockets.
Puede haber otras sesiones de SSH o herramientas usando el mismo directorio de multiplexación.&lt;/p&gt;
&lt;h4 id="3-analisis-de-inventario-flexible"&gt;3. Análisis de inventario flexible&lt;/h4&gt;
&lt;p&gt;El &lt;em&gt;script&lt;/em&gt; detecta el uso del argumento &lt;code&gt;-i/--inventory&lt;/code&gt; y lo analiza con &lt;code&gt;ansible-inventory&lt;/code&gt; para obtener los servidores donde se ejecutará el playbook.
Eso significa que cualquiera de los formatos de &lt;code&gt;--inventory&lt;/code&gt; funcionará, no se limita a un archivo de inventario.&lt;/p&gt;
&lt;h4 id="4-usa-la-configuracion-de-ssh-como-fuente-de-la-verdad"&gt;4. Usa la configuración de SSH como fuente de la verdad&lt;/h4&gt;
&lt;p&gt;En lugar de extraer datos de ficheros de configuración, utiliza transparentemente la configuración de SSH.&lt;/p&gt;
&lt;p&gt;El &lt;em&gt;script&lt;/em&gt; no intenta adivinar nombres de usuario, puertos o alias de servidor desde Ansible.&lt;/p&gt;
&lt;p&gt;Todo eso se queda en la configuración de SSH, donde pertenece.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id="notas-de-seguridad"&gt;Notas de seguridad&lt;/h2&gt;
&lt;p&gt;Esta configuración &lt;strong&gt;mejora la usabilidad&lt;/strong&gt;, pero a cambio de &lt;strong&gt;disminuir la seguridad&lt;/strong&gt; mientras el &lt;em&gt;script&lt;/em&gt; se está ejecutando.&lt;/p&gt;
&lt;h3 id="el-compromiso-fundamental"&gt;El compromiso fundamental&lt;/h3&gt;
&lt;p&gt;Después de autenticar un servidor con OnlyKey, esa conexión maestra de SSH permanece abierta durante el tiempo de vida configurado o hasta que el &lt;em&gt;script&lt;/em&gt; termina.&lt;/p&gt;
&lt;p&gt;Eso significa que:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Las sesiones SSH posteriores pueden reutilizar la conexión maestra..&lt;/li&gt;
&lt;li&gt;No hace falta interactuar con la OnlyKey mientras esa conexión maestra siga viva.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ese es el objetivo de la solución, pero también significa que es aún más importante proteger tu sesión local durante esa ventana temporal.&lt;/p&gt;
&lt;p&gt;Si hubiese otros programas corriendo en el ordenador con acceso al directorio de sockets SSH podrían obtener acceso directo al servidor remoto.
Y esa es precisamente una de las principales razones para usar un dispositivo de seguridad de hardware como OnlyKey.&lt;/p&gt;
&lt;h3 id="cosas-que-hace-esta-configuracion-para-limitar-el-riesgo"&gt;Cosas que hace esta configuración para limitar el riesgo&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Los sockets viven en &lt;code&gt;~/.ssh/sockets&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El directorio tiene permisos &lt;code&gt;0700&lt;/code&gt;, o sea sólo para el usuario propietario.&lt;/li&gt;
&lt;li&gt;El reenvío del agente SSH (&lt;em&gt;Agent forwarding&lt;/em&gt;) está desactivado.&lt;/li&gt;
&lt;li&gt;El reenvío de X11 (&lt;em&gt;X11 forwarding&lt;/em&gt;) está desactivado.&lt;/li&gt;
&lt;li&gt;El &lt;em&gt;script&lt;/em&gt; cierra las conexiones al terminar.&lt;/li&gt;
&lt;li&gt;El tiempo de espera es explícito y finito en caso de que la limpieza automática falle por alguna razón.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Además, personalmente uso &lt;a href="https://github.com/netblue30/firejail/"&gt;Firejail&lt;/a&gt; para aislar (&lt;em&gt;sandbox&lt;/em&gt;) la mayor parte del software que se ejecuta en mi ordenador.
Mi configuración de Firejail bloquea el acceso a &lt;code&gt;~/.ssh&lt;/code&gt; para la mayoría de los programas, así que me siento un poco más seguro con este enfoque.&lt;/p&gt;
&lt;h3 id="por-que-la-limpieza-automatica-se-mantiene-activada"&gt;Por qué la limpieza automática se mantiene activada&lt;/h3&gt;
&lt;p&gt;El &lt;em&gt;script&lt;/em&gt; siempre cierra todas las conexiones maestras cuando termina (vía &lt;code&gt;trap&lt;/code&gt;).
Esta es una decisión de diseño para incrementar la seguridad.&lt;/p&gt;
&lt;p&gt;Sería aún más simple dejar las conexiones abiertas hasta que se agotase su tiempo establecido.
Esto también haría que se pudiese ejecutar Ansible más veces sin nuevas autenticaciones.&lt;/p&gt;
&lt;p&gt;Personalmente no quería eso, y no lo recomendaría como opción predeterminada ya que reduce aún más la seguridad, pero ya es cuestión de cada cuál el equilibrar la facilidad de uso con la seguridad.&lt;/p&gt;
&lt;p&gt;Cuanto menos tiempo permanezcan abiertas las conexiones, menos probable es que puedan ser aprovechadas por un atacante o software malicioso.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id="reflexiones-finales"&gt;Reflexiones finales&lt;/h2&gt;
&lt;p&gt;Me entristece no haber podido llegar al fondo de por qué la reutilización de conexiones de Ansible no funcionaba correctamente, pero no pude dedicar más tiempo a esa investigación.&lt;/p&gt;
&lt;p&gt;Me alegra haber encontrado esta solución alternativa, que es un punto medio razonable entre comodidad y seguridad.
Si esto no hubiera funcionado, estaba a punto de volver a las claves SSH normales, lo que habría reducido considerablemente la seguridad de mis servidores.&lt;/p&gt;
&lt;p&gt;Si, como yo, tienes una forma engorrosa para conectar por SSH, espero que este &lt;em&gt;script&lt;/em&gt; te ayude a disfrutar más de Ansible.&lt;/p&gt;
&lt;p&gt;Si has llegado hasta aquí y tienes una idea de cómo solucionarlo &lt;em&gt;Como $DEITY manda&lt;/em&gt;, ¡por favor deja un comentario!&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="ansible"/><category term="automation"/><category term="howto"/><category term="linux"/><category term="security"/></entry><entry><title>DNI electrónico en Firefox en Linux</title><link href="https://blog.fidelramos.net/es/software/dnie-firefox-linux" rel="alternate"/><published>2026-03-30T00:00:00+00:00</published><updated>2026-03-30T00:00:00+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-03-30:/es/software/dnie-firefox-linux</id><summary type="html">&lt;p&gt;Hace muchos años que tengo el DNI electrónico (o DNIe) en mi cartera, y no pocas veces he intentado hacerlo funcionar en Linux para autenticarme en las páginas de la administración pública.
Siempre me encontraba con algún problema que me hacía desistir.&lt;/p&gt;
&lt;p&gt;Hace poco me tocó renovarlo y pensé en …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hace muchos años que tengo el DNI electrónico (o DNIe) en mi cartera, y no pocas veces he intentado hacerlo funcionar en Linux para autenticarme en las páginas de la administración pública.
Siempre me encontraba con algún problema que me hacía desistir.&lt;/p&gt;
&lt;p&gt;Hace poco me tocó renovarlo y pensé en probar otra vez, a ver si caía la breva.
¡Y sí que cayó!
Al fin lo pude hacer funcionar sin paquetes extraños ni complicaciones, con un lector de tarjetas baratito.&lt;/p&gt;
&lt;p&gt;Voy a dejar los pasos que he seguido para configurarlo en &lt;strong&gt;Arch Linux&lt;/strong&gt;.
Dejaré comentarios sobre los paquetes equivalentes en &lt;strong&gt;Debian/Ubuntu&lt;/strong&gt; y &lt;strong&gt;Fedora&lt;/strong&gt;, porque el resto de la configuración debería ser la misma para todos.&lt;/p&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#que-necesitas"&gt;Qué necesitas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-1-instalar-los-paquetes-necesarios"&gt;Paso 1: Instalar los paquetes necesarios&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#arch-linux"&gt;Arch Linux&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#debian-ubuntu"&gt;Debian / Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#fedora"&gt;Fedora&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-2-activar-el-servicio-pcscd"&gt;Paso 2: Activar el servicio pcscd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-3-comprobar-que-el-lector-y-la-tarjeta-se-detectan"&gt;Paso 3: Comprobar que el lector y la tarjeta se detectan&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#verificar-que-opensc-reconoce-el-dnie"&gt;Verificar que OpenSC reconoce el DNIe&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-4-configurar-firefox"&gt;Paso 4: Configurar Firefox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-5-probar-el-acceso"&gt;Paso 5: Probar el acceso&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#solucion-de-problemas"&gt;Solución de problemas&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#firefox-dice-no-se-puede-anadir-el-modulo"&gt;Firefox dice "No se puede añadir el módulo"&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#los-certificados-no-aparecen-aunque-la-tarjeta-se-detecta"&gt;Los certificados no aparecen aunque la tarjeta se detecta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#el-pin-se-bloquea"&gt;El PIN se bloquea&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#firefox-instalado-con-flatpak-o-snap"&gt;Firefox instalado con Flatpak o Snap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#y-que-hay-de-las-firmas"&gt;¿Y qué hay de las firmas?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#resumen"&gt;Resumen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="que-necesitas"&gt;Qué necesitas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;lector de tarjetas inteligentes&lt;/strong&gt; compatible con el estándar CCID (la mayoría lo son). Por 10€ hay muchos para elegir.&lt;/li&gt;
&lt;li&gt;Tu &lt;strong&gt;DNIe&lt;/strong&gt; con los certificados activos. Si no los has activado o han caducado, debes renovarlos en un Punto de Actualización del DNIe (PAD) en una comisaría de policía.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;PIN&lt;/strong&gt; de tu DNIe, que configuraste al obtenerlo o renovar los certificados. Cuando lo renuevas te dan un papel con el PIN.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firefox&lt;/strong&gt; instalado de forma nativa. Si usas Flatpak o Snap probablemente tengas problemas porque limitan el acceso a los dispositivos y ficheros del resto del sistema. Lo comento más abajo en la sección de &lt;em&gt;Solución de problemas&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aprovecho para comentar que Chrome y derivados (Chromium, Brave...) también funcionan con los mismos paquetes, pero la forma de configurarlos es diferente, no tienen interfaz gráfica como la de Firefox.
Vamos eso a mí no me asusta, simplemente uso Firefox para las páginas del gobierno por su extensión &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/"&gt;Multi-Account Containers&lt;/a&gt; que da un buen aislamiento entre sesiones.&lt;/p&gt;
&lt;p&gt;En &lt;a href="https://bandaancha.eu/foros/dnie-debian-ubuntu-mint-chromium-chrome-1747958"&gt;este artículo&lt;/a&gt; hay instrucciones para configurar Chrome/Chromium/Brave.&lt;/p&gt;
&lt;h2 id="paso-1-instalar-los-paquetes-necesarios"&gt;Paso 1: Instalar los paquetes necesarios&lt;/h2&gt;
&lt;p&gt;Necesitamos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pcscd / pcsc-lite&lt;/strong&gt;: el demonio que gestiona la comunicación con lectores de tarjetas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ccid&lt;/strong&gt;: el driver genérico para lectores &lt;a href="https://es.wikipedia.org/wiki/CCID_(protocolo)"&gt;CCID&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;opensc&lt;/strong&gt;: el middleware PKCS#11 que permite a las aplicaciones acceder a los certificados guardados en la tarjeta. Soporta el DNIe de forma nativa.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pcsc-tools&lt;/strong&gt;: utilidades de diagnóstico como &lt;code&gt;pcsc_scan&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="arch-linux"&gt;Arch Linux&lt;/h3&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;pacman&lt;span class="w"&gt; &lt;/span&gt;-S&lt;span class="w"&gt; &lt;/span&gt;ccid&lt;span class="w"&gt; &lt;/span&gt;opensc&lt;span class="w"&gt; &lt;/span&gt;pcsc-tools
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h3 id="debian-ubuntu"&gt;Debian / Ubuntu&lt;/h3&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pcscd&lt;span class="w"&gt; &lt;/span&gt;pcsc-tools&lt;span class="w"&gt; &lt;/span&gt;opensc&lt;span class="w"&gt; &lt;/span&gt;libccid
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h3 id="fedora"&gt;Fedora&lt;/h3&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;pcsc-lite&lt;span class="w"&gt; &lt;/span&gt;pcsc-tools&lt;span class="w"&gt; &lt;/span&gt;opensc&lt;span class="w"&gt; &lt;/span&gt;ccid
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="paso-2-activar-el-servicio-pcscd"&gt;Paso 2: Activar el servicio pcscd&lt;/h2&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pcscd.service
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;pcscd.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Opcionalmente se puede habilitar la activación bajo demanda mediante el socket, para que se inicie cuando haga falta:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pcscd.socket
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;pcscd.socket
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="paso-3-comprobar-que-el-lector-y-la-tarjeta-se-detectan"&gt;Paso 3: Comprobar que el lector y la tarjeta se detectan&lt;/h2&gt;
&lt;p&gt;Lo primero es comprobar que el lector de tarjetas es reconocido por &lt;strong&gt;pcscd&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Conecta el lector USB e inserta el DNIe.
Luego ejecuta:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pcsc_scan
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Deberías ver una salida similar a:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Scanning present readers...
0: ACS ACR38U-CCID 00 00

Reader 0: ACS ACR38U-CCID 00 00
  Event number: 0
  Card state: Card inserted,
  ATR: 3B 7F 96 00 00 ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Si en su lugar ves &lt;strong&gt;"Waiting for the first reader..."&lt;/strong&gt;, el sistema no detecta el lector. En ese caso:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Comprueba que el lector aparece en la salida de &lt;code&gt;lsusb&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspecciona los mensajes del kernel con &lt;code&gt;dmesg | tail -30&lt;/code&gt; tras conectar el lector.&lt;/li&gt;
&lt;li&gt;Asegúrate de que el servicio &lt;code&gt;pcscd&lt;/code&gt; está corriendo con &lt;code&gt;systemctl status pcscd.service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Prueba otro puerto USB.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="verificar-que-opensc-reconoce-el-dnie"&gt;Verificar que OpenSC reconoce el DNIe&lt;/h3&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;opensc-tool&lt;span class="w"&gt; &lt;/span&gt;-n
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Debería devolver &lt;code&gt;DNIe&lt;/code&gt; o &lt;code&gt;DNIE&lt;/code&gt;. A continuación, comprueba que los certificados son legibles:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkcs11-tool&lt;span class="w"&gt; &lt;/span&gt;--module&lt;span class="w"&gt; &lt;/span&gt;/usr/lib/opensc-pkcs11.so&lt;span class="w"&gt; &lt;/span&gt;-O
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Este comando listará los objetos almacenados en la tarjeta: certificados de autenticación y firma, y sus claves asociadas.
Si te pide el PIN, introdúcelo.&lt;/p&gt;
&lt;p&gt;Para un volcado más detallado:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pkcs15-tool&lt;span class="w"&gt; &lt;/span&gt;-D
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="paso-4-configurar-firefox"&gt;Paso 4: Configurar Firefox&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Abre Firefox.&lt;/li&gt;
&lt;li&gt;Ve a &lt;strong&gt;Ajustes&lt;/strong&gt; → &lt;strong&gt;Privacidad &amp;amp; Seguridad&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Baja hasta la sección &lt;strong&gt;Certificados&lt;/strong&gt; y haz clic en &lt;strong&gt;Administrar dispositivos de seguridad...&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Haz clic en &lt;strong&gt;Cargar&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Rellena los campos:&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nombre del módulo&lt;/strong&gt;: &lt;code&gt;OpenSC PKCS#11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nombre del archivo del módulo&lt;/strong&gt;: &lt;code&gt;/usr/lib/opensc-pkcs11.so&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Haz clic en &lt;strong&gt;Aceptar&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/dnie_firefox_linux/security_device_manager.png" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/dnie_firefox_linux/security_device_manager.png" title="Click para ver a pantalla completa"/&gt;&lt;figcaption&gt;Administrador de dispositivos de seguridad en Firefox&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nota sobre la ruta en sistemas de 64 bits&lt;/strong&gt;: en la mayoría de distribuciones actuales la ruta es &lt;code&gt;/usr/lib/opensc-pkcs11.so&lt;/code&gt;. En algunas instalaciones de Fedora o Debian puede estar en &lt;code&gt;/usr/lib64/opensc-pkcs11.so&lt;/code&gt; o &lt;code&gt;/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so&lt;/code&gt;. Puedes encontrar la ruta exacta con:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bash
find /usr/ -name "opensc-pkcs11.so" 2&amp;gt;/dev/null&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="paso-5-probar-el-acceso"&gt;Paso 5: Probar el acceso&lt;/h2&gt;
&lt;p&gt;Visita cualquier sede electrónica que ofrezca identificación con certificado, como por ejemplo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agencia Tributaria: &lt;a href="https://sede.agenciatributaria.gob.es" target="_blank"&gt;https://sede.agenciatributaria.gob.es&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Seguridad Social: &lt;a href="https://sede.seg-social.gob.es" target="_blank"&gt;https://sede.seg-social.gob.es&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DGT: &lt;a href="https://sede.dgt.gob.es" target="_blank"&gt;https://sede.dgt.gob.es&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El navegador te pedirá seleccionar un certificado y a continuación el PIN del DNIe.
Tras introducirlo, deberías acceder autenticado.&lt;/p&gt;
&lt;h2 id="solucion-de-problemas"&gt;Solución de problemas&lt;/h2&gt;
&lt;h3 id="firefox-dice-no-se-puede-anadir-el-modulo"&gt;Firefox dice "No se puede añadir el módulo"&lt;/h3&gt;
&lt;p&gt;Verifica que la librería existe y que todas sus dependencias están satisfechas:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/usr/lib/opensc-pkcs11.so
ldd&lt;span class="w"&gt; &lt;/span&gt;/usr/lib/opensc-pkcs11.so&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not found"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Si &lt;code&gt;ldd&lt;/code&gt; muestra alguna dependencia como &lt;strong&gt;"not found"&lt;/strong&gt;, instala el paquete que la proporciona.&lt;/p&gt;
&lt;h3 id="los-certificados-no-aparecen-aunque-la-tarjeta-se-detecta"&gt;Los certificados no aparecen aunque la tarjeta se detecta&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Certificados caducados&lt;/strong&gt;: los certificados del DNIe tienen una validez limitada. Si han caducado debes renovarlos en un PAD en una comisaría de policía.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reinicia Firefox&lt;/strong&gt;: cierra completamente Firefox, inserta el DNIe y vuelve a abrirlo. A veces no detecta una tarjeta insertada después de cargar el módulo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reinicia pcscd&lt;/strong&gt;: desconecta el lector de tarjetas, reinicia con &lt;code&gt;sudo systemctl restart pcscd.service&lt;/code&gt; y vuelve a conectarlo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="el-pin-se-bloquea"&gt;El PIN se bloquea&lt;/h3&gt;
&lt;p&gt;Tras &lt;strong&gt;tres intentos fallidos&lt;/strong&gt; el DNIe se bloquea permanentemente.
Para desbloquearlo es necesario acudir a una comisaría de policía.
Asegúrate de recordar el PIN antes de intentar usarlo.&lt;/p&gt;
&lt;h3 id="firefox-instalado-con-flatpak-o-snap"&gt;Firefox instalado con Flatpak o Snap&lt;/h3&gt;
&lt;p&gt;Las versiones Flatpak y Snap de Firefox corren en un entorno limitado (&lt;em&gt;sandbox&lt;/em&gt;) que impide el acceso a los dispositivos del sistema y al socket de &lt;code&gt;pcscd&lt;/code&gt;.
La solución más sencilla (aunque no la más segura) es instalar Firefox desde los repositorios nativos de la distribución:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Arch Linux&lt;/strong&gt;: &lt;code&gt;sudo pacman -S firefox&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debian/Ubuntu&lt;/strong&gt;: &lt;code&gt;sudo apt install firefox&lt;/code&gt; (o &lt;code&gt;firefox-esr&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fedora&lt;/strong&gt;: &lt;code&gt;sudo dnf install firefox&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si tienes Firefox instalado a través de &lt;strong&gt;Snap&lt;/strong&gt; puedes probar a conectarlo a &lt;em&gt;pcscd&lt;/em&gt; con:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;snap&lt;span class="w"&gt; &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;firefox:pcscd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;A algunas personas parece que les funciona, pero otros tienen problemas, como el acceso a la librería &lt;em&gt;/usr/lib/opensc-pkcs11.so&lt;/em&gt;. Hay un &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1734371"&gt;bug abierto en Mozilla desde 2021&lt;/a&gt; que documenta que cargar módulos PKCS#11 del sistema host no funciona de forma fiable con Snap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flatpak&lt;/strong&gt; sí ofrece más herramientas para resolver la situación:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Acceso al socket de pcscd&lt;/span&gt;
flatpak&lt;span class="w"&gt; &lt;/span&gt;override&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;--filesystem&lt;span class="o"&gt;=&lt;/span&gt;/run/pcscd&lt;span class="w"&gt; &lt;/span&gt;org.mozilla.firefox

&lt;span class="c1"&gt;# Acceso a la librería de OpenSC&lt;/span&gt;
flatpak&lt;span class="w"&gt; &lt;/span&gt;override&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;--filesystem&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/opensc-pkcs11.so:ro&lt;span class="w"&gt; &lt;/span&gt;org.mozilla.firefox

&lt;span class="c1"&gt;# Acceso a dispositivos (lector USB)&lt;/span&gt;
flatpak&lt;span class="w"&gt; &lt;/span&gt;override&lt;span class="w"&gt; &lt;/span&gt;--user&lt;span class="w"&gt; &lt;/span&gt;--device&lt;span class="o"&gt;=&lt;/span&gt;all&lt;span class="w"&gt; &lt;/span&gt;org.mozilla.firefox
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;h2 id="y-que-hay-de-las-firmas"&gt;¿Y qué hay de las firmas?&lt;/h2&gt;
&lt;p&gt;Esta configuración permite &lt;strong&gt;autenticarse&lt;/strong&gt; en una página web, pero los navegadores no gestionan el firmado de documentos digitales.
Para eso hace falta instalar y configurar &lt;a href="https://firmaelectronica.gob.es/ciudadanos/descargas" target="_blank"&gt;AutoFirma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Lo bueno es que la parte de &lt;em&gt;pcscd&lt;/em&gt; y &lt;em&gt;opensc&lt;/em&gt; es común, así que una vez funcionando no debería ser difícil.
Pero no lo he probado aún, lo dejo como ejercicio para el lector. :o)&lt;/p&gt;
&lt;h2 id="resumen"&gt;Resumen&lt;/h2&gt;
&lt;p&gt;La configuración se reduce a tres cosas: instalar &lt;code&gt;opensc&lt;/code&gt;, &lt;code&gt;ccid&lt;/code&gt; y &lt;code&gt;pcscd&lt;/code&gt;; arrancar el servicio; y cargar &lt;code&gt;/usr/lib/opensc-pkcs11.so&lt;/code&gt; como módulo de seguridad en Firefox.
No se necesita software propietario ni paquetes específico del DNIe.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="free-software"/><category term="firefox"/><category term="howto"/><category term="linux"/><category term="web"/></entry><entry><title>Anunciando Reflex, un tema para Pelican</title><link href="https://blog.fidelramos.net/es/software/announce-reflex-theme" rel="alternate"/><published>2026-02-16T00:00:00+00:00</published><updated>2026-02-19T00:00:00+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-02-16:/es/software/announce-reflex-theme</id><summary type="html">&lt;p&gt;Me complace anunciar la disponibilidad pública de mi tema para Pelican: &lt;a href="https://github.com/haplo/python-theme-reflex"&gt;Reflex&lt;/a&gt;.
Está &lt;a href="https://pypi.org/project/pelican-reflex/"&gt;publicado en PyPI&lt;/a&gt;, esa deber'ia ser la mejor forma de instalarlo para la mayoría de los usuarios.&lt;/p&gt;
&lt;p&gt;Cuando empecé este blog, me decidí por &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt; porque tenía mucha experiencia con Python, así que me resultaría más fácil …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Me complace anunciar la disponibilidad pública de mi tema para Pelican: &lt;a href="https://github.com/haplo/python-theme-reflex"&gt;Reflex&lt;/a&gt;.
Está &lt;a href="https://pypi.org/project/pelican-reflex/"&gt;publicado en PyPI&lt;/a&gt;, esa deber'ia ser la mejor forma de instalarlo para la mayoría de los usuarios.&lt;/p&gt;
&lt;p&gt;Cuando empecé este blog, me decidí por &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt; porque tenía mucha experiencia con Python, así que me resultaría más fácil involucrarme y contribuir.
Luego eché un vistazo a todos los &lt;a href="http://www.pelicanthemes.com/"&gt;temas de Pelican&lt;/a&gt; y me decanté por el &lt;a href="https://github.com/alexandrevicenzi/Flex"&gt;tema Flex de Alexandre Vizenzi&lt;/a&gt; porque cumplía todos mis requisitos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimalista y bonito.&lt;/li&gt;
&lt;li&gt;Diseño flexible para distintas pantallas (&lt;em&gt;responsive&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;Soporte para archivos, categorías y etiquetas.&lt;/li&gt;
&lt;li&gt;Soporte de Pygments para resaltado de código.&lt;/li&gt;
&lt;li&gt;Soporte para modos claro/oscuro.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Como era inevitable, al final me topé con algunas funciones que echaba en falta y pequeños retoques que quería implementar.
Esa es la belleza del software libre, y empecé a &lt;a href="https://github.com/alexandrevicenzi/Flex/pulls?q=is%3Apr+author%3Ahaplo+"&gt;contribuir en Flex&lt;/a&gt;.
Sin embargo, algunos de los cambios que quería hacer eran demasiado profundos y no encajaban en Flex, ya que el proyecto estaba en modo de mantenimiento.
Después de pensarlo un poco, decidí hacer un &lt;em&gt;fork&lt;/em&gt; del tema para introducir mis cambios más rompedores, y así es como nació Reflex.&lt;/p&gt;
&lt;p&gt;Las &lt;strong&gt;principales diferencias con Flex&lt;/strong&gt; por ahora son:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instalable vía PyPI (o sea que funciona &lt;code&gt;pip install pelican-theme-reflex&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Estilo para la tabla de contenidos creada por la extensión de Markdown &lt;em&gt;toc&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Estilos para los elementos HTML &lt;code&gt;figure&lt;/code&gt; y &lt;code&gt;figcaption&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Soporte para analíticas de tráfico con Shynet (ver &lt;a href="https://blog.fidelramos.net/es/software/self-hosted-comments"&gt;mi artículo sobre comentarios autoalojados&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Documentación en el propio repositorio en lugar de en la wiki de Github.&lt;/li&gt;
&lt;li&gt;Visualización de banderas de idioma para los idiomas alternativos del artículo.&lt;/li&gt;
&lt;li&gt;Icono de la red social X.&lt;/li&gt;
&lt;li&gt;Mejoras en la experiencia de desarrollo: soporte para &lt;code&gt;gulp watch&lt;/code&gt; para ver cambios al instante, chequeos de &lt;em&gt;pre-commit&lt;/em&gt;, &lt;em&gt;AGENTS.md&lt;/em&gt; para el desarrollo con IA, flujos de trabajo con GitHub Actions renovados.&lt;/li&gt;
&lt;li&gt;Estilos de Pygments y FontAwesome actualizados.&lt;/li&gt;
&lt;li&gt;Mejoras de accesibilidad.&lt;/li&gt;
&lt;li&gt;Actualizada la integración de comentarios de Isso, con soporte para resaltar los comentarios del autor y notificaciones de respuesta por defecto.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No tengo grandes planes para Reflex, principalmente implementaré los cambios que me parezcan útiles para este blog.
Estoy abierto a nuevas integraciones e ideas, ¡así que bienvenidos sean los colaboradores!&lt;/p&gt;
&lt;p&gt;Para ver los errores conocidos y las funcionalidades planeadas, echa un vistazo a las &lt;a href="https://github.com/haplo/pelican-theme-reflex/issues"&gt;incidencias abiertas&lt;/a&gt;.
Para cualquier pregunta, no dudes en abrir un &lt;a href="https://github.com/haplo/pelican-theme-reflex/discussions"&gt;hilo de discusión&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Si usas Reflex, anímate a añadir tu sitio a la &lt;a href="https://github.com/haplo/pelican-theme-reflex?tab=readme-ov-file#sites-using-reflex"&gt;lista de usuarios de Reflex&lt;/a&gt;, ¡me encantaría saber de ti!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actualización del 19 de febrero de 2016&lt;/strong&gt;: Se publicó &lt;a href="https://haplo.github.io/pelican-theme-reflex/reflex-3.1.0.html"&gt;Reflex 3.1.0&lt;/a&gt; con algunas mejoras adicionales.&lt;/p&gt;</content><category term="Software"/><category term="free-software"/><category term="pelican"/></entry><entry><title>Casa Autoalojada, Parte 1: Diseño y planificación</title><link href="https://blog.fidelramos.net/es/software/self-hosted-home-1" rel="alternate"/><published>2026-01-25T00:00:00+00:00</published><updated>2026-01-25T16:09:34.038206+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-01-25:/es/software/self-hosted-home-1</id><summary type="html">&lt;p&gt;Quienes hayan estado atentos al blog sabrán que he comprado una casa y he estado planeando su reforma integral.
Como friki de la informática, lo que más me ilusiona es la oportunidad de implementar &lt;strong&gt;la casa inteligente, la red y el servidor doméstico de mis sueños&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tengo una idea clara …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Quienes hayan estado atentos al blog sabrán que he comprado una casa y he estado planeando su reforma integral.
Como friki de la informática, lo que más me ilusiona es la oportunidad de implementar &lt;strong&gt;la casa inteligente, la red y el servidor doméstico de mis sueños&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tengo una idea clara de lo que quiero conseguir: un &lt;strong&gt;homelab de código abierto&lt;/strong&gt; que sustituya los &lt;strong&gt;ecosistemas cerrados de la nube&lt;/strong&gt; por &lt;strong&gt;servicios locales&lt;/strong&gt;.
Domótica, cámaras de seguridad y videoportero, contactos y calendarios, reproductores multimedia, colección de fotos, documentos y más, todo funcionando &lt;strong&gt;en mi propia casa&lt;/strong&gt; y &lt;strong&gt;totalmente bajo mi control&lt;/strong&gt;.
Hay que tener en cuenta que &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;ya tengo un homelab&lt;/a&gt;, donde algunos de estos servicios llevan años funcionando.
Este nuevo proyecto lo sustituirá por completo, ¡será &lt;strong&gt;más grande&lt;/strong&gt; y &lt;strong&gt;mejor&lt;/strong&gt;!
Os recomiendo leer ese artículo si tenéis la oportunidad, ya que explica las razones que me llevaron a empezar a autoalojar servicios y el contexto histórico de cómo he llegado hasta aquí.&lt;/p&gt;
&lt;p&gt;En esta serie de artículos entraré en todos los detalles técnicos, empezando por el diseño al que he llegado tras meses de investigación.&lt;/p&gt;
&lt;p&gt;La configuración es completamente personal y subjetiva, adaptada a mis necesidades y gustos particulares, así que no os lo toméis como una recomendación.
¡Espero que sea una lectura entretenida y que aprendáis algo interesante!&lt;/p&gt;
&lt;p&gt;Mis objetivos son:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Todo local&lt;/strong&gt;: La domótica, las cámaras, la multimedia y los datos personales deben funcionar sin ninguna dependencia de la nube. El acceso a Internet podría caerse y todo seguiría funcionando localmente. Además, puedo confiar en que mis datos no saldrán de mi casa.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento estricto&lt;/strong&gt;: Los dispositivos domóticos (&lt;em&gt;IoT&lt;/em&gt;) y las cámaras se tratan como hostiles: &lt;strong&gt;no tienen salida a Internet&lt;/strong&gt; y su acceso a los servicios internos es limitado a lo necesario. La conectividad con la nube debe evitarse siempre que sea posible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basado en estándares y código abierto&lt;/strong&gt;: ONVIF, RTSP, IPv6, WireGuard, ZFS, Proxmox, TrueNAS, IPFire, Home Assistant, Frigate, Nextcloud, Jellyfin, Immich... &lt;strong&gt;evitar las cajas negras cerradas&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coste&lt;/strong&gt;: Aunque el montaje completo no será barato en absoluto, buscaré &lt;strong&gt;hardware de servidor reacondicionado&lt;/strong&gt; y daré prioridad a dispositivos de &lt;strong&gt;bajo consumo&lt;/strong&gt;. El sistema tendrá muchas responsabilidades, así que necesito hardware fiable y de calidad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preparado para el futuro&lt;/strong&gt;: Hasta 10 GbE mediante cableado Cat6a, servidores con muchos núcleos, espacio para ampliación, IPv6 en todas partes.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#1-vision-general"&gt;1. Visión general&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#servicios-autoalojados"&gt;Servicios autoalojados&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#2-red"&gt;2. Red&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#routercortafuegos-ipfire"&gt;Router/cortafuegos IPFire&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#switching-y-wifi-de-unifi"&gt;Switching y WiFi de UniFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#diseno-de-vlan-y-modelo-de-confianza"&gt;Diseño de VLAN y modelo de confianza&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#vpn-wireguard"&gt;VPN WireGuard&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#3-servidor-de-aplicaciones"&gt;3. Servidor de aplicaciones&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#proxmox-y-consideraciones-sobre-licencias"&gt;Proxmox y consideraciones sobre licencias&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#opciones-de-hardware-para-el-servidor"&gt;Opciones de hardware para el servidor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#redundancia-y-fiabilidad"&gt;Redundancia y fiabilidad&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#4-almacenamiento"&gt;4. Almacenamiento&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#por-que-no-synology"&gt;¿Por qué no Synology?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#truenas"&gt;TrueNAS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#diseno-del-almacenamiento"&gt;Diseño del almacenamiento&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#5-camaras-y-timbre"&gt;5. Cámaras y timbre&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#plan-inicial-unifi-o-reolink"&gt;Plan inicial: ¿UniFi o Reolink?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#frigate-y-camaras-dahua"&gt;Frigate y cámaras Dahua&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#diseno-de-red-para-las-camaras"&gt;Diseño de red para las cámaras&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#deteccion-de-objetos"&gt;Detección de objetos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#videoportero"&gt;Videoportero&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#6-electricidad-y-resiliencia"&gt;6. Electricidad y resiliencia&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#sobre-el-consumo-de-energia"&gt;Sobre el consumo de energía&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#emergencia-de-alimentacion-sai-y-estrategia-de-apagado"&gt;Emergencia de alimentación: SAI y estrategia de apagado&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#7-copias-de-seguridad-y-recuperacion"&gt;7. Copias de seguridad y recuperación&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#8-operaciones-monitorizacion-y-practicas-de-seguridad"&gt;8. Operaciones, monitorización y prácticas de seguridad&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#9-proximos-pasos"&gt;9. Próximos pasos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;Otros artículos de la serie:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Parte 1: Diseño y planificación&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Próximamente, ¡&lt;a href="https://blog.fidelramos.net/es/paginas/suscribirse"&gt;mantente al tanto&lt;/a&gt; para no perderte las novedades!&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="1-vision-general"&gt;1. Visión general&lt;/h1&gt;
&lt;p&gt;El homelab constará de múltiples componentes montados en un &lt;a href="https://es.wikipedia.org/wiki/Bastidor_de_19_pulgadas"&gt;bastidor (rack) de 42U en 19 pulgadas estándares&lt;/a&gt; en una sala dedicada en el sótano de la casa.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Red y seguridad principales&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Quiero tener el control de la red doméstica: impedir el acceso a Internet de la domótica y las cámaras y limitar el acceso a los servicios internos.&lt;/li&gt;
&lt;li&gt;Un servidor dedicado con IPFire proporcionará enrutamiento, cortafuegos, IDS (detección de intrusos) y VPN.&lt;/li&gt;
&lt;li&gt;Switch y puntos de acceso WiFi de UniFi.&lt;/li&gt;
&lt;li&gt;VLANs para una segmentación estricta del tráfico (LAN, IoT, cámaras, invitados, gestión).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Servidor de aplicaciones&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Migraré los servicios autoalojados que funcionan en mi &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;servidor doméstico actual&lt;/a&gt;, y los ampliaré.&lt;/li&gt;
&lt;li&gt;Proxmox VE en un servidor tipo Dell PowerEdge.&lt;/li&gt;
&lt;li&gt;Mezcla de máquinas virtuales y contenedores LXC para los servicios.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Almacenamiento&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Tengo considerables necesidades de almacenamiento, ya que mantengo una gran colección de fotos y una biblioteca multimedia local.&lt;/li&gt;
&lt;li&gt;TrueNAS correrá en una máquina virtual en Proxmox, controlando directamente un grupo de discos duros dedicado.&lt;/li&gt;
&lt;li&gt;ZFS para redundancia, instantáneas y replicación.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cámaras y videoportero&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Cámaras y videoportero Dahua con ONVIF/RTSP/IPv6 y audio bidireccional.&lt;/li&gt;
&lt;li&gt;Switch PoE dedicado.&lt;/li&gt;
&lt;li&gt;Frigate para grabación tipo NVR y detección de objetos.&lt;/li&gt;
&lt;li&gt;Coral TPU para inferencia eficiente.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resiliencia energética&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Paneles solares y baterías mantendrán en funcionamiento al menos la infraestructura principal.&lt;/li&gt;
&lt;li&gt;Un SAI proporcionará respaldo crítico para mantener la red y las cámaras en funcionamiento y permitir un apagado controlado del servidor de aplicaciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para los que disfrutan con un pequeño diagrama:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/self_hosted_home_1/rack.svg" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/self_hosted_home_1/rack.svg" title="Haz clic para ver a pantalla completa"&gt;&lt;figcaption&gt;Diagrama del rack&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="servicios-autoalojados"&gt;Servicios autoalojados&lt;/h2&gt;
&lt;p&gt;Entre los servicios que planeo desplegar se encuentran:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.truenas.com/"&gt;&lt;strong&gt;TrueNAS&lt;/strong&gt;&lt;/a&gt;, detallado más abajo en la sección de &lt;em&gt;Almacenamiento&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.home-assistant.io/"&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt;&lt;/a&gt; para domótica e integración de todas las plataformas y fuentes de datos.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://frigate.video/"&gt;&lt;strong&gt;Frigate NVR&lt;/strong&gt;&lt;/a&gt; para las cámaras, detección de objetos y comunicación con el timbre (también integrado en Home Assistant).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextcloud.com/"&gt;&lt;strong&gt;Nextcloud&lt;/strong&gt;&lt;/a&gt; como nube personal (contactos, calendario, archivos, notas, etc.).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://immich.app/"&gt;&lt;strong&gt;Immich&lt;/strong&gt;&lt;/a&gt; como biblioteca de fotos.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jellyfin.org/"&gt;&lt;strong&gt;Jellyfin&lt;/strong&gt;&lt;/a&gt; para la reproducción de vídeo.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.music-assistant.io/"&gt;&lt;strong&gt;Music Assistant&lt;/strong&gt;&lt;/a&gt; para la reproducción de música.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.paperless-ngx.com/"&gt;&lt;strong&gt;Paperless-ngx&lt;/strong&gt;&lt;/a&gt; para gestionar documentos.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AdguardTeam/AdGuardHome"&gt;&lt;strong&gt;AdGuard Home&lt;/strong&gt;&lt;/a&gt; para bloquear anuncios en toda la casa.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Servicios auxiliares&lt;/strong&gt;: monitorización, comprobaciones de disponibilidad, registros, orquestación de copias de seguridad, NTP...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Mi intención es dar acceso a la mayoría de estos servicios a mi familia.
Eso amortizaría los costes de montaje y mantenimiento, a la vez que les aportaría más privacidad a sus vidas.&lt;/p&gt;
&lt;h1 id="2-red"&gt;2. Red&lt;/h1&gt;
&lt;p&gt;La red es la base de todo lo demás, es clave para el rendimiento, seguridad y comodidad del sistema.&lt;/p&gt;
&lt;p&gt;Aprovechando la oportunidad que brinda una reforma integral, voy a instalar &lt;strong&gt;cables Ethernet Cat6a&lt;/strong&gt; por toda la casa.
Cada división tendrá &lt;strong&gt;al menos un puerto RJ-45&lt;/strong&gt; en una toma de pared, y algunas varias, dependiendo del uso previsto.
Los cables Cat6a garantizan velocidad de &lt;strong&gt;10 GbE&lt;/strong&gt;, que en un futuro serían alcanzables simplemente sustituyendo el switch, las tarjetas de red y los puntos de acceso WiFi; el cableado ya estaría listo.
Inicialmente las conexiones serán a 2,5 GbE, con la excepción de dos a 10 GbE para los dos ordenadores de sobremesa.&lt;/p&gt;
&lt;p&gt;Todos los cables irán por las paredes hasta la sala de servidores, a &lt;a href="https://es.wikipedia.org/wiki/Panel_de_conexiones"&gt;paneles de conexiones&lt;/a&gt; montados en el rack.&lt;/p&gt;
&lt;p&gt;Como router y cortafuegos instalaré &lt;strong&gt;IPFire&lt;/strong&gt; en un servidor dedicado, que será el núcleo de la instalación.
Un switch &lt;strong&gt;UniFi&lt;/strong&gt; y varios puntos de acceso WiFi UniFi agregarán el tráfico y lo delimitarán en VLANs.
Habrá un switch secundario para las cámaras de seguridad, que se tratará en la sección &lt;em&gt;Cámaras y timbre&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="routercortafuegos-ipfire"&gt;Router/cortafuegos IPFire&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.ipfire.org/"&gt;IPFire&lt;/a&gt; es un software de router y cortafuegos de código abierto basado en GNU/Linux.
Lo instalaré en un servidor en rack pequeño, de bajo consumo pero robusto, con estos requisitos (ideales):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Al menos 2 tarjetas de red SFP+ (una para la WAN, otra para la LAN, y opcionalmente otra para gestión dedicada o DMZ).&lt;/li&gt;
&lt;li&gt;CPU suficiente para sostener hasta 10 Gbps de velocidad WAN.&lt;/li&gt;
&lt;li&gt;Dos unidades SSD o NVMe para configurarlas en espejo para mayor redundancia.&lt;/li&gt;
&lt;li&gt;Fuentes de alimentación dobles para redundancia energética.&lt;/li&gt;
&lt;li&gt;El menor consumo posible, especialmente en reposo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todavía estoy investigando opciones para este servidor.
IPFire ofrece sus propios &lt;a href="https://store.lightningwirelabs.com/products/groups/firewall-appliances"&gt;dispositivos&lt;/a&gt;, pero se me hacen carísimos para lo que ofrecen, así que estoy buscando servidores en rack.&lt;/p&gt;
&lt;p&gt;Lo mejor que he encontrado hasta ahora es la &lt;a href="https://www.supermicro.com/en/products/motherboard/X10SDV-7TP8F"&gt;Supermicro X10SDV-7TP8F&lt;/a&gt;, una placa base &lt;em&gt;system-on-chip&lt;/em&gt; con una CPU Intel Xeon D-1587.
Cumple varios de los requisitos: potencia de cálculo suficiente para una WAN de 10 Gbps; bajo consumo de energía (10W en reposo, 45W TDP); 2 tarjetas de red SFP+ de 10 GbE integradas. Le faltan las dobles fuentes de alimentación y doble disco, pero nada es perfecto.&lt;/p&gt;
&lt;p&gt;El equipo con IPFire se conectará directamente al router del proveedor de Internet (en modo puente) o a una &lt;a href="https://es.wikipedia.org/wiki/Terminal_de_red_%C3%B3ptica"&gt;ONT&lt;/a&gt; dedicada.&lt;/p&gt;
&lt;p&gt;Se encargará de estas responsabilidades:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Terminar la conexión WAN (PPPoE o DHCP, dependiendo del proveedor).&lt;/li&gt;
&lt;li&gt;Proporcionar un &lt;strong&gt;cortafuegos con estado&lt;/strong&gt; (&lt;em&gt;stateful&lt;/em&gt;) tanto para IPv4 como para IPv6.&lt;/li&gt;
&lt;li&gt;Realizar &lt;strong&gt;DHCP&lt;/strong&gt; y &lt;strong&gt;DNS&lt;/strong&gt; para las redes internas.&lt;/li&gt;
&lt;li&gt;Gestionar túneles VPN &lt;strong&gt;WireGuard&lt;/strong&gt; para el acceso remoto a la red doméstica.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Detección de intrusos&lt;/strong&gt; (&lt;em&gt;IDS&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quiero que IPv6 sea un ciudadano de primera clase: IPFire obtendrá un prefijo delegado del proveedor y lo dividirá entre las VLAN internas.
Esto, junto con una política de denegación por defecto, facilitará la apertura de servicios al exterior de forma controlada y segura, sin las molestias y el impacto en el rendimiento del NAT de IPv4.&lt;/p&gt;
&lt;h2 id="switching-y-wifi-de-unifi"&gt;Switching y WiFi de UniFi&lt;/h2&gt;
&lt;p&gt;Conectado a IPFire habrá un &lt;a href="https://techspecs.ui.com/unifi/switching/usw-pro-hd-24-poe"&gt;switch UniFi Pro HD 24 PoE&lt;/a&gt; que:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agregará todos los segmentos cableados (habitaciones, rack, cámaras, puntos de acceso WiFi).&lt;/li&gt;
&lt;li&gt;Proporcionará PoE para los puntos de  acceso y potencialmente otros dispositivos.&lt;/li&gt;
&lt;li&gt;Implementará etiquetado de VLAN y segmentación de cortafuegos a nivel de capa 2/3 en cooperación con IPFire.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Busqué y rebusqué switches para montar en rack, y ningún otro se acercaba en especificaciones a este UniFi por un precio comparable.
Tiene &lt;strong&gt;4 puertos SFP+ de 10 GbE&lt;/strong&gt; para conectar los servidores IPFire y de aplicaciones, &lt;strong&gt;2 puertos RJ-45 de 10 GbE&lt;/strong&gt; para mis ordenadores de sobremesa y &lt;strong&gt;22 puertos RJ-45 de 2,5 GbE&lt;/strong&gt; con PoE.
La mayoría de los otros switches de 24 puertos que encontré solo tienen puertos de 1G, sin 10 GbE.
El tener 2,5 GbE en todas las conexiones va a darle un buen rendimiento a toda la red por mucho tiempo.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/self_hosted_home_1/unifi_switch_pro_hd_24_poe.avif" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/self_hosted_home_1/unifi_switch_pro_hd_24_poe.avif" title="Haz clic para ver a pantalla completa"&gt;&lt;figcaption&gt;Switch UniFi Pro HD 24 PoE&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Para los puntos de acceso WiFi consideré los &lt;a href="https://www.grandstream.com/products/networking-solutions/indoor-wifi-access-points"&gt;puntos de acceso de Grandstream&lt;/a&gt;, que según dicen son de buena calidad, pero los descarté porque no disponen de banda de 6 GHz, que es la novedad del estándar WiFi 7.
Finalmente me decidí por los puntos de acceso WiFi de UniFi (muy probablemente el &lt;a href="https://techspecs.ui.com/unifi/wifi/u7-pro"&gt;U7 Pro&lt;/a&gt;), porque funcionan con un solo cable Ethernet (2,5 GbE para el U7 Pro, pero en el futuro podría actualizar a un PA con 10 GbE), y se pueden gestionar junto con el switch UniFi.&lt;/p&gt;
&lt;p&gt;Los puntos de acceso permitirán un roaming sin interrupciones por toda la casa y tendrán múltiples SSID:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSID principal&lt;/strong&gt;: Para dispositivos de confianza (portátiles, móviles y tabletas de la familia).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSID IoT&lt;/strong&gt;: Para dispositivos inteligentes inalámbricos, mapeado a la VLAN de IoT.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSID de invitados&lt;/strong&gt;: Aislado de los servicios internos, solo con acceso a Internet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/self_hosted_home_1/unifi_u7_pro.avif" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/self_hosted_home_1/unifi_u7_pro.avif" title="Haz clic para ver a pantalla completa"&gt;&lt;figcaption&gt;UniFi U7 Pro&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Proxmox ejecutará un &lt;a href="https://help.ui.com/hc/en-us/articles/360012282453-Self-Hosting-a-UniFi-Network-Server"&gt;UniFi Network Server&lt;/a&gt; en una máquina virtual, que permitirá la configuración del switch y los puntos de acceso sin conexión a la nube.
Estoy confiando que Ubiquiti no me la líe con futuros cambios que requieran conectividad en la nube, me enfadaría mucho si lo hicieran.
Este riesgo es la razón por la que elegí no usar cámaras UniFi, por ejemplo.
En el peor de los casos, solo tendría que reemplazar el switch y los puntos de acceso, es un riesgo calculado.&lt;/p&gt;
&lt;h2 id="diseno-de-vlan-y-modelo-de-confianza"&gt;Diseño de VLAN y modelo de confianza&lt;/h2&gt;
&lt;p&gt;La red se dividirá en &lt;strong&gt;zonas de confianza&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interna (de confianza)&lt;/strong&gt;: Ordenadores, móviles y tablets de la familia.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IoT&lt;/strong&gt;: Dispositivos de domótica (enchufes, interruptores, electrodomésticos, TV, aire acondicionado...).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cámaras&lt;/strong&gt;: Cámaras de seguridad y el videoportero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invitados&lt;/strong&gt;: Para visitas; solo Internet, sin acceso a LAN/IoT/Cámaras.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gestión&lt;/strong&gt;: Ordenador personal, Proxmox, switches, router, puntos de acceso, gestión fuera de banda (IPMI, iDRAC).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Una vista de pájaro de las reglas de enrutamiento/cortafuegos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interna&lt;/strong&gt;: Puede acceder a servicios: Home Assistant, Frigate, recursos compartidos del NAS, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IoT&lt;/strong&gt;: Conexión a Home Assistant; sin acceso directo a Internet, excepto para un puñado de puntos finales en lista blanca si es absolutamente necesario; sin acceso a datos personales.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cámaras&lt;/strong&gt;: Conexión a Frigate; sin acceso a Internet en absoluto, no quiero que los datos de mis cámaras se exfiltren; sin acceso a datos personales.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invitados&lt;/strong&gt;: Solo acceso a Internet; sin acceso a ningún servicio interno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gestión&lt;/strong&gt;: Solo accesible desde un pequeño conjunto de dispositivos de confianza para administración de sistemas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Las reglas de cortafuegos y enrutamiento serán aplicadas conjuntamente por IPFire y el switch UniFi, asegurando que &lt;strong&gt;el tráfico de las cámaras y del IoT nunca llegue a Internet&lt;/strong&gt;, y que los invitados nunca lleguen a nada interno.&lt;/p&gt;
&lt;h2 id="vpn-wireguard"&gt;VPN WireGuard&lt;/h2&gt;
&lt;p&gt;Para un acceso externo seguro, configuraré &lt;a href="https://www.wireguard.com/"&gt;Wireguard&lt;/a&gt; en IPFire:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cada dispositivo (portátil, móvil, tableta) dispone de su propio par de claves pública y privada, y una IP virtual.&lt;/li&gt;
&lt;li&gt;Los dispositivos conectados por WireGuard aterrizan en una subred que tiene acceso controlado a la LAN y a la gestión.&lt;/li&gt;
&lt;li&gt;Esto evitará abrir los servicios internos a Internet, y en su lugar sólo es necesaria una única ruta auditable: WireGuard → IPFire → servicios internos.&lt;/li&gt;
&lt;li&gt;WireGuard tiene funciones limitadas, pero por eso mismo su superficie de ataque es mucho menor que la de otras VPN.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esto debería permitir el control remoto completo de Home Assistant, las cámaras y las tareas de administración sin exponer esos servicios directamente a Internet.&lt;/p&gt;
&lt;h1 id="3-servidor-de-aplicaciones"&gt;3. Servidor de aplicaciones&lt;/h1&gt;
&lt;p&gt;El núcleo del homelab se ejecutará en &lt;a href="https://proxmox.com/en/"&gt;Proxmox VE&lt;/a&gt;, instalado en un único equipo de servidor con gestión remota adecuada y redundancia.&lt;/p&gt;
&lt;p&gt;Proxmox es un sistema operativo seguro y robusto basado en Linux que simplifica la ejecución de servicios virtualizados o en contenedores.
Esto proporciona una serie de beneficios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Código abierto&lt;/strong&gt;: Puede parecer obvio, pero evita la dependencia de un proveedor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mezcla de VMs y contenedores&lt;/strong&gt;: Ambos se pueden gestionar a través de la misma interfaz.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad a través del aislamiento&lt;/strong&gt;: Si un servicio se ve comprometido, el atacante no obtendría acceso a todo el sistema, sino solo a los datos y la red a la que tenga acceso.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portabilidad y pruebas&lt;/strong&gt;: Podría tener una instancia de Proxmox de preproducción, por ejemplo en mi ordenador de sobremesa, como banco de pruebas para nuevos servicios. Cuando estén listos, se pueden migrar a la instancia de Proxmox de producción.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actualizaciones más seguras&lt;/strong&gt;: Hacer una instantánea de las máquinas virtuales es una forma fácil de probar una actualización de software y revertirla si se encuentran problemas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copias de seguridad&lt;/strong&gt;: Además de las instantáneas de ZFS, Proxmox tiene su propio &lt;a href="https://proxmox.com/en/products/proxmox-backup-server/overview"&gt;servidor de copias de seguridad&lt;/a&gt; de código abierto, para respaldos incrementales y deduplicados.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disponibilidad y clústeres&lt;/strong&gt;: No lo voy a usar, porque lo considero excesivo para mi caso de uso, pero es posible tener redundancia y conmutación por error automática de las instancias de Proxmox.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="proxmox-y-consideraciones-sobre-licencias"&gt;Proxmox y consideraciones sobre licencias&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://proxmox.com/en/products/proxmox-virtual-environment/pricing"&gt;La licencia de Proxmox es &lt;strong&gt;por CPU instalada&lt;/strong&gt;&lt;/a&gt;.
Para reducir los costes recurrentes, estoy buscando servidores de una sola CPU, pero con suficientes núcleos para ejecutar todos los servicios previstos y tener margen para crecer en el futuro.&lt;/p&gt;
&lt;p&gt;Las CPU modernas ya alcanzan los cientos de núcleos en un sólo zócalo, lo que es más que suficiente para un homelab.&lt;/p&gt;
&lt;h2 id="opciones-de-hardware-para-el-servidor"&gt;Opciones de hardware para el servidor&lt;/h2&gt;
&lt;p&gt;El formato objetivo es un servidor en rack reacondicionado, con estas características:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU de bajo consumo con al menos 24 núcleos, idealmente 32 o más.&lt;/li&gt;
&lt;li&gt;Al menos 128 GB de RAM ECC. ECC es fundamental en este servidor para evitar la corrupción silenciosa de datos. La RAM DDR5 sería ideal, pero con la &lt;a href="https://en.wikipedia.org/wiki/2024%E2%80%932026_global_memory_supply_shortage"&gt;actual escasez de memoria&lt;/a&gt; me conformaré con DDR4.&lt;/li&gt;
&lt;li&gt;Espacio en el chasis para al menos 8 HDD, idealmente 10 o 12 para tener margen de crecimiento si es necesario.&lt;/li&gt;
&lt;li&gt;Soporte para doble almacenamiento NVMe, o como mínimo SSD.&lt;/li&gt;
&lt;li&gt;Al menos una tarjeta de red SFP+ de 10 GbE.&lt;/li&gt;
&lt;li&gt;Fuentes de alimentación dobles redundantes.&lt;/li&gt;
&lt;li&gt;Gestión remota compatible con IPMI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La serie Dell PowerEdge tiene una amplia gama de modelos y son fáciles de encontrar reacondicionados, así que son mi punto de partida:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Basados en Intel&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://i.dell.com/sites/doccontent/shared-content/data-sheets/en/Documents/Dell-PowerEdge-R730-Spec-Sheet.pdf"&gt;R730&lt;/a&gt;&lt;/strong&gt;: Antiguo pero muy común y barato. Doble zócalo para Intel Xeon, hasta 3 TB de DDR4, 10 GbE a través de una NDC (&lt;em&gt;Network Daughter Card&lt;/em&gt;) opcional, &lt;strong&gt;sin NVMe&lt;/strong&gt;, gestión por iDRAC (todos los PowerEdge).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://www.dell.com/content/dam/digitalassets/active/en/unauth/data-sheets/products/servers/Dell_EMC_PowerEdge_R740_Spec_Sheet.pdf"&gt;R740&lt;/a&gt; / &lt;a href="https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/dell-emc-poweredge-r750-spec-sheet.pdf"&gt;R750&lt;/a&gt;&lt;/strong&gt;: Generaciones más nuevas con CPUs más eficientes, más núcleos, NVMe, mejor PCIe (Gen 3/4 según el modelo), mejor iDRAC...&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basados en AMD&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://i.dell.com/sites/csdocuments/Shared-Content_data-Sheets_Documents/en/poweredge-r7415-spec-sheet-en.pdf"&gt;&lt;strong&gt;R7415&lt;/strong&gt;&lt;/a&gt;: &lt;strong&gt;AMD EPYC de un solo zócalo (1ª generación)&lt;/strong&gt;; excelente densidad de núcleos y ancho de banda de memoria con un menor consumo y coste que los componentes equivalentes de Intel, y diseñados como plataformas de un solo zócalo.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://i.dell.com/sites/csdocuments/product_docs/en/poweredge-r7515-spec-sheet.pdf"&gt;&lt;strong&gt;R7515&lt;/strong&gt;&lt;/a&gt;: &lt;strong&gt;AMD EPYC de 2ª y 3ª generación&lt;/strong&gt; de un solo zócalo, hasta 64 núcleos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R7525&lt;/strong&gt;: Como el R7515 pero con doble zócalo. No es un factor decisivo porque Proxmox solo cobra por zócalo de CPU &lt;strong&gt;en uso&lt;/strong&gt;, por lo que puede ser una opción si el precio es justo.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Las plataformas &lt;strong&gt;AMD EPYC de un solo zócalo&lt;/strong&gt; parecen particularmente atractivas para Proxmox porque:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ofrecen un mayor número de núcleos que Intel y más ancho de banda de memoria en un solo socket, evitando sobrecostes en la licencia.&lt;/li&gt;
&lt;li&gt;Las plataformas EPYC son conocidas por su gran rendimiento en virtualización y su alta densidad de E/S.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/self_hosted_home_1/dell_poweredge_r7515.webp" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/self_hosted_home_1/dell_poweredge_r7515.webp" title="Haz clic para ver a pantalla completa"&gt;&lt;figcaption&gt;Dell PowerEdge R7515&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Un Dell PowerEdge R7415 o R7515 parece la mejor opción, pero lo compararé con ofertas similares de otros proveedores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.supermicro.com/en/products/system/datasheet/as-2014s-tr"&gt;Supermicro AS-2014S-TR&lt;/a&gt;: muy similar, pero tiene más líneas PCIe y más bahías de disco con espacio para 12 HDD, por lo que permitiría la expansión del NAS si fuera necesario.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.hpe.com/psnow/doc/a00073549enw"&gt;HPE ProLiant DL385 Gen10 Plus&lt;/a&gt;: Doble socket; hasta 28 unidades de disco. Excesivo.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lenovopress.lenovo.com/lp1269-thinksystem-sr665-server"&gt;Lenovo ThinkSystem SR665&lt;/a&gt;: Admite CPUs AMD EPYC de un solo socket con el sufijo "P". Según se informa, son difíciles de encontrar reacondicionados, y los de un solo socket aún más, por lo que no es mi primera opción.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Una cosa que me hace reacio a elegir HPE es que no ofrecen descargas gratuitas de actualizaciones de BIOS o firmware sin un contrato de soporte, que es prohibitivamente caro para un usuario doméstico.
Ya me pasó con mi actual HPE Microserver Gen10, así que probablemente me decante por Dell o Supermicro para el próximo, ya que tienen descargas gratuitas sin contrato.&lt;/p&gt;
&lt;p&gt;Mi elección final dependerá de la disponibilidad local y el precio de las unidades reacondicionadas, pero espero conseguir uno de esos fantásticos Dell PowerEdge con un AMD EPYC.&lt;/p&gt;
&lt;h2 id="redundancia-y-fiabilidad"&gt;Redundancia y fiabilidad&lt;/h2&gt;
&lt;p&gt;El servidor Proxmox se trata como el &lt;strong&gt;nodo de infraestructura principal&lt;/strong&gt;, no es un juguete.
Aunque mi casa inteligente está diseñada para que siga funcionando incluso si Home Assistant se cae, dado que todos mis servicios personales se ejecutarán en este hardware, es de suma importancia que se mantenga activo y en buen estado.&lt;/p&gt;
&lt;p&gt;Con esto en mente, estos son los pasos que tomaré para garantizar la disponibilidad del servidor de aplicaciones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fuentes de alimentación dobles&lt;/strong&gt;: Conectadas a enchufes separados, para que un fallo en una sola fuente de alimentación o en un circuito eléctrico no tumbe el servidor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAI&lt;/strong&gt;: Conectado al menos a un SAI, para energía de emergencia y apagado controlado. Más detalles en la sección &lt;em&gt;Electricidad y resiliencia&lt;/em&gt; más abajo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grupo de arranque / VMs&lt;/strong&gt;: &lt;strong&gt;2 unidades NVMe&lt;/strong&gt; en un grupo ZFS espejado (coloquialmente llamado "RAID1 en ZFS"). Esto proporciona redundancia para el host de Proxmox, los servicios y la configuración, de modo que si una unidad falla, el sistema seguirá funcionando (y la unidad defectuosa podría intercambiarse en caliente por una nueva y ZFS reconstruiría el grupo automáticamente).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Puertos de red dobles&lt;/strong&gt;: Usar al menos dos tarjetas de red: una para gestión; otra para el tráfico de las VMs y el almacenamiento. Podría también agregar enlaces de red para tener conexiones redundantes, lo que significaría 2 para gestión y/o 2 para tráfico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gestión remota&lt;/strong&gt;: &lt;a href="https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface"&gt;IPMI&lt;/a&gt;, iDRAC, iLO o equivalente para permitir la gestión fuera de banda, el acceso a la BIOS y el control remoto de la alimentación.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consola física para acceso de emergencia&lt;/strong&gt;: tendré una consola barata (monitor y teclado) a mano para conectarme directamente al servidor si fuese necesario.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="4-almacenamiento"&gt;4. Almacenamiento&lt;/h1&gt;
&lt;p&gt;Tengo grandes necesidades de almacenamiento, así que desde el principio planteo montar un buen &lt;a href="https://es.wikipedia.org/wiki/Almacenamiento_conectado_en_red"&gt;NAS&lt;/a&gt;, con velocidad, redundancia y fiabilidad.&lt;/p&gt;
&lt;h2 id="por-que-no-synology"&gt;¿Por qué no Synology?&lt;/h2&gt;
&lt;p&gt;Inicialmente consideré un NAS de &lt;a href="https://www.synology.com/es-es"&gt;Synology&lt;/a&gt;.
En particular, el &lt;a href="https://www.synology.com/es-es/products/RS1221+]"&gt;RS1221+&lt;/a&gt; parecía ser exactamente lo que estaba buscando.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/self_hosted_home_1/synology_rs1221+.webp" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/self_hosted_home_1/synology_rs1221+.webp" title="Haz clic para ver a pantalla completa"&gt;&lt;figcaption&gt;Synology RS1221+&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Los productos de Synology son populares por una buena razón: son fáciles de gestionar y tienen un ecosistema muy rico.
Sin embargo, sus recientes políticas y decisiones empresariales los han hecho menos atractivos, y para mí en particular son factores decisivos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Una postura cada vez más &lt;a href="https://www.theverge.com/news/652364/synology-nas-third-party-hard-drive-restrictions"&gt;restrictiva sobre los discos de otras marcas&lt;/a&gt;, con modelos que:&lt;ul&gt;
&lt;li&gt;Advierten, se quejan o degradan la experiencia de usuario cuando se usan discos que no son de Synology.&lt;/li&gt;
&lt;li&gt;En algunos casos, limitan el soporte o la funcionalidad según el proveedor del disco.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cambios progresivos para dificultar el cambiar de proveedor, algo que va en contra de la filosofía &lt;strong&gt;abierta y basada en estándares&lt;/strong&gt; de este homelab.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dadas estas preocupaciones, tomé la decisión de &lt;strong&gt;evitar Synology&lt;/strong&gt; y en su lugar usar plataformas abiertas y hardware genérico sobre el que tendría control total.&lt;/p&gt;
&lt;h2 id="truenas"&gt;TrueNAS&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.truenas.com/"&gt;TrueNAS&lt;/a&gt; es probablemente el sistema operativo de código abierto más conocido y especializado para NAS.
Es robusto, que es la característica número uno que uno quiere para su almacenamiento.&lt;/p&gt;
&lt;p&gt;Solo funciona con ZFS, que es lo que quiero usar para mi capa de datos, y su interfaz de usuario simplifica la gestión, la monitorización y las alertas.
Por mucho que me guste la tecnología y meterme en fregaos, ya voy a tener bastante trabajo montándolo todo, así que agradezco todo lo que me facilite la vida sin comprometer los principios.&lt;/p&gt;
&lt;p&gt;Consideré &lt;a href="https://www.openmediavault.org/"&gt;openmediavault (OMV)&lt;/a&gt; porque es un competidor directo, también de código abierto y basado en Linux.
Lo descarté porque es más flexible (por ejemplo ZFS es opcional y no de serie), y está pensado para gente que busca ejecutar aplicaciones en su NAS, pero yo no lo necesito porque tendré un servidor de aplicaciones separado para ese propósito.&lt;/p&gt;
&lt;p&gt;Así que empecé a considerar montar un servidor separado para TrueNAS.
Dadas mis necesidades de almacenamiento me haría falta otra máquina potente (al menos 64 GB de RAM, y otra tarjeta de red de 10 GbE).
Como todo en la vida, tiene sus pros y sus contras:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pros:&lt;ul&gt;
&lt;li&gt;Clara separación de responsabilidades: almacenamiento en una máquina, computación en otra.&lt;/li&gt;
&lt;li&gt;Buen aislamiento de fallos: si el nodo de computación falla, el almacenamiento sigue funcionando.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Contras:&lt;ul&gt;
&lt;li&gt;Mayor &lt;strong&gt;coste inicial&lt;/strong&gt;: otro chasis de servidor, más fuentes de alimentación, más tarjetas de red.&lt;/li&gt;
&lt;li&gt;Mayor &lt;strong&gt;consumo eléctrico en el día a día&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Para un homelab familiar parece excesivo.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Había dos alternativas sobre las que había leído:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Proxmox tiene soporte para ZFS, por lo que podría hacer también de NAS.&lt;ul&gt;
&lt;li&gt;Configuración &lt;strong&gt;manual de ZFS&lt;/strong&gt;, una cosa más que aprender.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sin monitorización ni alertas&lt;/strong&gt; de serie.&lt;/li&gt;
&lt;li&gt;Acceso directo al hardware, &lt;strong&gt;sin pérdida de rendimiento por virtualización&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ejecutar una &lt;strong&gt;máquina virtual de TrueNAS&lt;/strong&gt; dentro de Proxmox:&lt;ul&gt;
&lt;li&gt;TrueNAS se ejecuta como una máquina virtual con &lt;strong&gt;acceso directo al conjunto de discos duros&lt;/strong&gt;, pasando directamente el &lt;a href="https://es.wikipedia.org/wiki/Adaptador_de_host"&gt;HBA&lt;/a&gt; en PCIe al que estarán conectados.&lt;/li&gt;
&lt;li&gt;Los discos duros están &lt;strong&gt;dedicados a TrueNAS&lt;/strong&gt;; Proxmox no los toca.&lt;/li&gt;
&lt;li&gt;ZFS se utiliza en la capa de TrueNAS para redundancia, instantáneas y replicación.&lt;/li&gt;
&lt;li&gt;La pérdida de rendimiento es &amp;lt;5% según comentarios de usuarios.&lt;/li&gt;
&lt;li&gt;TrueNAS disfrutará de la &lt;strong&gt;alta disponibilidad&lt;/strong&gt; del servidor de aplicaciones (doble fuente de alimentación, doble NVMe, redes redundantes...)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tras analizar las opciones con sus pros y sus contras, mi decisión es optar por TrueNAS como máquina virtual, me parece el mejor compromiso en cuanto a mantenibilidad, reducción del número de equipos, coste inicial, consumo eléctrico y fiabilidad del sistema.
Dejar que Proxmox gestione los discos duros debería funcionar, pero perdería más tiempo y me quedaría sin la monitorización y las alertas integradas de TrueNAS.&lt;/p&gt;
&lt;h2 id="diseno-del-almacenamiento"&gt;Diseño del almacenamiento&lt;/h2&gt;
&lt;p&gt;A grandes rasgos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IPFire (rápido)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Registros de red (accesos al cortafuegos, informes del IDS, conexiones VPN, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pool de Proxmox (muy rápido, redundante)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Dos SSD NVMe en espejo ZFS para:&lt;ul&gt;
&lt;li&gt;El host de Proxmox.&lt;/li&gt;
&lt;li&gt;Máquinas virtuales y contenedores.&lt;/li&gt;
&lt;li&gt;Configuración, bases de datos y metadatos.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;El sistema seguirá funcionando si falla un disco.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pool de TrueNAS (lento, redundante)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Múltiples discos duros en un pool ZFS.&lt;/li&gt;
&lt;li&gt;Inicialmente planeo 8 discos duros en RAIDZ2, idealmente con espacio para ampliar hasta 12.&lt;/li&gt;
&lt;li&gt;Tolerancia al fallo de hasta 2 discos simultáneamente.&lt;/li&gt;
&lt;li&gt;Almacena:&lt;ul&gt;
&lt;li&gt;Las fotografías de Immich.&lt;/li&gt;
&lt;li&gt;Los vídeos de Jellyfin.&lt;/li&gt;
&lt;li&gt;La música de Music Assistant.&lt;/li&gt;
&lt;li&gt;Los datos de Nextcloud.&lt;/li&gt;
&lt;li&gt;Los documentos de Paperless.&lt;/li&gt;
&lt;li&gt;Las grabaciones de las cámaras.&lt;/li&gt;
&lt;li&gt;Las copias de seguridad de Proxmox.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instantáneas y replicación&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;TrueNAS tomará &lt;strong&gt;instantáneas periódicas de los datos&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Las instantáneas de datos críticos (Nextcloud, Immich, Paperless, algunas grabaciones de Frigate) se replicarán a una &lt;strong&gt;copia de seguridad remota&lt;/strong&gt; (fuera de casa).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esta combinación proporciona tanto &lt;strong&gt;redundancia local&lt;/strong&gt; como &lt;strong&gt;copias de seguridad externas&lt;/strong&gt;, lo cual es esencial: RAID/ZFS &lt;em&gt;no&lt;/em&gt; es una copia de seguridad.&lt;/p&gt;
&lt;h1 id="5-camaras-y-timbre"&gt;5. Cámaras y timbre&lt;/h1&gt;
&lt;p&gt;Las cámaras son una parte integral de este homelab, pero también una importante &lt;strong&gt;preocupación de seguridad y privacidad&lt;/strong&gt;.
Quiero tener cámaras tanto fuera como dentro de la casa, pero tengo que asegurarme por completo de que &lt;strong&gt;nadie esté espiando&lt;/strong&gt; a través de ellas.
También quiero tener un videotimbre que suene en mi smartphone aunque esté fuera de casa, pero tiene que funcionar sin conexión a la nube.&lt;/p&gt;
&lt;p&gt;Primero haré que un instalador pase cables Ethernet por las paredes para las cámaras, de modo que estarán conectadas y alimentadas a través de PoE.
Hay una amplia gama de cámaras y timbres PoE disponibles.&lt;/p&gt;
&lt;h2 id="plan-inicial-unifi-o-reolink"&gt;Plan inicial: ¿UniFi o Reolink?&lt;/h2&gt;
&lt;p&gt;Al principio dudaba si optar por cámaras UniFi, porque son de buena calidad (aunque caras), su software y aplicaciones son de lo mejor y ya estoy planeando entrar en su ecosistema con el switch y los puntos de acceso.
Sin embargo, Ubiquiti ha intentado en el pasado &lt;a href="https://www.reddit.com/r/Ubiquiti/comments/1cifnut/unifi_protect_now_requires_cloudremote_access_for/"&gt;obligar a usar una cuenta en su nube para UniFi Protect&lt;/a&gt;, así que no puedo confiar en que sus sistemas sigan funcionando de forma exclusivamente local, que es uno de mis objetivos declarados.&lt;/p&gt;
&lt;p&gt;Habiendo descartado las cámaras UniFi, mi otra opción era &lt;a href="https://reolink.com/"&gt;Reolink&lt;/a&gt; por su buena relación calidad-precio y su perfecta &lt;a href="https://www.home-assistant.io/integrations/reolink/"&gt;integración con Home Assistant&lt;/a&gt;.
El &lt;a href="https://reolink.com/product/rln16-410/"&gt;Reolink RLN16-410&lt;/a&gt; es un NVR con 16 puertos PoE, por lo que las cámaras se conectarían y grabarían directamente en él.
Eso simplificaría la configuración y se integraría con Home Assistant.&lt;/p&gt;
&lt;p&gt;Sin embargo, después de investigar un poco, acabé descubriendo que Reolink &lt;strong&gt;no tiene soporte para IPv6&lt;/strong&gt; en absoluto, y no hay planes para añadirlo.
La falta total de soporte para IPv6 es especialmente problemática en una red donde IPv6 es un ciudadano de primera clase.
Con el tiempo, los servicios y los proveedores de internet dependerán más de IPv6, y mantener un ecosistema de cámaras solo con IPv4 crea fricciones y deuda técnica innecesarias.
Sin ningún tipo de promesa de que IPv6 será soportado en el futuro, me arriesgaría a tener que reemplazar todas las cámaras y el NVR en algún momento, así que volví a buscar alternativas.&lt;/p&gt;
&lt;h2 id="frigate-y-camaras-dahua"&gt;Frigate y cámaras Dahua&lt;/h2&gt;
&lt;p&gt;Mi solución final es dejar de depender de sistemas cerrados y en su lugar tener el excelente &lt;a href="https://frigate.video/"&gt;Frigate NVR&lt;/a&gt; gestionando las cámaras directamente.
Frigate se ejecutará como una máquina virtual en el servidor Proxmox.&lt;/p&gt;
&lt;p&gt;Buscando un fabricante de cámaras que respete los estándares y tenga soporte para IPv6, encontré &lt;a href="https://www.dahuasecurity.com/"&gt;Dahua&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Soporte de estándares&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;ONVIF para descubrimiento y control básico.&lt;/li&gt;
&lt;li&gt;RTSP para los flujos de vídeo y audio.&lt;/li&gt;
&lt;li&gt;Soporte de IPv6.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Capacidades de &lt;strong&gt;audio bidireccional&lt;/strong&gt; que respetan los estándares.&lt;/li&gt;
&lt;li&gt;Un amplio ecosistema y documentación de la comunidad de &lt;em&gt;self-hosting&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instalaré:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;12 cámaras Dahua&lt;/strong&gt;, una mezcla de interiores y exteriores.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1 videoportero Dahua&lt;/strong&gt; con audio bidireccional.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todas las cámaras y el timbre estarán conectados mediante cableado &lt;strong&gt;Cat6a con PoE&lt;/strong&gt;, asegurando una buena señal y haciendo que sean más difíciles de manipular e interferir.&lt;/p&gt;
&lt;p&gt;Frigate almacenará las grabaciones en TrueNAS a través de NFS o SMB, por lo que se beneficiarán automáticamente de la redundancia de sus discos duros, las instantáneas de ZFS y las rutinas de copia de seguridad.&lt;/p&gt;
&lt;h2 id="diseno-de-red-para-las-camaras"&gt;Diseño de red para las cámaras&lt;/h2&gt;
&lt;p&gt;Todas las cámaras se conectarán a un switch Ethernet dedicado en el rack de capa 2 con PoE.
Este switch se conectará a un único puerto del switch UniFi, de esa manera éste podrá etiquetar todas las cámaras en una &lt;strong&gt;VLAN dedicada&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Recibirán direcciones solo en la &lt;strong&gt;VLAN de cámaras&lt;/strong&gt; (tanto IPv4 como IPv6).&lt;/li&gt;
&lt;li&gt;El cortafuegos IPFire &lt;strong&gt;bloqueará cualquier salida a Internet&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Solo se podrá acceder a ellas desde:&lt;ul&gt;
&lt;li&gt;Frigate (para la ingesta de vídeo y la comunicación bidireccional con el videoportero).&lt;/li&gt;
&lt;li&gt;Un conjunto muy pequeño de sistemas de administración (para configuración).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La principal desventaja de este enfoque es que el switch secundario solo podrá usarse para conectar las cámaras, porque será el router UniFi el que gestione la VLAN, pero debería ser lo suficientemente barato como para que tener algunos puertos sin usar no sea un problema.&lt;/p&gt;
&lt;h2 id="deteccion-de-objetos"&gt;Detección de objetos&lt;/h2&gt;
&lt;p&gt;Frigate NVR tiene detección de objetos integrada:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frigate se ejecuta como una máquina virtual en Proxmox.&lt;/li&gt;
&lt;li&gt;Recibe los flujos RTSP de las cámaras Dahua y el videoportero.&lt;/li&gt;
&lt;li&gt;Realiza &lt;strong&gt;detección de movimiento y de objetos&lt;/strong&gt; (personas, coches, etc.).&lt;/li&gt;
&lt;li&gt;Gestiona la grabación basada en eventos, las instantáneas y las políticas de retención.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para una detección de objetos eficiente, Frigate soporta &lt;a href="https://docs.frigate.video/configuration/object_detectors"&gt;aceleración hardware&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Se conectará un &lt;a href="https://docs.frigate.video/configuration/object_detectors/#edge-tpu-detector"&gt;Coral TPU&lt;/a&gt; por USB o PCIe al servidor Proxmox.&lt;/li&gt;
&lt;li&gt;Proxmox pasará el dispositivo a la máquina virtual de Frigate.&lt;/li&gt;
&lt;li&gt;Como dispositivo de hardware especializado, el TPU tiene un bajo consumo de energía pero es capaz de procesar múltiples flujos de vídeo en tiempo real.&lt;/li&gt;
&lt;li&gt;Es mucho más eficiente y rentable que usar una tarjeta gráfica para un despliegue de esta escala.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="videoportero"&gt;Videoportero&lt;/h2&gt;
&lt;p&gt;El videoportero puede verse como una cámara normal, pero además quiero poder hablar con quien llame a la puerta.
Frigate puede &lt;a href="https://docs.frigate.video/configuration/live/#two-way-talk"&gt;gestionar el audio bidireccional&lt;/a&gt; con el timbre Dahua, permitiendo una comunicación exclusivamente local y que preserva la privacidad, sin ninguna nube del fabricante.&lt;/p&gt;
&lt;p&gt;Recuerda que todas las cámaras solo serán accesibles a través de la red local, pero cuando esté fuera mis dispositivos se conectarán a través de la VPN de Wireguard y operarían como si estuvieran en casa.&lt;/p&gt;
&lt;p&gt;Cuando alguien llame al timbre se activará un evento en Home Assistant, y ahí podré realizar todo tipo de acciones, desde enviar una notificación a nuestros teléfonos, hacer sonar un timbre en casa si estamos presentes, reproducir un mensaje grabado o incluso hacer que una IA converse con el visitante.&lt;/p&gt;
&lt;h1 id="6-electricidad-y-resiliencia"&gt;6. Electricidad y resiliencia&lt;/h1&gt;
&lt;p&gt;La planificación de la alimentación eléctrica tiene dos partes separadas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reducir el consumo de energía, tanto en el día a día normal como en caso de emergencia (p. ej. en un apagón).&lt;/li&gt;
&lt;li&gt;Asegurar los datos y la degradación gradual de los servicios en caso de pérdida de energía.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="sobre-el-consumo-de-energia"&gt;Sobre el consumo de energía&lt;/h2&gt;
&lt;p&gt;La casa tendrá una instalación solar completa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Un montón de paneles solares que deberían cubrir las necesidades de la casa la mayoría de los días del año.&lt;/li&gt;
&lt;li&gt;Suficientes baterías para cubrir el consumo de la casa la mayoría de las noches del año.&lt;/li&gt;
&lt;li&gt;La red eléctrica probablemente solo será necesaria en días nublados de invierno o días de consumo excepcional. No planeo ser completamente independiente de la red eléctrica.&lt;/li&gt;
&lt;li&gt;Un circuito de emergencia permitirá que las baterías y los paneles proporcionen electricidad en caso de un apagón de la red.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Con este sistema instalado, el rack debería estar correctamente alimentado en la mayoría de situaciones.
Sin embargo, me puedo imaginar algunos casos en los que podría quedarse corto:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Varios días nublados en invierno podrían agotar las baterías por completo. No sabré el rendimiento con seguridad hasta que nos mudemos y empiece a recopilar estadísticas. Esto significa que no puedo depender solo de las baterías solares para alimentar el rack.&lt;/li&gt;
&lt;li&gt;Un apagón prolongado de la red (como &lt;a href="https://es.wikipedia.org/wiki/Apag%C3%B3n_en_la_pen%C3%ADnsula_ib%C3%A9rica_de_2025"&gt;el de 2025&lt;/a&gt;) podría ser problemático, especialmente en invierno.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Como protección contra estos casos extremos, mi plan es instalar un SAI en el rack y programar los servicios para esa eventualidad.&lt;/p&gt;
&lt;h2 id="emergencia-de-alimentacion-sai-y-estrategia-de-apagado"&gt;Emergencia de alimentación: SAI y estrategia de apagado&lt;/h2&gt;
&lt;p&gt;Los componentes críticos estarán conectados a un &lt;a href="https://es.wikipedia.org/wiki/Sistema_de_alimentaci%C3%B3n_ininterrumpida"&gt;SAI&lt;/a&gt; (Sistema de Alimentación Ininterrumpida):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El router IPFire.&lt;/li&gt;
&lt;li&gt;El switch principal UniFi.&lt;/li&gt;
&lt;li&gt;El switch PoE de las cámaras.&lt;/li&gt;
&lt;li&gt;El servidor Proxmox.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tengo dos objetivos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mantener &lt;strong&gt;la red + cámaras + servicios principales&lt;/strong&gt; funcionando el mayor tiempo posible durante un corte de luz.&lt;/li&gt;
&lt;li&gt;Asegurar que el &lt;strong&gt;servidor Proxmox se apague limpiamente&lt;/strong&gt; antes de que la batería del SAI se agote, para mantener los datos a salvo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El flujo de apagado sería algo así:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;El SAI se comunica por USB o SNMP con un agente de monitorización (ya sea en Proxmox o en un pequeño dispositivo auxiliar).&lt;/li&gt;
&lt;li&gt;Al activarse la batería del SAI (y lo mismo si hay un apagón de la red y el sistema depende de las baterías solares) se activa un modo de ahorro de energía en todos los sistemas y se me notifica.&lt;/li&gt;
&lt;li&gt;Al alcanzar un umbral de batería baja:&lt;ul&gt;
&lt;li&gt;Proxmox inicia el apagado ordenado de las VM no críticas por orden de prioridad (p. ej., Jellyfin, Music Assistant, Immich y Nextcloud primero).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Al alcanzar un umbral de batería crítico:&lt;ul&gt;
&lt;li&gt;Proxmox apaga todas las VM para evitar la pérdida de datos.&lt;/li&gt;
&lt;li&gt;El host de Proxmox se apaga.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;La red y las cámaras permanecen activas mientras el SAI pueda mantenerlas.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Esto protege la integridad de los datos mientras se mantiene cierto nivel de vigilancia y capacidad de resolución de problemas remota.&lt;/p&gt;
&lt;h1 id="7-copias-de-seguridad-y-recuperacion"&gt;7. Copias de seguridad y recuperación&lt;/h1&gt;
&lt;p&gt;La redundancia no es una copia de seguridad.
Tiene que haber una copia externa de los datos críticos en caso catástrofe en la casa:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copias de seguridad regulares de las &lt;strong&gt;VM de Proxmox&lt;/strong&gt; (configuración + imágenes de disco) al NAS.&lt;/li&gt;
&lt;li&gt;Replicación de las copias de seguridad de Proxmox a una ubicación externa.&lt;/li&gt;
&lt;li&gt;Copia de seguridad de los &lt;strong&gt;conjuntos de datos críticos&lt;/strong&gt; del NAS a la ubicación externa:&lt;ul&gt;
&lt;li&gt;Colección de fotos (Immich), con la base de datos de metadatos.&lt;/li&gt;
&lt;li&gt;Archivos y datos de Nextcloud (calendario, contactos...), y su base de datos.&lt;/li&gt;
&lt;li&gt;Documentos de Paperless y su base de datos.&lt;/li&gt;
&lt;li&gt;Clips de eventos de Frigate (al menos metadatos y grabaciones críticas, p. ej., detecciones de objetos o alertas).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Las &lt;strong&gt;pruebas de restauración&lt;/strong&gt; periódicas formarán parte del modelo operativo: una copia de seguridad solo es tan buena como la última prueba de restauración exitosa.
El plan es probar la restauración de al menos una VM y un conjunto de datos periódicamente para asegurar que las herramientas y los procedimientos realmente funcionan.&lt;/p&gt;
&lt;h1 id="8-operaciones-monitorizacion-y-practicas-de-seguridad"&gt;8. Operaciones, monitorización y prácticas de seguridad&lt;/h1&gt;
&lt;p&gt;La facilidad de mantenimiento es una preocupación clave para mis sistemas.
Disfruto trasteando con todo esto, es un hobby, pero una vez todo esté montado y configurado mi expectativa es que funcione sin problemas durante meses.
Esa es la experiencia con &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;mi homelab actual&lt;/a&gt;, y lo que busco con el nuevo.&lt;/p&gt;
&lt;p&gt;Las prácticas operativas que planeo para mantener el sistema:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Monitorización&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Comprobaciones básicas de disponibilidad para los servicios principales, por ejemplo con &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt;, con notificaciones en tiempo real.&lt;/li&gt;
&lt;li&gt;Monitorización de recursos de CPU/RAM/disco/red, con alertas para estar al tanto de posibles cuellos de botella.&lt;/li&gt;
&lt;li&gt;Alertas de SMART y de &lt;em&gt;scrub&lt;/em&gt; de ZFS para el almacenamiento.&lt;/li&gt;
&lt;li&gt;Alertas de intrusión de IPFire.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estrategia de actualización&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Solo versiones estables.&lt;/li&gt;
&lt;li&gt;Tomar instantáneas de Proxmox y de las VM antes de actualizaciones críticas.&lt;/li&gt;
&lt;li&gt;Actualizar primero los servicios menos críticos, luego los componentes principales.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Cortafuegos con política de &lt;strong&gt;denegación por defecto&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;VLANs para IoT y cámaras &lt;strong&gt;sin salida a Internet&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El menor número posible de servicios expuestos a Internet, y esos con &lt;strong&gt;mínimos permisos y acceso a datos&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solo VPN&lt;/strong&gt; para el acceso remoto a Home Assistant, Frigate y cualquier otro servicio de la casa que no esté expuesto a Internet.&lt;/li&gt;
&lt;li&gt;Mínima superficie de ataque en IPFire y Proxmox:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Solo puertos necesarios&lt;/strong&gt; abiertos.&lt;/li&gt;
&lt;li&gt;Administración solo a través de &lt;strong&gt;dispositivos de confianza&lt;/strong&gt; (ordenadores de sobremesa y portátiles).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claves y contraseñas robustas&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MFA&lt;/strong&gt; (autenticación de múltiples factores) donde sea compatible.&lt;/li&gt;
&lt;li&gt;Software &lt;strong&gt;actualizado&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="9-proximos-pasos"&gt;9. Próximos pasos&lt;/h1&gt;
&lt;p&gt;Este artículo recoge el &lt;strong&gt;diseño objetivo&lt;/strong&gt; al que he llegado después de meses de investigación.
La implementación se llevará a cabo por etapas, que sin duda acabarán solapándose:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Adquirir el servidor para el router/cortafuegos&lt;/strong&gt;, instalar IPFire y comenzar la configuración básica de enrutamiento/cortafuegos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adquirir el servidor de aplicaciones&lt;/strong&gt; e instalar Proxmox VE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adquirir e instalar el rack, los paneles de parcheo, los switches&lt;/strong&gt; una vez que la sala de servidores dedicada de la casa esté lista.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instalar los servidores y conectarlo todo&lt;/strong&gt;; conectar todos los cables de red; conectar los dispositivos al SAI para la alimentación; arrancar los servidores.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adquirir e instalar los discos duros&lt;/strong&gt; en el servidor de aplicaciones; configurar la &lt;strong&gt;VM de TrueNAS&lt;/strong&gt; en Proxmox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configurar la monitorización, el registro de eventos y las alertas&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poner en marcha Home Assistant&lt;/strong&gt;, integrar los dispositivos inteligentes principales y empezar a definir automatizaciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desplegar Frigate&lt;/strong&gt; e integrar todas las cámaras Dahua y el timbre.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desplegar Nextcloud, Immich, Jellyfin, Music Assistant, Paperless&lt;/strong&gt;, apuntándolos al almacenamiento de TrueNAS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Servicios de apoyo&lt;/strong&gt; para monitorización, registro de eventos, alertas, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adquirir y configurar el SAI&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cada uno de estos pasos generará su propio conjunto de notas, fragmentos de configuración y reflexiones del tipo "ojalá hubiera sabido esto antes".
Por eso este artículo está etiquetado como "Parte 1", porque tengo la intención de seguir documentando cómo este homelab evoluciona del diseño a la realidad.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/paginas/suscribirse"&gt;Suscríbete&lt;/a&gt; para estar al tanto de los nuevos artículos.&lt;/p&gt;
&lt;p&gt;¿Tienes alguna idea, sugerencia o comentario? ¡Déjame un comentario abajo!&lt;/p&gt;</content><category term="Software"/><category term="free-software"/><category term="linux"/><category term="self-hosting"/></entry><entry><title>Respaldo automático periódico de los paquetes instalados en el SO</title><link href="https://blog.fidelramos.net/es/software/backup-installed-packages" rel="alternate"/><published>2025-12-08T00:00:00+00:00</published><updated>2025-12-08T11:08:49.765848+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2025-12-08:/es/software/backup-installed-packages</id><summary type="html">&lt;p&gt;Hace un tiempo &lt;a href="https://github.com/haplo/dotfiles/commit/0f7aedfa2ee71ca4a5d709d2f2a21c57a83be264"&gt;me pasé a yadm&lt;/a&gt; para gestionar mis dotfiles.
Una de las características que más me gustan es su &lt;a href="https://yadm.io/docs/bootstrap"&gt;soporte para el arranque (bootstrap)&lt;/a&gt;.
He estado añadiendo operaciones de configuración idempotentes.&lt;/p&gt;
&lt;p&gt;Mi última incorporación es un script que crea archivos de copia de seguridad con todos los paquetes …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hace un tiempo &lt;a href="https://github.com/haplo/dotfiles/commit/0f7aedfa2ee71ca4a5d709d2f2a21c57a83be264"&gt;me pasé a yadm&lt;/a&gt; para gestionar mis dotfiles.
Una de las características que más me gustan es su &lt;a href="https://yadm.io/docs/bootstrap"&gt;soporte para el arranque (bootstrap)&lt;/a&gt;.
He estado añadiendo operaciones de configuración idempotentes.&lt;/p&gt;
&lt;p&gt;Mi última incorporación es un script que crea archivos de copia de seguridad con todos los paquetes instalados en el sistema operativo.
En caso de desastre, si tengo que reinstalar el sistema operativo, solo tengo que pasarle al gestor de paquetes la última lista respaldada para ese host.
Para la copia de seguridad de los datos estoy trabajando en una configuración aparte, ese será tema para un futuro artículo.&lt;/p&gt;
&lt;p&gt;En cuanto al &lt;em&gt;script&lt;/em&gt;, es sencillo y está adaptado a mis sistemas.
Solo es compatible con APT (uso Debian en mis servidores sin interfaz gráfica) y &lt;em&gt;pacman&lt;/em&gt; (uso Arch Linux en mi ordenador de sobremesa), pero añadir DNF u otros gestores de paquetes debería ser bastante fácil.
Lo escribí en fish shell, porque ahora es &lt;a href="https://blog.fidelramos.net/software/fish-shell"&gt;mi shell preferida&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Puedes ver el &lt;a href="https://github.com/haplo/dotfiles/commit/3d9c8e0f545a9f7636148cce050dbd88c447ea29"&gt;commit en mis dotfiles&lt;/a&gt; para ver todos los cambios, pero los reproduciré a continuación tal y como estaban en el momento de escribir este artículo.&lt;/p&gt;
&lt;p&gt;El script principal lo he puesto en &lt;em&gt;~/.local/bin/backup_installed_packages_list.fish&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env fish&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; get_hostname
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; -q hostname
        hostname
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; -q hostnamectl
        hostnamectl --static
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;test&lt;/span&gt; -f /etc/hostname
        &lt;span class="nb"&gt;read&lt;/span&gt; -l host_from_file &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; /etc/hostname
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$host_from_file&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; string trim
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: cannot reliably get hostname"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;2
        &lt;span class="k"&gt;exit&lt;/span&gt; 2
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;set&lt;/span&gt; current_hostname &lt;span class="o"&gt;(&lt;/span&gt;get_hostname&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; current_date &lt;span class="o"&gt;(&lt;/span&gt;date +%Y-%m-%d&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; backup_dir &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Backups/&lt;/span&gt;&lt;span class="nv"&gt;$current_hostname&lt;/span&gt;&lt;span class="s2"&gt;/packages"&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; temp_file &lt;span class="o"&gt;(&lt;/span&gt;mktemp&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; save_if_changed -a input_file -a suffix
    &lt;span class="k"&gt;set&lt;/span&gt; -l target_file &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup_dir&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$current_date$suffix&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt;

    &lt;span class="c"&gt;# regex matches YYYY-MM-DD followed optionally by suffix, ending in .txt&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; -l pattern
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;test&lt;/span&gt; -z &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$suffix&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; pattern &lt;span class="s1"&gt;'^\d{4}-\d{2}-\d{2}\.txt$'&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; pattern &lt;span class="s2"&gt;"^\d{4}-\d{2}-\d{2}&lt;/span&gt;&lt;span class="nv"&gt;$suffix&lt;/span&gt;&lt;span class="s2"&gt;\.txt\$"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c"&gt;# find the most recent backup file that matches the specific pattern&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; -l latest_backup_name &lt;span class="o"&gt;(&lt;/span&gt;ls -1 &lt;span class="nv"&gt;$backup_dir&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;|&lt;/span&gt; string match -r &lt;span class="nv"&gt;$pattern&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; sort &lt;span class="o"&gt;|&lt;/span&gt; tail -n 1&lt;span class="o"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;test&lt;/span&gt; -n &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$latest_backup_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;set&lt;/span&gt; -l latest_backup_path &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backup_dir&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$latest_backup_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; cmp -s &lt;span class="nv"&gt;$input_file&lt;/span&gt; &lt;span class="nv"&gt;$latest_backup_path&lt;/span&gt;
            &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No changes in package list compared to &lt;/span&gt;&lt;span class="nv"&gt;$latest_backup_path&lt;/span&gt;&lt;span class="s2"&gt;. Skipping."&lt;/span&gt;
            rm &lt;span class="nv"&gt;$input_file&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c"&gt;# if we reach here, either no backup exists or content is different&lt;/span&gt;
    mv &lt;span class="nv"&gt;$input_file&lt;/span&gt; &lt;span class="nv"&gt;$target_file&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installed packages backup saved: &lt;/span&gt;&lt;span class="nv"&gt;$target_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    notify-send &lt;span class="se"&gt;\&lt;/span&gt;
        --urgency&lt;span class="o"&gt;=&lt;/span&gt;normal &lt;span class="se"&gt;\&lt;/span&gt;
        --wait &lt;span class="se"&gt;\&lt;/span&gt;
        --icon&lt;span class="o"&gt;=&lt;/span&gt;backup &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"New backup of installed OS packages: &lt;/span&gt;&lt;span class="nv"&gt;$target_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

mkdir -p &lt;span class="nv"&gt;$backup_dir&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; -q apt
    &lt;span class="c"&gt;# https://www.debian.org/doc/manuals/debian-reference/ch10.en.html#_backup_and_recovery_policy&lt;/span&gt;
    dpkg --get-selections &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$temp_file&lt;/span&gt;
    save_if_changed &lt;span class="nv"&gt;$temp_file&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; -q pacman
    &lt;span class="c"&gt;# https://wiki.archlinux.org/title/Migrate_installation_to_new_hardware#List_of_installed_packages&lt;/span&gt;
    pacman -Qqen &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$temp_file&lt;/span&gt;
    save_if_changed &lt;span class="nv"&gt;$temp_file&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    pacman -Qqem &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$temp_file&lt;/span&gt;
    save_if_changed &lt;span class="nv"&gt;$temp_file&lt;/span&gt; &lt;span class="s2"&gt;"_aur"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: Neither apt nor pacman found."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt;2
    rm &lt;span class="nv"&gt;$temp_file&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt; 1
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Creé la función de ayuda &lt;code&gt;get_hostname&lt;/code&gt; porque mi instalación de Arch Linux no tenía disponible el comando &lt;code&gt;hostname&lt;/code&gt;, solo &lt;code&gt;hostnamectl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;notify-send&lt;/code&gt; es muy útil para recibir notificaciones de escritorio, lo he estado usando con frecuencia en mis nuevos &lt;em&gt;scripts&lt;/em&gt;, sobre todo en los que se ejecutan en segundo plano como este.
Ten en cuenta que requiere tener instalado &lt;em&gt;libnotify&lt;/em&gt; (&lt;em&gt;libnotify-bin&lt;/em&gt; en instalaciones basadas en Debian).
Yo lo instalo como un paquete común en mi configuración de arranque de yadm.&lt;/p&gt;
&lt;p&gt;Luego defino las unidades de servicio y temporizador de systemd, en &lt;em&gt;~/.config/systemd/user/backup-installed-packages.service&lt;/em&gt; y &lt;em&gt;~/.config/systemd/user/backup-installed-packages.timer&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Backup list of installed OS packages&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;graphical-session.target&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%h/.local/bin/backup_installed_packages_list.fish&lt;/span&gt;
&lt;span class="na"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;span class="na"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;journal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Timer to backup list of installed OS packages&lt;/span&gt;

&lt;span class="k"&gt;[Timer]&lt;/span&gt;
&lt;span class="na"&gt;OnCalendar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;
&lt;span class="na"&gt;Persistent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Y después activa el temporizador:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;reload&lt;/span&gt;
&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;systemctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;backup&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Activo el temporizador de systemd en mi archivo de &lt;a href="https://github.com/haplo/dotfiles/blob/master/.config/yadm/bootstrap"&gt;arranque de yadm&lt;/a&gt;.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="backup"/><category term="fish"/><category term="free-software"/><category term="howto"/><category term="linux"/><category term="shell"/></entry><entry><title>Educación responsable en informática</title><link href="https://blog.fidelramos.net/es/software/computing-education-1" rel="alternate"/><published>2025-11-09T00:00:00+00:00</published><updated>2025-11-10T10:26:36.211650+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2025-11-09:/es/software/computing-education-1</id><summary type="html">&lt;p&gt;Mi hija mayor va a cumplir pronto 7 años y le he prometido un regalo: su primer ordenador.
Secretamente espero que desarrolle un interés por la informática similar al mío.
No porque quiera reproducir mi infancia en ella (aunque, por supuesto, me gustaría compartir algunos de mis pasatiempos y pasiones …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Mi hija mayor va a cumplir pronto 7 años y le he prometido un regalo: su primer ordenador.
Secretamente espero que desarrolle un interés por la informática similar al mío.
No porque quiera reproducir mi infancia en ella (aunque, por supuesto, me gustaría compartir algunos de mis pasatiempos y pasiones, como cualquier padre), sino porque creo que es importante aprender cómo funcionan las computadoras y el software: eso es la alfabetización digital.&lt;/p&gt;
&lt;p&gt;Piensa en cuán digitales son nuestras vidas hoy en día.
El gobierno, la banca, las compras, los juegos, las relaciones sociales, el trabajo, las películas, los programas, los videos, la música, los ligues... todo sucede cada vez más en la red.
&lt;a href="https://es.wikipedia.org/wiki/Privacidad_digital"&gt;La privacidad digital&lt;/a&gt; puede ser tan importante como la privacidad física, y &lt;a href="https://digitalprivacy.ieee.org/publications/topics/what-is-digital-privacy-and-its-importance/"&gt;deberías cuidarla&lt;/a&gt;, pero eso es difícil si no entiendes cómo funcionan los sistemas o qué alternativas existen.
Además, existen amenazas en línea como estafas, &lt;em&gt;phishing&lt;/em&gt;, virus, troyanos, &lt;em&gt;malware&lt;/em&gt;, &lt;em&gt;adware&lt;/em&gt;, &lt;em&gt;spyware&lt;/em&gt;, &lt;em&gt;ransomware&lt;/em&gt;, vulnerabilidades de seguridad y &lt;em&gt;rootkits&lt;/em&gt;. También está el robo de identidad, la ingeniería social, las filtraciones de datos y la necesidad de herramientas como gestores de contraseñas, cortafuegos y cifrado.&lt;/p&gt;
&lt;p&gt;Cuando yo era joven, los ordenadores eran el futuro, pero para mis hijos son el presente.
¿Cómo será su futuro?
Mi bola de cristal está en el taller, pero identifico algunas tecnologías clave que probablemente darán nueva forma a nuestras sociedades:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;La IA en sus diferentes formas.&lt;/li&gt;
&lt;li&gt;La robótica.&lt;/li&gt;
&lt;li&gt;Las criptomonedas.&lt;/li&gt;
&lt;li&gt;La impresión 3D.&lt;/li&gt;
&lt;li&gt;La Realidad Aumentada y la Realidad Virtual.&lt;/li&gt;
&lt;li&gt;La energía solar/eólica, la autosuficiencia energética.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Creo que, como padres, tenemos que preparar a nuestros hijos para el mundo en el que viven y probablemente vivirán, no para el mundo en el que crecimos nosotros.
Por eso investigaré, planificaré y desarrollaré formas responsables de exponer a mis hijos a estas y otras tecnologías.
Considero que la informática es la base de todo lo demás, al igual que aprender matemáticas es importante para muchas otras disciplinas, por lo que es mi primer paso en este viaje.&lt;/p&gt;
&lt;h1 id="un-poco-de-historia-personal"&gt;Un poco de historia personal&lt;/h1&gt;
&lt;p&gt;Cuando tenía 4 años (casi 5), pedí a los &lt;a href="https://es.wikipedia.org/wiki/Reyes_Magos"&gt;Reyes Magos&lt;/a&gt; una &lt;a href="https://es.wikipedia.org/wiki/Nintendo_Entertainment_System"&gt;consola Nintendo NES&lt;/a&gt;, porque esos videojuegos me parecían chulísimos.
Sin embargo, los Reyes Magos tenían otros planes para mí y, en su lugar, me trajeron mi primer ordenador, un &lt;a href="https://es.wikipedia.org/wiki/Sinclair_ZX_Spectrum"&gt;ZX Sinclair Spectrum+&lt;/a&gt;.
Al principio me sentí un poco decepcionado cuando abrí mi regalo y vi que no era la consola que había pedido, sino un bloque negro con un teclado de teclas cuadradas.
Mi decepción se disipó rápidamente y se convirtió en éxtasis en cuanto mi padre lo conectó al televisor y se cargó el primer juego.
Me quedé &lt;strong&gt;enganchado&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/computing_education/fidel_and_spectrum.jpg" title="Click para tamaño completo"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/computing_education/fidel_and_spectrum.jpg" title="Click para tamaño completo"&gt;&lt;figcaption&gt;Fidel jugando con su ZX Spectrum+&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Jugué a todos los juegos que podía conseguir.
Todavía puedo oír el &lt;a href="https://www.youtube.com/watch?v=MtBoRp_cSxQ"&gt;sonido de los juegos cargándose desde el reproductor de casetes&lt;/a&gt;.
Algunos de mis juegos favoritos de aquella época eran:
&lt;a href="https://www.youtube.com/watch?v=FCvgGGe71X4"&gt;Manic Miner&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=9Hy29KoBnGY"&gt;Jet Pac&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=-A0F2yCI8eA"&gt;Sacred Armor of Antiriad&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=lzdv_fOhZUg"&gt;Pipe Mania&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=T0_A7WIFX1k"&gt;Gaunglet&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=A5hM7Cl2TAI"&gt;Arkanoid&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=gdsVwwmv4N4"&gt;Chuckie Egg&lt;/a&gt; y &lt;a href="https://www.youtube.com/watch?v=nW-0rOIudzU"&gt;Thanathos&lt;/a&gt;, por nombrar algunos.
También fue una época dorada para los videojuegos españoles, empresas como Opera Soft, Topo Soft y Dinamic crearon joyas que fueron muy populares en España y en el extranjero.
Recuerdo haber jugado a &lt;a href="https://www.youtube.com/watch?v=pFod1VtzMQ0"&gt;La Abadía del Crimen&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=74HLnyQzuEc"&gt;Abu Simbel Profanation&lt;/a&gt; y &lt;a href="https://www.youtube.com/watch?v=JWkKQXh190I"&gt;Fernando Martín Basket Master&lt;/a&gt;, entre otros.
Estos juegos eran bien difíciles, lejos de la dopamina fácil de los juegos móviles modernos o de los que pagas para ganar.
Mejoraron mis reflejos y mi concentración, me iniciaron con el inglés y me enseñaron a ser perseverante y no rendirme.&lt;/p&gt;
&lt;p&gt;Pero al final los videojuegos solo fueron una puerta de entrada, y el verdadero destino que daría forma a mi vida fue la programación.
Mi padre siempre ha sido un amante de la ciencia ficción.
Había leído tantas novelas de ciencia ficción que sabía que los ordenadores iban a ser muy importantes en el futuro.
Se había comprado un par de libros para aprender &lt;a href="https://es.wikipedia.org/wiki/BASIC"&gt;BASIC&lt;/a&gt;, el lenguaje de programación compatible con el ZX Spectrum (aparte del ensamblador, por supuesto), junto con el ordenador.
Tenía pensado aprender él, pero en cuanto le vi teclear, me interesé, me enseñó cómo hacerlo y mi curiosidad hizo el resto.
Intenté copiar los programas de ejemplo, algunos de los cuales ocupaban muchas páginas, y fue toda una revelación ver que, de alguna manera, ese texto especial le decía al ordenador qué tenía que hacer.
Luego intenté hacer algunas modificaciones y me maravilló cómo cambiaba el comportamiento cuando ajustaba esta o aquella variable.
No escribí ningún programa complicado (ten en cuenta que sólo tenía entre 5 y 8 años), pero la semilla ya había germinado.&lt;/p&gt;
&lt;p&gt;Unos años más tarde, cuando quedó claro que me gustaba lo de los ordenadores, mi padre me compró mi primer PC IBM: un sensacional AMD 286 a 12 MHz con 1 MB de RAM, una unidad de disquete de 5¼ pulgadas y un disco duro de 40 MB (¡en una época en la que la mayoría de los ordenadores domésticos no tenían disco duro!).
Esto me abrió un mundo nuevo de posibilidades.&lt;br&gt;
Aprendí MS-DOS, mi primer sistema operativo.&lt;br&gt;
Aprendí sobre &lt;em&gt;AUTOEXEC.BAT&lt;/em&gt; y &lt;em&gt;CONFIG.SYS&lt;/em&gt; y cómo modificarlos para arañar esos preciosos KB de RAM disponible para ejecutar mis juegos.&lt;br&gt;
Aprendí a usar editores hexadecimales para hackear partidas guardadas y conseguir dinero o recursos extra.&lt;br&gt;
Un día conseguí una copia de Borland Turbo C y un curso de C, y aprendí C de forma autodidacta, y más tarde algo de ensamblador x86.
Mi mente de 12 años tuvo que esforzarse un poco con los condicionales, los bucles y, sobre todo, los punteros, pero al final lo entendí todo.
Ya podía llamarme programador.&lt;/p&gt;
&lt;p&gt;Con estos antecedentes, estudié Ingeniería Informática en la universidad y fui el mejor estudiante del campus.
Empecé a usar Linux en 1999 y, desde 2003, lo uso en exclusiva.
Conseguí un trabajo como programador en 2007, justo después de graduarme.
Y en 2012 conseguí un gran trabajo en Silicon Valley, sin duda el mayor salto en mi carrera.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/computing_education/fidel_and_pc.jpg" title="Click para tamaño completo"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/computing_education/fidel_and_pc.jpg" title="Click para tamaño completo"&gt;&lt;figcaption&gt;Fidel en su PC&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;No quiero terminar este viaje nostálgico sin recordar algunos de los muchos, muchos juegos a los que jugué en mi ordenador.
No voy a defender que todos los juegos sean obras de arte, pero creo que son una experiencia más positiva de lo que la mayoría de los adultos piensan.
Hoy en día existe la preocupación por el daño que las pantallas pueden causar a los niños, pero según mi experiencia, el problema no son las pantallas en sí, sino lo que se hace con ellas.
A menudo me han elogiado por mi buen nivel de inglés, que aprendí principalmente jugando a videojuegos con un diccionario siempre a mano.
También aprendí mucho de los juegos.
Cuando se es niño, se adquieren conocimientos y habilidades con facilidad, y la mayoría de los juegos ofrecen contenidos educativos, acertijos o mejoran los reflejos y la motricidad fina.&lt;/p&gt;
&lt;p&gt;En fin, aquí va una lista inevitablemente incompleta de mis juegos favoritos de la década de los 90:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Los juegos de aventura me enseñaron a resolver acertijos, el humor y a apreciar las narrativas, los guiones y la construcción de mundos. Títulos como Monkey Island 1 y 2, LOOM, Indiana Jones and the Fate of Atlantis, Day of the Tentacle, Sam and Max Hit the Road, Legend of Kyrandia 1 y 2, Simon the Sorcerer, Space Quest 1-6, King's Quest 1-6, Discworld, Full Throttle, The Dig y muchos otros.&lt;/li&gt;
&lt;li&gt;Me gustaban mucho los juegos de rol, porque también jugaba a muchos juegos de rol de mesa. La serie Eye of the Beholder, Dungeon Master 2, Ultima Underworld, Fallout 1 y 2, Anvil of Dawn, Wizardry VII, The Elder Scrolls, Dark Sun...&lt;/li&gt;
&lt;li&gt;Estrategia: Civilization I y II, Colonization, Sim City, Sim City 2000, Sim Ant, Sim Earth, Master of Orion 1 y 2, Master of Magic, Star Control 2, Dune 2, Stronghold, Warcraft, Starcraft, Age of Empires, UFO Enemy Unknown, X-COM Terror from the Deep, Command &amp;amp; Conquer, Theme Park, Theme Hospital, The Settlers, Heroes of Might &amp;amp; Magic, Mad TV...&lt;/li&gt;
&lt;li&gt;Puzzles: Gobliiins 2 y 3, Lemmings 1 y 2, The Incredible Machine, Chip's Challenge, Blockout, Tetris...&lt;/li&gt;
&lt;li&gt;Arcade: Pac-Man, Doom 2, Duke Nukem 3D, Golden Axe, Raptor, Tyrian, Arkanoid, Descent 1 y 2, Grand Theft Auto, Pinball Fantasies, Tomb Raider, One Must Fall 2097, Cannon Fodder 1 y 2...&lt;/li&gt;
&lt;li&gt;Plataformas: Bubble Bobble, Prince of Persia, Commander Keen, Rick Dangerous 2, Rayman...&lt;/li&gt;
&lt;li&gt;Carreras: Lotus Turbo Challenge, WipeOut, Carmageddon, Destruction Derby, IndyCar Racing, Test Drive...&lt;/li&gt;
&lt;li&gt;Deportes: Advantage Tennis, Skate or Die, Ski or Die, Summer Games, Winter Games, 4D Sports Boxing, NBA Jam, PC Futbol, FIFA 94-98, Sensible Soccer, NBA Live, NHL Hockey...&lt;/li&gt;
&lt;li&gt;Simuladores de vuelo: Microsoft Flight Simulator 4 y 5, F-19 Stealth Fighter, F-15 Strike Eagle II, X-Wing, Tie Fighter, Strike Commander, Privateer 1 y 2, Comanche Maximum Overkill, Secret Weapons of the Luftwaffe...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Echando la vista atrás, creo que la clave fue pasar tiempo delante del ordenador.
Si lo haces durante el tiempo suficiente, acabas aprendiendo informática.&lt;/p&gt;
&lt;p&gt;Es como &lt;a href="https://web.archive.org/web/20250210040613/https://www.reddit.com/r/loseit/comments/2u75am/terry_crews_gives_sage_advice_on_going_to_the_gym/"&gt;esta cita de Terry Crews&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ve al gimnasio, ni siquiera hagas ejercicio. Solo VE. Porque el hábito de ir al gimnasio es más importante que el ejercicio. Porque no importa lo que hagas. Puedes divertirte, pero mientras te diviertas, seguirás haciéndolo.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="un-ordenador-adecuado"&gt;Un ordenador adecuado&lt;/h1&gt;
&lt;p&gt;La herramienta más importante para aprender informática es, por supuesto, un ordenador.
Un ordenador &lt;em&gt;adecuado&lt;/em&gt;.
¿Qué quiero decir con adecuado?
En pocas palabras, es aquel que ofrece al usuario control absoluto:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instalar un sistema operativo diferente.&lt;/li&gt;
&lt;li&gt;Instalar cualquier software que se desee (siempre que sea compatible con el sistema operativo y el hardware, por supuesto).&lt;/li&gt;
&lt;li&gt;Reproducir cualquier tipo de archivo multimedia (otra vez, limitado sólo por el hardware).&lt;/li&gt;
&lt;li&gt;Programar software sin restricciones, acceder al hardware en su totalidad.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esta definición descarta todos los teléfonos móviles y tabletas populares, tanto Android como iOS.
No por las pantallas táctiles, fíjate que eso no está entre los requisitos.
La cuestión es &lt;strong&gt;ser amo y señor de tu propio dominio&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;iOS ha sido cerrado desde el principio.
Sus usuarios intercambian soberanía por comodidad.
Puedes instalar las aplicaciones que Apple te permite instalar, y punto.
Son dispositivos cómodos, con gran calidad, interfaces pulidas y una buena experiencia de usuario, eso no lo discuto, pero sus usuarios están a merced de Apple.
La programación de aplicaciones también está restringida, ya que se necesita un ordenador Mac para programar y una cuenta de desarrollador de Apple ($99 al año) si se quiere distribuir.&lt;/p&gt;
&lt;p&gt;Android comenzó como código abierto, con Linux como base, pero desde entonces ha sido corrompido por Google.
Prácticamente todos los dispositivos Android son ahora cerrados debido a Google Play Services, un componente del sistema con acceso root (es decir, sin restricciones) que, aunque no forma parte del sistema operativo Android básico, es una dependencia necesaria para que la mayoría de las aplicaciones Android funcionen.
Google también quiere &lt;a href="https://keepandroidopen.org/"&gt;obligar a los desarrolladores a registrarse&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Si aún no estás convencido, otros han argumentado mejor que yo por qué es importante tener control sobre nuestros dispositivos informáticos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/philosophy/right-to-read.es.html"&gt;https://www.gnu.org/philosophy/right-to-read.es.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medhir.com/blog/right-to-root-access"&gt;https://medhir.com/blog/right-to-root-access&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hackaday.com/2025/10/22/what-happened-to-running-what-you-wanted-on-your-own-machine/"&gt;https://hackaday.com/2025/10/22/what-happened-to-running-what-you-wanted-on-your-own-machine/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vale, vale, entonces nada de teléfonos móviles ni tabletas.
¿Y entonces qué?
¿Son seguros los ordenadores tradicionales?
En general sí, aunque también hay tendencias preocupantes en las que no quiero entrar ahora mismo.
Siguen siendo el baluarte de la libertad informática, y es fundamental que lo sigan siendo.&lt;/p&gt;
&lt;h1 id="opciones"&gt;Opciones&lt;/h1&gt;
&lt;p&gt;Hoy en día hay más opciones que nunca para comprar un ordenador, así que voy a ser muy específico con mi opinión.&lt;/p&gt;
&lt;p&gt;Como defensor del &lt;a href="https://www.fsf.org/es/recursos/que-es-el-software-libre"&gt;software libre&lt;/a&gt;, descarté en primer lugar todos los sistemas de código cerrado.
Eso significa que nada de Windows ni macOS.
Creo que es importante que los niños aprendan los &lt;a href="https://www.gnu.org/philosophy/free-sw.es.html"&gt;principios que subyacen al software libre&lt;/a&gt; y valoren no solo su ética, sino también su practicidad.
El uso de software libre nos permite controlar nuestras vidas digitales y, a medida que esas vidas se amplían, ese control se vuelve más importante.&lt;/p&gt;
&lt;p&gt;Alguna de las innumerables distribuciones de GNU/Linux servirá (más adelante hablaré de ello).
Por tanto mi primer requisito para el hardware es que sea perfectamente compatible con Linux.
Esto no es difícil hoy en día, hace 20 años era otro cantar.&lt;/p&gt;
&lt;p&gt;Otro punto para mí es que no quiero darles un portátil a mis hijos.
Prefiero un ordenador de sobremesa para que no intenten llevárselo a todas partes.
Esto hará que el tiempo que pasan frente al ordenador sea más intencional y más fácil de controlar.&lt;/p&gt;
&lt;p&gt;Para los ordenadores de sobremesa hay una &lt;a href="https://en.wikipedia.org/wiki/Desktop_computer#Types"&gt;amplia gama de opciones&lt;/a&gt;, desde torres de tamaño completo o reducido hasta mini PC y ordenadores con teclado.
Sabía que quería algo pequeño y asequible para empezar, alrededor de 200€.&lt;/p&gt;
&lt;p&gt;Mientras investigaba, el formato del ordenador dentro del teclado me hizo sentir nostalgia por mi ZX Spectrum+.
Me enamoré al instante de la &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-400-unit/"&gt;Raspberry Pi 400&lt;/a&gt;, pero deseaba un poco más de potencia.
Busqué alternativas similares y encontré la &lt;a href="http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/orange-pi-800.html"&gt;Orange Pi 800&lt;/a&gt;, la &lt;a href="https://pine64.org/devices/pinetab2/"&gt;PineTab2&lt;/a&gt; y la &lt;a href="https://shop.cutiepi.io/products/cutiepi-tablet"&gt;CutiePi&lt;/a&gt;, &lt;a href="https://junocomputers.com/us/product/juno-tab-2/"&gt;Juno Tab 2&lt;/a&gt; y &lt;a href="https://raspad.com/products/raspadv3"&gt;RaspPad 3&lt;/a&gt;, pero ninguna me convenció por diferentes motivos.
Entonces Raspberry Pi anunció la &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-500/"&gt;Pi 500&lt;/a&gt;, que parecía estupenda, salvo por la falta de compatibilidad con NVMe.
Decidí esperar y mi paciencia se vio recompensada: recientemente se ha lanzado la &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-500-plus/"&gt;Raspberry Pi 500+&lt;/a&gt;.
¡Tiene NVMe, 16 GB de RAM &lt;em&gt;y&lt;/em&gt; un teclado mecánico!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/computing_education/raspberry_pi_500+_vs_zx_spectrum+.jpg" title="Haga clic para ver la imagen a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/computing_education/raspberry_pi_500+_vs_zx_spectrum+.jpg" title="Haga clic para ver la imagen a pantalla completa"&gt;&lt;figcaption&gt;Raspberry Pi 500+ vs. ZX Spectrum+&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="proximos-pasos"&gt;Próximos pasos&lt;/h1&gt;
&lt;p&gt;¡Acabo de conseguir la Raspberry Pi 500+ y estoy muy ilusionado!
Estuve tentado de enseñársela a mi hija, pero voy a esperar hasta su cumpleaños.
Mientras tanto la voy a preparar para que tenga la mejor primera experiencia posible, con la esperanza de que se produzca la magia.&lt;/p&gt;
&lt;p&gt;La Raspberry Pi 500+ viene con &lt;a href="https://www.raspberrypi.com/software/operating-systems/"&gt;Raspbian OS&lt;/a&gt; preinstalado, que es una buena opción, pero tengo que evaluarlo para las necesidades de una niña de 7 años:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sin cuenta root (por ahora).&lt;/li&gt;
&lt;li&gt;Sin acceso a Internet y con un filtro muy estricto más adelante.&lt;/li&gt;
&lt;li&gt;Software educativo.&lt;/li&gt;
&lt;li&gt;Software creativo (pintura, música, vídeo, impresión...)&lt;/li&gt;
&lt;li&gt;Entornos de programación.&lt;/li&gt;
&lt;li&gt;Videojuegos apropiados para su edad.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Así que mi siguiente paso es investigar distribuciones GNU/Linux aptas para niños.
Espero que el requisito de compatibilidad con Raspberry Pi reduzca la búsqueda.
No creo que encuentre una distribución que se ajuste perfectamente a mis requisitos, pero solo quiero encontrar una buena configuración básica.&lt;/p&gt;
&lt;p&gt;Una de las conclusiones que he sacado de todo esto es que, dado lo entusiasmado que estoy con la idea de educar a los niños en informática, podría ser mi vocación.
Estoy seguro de que a otros padres les gustaría enseñar a sus hijos a utilizar el ordenador de forma responsable, pero quizá no tengan el tiempo o los recursos necesarios.
Tengo pensado documentar mis esfuerzos y crear recursos para ayudarles y guiarles; este artículo es el primer paso.&lt;/p&gt;
&lt;p&gt;¡&lt;a href="/es/paginas/suscribirse"&gt;Mantente atento&lt;/a&gt; a las novedades!&lt;/p&gt;</content><category term="Software"/><category term="educación"/><category term="software-libre"/><category term="linux"/></entry><entry><title>Comentarios autoalojados</title><link href="https://blog.fidelramos.net/es/software/self-hosted-comments" rel="alternate"/><published>2025-02-10T00:00:00+00:00</published><updated>2025-02-10T17:31:31.139057+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2025-02-10:/es/software/self-hosted-comments</id><summary type="html">&lt;p&gt;Hace unos días recibí un email de un lector y &lt;a href="https://zenodotus280.mataroa.blog/"&gt;compañero bloguero&lt;/a&gt;, para hacerme saber que seguía mi blog y también me dio un toque de atención sobre &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025#blog"&gt;mi resolución de Año Nuevo de escribir al menos un artículo cada mes de 2025&lt;/a&gt;, y que ya estaba empezando a incumplir …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hace unos días recibí un email de un lector y &lt;a href="https://zenodotus280.mataroa.blog/"&gt;compañero bloguero&lt;/a&gt;, para hacerme saber que seguía mi blog y también me dio un toque de atención sobre &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025#blog"&gt;mi resolución de Año Nuevo de escribir al menos un artículo cada mes de 2025&lt;/a&gt;, y que ya estaba empezando a incumplir.&lt;/p&gt;
&lt;p&gt;Me encanta recibir mensajes así, porque si una persona se toma la molestia de enviarme un email seguro que hay otras 99 que lo han pensado pero no lo han hecho.
Por eso tras recibir el email decidí dar prioridad a algo que tenía pendiente desde hacía tiempo: añadir comentarios al blog.
Y ojo, spoiler: &lt;strong&gt;objetivo conseguido&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Quería que fuese algo rápido, por lo que en vez de buscar la opción perfecta iba a optimizar por la que me tomase menos tiempo para implementarla.
Eso significaba empezar probando con las opciones ya integradas en el tema &lt;a href="https://github.com/haplo/reflex"&gt;Reflex&lt;/a&gt; que uso en el blog (heredero de &lt;a href="https://github.com/alexandrevicenzi/Flex"&gt;Flex&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://disqus.com/"&gt;Disqus&lt;/a&gt;: probablemente la plataforma más popular para comentarios en la web.
Es muy cómoda y potente, pero totalmente cerrada y muy seguramente espía y vende nuestros datos.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://isso-comments.de/"&gt;Isso&lt;/a&gt;: software autoalojado al estilo Disqus.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quienes me conocen pensarán que habría saltado de cabeza para Isso, pero sinceramente mi primer intento fue con Disqus, porque habría sido lo más rápido: abrir una cuenta, poner una clave en las opciones de Pelican y a correr.
Bueno pues mi gozo en un pozo porque resulta que ¡&lt;a href="https://disqus.com/pricing/"&gt;Disqus ya no tiene plan gratuito&lt;/a&gt;!
No me importa pagar por servicios que merezcan la pena, pero no por los comentarios en este humilde blog, así que me olvidé de Disqus y me puse a mirar los detalles de Isso.&lt;/p&gt;
&lt;p&gt;Lo que vi de Isso me gustó:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Autoalojado en mi propio dominio.&lt;/li&gt;
&lt;li&gt;Programado en Python: instalación sencilla en un virtualenv y me facilitaría colaborar con el proyecto llegado el caso.&lt;/li&gt;
&lt;li&gt;SQLite como base de datos, no hace falta un servidor MariaDB o Postgres.
Esta fue una de las principales razones por la que &lt;a href="https://blog.fidelramos.net/es/software/privacy-respecting-self-hosted-web-analytics"&gt;elegí Shynet para las analíticas del blog&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo único que no me gustó de Isso es que el desarrollo parece algo parado, hay varios PRs que llevan meses esperando merge.
Pero no parece ser un proyecto muerto, tengo la esperanza de que siga habiendo nuevas versiones al menos para mantenerlo al día en cuanto a compatibilidad y seguridad.&lt;/p&gt;
&lt;p&gt;Por cierto que acabé descubriendo &lt;a href="https://github.com/umputun/remark42/"&gt;Remark42&lt;/a&gt; como alternativa, después de haber desplegado Isso en mi servidor.
Pinta muy bien tanto en funcionalidades como en estado de desarrollo, pero personalmente habría seguido eligiendo Isso por estar ya integrado en el tema de mi blog.&lt;/p&gt;
&lt;h2 id="detalles-tecnicos"&gt;Detalles técnicos&lt;/h2&gt;
&lt;p&gt;Instalé Isso en el mismo servidor que alberga este blog, un humilde VPS de 2 GB de RAM.
No necesité más que seguir la &lt;a href="https://isso-comments.de/docs/reference/installation/#init-scripts"&gt;documentación oficial&lt;/a&gt;, pero por si alguien tiene interés dejo aquí el proceso:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Instalar &lt;em&gt;isso&lt;/em&gt; y un servidor WSGI (yo prefiero gunicorn):&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# create new user for isolation&lt;/span&gt;
adduser&lt;span class="w"&gt; &lt;/span&gt;--disabled-password&lt;span class="w"&gt; &lt;/span&gt;isso

&lt;span class="c1"&gt;# install isso&lt;/span&gt;
su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;isso
virtualenv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;venv/bin/activate
pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;isso&lt;span class="w"&gt; &lt;/span&gt;gunicorn
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Crear el fichero de configuración de &lt;em&gt;isso&lt;/em&gt; en &lt;em&gt;/home/isso/isso.cfg&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# https://isso-comments.de/docs/reference/server-config/&lt;/span&gt;

&lt;span class="k"&gt;[general]&lt;/span&gt;
&lt;span class="na"&gt;dbpath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/home/isso/comments.db&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://blog.fidelramos.net/&lt;/span&gt;
&lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;smtp&lt;/span&gt;

&lt;span class="k"&gt;[server]&lt;/span&gt;
&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:8081/&lt;/span&gt;

&lt;span class="k"&gt;[smtp]&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;25&lt;/span&gt;
&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;
&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;notification_email@example.com&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;isso@blog.fidelramos.net&lt;/span&gt;
&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="k"&gt;[hash]&lt;/span&gt;
&lt;span class="na"&gt;salt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Eech7co8Ohloopo9Ol6baimi&lt;/span&gt;
&lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;pbkdf2:10000:6:sha256&lt;/span&gt;

&lt;span class="k"&gt;[admin]&lt;/span&gt;
&lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;lt;PASSWORD&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Configurar el proxy inverso con Caddy añadiendo una sección en &lt;em&gt;/etc/caddy/Caddyfile&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;isso.fidelramos.net {
    reverse_proxy localhost:8081
    encode zstd gzip
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Añadir una unidad de servicio de &lt;em&gt;systemd&lt;/em&gt; en &lt;em&gt;/etc/systemd/system/isso.service&lt;/em&gt; para ejecutar &lt;em&gt;isso&lt;/em&gt; automáticamente. &lt;a href="https://isso-comments.de/docs/reference/installation/#init-scripts"&gt;La documentación de Isso incluye otros sistemas de init&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;isso commenting system&lt;/span&gt;
&lt;span class="na"&gt;Documentation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;man:isso(8)&lt;/span&gt;

&lt;span class="c1"&gt;# Require the filesystems containing the following directories to be mounted&lt;/span&gt;
&lt;span class="na"&gt;RequiresMountsFor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso&lt;/span&gt;

&lt;span class="na"&gt;ConditionPathIsDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso&lt;/span&gt;
&lt;span class="na"&gt;ConditionPathIsReadWrite&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso&lt;/span&gt;

&lt;span class="na"&gt;ConditionFileIsExecutable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso/venv/bin/gunicorn&lt;/span&gt;

&lt;span class="c1"&gt;# Start only if there is a conf file&lt;/span&gt;
&lt;span class="na"&gt;ConditionPathExistsGlob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso/isso.cfg&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;

&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ISSO_SETTINGS="/home/isso/isso.cfg"&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/isso/venv/bin/gunicorn -b localhost:8081 -w 4 --preload -n gunicorn-isso --log-file /home/isso/isso.log isso.run&lt;/span&gt;

&lt;span class="c1"&gt;#UMask=0007&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="na"&gt;TimeoutSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;isso&lt;/span&gt;

&lt;span class="na"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;16384&lt;/span&gt;
&lt;span class="na"&gt;LimitNPROC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;16384&lt;/span&gt;
&lt;span class="na"&gt;LimitLOCKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;16384&lt;/span&gt;

&lt;span class="c1"&gt;# ensures that the service process and all its children can never gain new&lt;/span&gt;
&lt;span class="c1"&gt;# privileges.&lt;/span&gt;
&lt;span class="na"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Añadir registros A y/o AAAA a tu DNS (&lt;em&gt;isso.fidelramos.net&lt;/em&gt; en mi caso) apuntando al servidor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Activar e iniciar &lt;em&gt;isso&lt;/em&gt;, y recargar &lt;em&gt;caddy&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;shell&lt;/span&gt;
&lt;span class="n"&gt;systemctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;isso&lt;/span&gt;
&lt;span class="n"&gt;systemctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;isso&lt;/span&gt;
&lt;span class="n"&gt;systemctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;caddy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ahora puedo acceder a &lt;a href="https://isso.fidelramos.net/admin"&gt;isso.fidelramos.net/admin&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Añadir la configuración necesaria en &lt;em&gt;pelicanconf.py&lt;/em&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ISSO_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"//isso.fidelramos.net"&lt;/span&gt;
&lt;span class="n"&gt;ISSO_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"vote"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Desplegar el blog. ¡Ya hay comentarios!&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="el-futuro"&gt;El futuro&lt;/h2&gt;
&lt;p&gt;Estoy deseoso de ver cuánto uso tendrán los comentarios.
Seguro que me animan a escribir más.&lt;/p&gt;
&lt;p&gt;Me preocupa el spam, porque Isso no tiene ningún sistema automático de filtrado.
No quiero anticiparme a resolver un problema antes de que se manifieste, pero veremos cuánto dura la paz...&lt;/p&gt;
&lt;p&gt;¿Llegaste a leer hasta aquí? ¡No puedes irte sin dejar un comentario!&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="blog"/><category term="free-software"/><category term="howto"/><category term="linux"/><category term="privacy"/><category term="self-hosting"/></entry><entry><title>Reemplazando Nginx con Caddy en Docker Compose</title><link href="https://blog.fidelramos.net/es/software/switch-nginx-caddy-docker-compose" rel="alternate"/><published>2023-11-09T00:00:00+00:00</published><updated>2023-11-09T15:11:39.941499+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2023-11-09:/es/software/switch-nginx-caddy-docker-compose</id><summary type="html">&lt;p&gt;El servidor principal de mi homelab ejecuta varios servicios, pero el corazón de todo es una configuración de &lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt;.
Varios servicios están expuestos a Internet a través de un servidor web de proxy inverso.&lt;/p&gt;
&lt;p&gt;Antes estaba usando tres imágenes de Docker para ejecutar Nginx, dirigir el tráfico a otros …&lt;/p&gt;</summary><content type="html">&lt;p&gt;El servidor principal de mi homelab ejecuta varios servicios, pero el corazón de todo es una configuración de &lt;a href="https://docs.docker.com/compose/"&gt;Docker Compose&lt;/a&gt;.
Varios servicios están expuestos a Internet a través de un servidor web de proxy inverso.&lt;/p&gt;
&lt;p&gt;Antes estaba usando tres imágenes de Docker para ejecutar Nginx, dirigir el tráfico a otros servicios y generar certificados Letsencrypt para los dominios necesarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/nginx"&gt;nginx&lt;/a&gt; con el servidor Nginx real en funcionamiento.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jwilder/nginx-proxy"&gt;nginx-proxy&lt;/a&gt; para generar configuraciones de Nginx.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jwilder/docker-letsencrypt-nginx-proxy-companion"&gt;docker-letsencrypt-nginx-proxy-companion&lt;/a&gt;: para automatizar la provisión y renovación de certificados LetsEncrypt.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;La parte relevante de la configuración en &lt;em&gt;compose.yml&lt;/em&gt; lucía así:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nginx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nginx&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;UFW_MANAGED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_WEB:-nginx}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${DOCKER_HTTP:-80}:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${DOCKER_HTTPS:-443}:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/html:/usr/share/nginx/html&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/certs:/etc/nginx/certs:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/htpasswd:/etc/nginx/htpasswd:ro&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_WEB_LOG_DRIVER:-json-file}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_WEB_LOG_MAX_SIZE:-4m}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_WEB_LOG_MAX_FILE:-10}&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nginx-gen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nginxproxy/docker-gen&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;-notify-sighup ${NGINX_WEB:-nginx} -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${DOCKER_GEN:-nginx-gen}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/html:/usr/share/nginx/html&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/certs:/etc/nginx/certs:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/htpasswd:/etc/nginx/htpasswd:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/docker.sock:/tmp/docker.sock:ro&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_GEN_LOG_DRIVER:-json-file}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_GEN_LOG_MAX_SIZE:-2m}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_GEN_LOG_MAX_FILE:-10}&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nginx-letsencrypt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nginxproxy/acme-companion&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${LETSENCRYPT_CONTAINER:-nginx-letsencrypt}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;always&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/conf.d:/etc/nginx/conf.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/vhost.d:/etc/nginx/vhost.d&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/html:/usr/share/nginx/html&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_FILES_PATH}/certs:/etc/nginx/certs:rw&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/opt/acme.sh:/etc/acme.sh&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;NGINX_DOCKER_GEN_CONTAINER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${DOCKER_GEN:-nginx-gen}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;NGINX_PROXY_CONTAINER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_WEB:-nginx}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_LETSENCRYPT_LOG_DRIVER:-json-file}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_LETSENCRYPT_LOG_MAX_SIZE:-2m}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;max-file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NGINX_LETSENCRYPT_LOG_MAX_FILE:-10}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Para dirigir tráfico a un servicio sólo necesitaba definir las variables de entorno &lt;code&gt;VIRTUAL_HOST&lt;/code&gt; y &lt;code&gt;LETSENCRYPT_HOST&lt;/code&gt;.
Por ejemplo, esta era la configuración de mi servicio de Nextcloud (solo mostraré las partes relevantes):&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nextcloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nextcloud:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nextcloud&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;VIRTUAL_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NEXTCLOUD_HOST}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;LETSENCRYPT_HOST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NEXTCLOUD_HOST}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;LETSENCRYPT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${LETSENCRYPT_EMAIL}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Además de la configuración de servicios en Docker Compose, también necesitaba una plantilla de configuración de Nginx (ese es el &lt;em&gt;nginx.tmpl&lt;/em&gt; que puedes ver en la configuración), y otros ficheros con ajustes específicos para algunos servicios (como por ejemplo aumentar el tamaño máximo de las peticiones HTTP para mi instancia de Nextcloud).&lt;/p&gt;
&lt;p&gt;Ya había reemplazado el servidor web Nginx que aloja este blog por Caddy, así que quería hacer lo mismo con el servidor casero.
Decidí usar &lt;a href="https://github.com/lucaslorentz/caddy-docker-proxy"&gt;caddy-docker-proxy&lt;/a&gt; porque era similar a &lt;em&gt;nginx-proxy&lt;/em&gt;, por lo que simplificaría la migración.
Lo que no me esperaba es cuán más simple se iba a volver.&lt;/p&gt;
&lt;p&gt;La configuración anterior fue reemplazada por:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;lucaslorentz/caddy-docker-proxy:ci-alpine&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;unless-stopped&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${DOCKER_HTTP:-80}:80"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"${DOCKER_HTTPS:-443}:443"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;networks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;default&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;webproxy&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Opciones globales&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy.email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_EMAIL}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;UFW_MANAGED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# este volumen es necesario para mantener los certificados&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# de lo contrario, se emitirán nuevos al reiniciar&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${CADDY_FILES_PATH}:/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Redirigir a un servicio ahora es solo cuestión de agregar algunas etiquetas:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;nextcloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nextcloud:latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nextcloud&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NEXTCLOUD_HOST}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy.reverse_proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"{{upstreams&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;80}}"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;caddy.request_body.max_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${NEXTCLOUD_MAX_UPLOAD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;¡Y listo!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://caddyserver.com/docs/automatic-https"&gt;Caddy gestiona automáticamente los certificados TLS&lt;/a&gt;, así que no es necesario ningún servicio adicional.
No puede ser más fácil.&lt;/p&gt;
&lt;p&gt;También pude eliminar los archivos de configuración de Nginx, en su lugar los reemplacé con un par de etiquetas en los contenedores que requerían alguna configuración especial.&lt;/p&gt;
&lt;p&gt;Antes de terminar este artículo, quiero destacar lo bien que &lt;em&gt;nginx-proxy&lt;/em&gt; y su &lt;em&gt;acme-companion&lt;/em&gt; habían estado funcionando durante &lt;strong&gt;años&lt;/strong&gt; con un mantenimiento mínimo.
Su estabilidad y compatibilidad hacia atrás fueron increíblemente sólidas, ¡mis felicidades y agradecimientos a su creador!&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="caddy"/><category term="docker"/><category term="homelab"/><category term="nginx"/></entry><entry><title>Analítica web en tu servidor y respetuosa con la privacidad</title><link href="https://blog.fidelramos.net/es/software/privacy-respecting-self-hosted-web-analytics" rel="alternate"/><published>2022-05-27T00:00:00+00:00</published><updated>2023-12-28T14:55:18.288821+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2022-05-27:/es/software/privacy-respecting-self-hosted-web-analytics</id><summary type="html">&lt;p&gt;Todo empezó con un anhelo: quería tener algunas analíticas básicas de este blog para saber
qué tal va, qué artículos son populares y de dónde viene el tráfico (los
&lt;em&gt;referrers&lt;/em&gt;). Pero hete aquí mi problema: soy un obseso de la privacidad, me preocupa
mucho el rastreo online masivo (sólo echa …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Todo empezó con un anhelo: quería tener algunas analíticas básicas de este blog para saber
qué tal va, qué artículos son populares y de dónde viene el tráfico (los
&lt;em&gt;referrers&lt;/em&gt;). Pero hete aquí mi problema: soy un obseso de la privacidad, me preocupa
mucho el rastreo online masivo (sólo echa un vistazo a las &lt;a href="https://blog.fidelramos.net/es/software/web-browser-addons"&gt;extensiones de navegador web
que utilizo&lt;/a&gt;), así que me negaba en rotundo
a utilizar un servicio que recopilase la información de mis visitantes.&lt;/p&gt;
&lt;p&gt;Sabía que había sistemas de analítica web de software libre. Conocía Piwik, que lleva
muchos años funcionando, pero nunca había tenido el tiempo ni la necesidad de mirarlos en
detalle. Hasta ahora.&lt;/p&gt;
&lt;p&gt;Ojo que este artículo no presenta en absoluto una lista completa, hay muchísimas más
opciones disponibles, recopiladas en &lt;a href="https://github.com/newTendermint/awesome-analytics"&gt;esta enorme
lista&lt;/a&gt;. Cada programa tiene sus pros y
sus contras, pero espero que este artículo te ayude a elegir y dar el salto hacia
analíticas más respetuosas con la privacidad de los internautas, o al menos a abandonar
Google Analytics y elegir una opción en la nube basada en software libre.&lt;/p&gt;
&lt;div class="toc"&gt;&lt;span class="toctitle"&gt;Índice de contenidos&lt;/span&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#mis-requisitos"&gt;Mis requisitos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#matomo-anteriormente-piwik"&gt;Matomo (anteriormente Piwik)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#plausible"&gt;Plausible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#shynet"&gt;Shynet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#open-web-analytics-owa"&gt;Open Web Analytics (OWA)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusiones-y-mi-eleccion"&gt;Conclusiones y mi elección&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h1 id="mis-requisitos"&gt;Mis requisitos&lt;/h1&gt;
&lt;p&gt;Mi búsqueda tenía unos parámetros específicos para cada proyecto:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Debe ser software libre y con posibilidad de alojarse en un servidor propio.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Despliegue sencillo, cuantos menos componentes mejor. SQLite si es posible, para no
  necesitar servidor de BD. A evitar Redis o Memcached. Estaría bien tener opción de
  despliegue sin Docker.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Uso razonable de recursos. Mis páginas no suelen recibir mucho tráfico, así que pienso
   reutilizar el servidor virtual de 2 GB de memoria que hospeda este blog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tecnología que pueda entender, manipular y colaborar. Yo prefiero proyectos en Python,
   pero eso ya depende de las capacidades de cada uno.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ligero y no demasiado intrusivo. Sólo necesito saber qué artículos son vistos y cuándo,
   además de saber de qué otras páginas vienen los usuarios. No necesito analíticas
   avanzadas.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mantenido activamente.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="matomo-anteriormente-piwik"&gt;Matomo (anteriormente Piwik)&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://matomo.org/"&gt;Página de inicio&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;GPLv3. Servicio en la nube (SAAS) con &lt;a href="https://matomo.org/matomo-on-premise/"&gt;opción auto-hospedada gratuita y
   libre&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://matomo.org/faq/on-premise/installing-matomo/"&gt;Documentación de instalación&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Necesita un servidor web (Apache, Nginx, etc.)&lt;/li&gt;
&lt;li&gt;Necesita MySQL.&lt;/li&gt;
&lt;li&gt;La configuración de Matomo se realiza en la propia aplicación.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;De los &lt;a href="https://matomo.org/faq/on-premise/matomo-requirements/"&gt;requisitos para alojar
   Matomo&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;100k páginas/mes: un servidor para aplicación y BD. 2 CPU, 2 GB RAM, 50GB disco
  SSD.&lt;/li&gt;
&lt;li&gt;~1M páginas/mes: un servidor para aplicación y BD. 4 CPU, 8 GB RAM, 250GB disco SSD.&lt;/li&gt;
&lt;li&gt;~10M páginas/mes: dos servidores para aplicación y BD. 8 CPUs, 16 GB RAM, 400GB
  disco SSD&lt;/li&gt;
&lt;li&gt;~100M páginas/mes: tres o más servidores para aplicación, BD y balanceador de
  carga. 16 CPUs, 32 GB RAM, 1 TB disco SSD.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Backend&lt;/em&gt; en PHP. &lt;em&gt;Frontend&lt;/em&gt; en VueJS y TypeScript. &lt;a href="https://github.com/matomo-org/matomo"&gt;Repositorio
   código&lt;/a&gt; y &lt;a href="https://developer.matomo.org/guides/contributing-to-piwik-core"&gt;guía para
   desarrolladores&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Amplia gama de &lt;a href="https://matomo.org/feature-overview/"&gt;métricas recopiladas&lt;/a&gt;. Dispone de
   &lt;a href="https://matomo.org/installing-matomo-for-wordpress/"&gt;plugin para Wordpress&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unos 10 commits por semana, múltiples &lt;a href="https://matomo.org/team/"&gt;contribuidores&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="plausible"&gt;Plausible&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://plausible.io/open-source-website-analytics"&gt;Página de inicio&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;AGPLv3. Servicio en la nube (SAAS) con &lt;a href="https://plausible.io/self-hosted-web-analytics"&gt;opción auto-hospedada gratuita y
   libre&lt;/a&gt;, pero esta última va con retraso
   en nuevas características (2 nuevas versiones cada año).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://plausible.io/docs/self-hosting#up-and-running"&gt;Documentación oficial de
   instalación&lt;/a&gt;. En resumen:
   necesita &lt;em&gt;docker-compose&lt;/em&gt;, PostgreSQL y Clickhouse.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;De la &lt;a href="https://plausible.io/docs/self-hosting#requirements"&gt;documentación de requisitos de
   Plausible&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The server must have a CPU with x86_64 architecture and support for SSE 4.2
instructions. We recommend using a minimum of 4GB of RAM but the requirements will
depend on your site traffic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Backend&lt;/em&gt; en Elixir/Phoenix. &lt;em&gt;Frontend&lt;/em&gt; en React y Tailwind. &lt;a href="https://github.com/plausible/analytics"&gt;Repositorio de
   código&lt;/a&gt; y &lt;a href="https://github.com/plausible/analytics/blob/master/CONTRIBUTING.md"&gt;guía para
   desarrolladores&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Script de rastreo de menos de 1 KiB. Sin cookies. No pude encontrar una lista concreta
   de métricas, pero parece menos completo que Matomo u OWA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Actividad semanal, normalmente condensada en &lt;em&gt;pull requests&lt;/em&gt; largos.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="shynet"&gt;Shynet&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://github.com/milesmcc/shynet"&gt;Página de inicio&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Apache License Version 2.0. Sólo permite hospedaje propio.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Despliegue sencillo con Docker o &lt;em&gt;docker-compose&lt;/em&gt;, de otra forma sería un despliegue
   manual indocumentado. Soporta cualquier BD que soporte Django, incluyendo
   SQLite. Servidor de caché opcional.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bajo consumo de recursos, en su forma más básica con SQLite sólo un proceso Python
   ejecutando Django, unos 100 MiB de RAM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Backend&lt;/em&gt; en Python/Django. Usa su propio &lt;a href="https://github.com/milesmcc/a17t"&gt;plugin de
   TailwindCSS&lt;/a&gt; para &lt;em&gt;frontend&lt;/em&gt;. Fácil de recrear desde
   el código fuente usando Docker. &lt;a href="https://github.com/milesmcc/shynet"&gt;Repositorio de
   código&lt;/a&gt; y &lt;a href="https://github.com/milesmcc/shynet/blob/master/CONTRIBUTING.md"&gt;guía para
   desarrolladores&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Script de rastreo de menos de 1 KiB. Sin cookies. Rastreo con pixel de 1x1 si
   javascript está desactivado. Lista bien definida de &lt;a href="https://github.com/milesmcc/shynet#metrics"&gt;métricas
   capturadas&lt;/a&gt;, todo bastante básico.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;No tiene un fuerte desarrollo de nuevas características, pero las dependencias se
   mantienen actualizadas y el desarrollador principal responde rápido a los problemas y
   &lt;em&gt;pull requests&lt;/em&gt; (lee las conclusiones para más detalles).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="open-web-analytics-owa"&gt;Open Web Analytics (OWA)&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://www.openwebanalytics.com/"&gt;Página de inicio&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;GPLv2. Sólo permite hospedaje propio.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;De la &lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Installation"&gt;documentación de instalación de
   OWA&lt;/a&gt; y los
   &lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Technical-Requirements"&gt;requisitos
   técnicos&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sólo soporta proyectos PHP.&lt;/li&gt;
&lt;li&gt;Sistema de instalación en la propia aplicación.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Technical-Requirements#web-server"&gt;Sólo soporta oficialmente Apache
  2.x&lt;/a&gt;. Nginx
  supuestamente funciona pero necesita configuración manual sin documentar.&lt;/li&gt;
&lt;li&gt;Necesita &lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Technical-Requirements#databases="&gt;MySQL &lt;em&gt;con strict mode
  desactivado&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Sus requisitos exactos no están claros.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;Backend&lt;/em&gt; en PHP. Webpack para &lt;em&gt;frontend&lt;/em&gt;. Múltiples repositorios: &lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Installation"&gt;repositorio
   base&lt;/a&gt;,
   &lt;a href="https://github.com/Open-Web-Analytics/owa-wordpress-plugin"&gt;repositorio del plugin de
   Wordpress&lt;/a&gt;, &lt;a href="https://github.com/Open-Web-Analytics/owa-php-sdk"&gt;repositorio
   del SDK para PHP&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tracking muy detallado, incluyendo mapas de calor y grabación de sesiones
   &lt;em&gt;Domstream&lt;/em&gt;, pero sólo para aplicaciones PHP. Dispone de &lt;a href="https://github.com/Open-Web-Analytics/Open-Web-Analytics/wiki/Integration-Plugins"&gt;plugins de integración
   dedicados&lt;/a&gt;
   para WordPress y MediaWiki.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Activamente mantenido, pero aparentemente por un único desarrollador.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="conclusiones-y-mi-eleccion"&gt;Conclusiones y mi elección&lt;/h1&gt;
&lt;p&gt;Teniendo en cuenta mi lista de requisitos la elección para este humilde blog estaba clara:
Shynet gana de calle, es justo lo que buscaba. Para páginas más grandes tanto Plausible
como Matomo parecen ser buenas opciones. Yo diría que el primero tiene mejor tecnología
fundacional (se liberó en 2019) pero Matomo lleva más tiempo funcionando (2007), está más
curtido y parece recopilar más datos. Plausible tiene &lt;a href="https://plausible.io/vs-matomo"&gt;una página comparándolo con
Matomo&lt;/a&gt;. OWA podría ser buena opción para páginas en PHP o
Wordpress.&lt;/p&gt;
&lt;p&gt;El poder trastear con Shynet, el punto 4 en mi lista de requisitos, me vino bien en cuanto
empecé a configurarlo en mi servidor porque &lt;a href="https://github.com/milesmcc/shynet/issues/208"&gt;el soporte de SQLite estaba
roto&lt;/a&gt;. Afortunadamente &lt;a href="https://github.com/milesmcc/shynet/pull/210"&gt;pude
arreglarlo&lt;/a&gt; y desplegarlo en el servidor en
el que estás leyendo este artículo. Por esto es que me encanta el software libre.&lt;/p&gt;
&lt;p&gt;Otro problema con el despliegue fue que el servidor no tenía Docker instalado, y quería
mantenerlo así, por lo que aproveché la oportunidad para aprender sobre
&lt;a href="https://podman.io/"&gt;Podman&lt;/a&gt;. Me encantó su capacidad para ejecutar contenedores sin usar
&lt;em&gt;root&lt;/em&gt; (&lt;em&gt;rootless containers&lt;/em&gt;), eso mejora muchísimo la seguridad. El único problema que
tuve era que necesitaba cambiar los permisos del volumen de datos, porque de otra forma el
proceso Django de Shynet no era capaz de escribir la base de datos SQLite porque los
ficheros del volumen pertenecían a &lt;em&gt;root&lt;/em&gt; dentro del contenedor. &lt;a href="https://www.tutorialworks.com/podman-rootless-volumes/"&gt;Este
artículo&lt;/a&gt; lo explica muy bien,
básicamente sólo hace falta usar &lt;code&gt;podman unshare&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Para terminar, he aquí una captura de pantalla de los datos de Shynet para este blog (¿no
es fabuloso poder compartir esto sabiendo que no se vulnera la privacidad de nadie?):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/privacy_respecting_self_hosted_web_analytics/shynet.png" title="&amp;quot;Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/privacy_respecting_self_hosted_web_analytics/shynet.png" title="Click to see full size"&gt;&lt;figcaption&gt;blog.fidelramos.net en mi instancia de Shynet&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;</content><category term="Software"/><category term="privacy"/><category term="web"/><category term="analytics"/><category term="self-hosting"/></entry><entry><title>Extensiones de navegador web que uso</title><link href="https://blog.fidelramos.net/es/software/web-browser-addons" rel="alternate"/><published>2022-02-23T00:00:00+00:00</published><updated>2023-04-20T00:00:00+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2022-02-23:/es/software/web-browser-addons</id><summary type="html">&lt;p&gt;Este artículo enumera las extensiones de navegador web que utilizo y por qué.
Lo mantendré actualizado de vez en cuando.&lt;/p&gt;
&lt;p&gt;Este artículo enumera las extensiones de navegador web que utilizo y por qué.
Lo mantendré actualizado de vez en cuando.&lt;/p&gt;
&lt;p&gt;Actualmente uso &lt;a href="https://brave.com/"&gt;Brave&lt;/a&gt; como mi navegador web principal, y es …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Este artículo enumera las extensiones de navegador web que utilizo y por qué.
Lo mantendré actualizado de vez en cuando.&lt;/p&gt;
&lt;p&gt;Este artículo enumera las extensiones de navegador web que utilizo y por qué.
Lo mantendré actualizado de vez en cuando.&lt;/p&gt;
&lt;p&gt;Actualmente uso &lt;a href="https://brave.com/"&gt;Brave&lt;/a&gt; como mi navegador web principal, y es el que recomiendo a todo el mundo.
Ofrece una gran experiencia por defecto, con características como un bloqueador de anuncios integrado, integración con &lt;a href="https://en.wikipedia.org/wiki/Tor_(network)"&gt;Tor&lt;/a&gt; incorporada, pestañas verticales y agrupadas, una cartera de criptomonedas decente, soporte para &lt;a href="https://en.wikipedia.org/wiki/InterPlanetary_File_System"&gt;IPFS&lt;/a&gt;, &lt;a href="https://talk.brave.com/"&gt;videollamadas&lt;/a&gt; integradas.
La experiencia lista para usar es mejor que las alternativas en mi opinión, especialmente para los usuarios habituales que no saben o no quieren trastear con su software.&lt;/p&gt;
&lt;p&gt;Anteriormente usaba &lt;a href="https://www.mozilla.org/en-US/firefox/new/"&gt;Firefox&lt;/a&gt;.
Aprecio que sea el único navegador importante con su propio motor de renderizado (Gecko) que no está basado en el hegemónico WebKit que es la base de prácticamente todos los demás navegadores web.
Como desarrollador web sé lo peligroso que es tener un monocultivo en torno a un único motor de navegador web, que es lo que sufrimos a finales de los 90 y principios de los 2000 con Internet Explorer.&lt;/p&gt;
&lt;p&gt;Intento tener cuidado con las extensiones de navegador que instalo, ya que tienen un acceso profundo a los datos del navegador.
Esto significa que un complemento malicioso o comprometido puede convertirse rápidamente en una pesadilla si consigue, por ejemplo, robar las credenciales de mi cuenta de correo electrónico (aunque el 2FA debería mitigar ese riesgo).
Por esta razón sopeso cuidadosamente la utilidad de los complementos frente a ese riesgo.
Los que utilizo suelen ser populares, con miles de usuarios y a menudo seleccionados por los editores.&lt;/p&gt;
&lt;p&gt;Sin más dilación, estos son los complementos que estoy usando actualmente en Brave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration"&gt;KeepassXC-Browser&lt;/a&gt;:
  Uso &lt;a href="https://keepassxc.org/"&gt;KeepassXC&lt;/a&gt; para almacenar mis contraseñas y secretos, esta
  es la extensión oficial para navegadores web, con autocompletado de
  formularios. Funciona bastante bien aunque no tanto como competidores como Lastpass o
  1Password, pero no les confiaría mis datos más íntimos a terceros.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;En Firefox solía usar estas extensiones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ublockorigin.com/"&gt;uBlock Origin&lt;/a&gt;: el mejor bloqueador de anuncios que existe
  (que yo sepa, si conoces alguno mejor por favor házmelo saber). Usa poco procesador y
  memoria, normalmente ahorra más que consume, dado que las páginas web cargan más rápido
  sin toda la mierda que suelen incluir (anuncios, rastreadores, etc.). Especialmente en
  móvil se puede notar mucho. Solía instalar otro puñado de extensiones para mejorar la
  privacidad (NoScript, ClearURLs, PrivacyBadger, HTTPSEverywhere y DecentralEyes), pero
  tras leer &lt;a href="https://github.com/arkenfox/user.js/wiki/4.1-Extensions"&gt;este artículo en
  Arkenfox&lt;/a&gt; aprendí que son
  superfluas si se configura bien uBlock Origin. Y déjame decirte una cosa más: mi Firefox
  &lt;em&gt;vuela&lt;/em&gt; después de eliminar esas extensiones.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/"&gt;Firefox Multi-Account
  Container&lt;/a&gt;:
  Firefox tiene soporte de serie para contenedores, esta extensión es necesaria para
  gestionarlos. Es oficial del propio equipo de Firefox, así que tiene mi plena
  confianza. ¿Qué son los contenedores? Son una forma de categorizar pestañas, para aislar
  la información a la que pueden acceder. Por ejemplo, configuro la web de mi banco para
  abrirse siempre en el contenedor &lt;em&gt;Banking&lt;/em&gt;, de forma que un potencial atacante que
  consiga ejecutar código que aproveche un agujero de seguridad en otra pestaña no debería
  tener acceso salvo que también hubiese sido abierta en el mismo contenedor
  &lt;em&gt;Banking&lt;/em&gt;. También uso contenedores para aislar el rastreo cuando inicio sesión en
  ciertos servicios, por ejemplo tengo un contenedor &lt;em&gt;Google&lt;/em&gt; donde inicio sesión en
  Google, pero en todos los demás contenedores estoy desconectado. Tengo un puñado de
  contenedores: &lt;em&gt;Gov&lt;/em&gt; para páginas del gobierno, &lt;em&gt;Personal&lt;/em&gt; para las aplicaciones web
  alojadas en mis propios servidores, &lt;em&gt;Shopping&lt;/em&gt; siempre que compro algo...&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/sidebery/"&gt;Sidebery&lt;/a&gt;: un gestor de
  pestañas para Firefox. Si eres como yo y tienes no ya docenas, sino cientos de pestañas
  abiertas, esta extensión lo cambia todo. Tener tantas pestañas abiertas se convierte en
  una ventaja en vez de un inconveniente. Las pestañas se organizan jerárquicamente como
  un árbol; se pueden agrupar las pestañas de diferentes formas (grupos y paneles); hacer
  que las pestañas se abran en un panel determinado según el contenedor en el que se abren
  (mira la extensión Firefox Multi-Account Container que menciono más arriba); descargar
  de la memoria pestañas o paneles completos. Es configurable hasta el hartazgo, y esto lo
  dice alguien que es un loco de configurarlo todo al detalle. Sigo descubriendo nuevas
  características útiles que mejoran mi uso y disfrute del navegador web.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/skip-redirect/"&gt;Skip Redirect&lt;/a&gt;: detecta
  redirecciones en ciertas páginas y se las salta, para evitar rastreos innecesarios. Por
  ejemplo si te fijas al buscar algo en Google, cuando vas a un resultado la dirección es
  la web destino, sino que apunta a Google, y luego te redirecciona al destino. Esta
  extensión extrae el destino para evitar que Google (o muchos otros servicios) sepa qué
  resultados visitas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Actualizaciones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2022-11-19: Reemplacé Metamask por Tally Ho; añadí Sidebery.&lt;/li&gt;
&lt;li&gt;2023-01-20: Añadí Skip Redirect; eliminé NoScript, ClearURLs, PrivacyBadger, HTTPSEverywhere, DecentralEyes.&lt;/li&gt;
&lt;li&gt;2023-04-20: Actualicé Tally Ho a Taho, &lt;a href="https://blog.taho.xyz/rename-announcement/"&gt;cambiaron de marca el 21 de febrero de 2023&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;2026-03-31: Ahora uso Brave como mi navegador web principal.&lt;/li&gt;
&lt;/ul&gt;</content><category term="Software"/><category term="firefox"/><category term="web"/><category term="extensiones"/><category term="privacidad"/><category term="seguridad"/><category term="criptomoneda"/></entry><entry><title>Descubrimiento de Nextcloud CardDAV y CalDAV con entradas DNS</title><link href="https://blog.fidelramos.net/es/software/nextcloud-caldav-carddav-dns" rel="alternate"/><published>2022-02-17T00:00:00+00:00</published><updated>2023-12-28T14:55:18.288821+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2022-02-17:/es/software/nextcloud-caldav-carddav-dns</id><summary type="html">&lt;p&gt;Tengo una instancia propia de Nextcloud que almacena muchos de mis datos personales,
incluyendo mis contactos y mis calendarios. Recientemente tuve the reinstalar
&lt;a href="https://www.davx5.com"&gt;DAVx5&lt;/a&gt; en mi teléfono, y me sorprendió que la cuenta de Nextcloud
daba error al intentar añadirla.&lt;/p&gt;
&lt;p&gt;Los registros de error de DAVx5 mostraban que estaba haciendo …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Tengo una instancia propia de Nextcloud que almacena muchos de mis datos personales,
incluyendo mis contactos y mis calendarios. Recientemente tuve the reinstalar
&lt;a href="https://www.davx5.com"&gt;DAVx5&lt;/a&gt; en mi teléfono, y me sorprendió que la cuenta de Nextcloud
daba error al intentar añadirla.&lt;/p&gt;
&lt;p&gt;Los registros de error de DAVx5 mostraban que estaba haciendo peticiones HTTP a las URL
&lt;code&gt;/.well-known/caldav&lt;/code&gt; y &lt;code&gt;/.well-known/carddav&lt;/code&gt;, como describe el &lt;a href="https://datatracker.ietf.org/doc/html/rfc6764#section-5"&gt;RFC
6764&lt;/a&gt;. Nextcloud devolvía
respuestas correctas HTTP 301 redirigiendo a la dirección real para los servicios DAV de
la instancia Nextcloud, pero por alguna razón DAVx5 no estaba procesando las redirecciones
correctamente.&lt;/p&gt;
&lt;p&gt;Leyendo la &lt;a href="https://www.davx5.com/manual/accounts_collections.html#how-does-service-discovery-work"&gt;documentación de DAVx5 sobre descubrimiento de
servicios&lt;/a&gt;
mencionan que el standard DAV también permite definir entradas DNS de tipo SRV y TXT que
apunten a los servicios exactos a ser usados para CardDAV y CalDAV, así que decido probar
suerte porque parecía más sencillo que debuggear DAVx5 or ponerme a mirar en la
configuración de Nextcloud y/o del servidor web.&lt;/p&gt;
&lt;p&gt;Añado estas entradas DNS a &lt;code&gt;fidelramos.net&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;_caldavs._tcp 10800 IN SRV 0 1 443 cloud.fidelramos.net.
_caldavs._tcp 10800 IN TXT "path=/remote.php/dav/"
_carddavs._tcp 10800 IN SRV 0 1 443 cloud.fidelramos.net.
_carddavs._tcp 10800 IN TXT "path=/remote.php/dav/"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;path&lt;/code&gt; lo encuentro simplemente mirando a dónde redirigen &lt;code&gt;/.well-known/caldav&lt;/code&gt; y
&lt;code&gt;/.well-known/carddav&lt;/code&gt; en mi instancia de Nextcloud.&lt;/p&gt;
&lt;p&gt;DAVx5 conectó con éxito una vez creadas las entradas DNS, pero tuve que usar
&lt;code&gt;fidelramos.net&lt;/code&gt; como URL base, en vez de &lt;code&gt;cloud.fidelramos.net&lt;/code&gt; como usaba antes.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="nextcloud"/><category term="howto"/><category term="self-hosting"/><category term="dns"/></entry><entry><title>Desbloqueando en el arranque una partición cifrada con LUKS con una memoria USB</title><link href="https://blog.fidelramos.net/es/software/desbloqueando-luks-memoria-usb" rel="alternate"/><published>2019-10-13T00:00:00+00:00</published><updated>2019-10-27T14:43:01+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2019-10-13:/es/software/desbloqueando-luks-memoria-usb</id><summary type="html">&lt;p&gt;El caso de uso que quería resolver era: tengo un servidor sin monitor
con un disco duro cifrado por software usando LUKS, y quiero ser capaz
de reiniciarlo sin tener que escribir la contraseña en un teclado. La
solución que implementé fue crear un fichero con una clave LUKS en …&lt;/p&gt;</summary><content type="html">&lt;p&gt;El caso de uso que quería resolver era: tengo un servidor sin monitor
con un disco duro cifrado por software usando LUKS, y quiero ser capaz
de reiniciarlo sin tener que escribir la contraseña en un teclado. La
solución que implementé fue crear un fichero con una clave LUKS en una
memoria USB, de forma que si se conecta al iniciar el sistema el
fichero se usará para desbloquear la partición LUKS en vez de la
contraseña.&lt;/p&gt;
&lt;p&gt;Antes de empezar quiero mencionar someramente las implicaciones de
seguridad de esta solución:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Quienquiera obtenga acceso al dispositivo USB será capaz de
  descifrar el dispositivo objetivo. Considera los requisitos de
  seguridad de la memoria USB: guardarla en una caja fuerte,
  esconderla, llevarla siempre contigo...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Idealmente la memoria USB se usaría exclusivamente para este
  propósito, dado que si se conecta a otros sistemas estos podrían
  leer y filtrar la clave de cifrado.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Estamos obviamente reduciendo la seguridad del sistema a cambio de
  ganar usabilidad. A veces este es el equilibrio que buscamos, otras
  veces no tanto. Solo tú puedes juzgar el valor de lo que estás
  protegiendo, las potenciales formas de ataque y qué medidas de
  seguridad son apropiadas.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bien, manos a la obra. Asumiremos que tienes una memoria USB ya
formateada y montada (p.ej en &lt;code&gt;/mnt&lt;/code&gt;) en el ordenador con el
dispositivo LUKS que quieres desbloquear en el arranque. El primer
paso será crear un fichero con información aleatoria:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# dd if=/dev/urandom of=/mnt/key bs=4096 count=1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;A continuación añadimos la clave generada a LUKS, para que pueda ser
utilizada para desbloquear la partición raíz:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# cryptsetup luksAddKey /dev/sda3 /mnt/key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;/dev/sda3&lt;/code&gt; es un ejemplo, debes mirar cuál es la partición raíz en tu
sistema.&lt;/p&gt;
&lt;p&gt;El siguiente paso será actualizar &lt;code&gt;/etc/crypttab&lt;/code&gt;, que define los
volúmenes cifrados que son gestionados por LUKS. Tu fichero &lt;code&gt;crypttab&lt;/code&gt;
debería tener una entrada similar a esta:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cryptroot UUID=452ac6ac-8bbb-484f-b508-a11a5585e031 none luks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Tenemos que actualizar esta línea para usar el &lt;em&gt;script&lt;/em&gt; &lt;code&gt;passdev&lt;/code&gt; que
viene proporcionado de serie por &lt;code&gt;cryptsetup&lt;/code&gt;. &lt;code&gt;passdev&lt;/code&gt; se encarga de
esperar a que un dispositivo determinado aparezca como disponible,
entonces lo monta y lee la clave de un fichero. Es buena idea
referirse a la memoria USB utilizando un dispositivo que no vaya a
cambiar de forma inesperada. Yo uso &lt;code&gt;/dev/disk/by-label/&amp;lt;LABEL&amp;gt;&lt;/code&gt;, que
es un enlace simbólico al dispositivo basado en la etiqueta de la
partición. Tras actualizar la línea en &lt;code&gt;/etc/crypttab&lt;/code&gt; debería lucir
así:&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cryptroot UUID=452ac6ac-8bbb-484f-b508-a11a5585e031 /dev/disk/by-label/FIDELRAMOS.NET:/key:20 luks,keyscript=/lib/cryptsetup/scripts/passdev
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;Deja el nombre de mapeado original (&lt;code&gt;cryptroot&lt;/code&gt; en este ejemplo) y el
mismo UUID, de otra forma se romperá el arranque dado que estos datos
se referencian en &lt;code&gt;/etc/fstab&lt;/code&gt;. Solo cambia &lt;code&gt;none&lt;/code&gt; y añade el
&lt;code&gt;keyscript&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;El último paso es regenerar la imagen de initramfs para que incluya el
&lt;em&gt;script&lt;/em&gt; &lt;code&gt;passdev&lt;/code&gt;, si no fallará el arranque.&lt;/p&gt;
&lt;div class="code-block-wrapper" style="--copy-code-button-bg: #f8f8f2; --copy-code-button-color: #242121"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;# update-initramfs -k all -u
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;button aria-label="Copy code to clipboard" class="copy-button"&gt;Copiar&lt;/button&gt;&lt;/div&gt;
&lt;p&gt;¡Y ya está! Al reiniciar el sistema debería detectar la memoria USB,
montarla automáticamente y usar el fichero clave para desbloquear la
partición raíz. Si la memoria USB no se detecta por cualquier razón en
20 segundos debería pasarse a leer la clave por teclado.&lt;/p&gt;&lt;div data-button-text="¡Copiado!" hidden="" id="copy-code-settings"&gt;&lt;/div&gt;&lt;link href="https://blog.fidelramos.net/es/copy_code/copy-code.css" rel="stylesheet"/&gt;&lt;script defer="" src="https://blog.fidelramos.net/es/copy_code/copy-code.js"&gt;&lt;/script&gt;</content><category term="Software"/><category term="linux"/><category term="seguridad"/><category term="cifrado"/><category term="howto"/></entry></feed>