<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>blog.fidelramos.net</title><link href="https://blog.fidelramos.net/es/" rel="alternate"/><link href="https://blog.fidelramos.net/feeds/es.atom.xml" rel="self"/><id>https://blog.fidelramos.net/es/</id><updated>2026-09-01T12:34:00.942285+00:00</updated><entry><title>Omarchy y el punto óptimo de seguridad</title><link href="https://blog.fidelramos.net/es/software/omarchy-security-sweet-spot" rel="alternate"/><published>2026-09-01T00:00:00+00:00</published><updated>2026-09-01T12:34:00.942285+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-09-01:/es/software/omarchy-security-sweet-spot</id><summary type="html">&lt;p&gt;&lt;a href="https://omarchy.org/"&gt;Omarchy&lt;/a&gt;, la distribución Linux
&lt;a href="https://en.wikipedia.org/wiki/Omakase"&gt;omakase&lt;/a&gt; creada por &lt;a href="https://x.com/dhh"&gt;David Heinemeier
Hansson&lt;/a&gt; (el creador de Ruby on Rails), está ahora mismo en boca de
la comunidad del software libre. &lt;a href="https://x.com/bonkcity_/status/2094225788987961854?s=20"&gt;Algunos la
aman&lt;/a&gt;, &lt;a href="https://x.com/jpschroeder/status/2094494281129918592?s=20"&gt;otros la
odian&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yo lo digo claro: me declaro fan de Omarchy. Hay muchas cosas que me encantan a primera …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://omarchy.org/"&gt;Omarchy&lt;/a&gt;, la distribución Linux
&lt;a href="https://en.wikipedia.org/wiki/Omakase"&gt;omakase&lt;/a&gt; creada por &lt;a href="https://x.com/dhh"&gt;David Heinemeier
Hansson&lt;/a&gt; (el creador de Ruby on Rails), está ahora mismo en boca de
la comunidad del software libre. &lt;a href="https://x.com/bonkcity_/status/2094225788987961854?s=20"&gt;Algunos la
aman&lt;/a&gt;, &lt;a href="https://x.com/jpschroeder/status/2094494281129918592?s=20"&gt;otros la
odian&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Yo lo digo claro: me declaro fan de Omarchy. Hay muchas cosas que me encantan a primera
vista: el optimismo; la actitud de "sí se puede" (&lt;a href="https://x.com/dhh/status/2094501042306113598?s=20"&gt;&lt;em&gt;we can fix
everything&lt;/em&gt;&lt;/a&gt;); &lt;a href="https://x.com/dhh/status/2092129715662242179?s=20"&gt;unir a la
gente&lt;/a&gt;; &lt;a href="https://x.com/dhh/status/1963822469829329203?s=20"&gt;volver a hacer divertidos a
los ordenadores&lt;/a&gt;; &lt;a href="https://x.com/dhh/status/2093681834265567463?s=20"&gt;distanciarse de la
política&lt;/a&gt;; &lt;a href="https://x.com/dhh/status/2092479124807852504?s=20"&gt;enfocarse en el
rendimiento&lt;/a&gt;... Vamos, si es que
hasta &lt;strong&gt;me encanta la experiencia por defecto&lt;/strong&gt;: el gestor de ventanas &lt;em&gt;tiling&lt;/em&gt; moderno;
el acceder a todo con el teclado; hasta la estética. Yo estoy entre la audiencia a la
que va dirigida Omarchy, sería un usuario feliz haciendo muy pocos cambios.&lt;/p&gt;
&lt;p&gt;Este artículo, sin embargo, se centra en las razones técnicas por las que &lt;strong&gt;todavía no
voy a instalar Omarchy&lt;/strong&gt;. Se resume en lo que llamo el &lt;strong&gt;punto óptimo de la seguridad&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="el-punto-optimo-de-la-seguridad"&gt;El punto óptimo de la seguridad&lt;/h2&gt;
&lt;p&gt;Me tomo en serio la seguridad y la privacidad, porque creo que nuestras vidas digitales
pesan cada vez más. Quiero proteger mis datos, ser consciente de dónde se alojan, quién
tiene acceso a qué, y no dárselos a terceros cuando puedo evitarlo. Para eso invierto
muchas horas a mis prácticas de seguridad y privacidad: leer, aprender, planificar e
implementar cambios. Pero aparte de ese tiempo las decisiones que tomo añaden fricción y
complicaciones a mi día a día, porque definen el cómo interactúo con mis
dispositivos. Encontrar el &lt;strong&gt;punto óptimo adecuado&lt;/strong&gt; de seguridad y privacidad &lt;strong&gt;para
mí&lt;/strong&gt; es algo a lo que le llevo dando vueltas muchos años.&lt;/p&gt;
&lt;p&gt;Me gusta pensar en la intersección de mis datos y mis dispositivos como círculos, donde
los de más confianza tienen acceso a los datos más íntimos.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/omarchy_security_sweet_spot/data_trust_zones_es.webp" target="_blank" title="Clic para pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/omarchy_security_sweet_spot/data_trust_zones_es.webp" title="Clic para pantalla completa"&gt;&lt;figcaption&gt;Zonas de confianza de los datos&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Según este diagrama, tengo que confiar más en la integridad de mi ordenador que en la de
mis dispositivos móviles (móvil, tablet). Eso hace que no solo la elección del SO, sino
cómo lo configuro y lo uso, sea una decisión clave.&lt;/p&gt;
&lt;h1 id="la-evolucion-de-mi-punto-optimo"&gt;La evolución de mi punto óptimo&lt;/h1&gt;
&lt;p&gt;Para entender cómo ha ido cambiando mi particular punto óptimo de seguridad, voy a repasar
la historia de mis sistemas operativos de cabecera:&lt;/p&gt;
&lt;div align="center"&gt;
ZX Spectrum (finales de los 80)&lt;br/&gt;
↓&lt;br/&gt;
MS-DOS (principios de los 90)&lt;br/&gt;
↓&lt;br/&gt;
Windows (finales de los 90)&lt;br/&gt;
↓&lt;br/&gt;
Debian (los 2000)&lt;br/&gt;
↓&lt;br/&gt;
Kubuntu (principios de los 10)&lt;br/&gt;
↓&lt;br/&gt;
Qubes OS (finales de los 10)&lt;br/&gt;
↓&lt;br/&gt;
Arch Linux (los 20 hasta ahora)&lt;br/&gt;
&lt;/div&gt;

&lt;p&gt;Empecé con la informática en un ZX Spectrum+ en 1988; eran solo juegos y un poco de
&lt;a href="https://en.wikipedia.org/wiki/BASIC"&gt;BASIC&lt;/a&gt;. Unos años después pasé a un IBM PC con
&lt;a href="https://en.wikipedia.org/wiki/MS-DOS"&gt;MS-DOS&lt;/a&gt;, y eventualmente Windows 3.11. Sin
Internet en casa, y prácticamente sin datos personales en los dispositivos (solo algún
trabajo del cole), nadie pensaba en la privacidad; o más bien se daba por sentada.&lt;/p&gt;
&lt;p&gt;Luego, con Windows 95, llegó Internet primero a través de módem, y más tarde por
ADSL. Empecé a participar en comunidades online, a bajarme música, vídeos y
software. Era un adolescente, pero por suerte lo bastante sensato como para usar un
apodo y no publicar nombres ni direcciones en público.&lt;/p&gt;
&lt;p&gt;El salto de Windows a Debian (después de trastear con otras distros de Linux) fue casi
inevitable tras leer &lt;a href="https://www.gnu.org/philosophy/"&gt;la filosofía del software
libre&lt;/a&gt;. Me di cuenta de que no podía fiarme del
software de código cerrado, y cada vez me sentía más incómodo con Windows. Cuando cambié
sobre todo eché de menos mis juegos (solo unos pocos conseguí hacer funcionar con
&lt;a href="https://www.winehq.org/"&gt;Wine&lt;/a&gt;), pero decidí pasarme a las consolas para quitarme el
gusanillo. Mi soberanía digital era más importante.&lt;/p&gt;
&lt;p&gt;Debian es una distro increíble, y sigue siendo mi elección para servidores a día de hoy.
No obstante sus versiones tables se quedan anticuadas para su uso en escritorio, ese
siempre ha sido un punto en contra para los que buscamos software reciente. Sí, usaba
&lt;a href="https://backports.debian.org/"&gt;backports&lt;/a&gt;, y acabé corriendo
&lt;a href="https://www.debian.org/releases/sid/"&gt;sid&lt;/a&gt;, pero eso significaba más inestabilidad y
roturas de vez en cuando. Por eso acabé cambiando a Ubuntu (al final me quedé con su
sabor &lt;a href="https://kubuntu.org/"&gt;Kubuntu&lt;/a&gt;), y me fue bien por bastantes años.&lt;/p&gt;
&lt;p&gt;En 2016 un amigo me presentó &lt;a href="https://www.qubes-os.org/"&gt;Qubes OS&lt;/a&gt;, y me flipó sus
mejoras de seguridad. Si no conoces Qubes OS, quizá quieras &lt;a href="https://doc.qubes-os.org/en/latest/introduction/intro.html"&gt;leer su
introducción&lt;/a&gt; para darte
cuenta de lo distinto que puede ser un SO de escritorio. Si te da pereza, el resumen es
este: en un SO tradicional todo el software que corre tiene acceso completo al
ordenador, a todos sus archivos y dispositivos; en Qubes OS, en cambio, hay un
componente del SO (lo que llaman &lt;em&gt;Dom0&lt;/em&gt;) con un mínimo de software de confianza, y luego
todo, absolutamente todo lo demás, corre en &lt;strong&gt;máquinas virtuales independientes y
aisladas&lt;/strong&gt;. Eso significa que si alguna vez ejecutas software malicioso (virus,
troyanos, spyware, ransomware, ataques de cadena de suministro...), no tomaría
automáticamente el control del ordenador y todos sus datos. Romper las barreras de una
máquina virtual no es imposible, pero es bastante más difícil que otras opciones de
aislamiento. Por eso se consideran el estándar de oro de la seguridad del software,
amén de ejecutar en otro hardware independiente, claro.&lt;/p&gt;
&lt;p&gt;Al final me pillé un portátil nuevo en 2017 e instalé Qubes OS. Lo usé con éxito varios
años. Me sentía empoderado, con confianza para ejecutar cualquier software con la
tranquilidad de que no podía joder el ordenador ni acceder a datos que no debiera. Sin
embargo, el día a día se hizo pesado y acabó siendo demasiado para mí. La lentitud,
incluso con VMs aceleradas por hardware, hacía penosas todas las tareas diarias: navegar
por la web, abrir imágenes o PDFs, y sobre todo mi &lt;a href="https://blog.fidelramos.net/es/photography/photography-workflow"&gt;trabajo
fotográfico&lt;/a&gt; se resintió
mucho. Después de unos años decidí que tanta seguridad no estaba justificada para mis
necesidades personales, y que había inclinado demasiado la balanza en esa dirección.&lt;/p&gt;
&lt;p&gt;Eso me llevó a mi última transición. Me pillé un ordenador de sobremesa nuevo y me quedé
con &lt;a href="https://archlinux.org/"&gt;Arch Linux&lt;/a&gt; por su amplitud de software al día y su
maleabilidad para configurarlo como quisiese. Implementé algunas prácticas de seguridad
que me acercarían a mi punto óptimo de seguridad, privacidad y UX, como &lt;a href="https://github.com/max-baz/arch-secure-boot"&gt;Secure Boot de
extremo a extremo basado en UKI&lt;/a&gt; y
&lt;a href="https://github.com/netblue30/firejail/"&gt;sandboxing ligero de software&lt;/a&gt;. Uso
&lt;a href="https://kde.org/"&gt;KDE&lt;/a&gt; porque me resulta familiar y es muy configurable, pero los
gestores de ventanas &lt;em&gt;tiling&lt;/em&gt; modernos me tiran mucho; simplemente ahora mismo no puedo
justificar el tiempo de montar uno.&lt;/p&gt;
&lt;h2 id="que-voy-a-buscar-en-mi-futuro-so-de-escritorio"&gt;Qué voy a buscar en mi futuro SO de escritorio&lt;/h2&gt;
&lt;p&gt;Ahora mismo estoy a gusto, con un ecosistema personal cómodo y lo bastante seguro para
lo que necesito. Pero la mente del &lt;em&gt;hacker&lt;/em&gt; no para, y siempre estoy pensando en cómo se
podría mejorar.&lt;/p&gt;
&lt;p&gt;Esta es mi lista de deseos de lo que me gustaría ver en mi próximo SO de escritorio:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Buen UX de serie&lt;/strong&gt;. Rápido. Fluido. Agradable en el día a día. Todo con el
   teclado. Bonito. Omarchy ya cumple este punto para mí, eso fue lo que me motivó a
   escribir este artículo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Software en sandbox&lt;/strong&gt;. La mayoría de los programas deberían ejecutarse en un
   sandbox por defecto, de una forma abstraída del usuario para que la UX no se
   resienta. No me importa que me pregunte por permisos; la mayoría sólo serían una vez
   por cada programa, me parecen un compromiso aceptable para la seguridad. Habría una
   interfaz cómoda para gestionar los permisos, incluyendo a qué archivos o directorios
   tiene acceso cada programa (lectura y/o escritura).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actualizaciones auditadas&lt;/strong&gt;. Después de &lt;a href="https://blog.fidelramos.net/es/software/emacs-straight-ai-review"&gt;implementar revisión con IA para la
   actualización de mis paquetes de
   Emacs&lt;/a&gt; he estado
   pensando cuánto me gustaría tener lo mismo pero para todo el SO. Quiero alguna
   garantía de que el software se ha auditado antes de aterrizar en mi máquina. No es
   una proposición sencilla, hay muchos retos que superar, porque es mucho software y
   sería muy costoso, pero creo que podría haber soluciones vía crowdfunding. Me parece
   que podría ser una &lt;em&gt;killer app&lt;/em&gt; para Linux, porque un SO de código cerrado no podría
   competir en este terreno: tienes inevitablemente que fiarte de un tercero y no puedes
   fiarte solamente del código. Estoy planeando un artículo para desarrollar esta idea,
   y quizá trabaje en ello como proyecto futuro si nadie más lo hace primero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuración declarativa&lt;/strong&gt;. La verdad es que me arrepiento un poco de no haberme
   subido al carro de &lt;a href="https://nixos.org/"&gt;NixOS&lt;/a&gt; en vez de decidirme por Arch Linux. La
   reproducibilidad, la capacidad de compartir configuraciones complejas y de deshacer
   errores es algo muy potente. Además, los agentes de IA simplifican hacer cambios. Lo
   cual me lleva al siguiente punto.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI integrada&lt;/strong&gt;. Como ya saben los lectores de mi blog, ya he abrazado la IA y la
   uso a saco. Estoy plenamente convencido de que ha venido para quedarse y que hay que
   integrarla y construir sistemas nuevos a su alrededor para que nos sirvan mejor. La
   uso para programar, investigar, depurar, aprender, crear... Sin embargo &lt;a href="https://blog.fidelramos.net/es/software/how-i-sandbox-ai-agents"&gt;constreñir a
   las IAs&lt;/a&gt; para
   satisfacer mis objetivos de seguridad y privacidad no ha sido tan sencillo como me
   gustaría. Quiero que mis agentes tengan acceso restringido solo a lo que necesitan
   para hacer el trabajo. Quiero ver un plan de ejecución que pueda revisar y
   aprobar. No voy a fiarle a una IA mis datos más personales a menos que corra en mis
   propias instalaciones. Mi SO ideal integraría la IA, pero debidamente en sandbox para
   cubrir estos y probablemente otros casos de uso. &lt;strong&gt;Omarchy ya integra IA&lt;/strong&gt;, es
   puntero en ese aspecto, pero le pediría que estuviese mejor aislada.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ahora mismo estoy liado con otros proyectos que demandan mi atención, incluida una
reforma gorda de la casa y poner en marcha mi &lt;a href="https://blog.fidelramos.net/es/software/self-hosted-home-1"&gt;nuevo
homelab&lt;/a&gt;. Estas movidas me van
a entretener varios meses, durante los cuales preferiría no distraerme cambiando mi
ordenador principal a otro SO. Sin embargo, cuando la casa y el homelab estén en marcha
pienso revisitar mi elección de SO de escritorio. Cuando llegue el momento regresaré a
esta lista de deseos y veré cómo está el panorama de las distros Linux.&lt;/p&gt;
&lt;p&gt;Omarchy está mejorando a un ritmo alucinante, su base de usuarios y el número de
contribuidores crecen por miles. La mayoría de las mejoras que leo parecen centrarse en
el escritorio y la UX, pero ojalá esta humilde lista dé que pensar sobre la arquitectura
del SO en términos de seguridad y privacidad.&lt;/p&gt;
&lt;p&gt;Sea como sea les deseo el mejor de los éxitos al proyecto y la comunidad Omarchy, su
positividad es algo muy necesario en estos tiempos que corren.&lt;/p&gt;
&lt;p&gt;¿Tienes ideas o comentarios? Déjame uno aquí abajo, o puedes &lt;a href="https://x.com/ampajaro"&gt;mencionarme en
X&lt;/a&gt;.&lt;/p&gt;</content><category term="Software"/><category term="linux"/><category term="personal"/><category term="privacidad"/><category term="seguridad"/><category term="software-libre"/></entry><entry><title>Añadir nuevos git trailers a Magit</title><link href="https://blog.fidelramos.net/es/software/emacs-custom-magit-trailers" rel="alternate"/><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T21:32:29.822692+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-08-12:/es/software/emacs-custom-magit-trailers</id><summary type="html">&lt;p&gt;Los &lt;em&gt;trailers&lt;/em&gt; de Git no son más que líneas de texto al final de un &lt;em&gt;commit&lt;/em&gt; que
registran algunos metadatos. Algunos de los más comunes son &lt;code&gt;Co-Authored-By&lt;/code&gt;
(&lt;em&gt;co-escrito con&lt;/em&gt;), &lt;code&gt;Reported-By&lt;/code&gt; (&lt;em&gt;reportado por&lt;/em&gt;), &lt;code&gt;Reviewed-By&lt;/code&gt; (&lt;em&gt;revisado por&lt;/em&gt;) o
&lt;code&gt;Signed-Off-By&lt;/code&gt; (&lt;em&gt;aprobado por&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Magit tiene una forma cómoda de añadir &lt;em&gt;trailers&lt;/em&gt;: basta con pulsar …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Los &lt;em&gt;trailers&lt;/em&gt; de Git no son más que líneas de texto al final de un &lt;em&gt;commit&lt;/em&gt; que
registran algunos metadatos. Algunos de los más comunes son &lt;code&gt;Co-Authored-By&lt;/code&gt;
(&lt;em&gt;co-escrito con&lt;/em&gt;), &lt;code&gt;Reported-By&lt;/code&gt; (&lt;em&gt;reportado por&lt;/em&gt;), &lt;code&gt;Reviewed-By&lt;/code&gt; (&lt;em&gt;revisado por&lt;/em&gt;) o
&lt;code&gt;Signed-Off-By&lt;/code&gt; (&lt;em&gt;aprobado por&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Magit tiene una forma cómoda de añadir &lt;em&gt;trailers&lt;/em&gt;: basta con pulsar &lt;code&gt;C-c TAB&lt;/code&gt; o &lt;code&gt;C-c
C-i&lt;/code&gt; en la vista de &lt;em&gt;commit&lt;/em&gt;. Magit mostrará entonces un menú transient con algunas
opciones:&lt;/p&gt;
&lt;p&gt;
&lt;figure&gt;&lt;img src="https://blog.fidelramos.net/es/images/emacs_magit_custom_trailer/emacs-magit-insert-trailer-menu.png"/&gt;&lt;figcaption&gt;Menú de inserción de trailer de Magit&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Hoy en día se está volviendo habitual usar el &lt;a href="https://github.com/microsoft/vscode/issues/313962"&gt;&lt;em&gt;trailer&lt;/em&gt;
&lt;code&gt;Assisted-by:&lt;/code&gt;&lt;/a&gt; para acreditar a los
agentes de IA de programación, y yo quería empezar a usarlo, ya que ahora hago la mayor
parte de mi código con ayuda de IA. Magit no soporta este &lt;em&gt;trailer&lt;/em&gt; (¡por ahora!), pero
hice que mi IA averiguara cómo añadirlo.&lt;/p&gt;
&lt;p&gt;En esta pequeña guía os mostraré cómo añadir un nuevo tipo de &lt;em&gt;trailer&lt;/em&gt; a Magit, para
que quede a solo un &lt;code&gt;C-c TAB&lt;/code&gt; de distancia. Usaré el ejemplo de &lt;code&gt;Assisted-by&lt;/code&gt;, pero
podéis adaptarlo a otros si os hiciese falta. Al usarlo pide un valor (texto libre,
p. ej. &lt;code&gt;Kimi K3&lt;/code&gt; o &lt;code&gt;venice/zai-org-glm-5-2&lt;/code&gt;) y lo añade al buffer de &lt;em&gt;commit&lt;/em&gt; como
&lt;code&gt;Assisted-by: &amp;lt;value&amp;gt;&lt;/code&gt;, después de cualquier otro &lt;em&gt;trailer&lt;/em&gt; existente.&lt;/p&gt;
&lt;p&gt;Recuerda los valores introducidos anteriormente, que se ofrecerán como opciones de
completado. Como bonus también mostraré cómo configurar el paquete &lt;code&gt;savehist&lt;/code&gt; incluido
en Emacs para conservar ese historial entre sesiones.&lt;/p&gt;
&lt;h2 id="configuracion-de-magit"&gt;Configuración de Magit&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;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;use-package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;magit&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:preface&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defvar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-git-commit-assisted-history&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;"History of values entered for &lt;/span&gt;&lt;span class="ss"&gt;`my-git-commit-assisted'&lt;/span&gt;&lt;span class="s"&gt;."&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="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-git-commit-assisted&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="s"&gt;"Insert an \"Assisted-by\" trailer crediting an AI coding agent.&lt;/span&gt;
&lt;span class="s"&gt;Prompt for the agent description (free text, e.g. \"Claude Opus 4.7\"),&lt;/span&gt;
&lt;span class="s"&gt;offering previously entered values as completion."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&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="k"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;completing-read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Assisted by: "&lt;/span&gt;
&lt;span class="w"&gt;                                  &lt;/span&gt;&lt;span class="nv"&gt;my-git-commit-assisted-history&lt;/span&gt;
&lt;span class="w"&gt;                                  &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="w"&gt;                                  &lt;/span&gt;&lt;span class="ss"&gt;'my-git-commit-assisted-history&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="nb"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-empty-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&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="ne"&gt;user-error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Empty agent description"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;;; NOTE: `git-commit--insert-trailer' is a private API; magit&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;;; offers no public function for inserting an arbitrary trailer.&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;git-commit--insert-trailer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Assisted-by"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:config&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;;; C-c TAB A or C-c C-i A inserts the trailer&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;transient-append-suffix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'git-commit-insert-trailer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'git-commit-test&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="s"&gt;"A"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Assisted"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-git-commit-assisted&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="nv"&gt;add-to-list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'git-commit-trailers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Assisted-by"&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;Cómo funciona:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;transient-append-suffix&lt;/code&gt; añade el comando a &lt;code&gt;git-commit-insert-trailer&lt;/code&gt; (&lt;code&gt;C-c TAB&lt;/code&gt;),
  justo después de la entrada integrada "Co-developed", de modo que aparece como &lt;code&gt;A&lt;/code&gt; en
  el menú de &lt;em&gt;trailers&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;El valor se pide con &lt;code&gt;completing-read&lt;/code&gt; en el minibúfer, así que podéis usar vuestro
  sistema de completado configurado (yo uso &lt;a href="https://github.com/minad/vertico"&gt;vertico&lt;/a&gt;)
  para elegir entre los candidatos, o escribir texto libre.&lt;/li&gt;
&lt;li&gt;La inserción pasa por el mismo helper interno que Magit usa para &lt;code&gt;Signed-off-by&lt;/code&gt;, de
  modo que el &lt;em&gt;trailer&lt;/em&gt; se coloca después de los &lt;em&gt;trailers&lt;/em&gt; existentes y antes de la
  sección de comentarios/diff, independientemente de la disposición del buffer.&lt;/li&gt;
&lt;li&gt;Añadir &lt;code&gt;"Assisted-by"&lt;/code&gt; a &lt;code&gt;git-commit-trailers&lt;/code&gt; hace que el token se resalte con
  font-lock como los integrados. Esto es necesario porque los valores son texto libre;
  Magit solo resalta tokens arbitrarios cuando tienen la forma &lt;code&gt;Name &amp;lt;email&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Una nota para quienes usen versiones antiguas de Magit: el menú de &lt;em&gt;trailers&lt;/em&gt; solía
  llamarse &lt;code&gt;git-commit-insert-pseudo-header&lt;/code&gt;; ahora es un alias obsoleto de
  &lt;code&gt;git-commit-insert-trailer&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bonus-recordando-los-valores-entre-sesiones-con-savehist"&gt;Bonus: recordando los valores entre sesiones con savehist&lt;/h2&gt;
&lt;p&gt;Emacs olvida el historial de completado cuando termina la sesión, lo cuál es un fastidio
porque normalmente estaremos introduciendo los mismos valores una y otra vez. Activando
&lt;code&gt;savehist-mode&lt;/code&gt;, las variables de historial del minibúfer se guardan automáticamente una
vez usadas, así que a menudo &lt;em&gt;no hace falta nada más que tener el modo activado&lt;/em&gt;. Si
prefieres ser explícito (o si solo guardas una lista blanca estableciendo
&lt;code&gt;savehist-save-minibuffer-history&lt;/code&gt; a &lt;code&gt;nil&lt;/code&gt;), registradla como variable adicional:&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="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;use-package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;savehist&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:config&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;savehist-additional-variables&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="nv"&gt;my-git-commit-assisted-history&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="nv"&gt;savehist-mode&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;/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 orden importa: &lt;code&gt;savehist-mode&lt;/code&gt; restaura las variables una sola vez al activarse, así que si lo ponéis antes de establecer &lt;code&gt;savehist-additional-variables&lt;/code&gt;, veréis que las nuevas variables (&lt;code&gt;my-git-commit-assisted-history&lt;/code&gt; en mi caso) se guardan en el archivo de &lt;code&gt;savehist&lt;/code&gt;, pero no se restauran cuando Emacs arranca de nuevo. Pregúntame cómo lo sé.&lt;/p&gt;
&lt;h2 id="reflexiones-finales"&gt;Reflexiones finales&lt;/h2&gt;
&lt;p&gt;No me gusta tener que usar una variable privada como &lt;code&gt;git-commit--insert-trailer&lt;/code&gt;, es
algo que podría romperse en cualquier momento. En este caso me parece aceptable porque
es un uso muy localizado y en una funcionalidad no crítica que no me importaría que se
rompiera. Si ocurriese procuraré actualizar este artículo.&lt;/p&gt;
&lt;p&gt;En cuanto a &lt;code&gt;Assisted-by&lt;/code&gt;, planeo usarlo mucho; quiero ser honesto sobre mi uso de IA.&lt;/p&gt;
&lt;p&gt;¿Usas &lt;code&gt;Assisted-by&lt;/code&gt; u otros &lt;em&gt;trailers&lt;/em&gt; en tus &lt;em&gt;commit&lt;/em&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="emacs"/><category term="ia"/><category term="software-libre"/><category term="howto"/><category term="programación"/></entry><entry><title>Higiene de la cadena de suministro en Emacs: revisando actualizaciones mediante IA</title><link href="https://blog.fidelramos.net/es/software/emacs-straight-ai-review" rel="alternate"/><published>2026-08-08T00:00:00+00:00</published><updated>2026-09-01T10:47:58.217594+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-08-08:/es/software/emacs-straight-ai-review</id><summary type="html">&lt;p&gt;Emacs no tiene &lt;em&gt;sandbox&lt;/em&gt;. Cada paquete que instalas ejecuta código Lisp arbitrario con todos tus privilegios: acceso a tus archivos, credenciales y red. Esto hace que me dé &lt;strong&gt;mucho miedo&lt;/strong&gt; instalar paquetes nuevos o actualizar los existentes.&lt;/p&gt;
&lt;p&gt;Los paquetes que usamos la mayoría de usuarios de Emacs no son verificados …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Emacs no tiene &lt;em&gt;sandbox&lt;/em&gt;. Cada paquete que instalas ejecuta código Lisp arbitrario con todos tus privilegios: acceso a tus archivos, credenciales y red. Esto hace que me dé &lt;strong&gt;mucho miedo&lt;/strong&gt; instalar paquetes nuevos o actualizar los existentes.&lt;/p&gt;
&lt;p&gt;Los paquetes que usamos la mayoría de usuarios de Emacs no son verificados: MELPA los
compila a partir de algún commit de un repositorio (normalmente sin firma
criptográfica), muchos paquetes están mantenidos por una sola persona y nadie audita lo
que incluye cada actualización. Si secuestran la cuenta de un mantenedor o este vende el
repositorio a un nuevo propietario, &lt;code&gt;M-x package-upgrade-all&lt;/code&gt; se convierte en una vía
directa para ejecutar código de forma remota en tu máquina. Incluso existe un vector más
sutil y propio de este ecosistema: la &lt;em&gt;redirección de recetas&lt;/em&gt;. Un cambio de una sola
línea en una receta de MELPA puede hacer que el nombre de un paquete existente apunte
silenciosamente a otro repositorio.&lt;/p&gt;
&lt;p&gt;La solución no es dejar de actualizar, sino hacerlo de forma &lt;strong&gt;deliberada&lt;/strong&gt;: fijar cada
paquete a un commit exacto y revisar los diffs antes de fusionar nada.&lt;/p&gt;
&lt;p&gt;Revisar los &lt;em&gt;diffs&lt;/em&gt; a mano no solo es engorroso, sino que exige un alto nivel de
conocimientos de Emacs Lisp. Mi solución consiste en recurrir a un LLM para que realice
una primera auditoría de seguridad de los cambios antes de que lleguen a ejecutarse.&lt;/p&gt;
&lt;p&gt;En este artículo muestro la configuración que utilizo, basada en
&lt;a href="https://github.com/radian-software/straight.el"&gt;straight.el&lt;/a&gt;,
&lt;a href="https://magit.vc/"&gt;Magit&lt;/a&gt; y &lt;a href="https://github.com/karthink/gptel"&gt;gptel&lt;/a&gt;.&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="#resumen-de-la-configuracion"&gt;Resumen de la configuración&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-1-verificar-la-instalacion-de-straightel"&gt;Paso 1: verificar la instalación de straight.el&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-2-fijar-las-versiones-conocidas"&gt;Paso 2: fijar las versiones conocidas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-3-revisar-los-diffs-antes-de-fusionar"&gt;Paso 3: revisar los diffs antes de fusionar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#paso-4-revision-del-diff-mediante-un-llm"&gt;Paso 4: revisión del diff mediante un LLM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#flujo-de-trabajo-para-las-actualizaciones"&gt;Flujo de trabajo para las actualizaciones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#limitaciones-y-reflexiones-finales"&gt;Limitaciones y reflexiones finales&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="resumen-de-la-configuracion"&gt;Resumen de la configuración&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;El script de arranque de &lt;em&gt;straight.el&lt;/em&gt; se verifica mediante una suma SHA-256
precalculada antes de evaluarlo.&lt;/li&gt;
&lt;li&gt;Cada paquete (y cada repositorio de recetas) queda fijado a un &lt;em&gt;commit&lt;/em&gt; y registrado
en un archivo (&lt;em&gt;lockfile&lt;/em&gt;) sometido a control de versiones.&lt;/li&gt;
&lt;li&gt;Primero se descargan las actualizaciones: &lt;code&gt;git fetch&lt;/code&gt; obtiene los cambios entrantes,
mientras que &lt;code&gt;git merge&lt;/code&gt; y la actualización propiamente dicha solo se ejecutan cuando
los cambios superan la revisión.&lt;/li&gt;
&lt;li&gt;Una vista personalizada de &lt;em&gt;Magit&lt;/em&gt; muestra los diffs de todos los cambios descargados
pero aún no fusionados en todos los repositorios de straight.&lt;/li&gt;
&lt;li&gt;Un comando personalizado de &lt;em&gt;gptel&lt;/em&gt; revisa el diff completo mediante un LLM.&lt;/li&gt;
&lt;li&gt;Si los cambios son seguros, &lt;em&gt;straight.el&lt;/em&gt; los fusiona y actualiza el archivo de
versiones.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/emacs_straight_ai_review/emacs-straight-gptel-review.png" target="_blank" title="Haz clic para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/emacs_straight_ai_review/emacs-straight-gptel-review.png" title="Haz clic para ver a pantalla completa"/&gt;&lt;figcaption&gt;gptel revisando una actualización de paquetes de straight.el&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="paso-1-verificar-la-instalacion-de-straightel"&gt;Paso 1: verificar la instalación de straight.el&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/radian-software/straight.el#getting-started"&gt;&lt;em&gt;straight.el&lt;/em&gt; se
instala&lt;/a&gt; descargando y
evaluando &lt;code&gt;install.el&lt;/code&gt; durante el arranque. Esta es una puerta abierta a ejecución de
código remoto, así que mi &lt;code&gt;early-init.el&lt;/code&gt; fija su SHA-256 a un valor conocido y detiene
el proceso si no coincide:&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;;; SHA-256 of straight.el's install.el; update after reviewing changes at&lt;/span&gt;
&lt;span class="c1"&gt;;; https://github.com/radian-software/straight.el/commits/develop/install.el&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defconst&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-straight-install-el-sha256&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s"&gt;"e29e07d52d16d4136971f0a822cb6a1a6e1e764a1cb9fe67cccbc7c048aba553"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defvar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bootstrap-version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;bootstrap-file&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expand-file-name&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;"straight/repos/straight.el/bootstrap.el"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;bound-and-true-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;straight-base-dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;user-emacs-directory&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="nv"&gt;bootstrap-version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&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="nb"&gt;unless&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-exists-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bootstrap-file&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="nb"&gt;with-current-buffer&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url-retrieve-synchronously&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s"&gt;"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="ss"&gt;'silent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'inhibit-cookies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;;; verify bootstrap script against its pinned checksum before eval&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'url-http&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="nb"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url-http-end-of-headers&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;delete-region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;url-http-end-of-headers&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="nf"&gt;delete-region&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&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="k"&gt;progn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;skip-chars-forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;" \t\n"&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="nf"&gt;point&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="k"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;checksum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;secure-hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'sha256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;current-buffer&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="nb"&gt;unless&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;string=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;checksum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-straight-install-el-sha256&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="ne"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"straight.el bootstrap checksum mismatch!"&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="nf"&gt;goto-char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-max&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="nv"&gt;eval-print-last-sexp&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="nf"&gt;load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bootstrap-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'nomessage&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="paso-2-fijar-las-versiones-conocidas"&gt;Paso 2: fijar las versiones conocidas&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;straight.el&lt;/em&gt; instala los paquetes clonando sus repositorios &lt;em&gt;Git&lt;/em&gt;, lo que facilita su
revisión. El archivo de versiones (&lt;em&gt;lockfile&lt;/em&gt;) registra el estado de todos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;M-x straight-freeze-versions&lt;/code&gt; escribe el commit exacto de cada paquete en
  &lt;code&gt;straight/versions/default.el&lt;/code&gt;, tanto de los paquetes como de los los repositorios de
  recetas, MELPA entre ellos.&lt;/li&gt;
&lt;li&gt;Añade el archivo de versiones al repositorio. Revisar &lt;code&gt;git diff
  straight/versions/default.el&lt;/code&gt; después de una actualización te permite ver exactamente
  qué ha cambiado.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;M-x straight-thaw-versions&lt;/code&gt; restaura esas revisiones si alguna vez necesitas volver
  atrás.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="paso-3-revisar-los-diffs-antes-de-fusionar"&gt;Paso 3: revisar los diffs antes de fusionar&lt;/h2&gt;
&lt;p&gt;La clave para actualizar de forma segura es separar la descarga de los cambios de su fusión. &lt;code&gt;M-x straight-fetch-all&lt;/code&gt; descarga los nuevos commits del repositorio remoto sin modificar ningún checkout. Después reviso, repositorio por repositorio, lo que cambiaría antes de que &lt;code&gt;M-x straight-merge-all&lt;/code&gt; aplique nada.&lt;/p&gt;
&lt;p&gt;A continuación, uso una función personalizada de Magit que muestra en una única vista combinada el diff de todos los repositorios que tienen commits entrantes en el upstream:&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://magit.vc/&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;use-package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;magit&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:bind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"C-c v U"&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="nv"&gt;my-straight-incoming-diffs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:preface&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-straight-repo-behind-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;"Return non-nil if REPO's current branch is behind its upstream."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;default-directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;repo&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="nv"&gt;lines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-git-lines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--porcelain=2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--branch"&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="nv"&gt;seq-some&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;line&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="nv"&gt;string-match-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"^# branch\\.ab [+][0-9]+ -[1-9]"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nv"&gt;lines&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="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-straight-incoming-diffs&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="s"&gt;"Concatenate the full patches of all incoming upstream changes.&lt;/span&gt;
&lt;span class="s"&gt;Create a &lt;/span&gt;&lt;span class="ss"&gt;`diff-mode'&lt;/span&gt;&lt;span class="s"&gt; buffer listing, for every straight.el checkout&lt;/span&gt;
&lt;span class="s"&gt;that is behind its upstream, the incoming commits with author and&lt;/span&gt;
&lt;span class="s"&gt;date, followed by the net patch that merging would apply.  The&lt;/span&gt;
&lt;span class="s"&gt;buffer is left writable so hunks can be trimmed before feeding it&lt;/span&gt;
&lt;span class="s"&gt;to a reviewing agent."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&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="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;repos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-list-repos-1&lt;/span&gt;
&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expand-file-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"straight/repos"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;user-emacs-directory&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;behind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;seq-filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;my-straight-repo-behind-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;repos&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;behind&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="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"No incoming upstream changes"&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="nb"&gt;with-current-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get-buffer-create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"*straight-incoming-diffs*"&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="nf"&gt;erase-buffer&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="nb"&gt;dolist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;behind&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="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;default-directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;repo&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="nv"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-git-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"rev-parse"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--abbrev-ref"&lt;/span&gt;
&lt;span class="w"&gt;                                             &lt;/span&gt;&lt;span class="s"&gt;"HEAD"&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="nv"&gt;upstream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-git-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"rev-parse"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--abbrev-ref"&lt;/span&gt;
&lt;span class="w"&gt;                                               &lt;/span&gt;&lt;span class="s"&gt;"@{upstream}"&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="nv"&gt;commits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-git-lines&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="s"&gt;"log"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"--format=%h  %ad  %an &amp;lt;%ae&amp;gt;  %s"&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="s"&gt;"--date=short"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"HEAD..@{upstream}"&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="nv"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;commits&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="nf"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;?=&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"\n"&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="nf"&gt;insert&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"%s  (%s &amp;lt;- %s, %d commit%s)\n"&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;file-name-nondirectory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;directory-file-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;repo&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="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"HEAD"&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="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;upstream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;n&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="s"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"s"&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="nf"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;?-&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"\n"&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="nf"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;commits&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"\n"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"\n\n"&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="nv"&gt;magit-git-insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"diff"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"HEAD...@{upstream}"&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="nf"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"\n"&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="nv"&gt;diff-mode&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="nf"&gt;goto-char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&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="nv"&gt;pop-to-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;current-buffer&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="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-straight-incoming-diffs-after-fetch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;"Show incoming full diffs after an interactive &lt;/span&gt;&lt;span class="ss"&gt;`straight-fetch-all'&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;this-command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'straight-fetch-all&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="k"&gt;condition-case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;err&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my-straight-incoming-diffs&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="ne"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"my-straight-incoming-diffs failed: %S"&lt;/span&gt;
&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;error-message-string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))))))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:init&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;;; automatically show the incoming diffs after fetching all remotes&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-eval-after-load&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'straight&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;advice-add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'straight-fetch-all&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nb"&gt;:after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;my-straight-incoming-diffs-after-fetch&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="paso-4-revision-del-diff-mediante-un-llm"&gt;Paso 4: revisión del diff mediante un LLM&lt;/h2&gt;
&lt;p&gt;Salvo que el diff sea muy sencillo, se lo paso a un LLM para que revise el código. Utilizo gptel, KWallet para guardar los secretos y Venice (&lt;a href="https://venice.ai/chat?ref=XPrCO2"&gt;enlace de
referido&lt;/a&gt;) para la inferencia de IA:&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;;; API key from KWallet via the freedesktop Secret Service API; or put&lt;/span&gt;
&lt;span class="c1"&gt;;;   machine api.venice.ai login apikey password &amp;lt;your-key&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;;; in ~/.authinfo.gpg and drop the secrets: entry.&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;auth-sources&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="s"&gt;"secrets:kdewallet"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"~/.authinfo.gpg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"~/.authinfo"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;use-package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gptel&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:bind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"C-c a R"&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="nv"&gt;my-gptel-review-malicious-code&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:preface&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defvar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-gptel-review-system-prompt&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;"You are a meticulous security reviewer auditing third-party code&lt;/span&gt;
&lt;span class="s"&gt;before it is merged or used. The user will show you one or more git&lt;/span&gt;
&lt;span class="s"&gt;diffs, each preceded by the list of incoming commits with author and&lt;/span&gt;
&lt;span class="s"&gt;date.&lt;/span&gt;

&lt;span class="s"&gt;Scan for anything that could act maliciously once merged or called:&lt;/span&gt;
&lt;span class="s"&gt;- backdoors, remote code execution, persistence mechanisms&lt;/span&gt;
&lt;span class="s"&gt;- credential, token, key or data exfiltration (also covert channels)&lt;/span&gt;
&lt;span class="s"&gt;- unexpected network activity, downloads or connections to new hosts&lt;/span&gt;
&lt;span class="s"&gt;- obfuscation designed to hide behavior (heavy encoding, dead stores)&lt;/span&gt;
&lt;span class="s"&gt;- tampering with build, installation or packaging scripts&lt;/span&gt;
&lt;span class="s"&gt;- anomalous commit authorship: new maintainer, changed email address,&lt;/span&gt;
&lt;span class="s"&gt;  commits by someone unrelated to the project&lt;/span&gt;

&lt;span class="s"&gt;For every finding, state: severity (high/medium/low), the file and&lt;/span&gt;
&lt;span class="s"&gt;hunk, and a one-paragraph rationale.  Close with an overall verdict:&lt;/span&gt;
&lt;span class="s"&gt;whether the changes look safe to merge.  If nothing is suspicious,&lt;/span&gt;
&lt;span class="s"&gt;say so plainly and briefly; do not invent findings."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;"System prompt for &lt;/span&gt;&lt;span class="ss"&gt;`my-gptel-review-malicious-code'&lt;/span&gt;&lt;span class="s"&gt;."&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="nb"&gt;defun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-gptel-review-malicious-code&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="s"&gt;"Review the region, or the whole buffer, for malicious code.&lt;/span&gt;
&lt;span class="s"&gt;Send the text to the LLM with a security-audit system prompt and&lt;/span&gt;
&lt;span class="s"&gt;show the response in the *gptel-review* buffer."&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interactive&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="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'gptel&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="k"&gt;let*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-substring-no-properties&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;use-region-p&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="nf"&gt;region-beginning&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="nf"&gt;point-min&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="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;use-region-p&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="nf"&gt;region-end&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="nf"&gt;point-max&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="nv"&gt;buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;get-buffer-create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"*gptel-review*"&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="nv"&gt;marker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;with-current-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;buffer&lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;erase-buffer&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="nv"&gt;org-mode&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="nf"&gt;goto-char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&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="nf"&gt;point-marker&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="nv"&gt;pop-to-buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;buffer&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="nv"&gt;gptel-request&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;"Review the following content from %s for malicious code or \&lt;/span&gt;
&lt;span class="s"&gt;exploits.\n\n%s"&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buffer-name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;:system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;my-gptel-review-system-prompt&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;:stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;t&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;:buffer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;buffer&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;:position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;:config&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;setq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;gptel-backend&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;gptel-make-openai&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Venice"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;:host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"api.venice.ai"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;:endpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"/api/v1/chat/completions"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;:key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;#'&lt;/span&gt;&lt;span class="nv"&gt;gptel-api-key&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;; auth-source, see above&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;:stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;t&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nb"&gt;:models&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="nv"&gt;kimi-k3&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nv"&gt;claude-opus-5&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nv"&gt;openai-gpt-56-sol&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nv"&gt;grok-4-5&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nv"&gt;zai-org-glm-5-2&lt;/span&gt;
&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nv"&gt;deepseek-v4-flash-0731&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;gptel-model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;'deepseek-v4-flash-0731&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;Utiliza &lt;code&gt;C-c a R&lt;/code&gt; para revisar el búfer completo o la región seleccionada. La auditoría se muestra progresivamente en un búfer de Org llamado &lt;code&gt;*gptel-review*&lt;/code&gt;: cada hallazgo incluye la gravedad, el archivo y el hunk, una justificación y un veredicto final.&lt;/p&gt;
&lt;h2 id="flujo-de-trabajo-para-las-actualizaciones"&gt;Flujo de trabajo para las actualizaciones&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Descargo los nuevos cambios a todos los paquetes con &lt;code&gt;M-x straight-fetch-all&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Echo un vistazo a los cambios.&lt;/li&gt;
&lt;li&gt;Ejecuto &lt;code&gt;C-c a R&lt;/code&gt; sobre el búfer completo con los &lt;em&gt;diff&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Espero a que termine la revisión de la IA.&lt;/li&gt;
&lt;li&gt;Ejecuto &lt;code&gt;M-x straight-merge-all&lt;/code&gt; cuando estoy conforme.&lt;/li&gt;
&lt;li&gt;Reinicio Emacs y compruebo que todo funciona (&lt;em&gt;straight.el&lt;/em&gt; habrá reconstruido los paquetes).&lt;/li&gt;
&lt;li&gt;Ejecuto &lt;code&gt;M-x straight-freeze-versions&lt;/code&gt; y registro los cambios al archivo de versiones en mi repositorio &lt;em&gt;Git&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="limitaciones-y-reflexiones-finales"&gt;Limitaciones y reflexiones finales&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;La auditoría por IA es un segundo par de ojos, no una garantía.&lt;/strong&gt; Las IA cometen
    errores, pueden pasar cosas por alto, y un adversario hábil puede escribir código
    que parezca inofensivo. En el caso de los paquetes críticos, sigue siendo buena idea
    leer el &lt;em&gt;diff&lt;/em&gt; personalmente. Considera la auditoría como una herramienta de triaje:
    un hallazgo de gravedad «alta» significa que debes examinarlo más de cerca, no que
    el modelo tenga razón.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Los diffs salen de tu máquina.&lt;/strong&gt; Venice afirma aplicar una política de retención
    cero de datos, pero, si tu modelo de privacidad no lo permite, puedes configurar
    &lt;em&gt;gptel&lt;/em&gt; para que utilice un servidor local. El código es el mismo; solo cambia el
    proveedor de &lt;em&gt;gptel&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sé que esta configuración no es perfecta, pero me da mucha más tranquilidad al instalar
o actualizar paquetes. Emacs es la pieza central de mi entorno informático y necesito
poder confiar plenamente en él.&lt;/p&gt;
&lt;p&gt;¿Se te ocurre algún punto ciego, limitación o posible mejora? ¡Déjame 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="emacs"/><category term="howto"/><category term="programación"/><category term="seguridad"/><category term="software-libre"/></entry><entry><title>Revisión de mis propósitos para 2026</title><link href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2026-review" rel="alternate"/><published>2026-07-04T00:00:00+00:00</published><updated>2026-07-04T19:20:12.644671+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-07-04:/es/personal/new-years-resolutions-2026-review</id><summary type="html">&lt;p&gt;Llegamos al ecuador de 2026, y es buen momento para revisar mi progreso con mis &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2026"&gt;propósitos de Año Nuevo de 2026&lt;/a&gt;.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Llegamos al ecuador de 2026, y es buen momento para revisar mi progreso con mis &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2026"&gt;propósitos de Año Nuevo de 2026&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="gastar-menos-tiempo-en-tareas"&gt;Gastar menos tiempo en tareas ❌&lt;/h2&gt;
&lt;p&gt;Fallo.
Y no porque el plan fuese malo, sino porque sinceramente no lo he seguido (¿aunque quizá por eso era malo?).&lt;/p&gt;
&lt;p&gt;De todas las ideas la única que suelo seguir es apuntar las tareas del día en un &lt;em&gt;post-it&lt;/em&gt;.
A veces me funciona y a veces no, porque me distraigo y no me enfoco en terminarlas.
Vamos el problema que ya había reconocido y pretendía atajar, pero sin éxito.&lt;/p&gt;
&lt;p&gt;Voy a probar algo diferente: en vez de tener 10 tareas en un &lt;em&gt;post-it&lt;/em&gt; pretendiendo que las haré en el día, y al final sólo hago una, cada día voy a poner una única tarea en la que enfocarme para terminarla.
Mientras no la termine no trabajaré en nada más.
Si la termino el resto del día pues haré lo que me plazca, y al día siguiente otra tarea con ese nivel de intensidad.&lt;/p&gt;
&lt;h2 id="fotografia"&gt;Fotografía ⚠&lt;/h2&gt;
&lt;p&gt;Lo clasifico como parcial, pero creo que es más éxito que fracaso.&lt;/p&gt;
&lt;p&gt;Por un lado estoy echando menos fotos en general, tanto con el móvil como con la cámara.
El usar mucho menos mi cámara fotográfica hace que casi no haya gastado tiempo en edición y postprocesado, eso es de lo que más esfuerzo me consumía en el pasado, días o semanas después de eventos como las navidades o los viajes.&lt;/p&gt;
&lt;p&gt;Por otro lado he reducido el tiempo para organizar mis fotos.
Pensando en la organización en Immich ya he dejado de etiquetar mis fotos manualmente, sólo las organizo en álbumes y reviso el etiquetado de caras (que es automático en Digikam).&lt;/p&gt;
&lt;p&gt;Lo que más he fallado es que no he impreso ningún álbum en lo que llevamos de año, pese a tener el álbum de 2025 casi terminado desde hace meses.
Ese empujón final es la procrastinación que tanto me cuesta superar.
Pero estoy decidido a terminarlo pronto, y después de eso trabajaré en el resto de álbumes, procurando ir más rápido.&lt;/p&gt;
&lt;h2 id="no-morir"&gt;No Morir ✅&lt;/h2&gt;
&lt;p&gt;Exitazo.
Y no sólo porque sigo vivo.&lt;/p&gt;
&lt;p&gt;He bajado de 90kg a 83kg en 3 meses, y sin pérdida de masa muscular.
Todos los pantalones me quedan grandes, esa es la auténtica prueba del algodón.&lt;/p&gt;
&lt;p&gt;La grasa corporal ha bajado notablemente, y la grasa visceral, mi principal objetivo a mejorar dado que está &lt;a href="https://www.consalud.es/influcare/redes/tu-corazon-se-vuelve-rigido-la-conexion-entre-tu-grasa-visceral-musculo-y-riesgo-cardiaco.html"&gt;correlacionada con problemas cardiovasculares&lt;/a&gt;, va reduciéndose aunque aún no está en niveles óptimos.
Ha bajado del 10% inicial al 7% medido hoy.
Pienso continuar para intentar bajarla hasta niveles sanos (idealmente del 2% al 4%).&lt;/p&gt;
&lt;p&gt;El principal cambio ha sido encontrar una dieta que me funciona, me gusta y es sostenible en el tiempo.
Estas son las claves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ayuno intermitente todas las mañanas. A partir de 12 horas de ayuno el cuerpo empieza a movilizar grasas para &lt;a href="https://es.wikipedia.org/wiki/Cetosis"&gt;crear cetonas&lt;/a&gt;, lo cuál debe estar ayudando con la pérdida de peso.&lt;/li&gt;
&lt;li&gt;Por la mañana sólo café negro.&lt;/li&gt;
&lt;li&gt;Almuerzo después de la 1. Una variedad de recetas, por lo general altas en proteínas, reducidas en grasas y con más carbohidratos los días de gimnasio.&lt;/li&gt;
&lt;li&gt;Merienda sobre las 4:30: avena con leche de soja, semillas trituradas, yogur skyr, proteína &lt;em&gt;whey&lt;/em&gt;, colágeno, frutos del bosque.&lt;/li&gt;
&lt;li&gt;Cena entre las 7 y las 8, frecuentemente una tortilla con claras de huevo, ensalada con mejillones o atún, u otras recetas altas en proteína. Pieza de fruta.&lt;/li&gt;
&lt;li&gt;Las tres comidas en un intervalo corto de tiempo hacen que coma menos, porque si me hartase en el almuerzo no me entraría la merienda, e igual con la cena. Sirviéndome el almuerzo pienso en el cuenco de la merienda y me modero. Las comidas proteicas también sacian más.&lt;/li&gt;
&lt;li&gt;Como ya he dicho, alta proporción de proteínas. Apunto a 1,6-1,8g de proteína por kilo de peso corporal.&lt;/li&gt;
&lt;li&gt;Muy &lt;em&gt;muy&lt;/em&gt; ocasional consumo de alcohol, por salud en general pero también por las calorías vacías que contiene.&lt;/li&gt;
&lt;li&gt;Ayunos prolongados ocasionales. Habitualmente practico un ayuno de 24 a 36 horas una vez por semana, y un ayuno de unos 3 días una vez cada 3 a 6 meses. Por un lado creo que la &lt;a href="https://www.bbc.com/mundo/noticias-44029658"&gt;autofagia es beneficiosa&lt;/a&gt; para el cuerpo. Por otro en mi caso la restricción calórica me ayuda a perder peso.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Tengo pendiente hacerme un análisis de sangre lo más completo posible, y continuaré las mediciones con mi nutricionista, pero me siento muy bien y contento de al fin encontrar un método que particularmente me funciona.&lt;/p&gt;
&lt;p&gt;Otros aspectos que mencionaba en el &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2026"&gt;post anterior&lt;/a&gt;, como el sueño, también van bien.
Tengo una rutina regular y mi puntuación de sueño suele estar en 90 con bastante REM.
Quiero conseguir un medidor de CO2 para ver si se acumula en la habitación o hay que airear más.
También control de la temperatura.&lt;/p&gt;
&lt;h2 id="portugues"&gt;Portugués ✅&lt;/h2&gt;
&lt;p&gt;He mejorado bastante mi portugués estos meses.
Ya mantengo conversaciones de forma natural, aunque sé que sigo cometiendo errores, pero los portugueses son muy agradecidos y no protestan nunca.&lt;/p&gt;
&lt;p&gt;Hace 2 meses me presenté al examen &lt;a href="https://caple.letras.ulisboa.pt/exame/2/ciple"&gt;CIPLE&lt;/a&gt; (equivalente a nivel A2) y lo aprobé con nota "muy buena".&lt;/p&gt;
&lt;h2 id="blog"&gt;Blog ✅&lt;/h2&gt;
&lt;p&gt;Estoy escribiendo con más frecuencia, eso es indudable vista la cadencia de los últimos meses.&lt;/p&gt;
&lt;p&gt;Me gustaría escribir de otros temas, pero lo más fácil es seguir escribiendo de temas técnicos, que me apasionan y no son normalmente controvertidos (con excepción de la IA, pero al que no le guste que no lea).&lt;/p&gt;
&lt;h2 id="self-hosting"&gt;Self-hosting ⚠&lt;/h2&gt;
&lt;p&gt;Lo pongo como éxito parcial porque aunque he montado algún servicio nuevo (como &lt;a href="https://blog.fidelramos.net/es/software/tailscale-1-headscale-and-clients"&gt;Tailscale autoalojado&lt;/a&gt;), aún no he configurado ni Home Assistant ni Paperless, que eran los principales que mencioné en mis propósitos.&lt;/p&gt;
&lt;p&gt;Contaré este objetivo como éxito si para final de año tengo montado &lt;a href="https://blog.fidelramos.net/es/software/self-hosted-home-1"&gt;mi nuevo servidor&lt;/a&gt;, con todos los servicios actuales migrados, más Home Assistant y Paperless por lo menos.&lt;/p&gt;</content><category term="Personal"/><category term="personal"/></entry><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-08-13T19:10:45.935300+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="ia"/><category term="kde"/><category term="programación"/></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-08-13T23:16:31.919078+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="seguridad"/><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-08-13T19:10:46.688252+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="blog"/><category term="ia"/><category term="pelican"/><category term="programación"/><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-08-13T19:10:47.145224+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="ia"/><category term="programación"/></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-08-13T19:10:47.386209+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="seguridad"/><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-08-13T19:10:47.662192+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="firejail"/><category term="ia"/><category term="linux"/><category term="seguridad"/></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-08-13T19:10:47.535200+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="seguridad"/><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-08-13T19:10:46.943237+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="automatización"/><category term="howto"/><category term="linux"/><category term="seguridad"/></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>Propósitos de Año Nuevo para 2026</title><link href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2026" rel="alternate"/><published>2026-01-08T00:00:00+00:00</published><updated>2026-01-08T10:38:23.374046+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2026-01-08:/es/personal/new-years-resolutions-2026</id><summary type="html">&lt;p&gt;Otro año que pasa, y lo que originalmente no pensaba convertir en tradición ha acabado como una que no falta: publicar mis propósitos para el nuevo año.
Siempre fallo en algunos de los propósitos, pero me gusta el esfuerzo de reflexión y el tenerlos presente todo a lo largo del …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Otro año que pasa, y lo que originalmente no pensaba convertir en tradición ha acabado como una que no falta: publicar mis propósitos para el nuevo año.
Siempre fallo en algunos de los propósitos, pero me gusta el esfuerzo de reflexión y el tenerlos presente todo a lo largo del año.&lt;/p&gt;
&lt;p&gt;Siguiendo el formato del año pasado, empezaré por revisar cómo han ido mis &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025"&gt;propósitos de 2025&lt;/a&gt;, para entonces plantear los de este año:&lt;/p&gt;
&lt;h2 id="revision-de-2025"&gt;Revisión de 2025&lt;/h2&gt;
&lt;p&gt;Lo que he cumplido:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Familia y amigos: &lt;em&gt;éxito&lt;/em&gt;. Tuve dos fines de semana con mis amigos y me lo pasé bomba. Espero que podamos repetir este año, a poder ser más veces, pero es complicado alinear los calendarios de todos. También le &lt;a href="https://blog.fidelramos.net/es/software/computing-education-1"&gt;compré a mi hija su primer ordenador&lt;/a&gt;, y estoy en proceso de enseñarle a usarlo. Hemos viajado como turistas con los niños y ya se han acostumbrado, se portan como campeones.&lt;/li&gt;
&lt;li&gt;Perder peso: &lt;em&gt;éxito&lt;/em&gt;. Perdí una talla de pantalón, pero sigo sin estar en mi momento más delgado. Pero he ganado músculo y mejorado mi nivel cardiovascular, por lo que me doy por contento con los resultados.&lt;/li&gt;
&lt;li&gt;Nueva casa: &lt;em&gt;éxito por ahora&lt;/em&gt;. Las reformas al fin han comenzado, aunque con retraso respecto a mis expectativas iniciales. Aún es pronto para saber si todos los esfuerzos de planificación habrán merecido la pena, pero por ahora avanza a buen ritmo y va según lo previsto.&lt;/li&gt;
&lt;li&gt;Inteligencia Artificial: &lt;em&gt;éxito parcial&lt;/em&gt;. Uso &lt;a href="https://www.perplexity.ai"&gt;Perplexity&lt;/a&gt; y &lt;a href="https://venice.ai"&gt;Venice.ai&lt;/a&gt; casi a diario para hacer investigaciones o resolver problemas (por ejemplo errores con el sistema operativo, o de programación). También he jugado un poco con generación de imágenes y vídeos, pero sólo con servicios online, eso lo considero un fallo porque quería hacerlo en local. Tampoco he probado aún a usar agentes para programación, necesitaría ejecutarlos o bien en local o de una forma razonablemente privada.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo que no he cumplido:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Blog: &lt;em&gt;fallo&lt;/em&gt;. No conseguí mi objetivo de al menos un artículo mensual en el blog. Tiendo a escribirlos largos y encima me obceco en revisarlos y perfeccionarlos una y otra vez. Tengo 3 artículos casi listos pero nada que los termino y publico. Al menos estoy contento con la calidad de los que he publicado.&lt;/li&gt;
&lt;li&gt;Home labbing: &lt;em&gt;en pausa&lt;/em&gt;. Con el retraso de la nueva casa no he podido empezar aún a montar el nuevo servidor, y no quiero dedicarle demasiado tiempo al antiguo, porque tendré que acabar migrándolo todo. No obstante he montado dos nuevos servicios que ahora uso casi a diario: &lt;a href="https://jellyfin.org"&gt;Jellyfin&lt;/a&gt; e &lt;a href="https://immich.app"&gt;Immich&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="gastar-menos-tiempo-en-tareas"&gt;Gastar menos tiempo en tareas&lt;/h2&gt;
&lt;p&gt;Como supongo que le pasa a mucha gente, siento que me ahogo en tareas por hacer, sobre todo las que no avanzan mis objetivos vitales.
Mi propósito es poner en práctica el conocimiento teórico que tengo sobre productividad personal a ver si encuentro un sistema que me sirva.&lt;/p&gt;
&lt;p&gt;Para empezar necesito plantear mejor mis objetivos.
Hay multitud de sistemas: &lt;a href="https://es.wikipedia.org/wiki/Criterios_SMART"&gt;SMART&lt;/a&gt;, &lt;a href="https://www.performyard.com/articles/alternatives-to-smart-goals"&gt;PACT, WOOP, FAST&lt;/a&gt;...
Sólo en el proceso de escribir este artículo ya me voy perdiendo analizándolos en detalle para decidir cuál es el mejor para mí. Tengo que decidir uno rápido, aplicarlo a mis resoluciones de este año y seguir adelante.&lt;/p&gt;
&lt;p&gt;Luego en el día a día pienso seguir este proceso:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cada mañana plantear las tareas que pretendo acometer.&lt;/li&gt;
&lt;li&gt;Interrupciones que vayan a tomar menos de 5 minutos, hacerlas en el momento.&lt;/li&gt;
&lt;li&gt;Interrupciones más largas y con cierta urgencia apuntarlas en una lista y hacerlas todas o al final del día o al principio del día siguiente, para que no me interrumpan.&lt;/li&gt;
&lt;li&gt;Interrupciones más largas y no inmediatamente urgentes apuntarlas en otra lista que tenga un día dedicado en la semana.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Soy bastante procrastinador, así que necesito todos los trucos posibles para no dejar las cosas para más tarde.
Algunos que tengo que aplicar en serio son:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dividir tareas grandes en varias pequeñas. Completar tareas segrega dopamina, así que más cortas mejor.&lt;/li&gt;
&lt;li&gt;Empezar por la tarea más difícil o desagradable, y al terminarla darme una recompensa.&lt;/li&gt;
&lt;li&gt;Retomar una práctica formal de meditación, 15 minutos al día.&lt;/li&gt;
&lt;li&gt;Comunicar a alguien las tareas más importantes, para rendirle cuentas.&lt;/li&gt;
&lt;li&gt;Mantener a raya mi perfeccionismo, plantear cuál es el nivel de calidad apropiado para lo que esté haciendo.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fotografia"&gt;Fotografía&lt;/h2&gt;
&lt;p&gt;Hace muchos años que me &lt;a href="https://www.flickr.com/photos/fidelramos/"&gt;gusta la fotografía&lt;/a&gt;, pero reconozco que he invertido demasiado tiempo &lt;a href="https://blog.fidelramos.net/es/photography/photography-workflow"&gt;procesando y organizando mi colección fotográfica&lt;/a&gt;.
Estoy simplificando mi proceso fotográfico para disponer de más tiempo para otros objetivos.&lt;/p&gt;
&lt;p&gt;En este sentido hace meses que hago la mayoría de fotos con el móvil, la "cámara grande" la saco sólo para retratos y ocasiones puntuales.
Obviamente echo de menos la calidad y precisión de la cámara profesional, pero se me iba demasiado tiempo en el postprocesado.&lt;/p&gt;
&lt;p&gt;Aparte estoy en proceso de migración de Digikam a Immich.
La idea es que las fotos del móvil se suban directa y automáticamente a Immich, y que sea ahí donde edite los metadatos.
Tengo un artículo casi terminado sobre la filosofía y detalles detrás de esta migración, por lo que no me voy a extender más por ahora.&lt;/p&gt;
&lt;p&gt;Por último a principios del año pasado, aunque no lo pusiese como resolución, decidí empezar a hacer álbumes de fotos.
Hice uno grande con las fotos familiares de 2024, y me encantó el resultado.
También hice uno de una sesión de fotos que tuve con mi familia y mis padres, y le regalé una copia a ellos.
Pienso seguir haciéndolos porque me encantaba ver los álbumes de fotos con mis padres y escuchar las historias cuando era pequeño, y me gustaría hacer lo mismo con mis hijos.
Como objetivo medible, antes de que termine el año quiero tener impreso al menos el álbum de 2025 y el de 2023.&lt;/p&gt;
&lt;h2 id="no-morir"&gt;No morir&lt;/h2&gt;
&lt;p&gt;Me siento inspirado por &lt;a href="https://es.wikipedia.org/wiki/Bryan_Johnson"&gt;Bryan Johnson&lt;/a&gt; y su movimiento &lt;a href="https://www.netflix.com/title/81757532"&gt;Don't Die&lt;/a&gt;.
La mayoría de lectores es probable que ni siquiera lo conozcan, y los que lo conozcan o echen un primer vistazo a sus ideas apuesto a que piensan que es un multimillonario loco y egoísta que busca su inmortalidad.
Pero a mi modo de ver es un pionero que experimenta con su propio cuerpo para encontrar lo que funciona, algo de lo que nos beneficiaremos el resto.
Lo que a él le cuesta $2 millones al año en un futuro estará al alcance de cualquiera, al igual que tantas medicinas y tratamientos actuales de las que no podían disfrutar ni reyes ni las personas más ricas del pasado.&lt;/p&gt;
&lt;p&gt;En fin, sin entrar en más filosofía, no busco ni mucho menos el replicar su estilo de vida, sino más bien el reflexionar sobre las principales causas de muerte prematura y atajarlas.
Para ello y como primer paso mi propósito es adoptar varias de las &lt;a href="https://x.com/bryan_johnson/status/2005404233458442480"&gt;recomendaciones de Bryan Johnson&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redoblar mis esfuerzos para perder peso.&lt;/li&gt;
&lt;li&gt;Reducir el consumo de alimentos procesados y ultraprocesados, así como el azúcar y otros antinutrientes. Que sean algo muy ocasional.&lt;/li&gt;
&lt;li&gt;Reducir el alcohol al mínimo. He podido comprobar con mi &lt;em&gt;smartwatch&lt;/em&gt; que afecta muy negativamente a mi sueño, me impide alcanzar la fase REM. Y aparte están los demás efectos negativos que no estaré midiendo.&lt;/li&gt;
&lt;li&gt;Horario regular para dormir. En mi caso será de 11:00pm a 7:15am.&lt;/li&gt;
&lt;li&gt;Cenar a las 7pm, 4 horas antes de dormir.&lt;/li&gt;
&lt;li&gt;Fuera pantallas a las 10:30pm.&lt;/li&gt;
&lt;li&gt;Leer de 10:30pm a 11:00pm.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sobre la pérdida de peso, estoy experimentando a ver si encuentro una dieta que me funcione y sea sostenible.&lt;/p&gt;
&lt;p&gt;Por un lado suelo hacer ayunos prolongados de 3-4 días dos veces al año.
Aunque el efecto sobre la pérdida de peso es limitado, sobre todo los hago por los &lt;a href="https://www.youtube.com/watch?v=9tRohh0gErM"&gt;indicios de que son sanos para el cuerpo en general&lt;/a&gt;, por efectos como la autofagia.&lt;/p&gt;
&lt;p&gt;También he probado a hacer ayuno intermitente estricto, comiendo sólo en una ventana de 6-7 horas.
Mi implementación ha sido cenar sobre las 7 de la tarde; por la mañana sólo café negro; romper el ayuno con un almuerzo a eso de la 1; batido de proteína a media tarde.
Voy al gimnasio justo antes del almuerzo, y aunque no me ha dado ningún bajón probablemente no sea ideal ir en ayuno.&lt;/p&gt;
&lt;p&gt;El tercer experimento es aumentar la cantidad de proteína en mi dieta, hasta los 1,8g de proteína por kg de peso.
Esto ayuda a crear y mantener masa muscular y también a saciarse más y comer menos.
Sin embargo es difícil combinar esto con el ayuno intermitente, así que estoy probando sin el ayuno.&lt;/p&gt;
&lt;p&gt;Mi métrica principal es bajar el porcentaje de grasa visceral, que está correlacionada con problemas cardiovasculares.
Si alcanzo el intervalo considerado sano sería la mejor medida de éxito.
Otra métrica más simple sería bajar al menos hasta los 85 kg manteniendo o mejorando mi nivel físico (cardio y resistencia).
No porque 85 kg sea mi peso ideal, sino porque ahora peso 90 kg, sería un buen progreso.&lt;/p&gt;
&lt;h2 id="portugues"&gt;Portugués&lt;/h2&gt;
&lt;p&gt;Llevo 4 años viviendo en Portugal, y ya me defiendo bastante bien con el portugués.
El acento de los portugueses es difícil, porque hablan rápido y se "comen" letras (¡como los andaluces, vaya!), pero ya les entiendo casi todo.
Mi pronunciación ha mejorado bastante y tengo suficiente vocabulario y expresiones como para tener conversaciones largas en portugués.
Meto bastantes palabras en español, pero me entienden y por lo general agradecen el esfuerzo.&lt;/p&gt;
&lt;p&gt;Mi propósito es buscar clases de portugués, al menos un día a la semana, para mejorar de verdad.
Probablemente busque un profesor particular, porque cunde mucho más el tiempo, siempre me da coraje cuando una clase en grupo va al ritmo del más lento de la clase.&lt;/p&gt;
&lt;h2 id="blog"&gt;Blog&lt;/h2&gt;
&lt;p&gt;El propósito perenne: escribir más en el blog.
No podía faltar.&lt;/p&gt;
&lt;p&gt;No creo que el año pasado me equivocase al identificar las razones por las que me cuesta publicar más, pero está claro que eso no ha sido suficiente para corregirme.
Quizá me lea el libro &lt;a href="https://en.wikipedia.org/wiki/Atomic_Habits"&gt;Atomic Habits&lt;/a&gt;, que viene muy recomendado para establecer nuevos hábitos.&lt;/p&gt;
&lt;p&gt;Una motivación adicional de la que me di cuenta hace unos meses es el carácter autobiográfico del blog.
Me ilusiona que mis hijos puedan leerlo en un futuro, es una ventana a mis pensamientos, mis intereses, mis preocupaciones.
Quiero verter más personalidad e historia en mis futuros artículos.&lt;/p&gt;
&lt;p&gt;Algo que, aunque no lo parezca, me ayudó el año pasado fue usar la tablet para escribir los artículos.
Me es mucho más asequible coger la tablet cuando tengo 5 minutos de inspiración que sentarme en el ordenador 5 minutos.
O me la llevo fuera de casa y escribo cuando tengo unos minutos muertos esperando.
Uso &lt;a href="https://github.com/gsantner/markor"&gt;Markor&lt;/a&gt; para escribir los artículos, se sincronizan al ordenador con Syncthing, y cuando están listos le doy los últimos ajustes en el ordenador y los publico.&lt;/p&gt;
&lt;h2 id="self-hosting"&gt;Self-hosting&lt;/h2&gt;
&lt;p&gt;En cuanto esté lista la nueva casa estaré muy entretenido montando mi nuevo servidor casero, me dará para meses y toda una nueva serie de artículos en este blog.
Pero mientras tanto no me voy a quedar de brazos cruzados con mi servidor actual, tengo al menos tres ideas que quiero implementar pronto:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.home-assistant.io"&gt;Home Assistant&lt;/a&gt;, para ir aprendiendo cómo funciona y que no me pille de novato al montarlo en la nueva casa, que va a tener domótica en cada rincón.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.paperless-ngx.com"&gt;Paperless-ngx&lt;/a&gt;, un sistema de organización del papeleo. Pienso hacer que el escáner guarde los ficheros directamente en el servidor, para que Paperless los procese y archive automáticamente. Creo que va a ser un sistema útil y que ahorrará tiempo una vez montado, en apoyo de mi resolución de perder menos tiempo en tareas.&lt;/li&gt;
&lt;li&gt;Backup remoto de mis dispositivos y datos críticos, eso no puede pasar de este año. Ya tengo backup local, pero no me da suficiente tranquilidad, aparte de que no está automatizado.&lt;/li&gt;
&lt;/ol&gt;</content><category term="Personal"/><category term="personal"/></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>Revisión de mis propósitos para 2025</title><link href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025-review" rel="alternate"/><published>2025-07-17T00:00:00+00:00</published><updated>2026-07-04T19:02:02.835322+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2025-07-17:/es/personal/new-years-resolutions-2025-review</id><summary type="html">&lt;p&gt;Estamos aproximadamente (bueno venga, ya bien pasado) a mitad del año, así que es buen momento para evaluar el progreso de mis &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025"&gt;propósitos para 2025&lt;/a&gt;.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Estamos aproximadamente (bueno venga, ya bien pasado) a mitad del año, así que es buen momento para evaluar el progreso de mis &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025"&gt;propósitos para 2025&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="familia-y-amigos"&gt;Familia y amigos ✅&lt;/h2&gt;
&lt;p&gt;Al igual que en 2024, este es mi propósito principal, el que no quiero fallar bajo ningún concepto, es la base de todo lo demás.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Les dedico mucho tiempo a mis hijos.
Estoy ahí cuando se levantan y cuando se acuestan.
Los he llevado al colegio todos los días, y los he recogido muchos otros.
Juego mucho con ellos, leemos libros, les enseño todo tipo de cosas, vamos a parques...&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He introducido a mi hija mayor a programar con &lt;a href="https://scratch.mit.edu/"&gt;Scratch&lt;/a&gt;.
Por ahora no hace nada sola, pero le voy presentando conceptos y sobre todo busco que gane interés y le guste.
Tengo pendiente montarle un ordenador propio (una &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-500/"&gt;Raspberry Pi 500&lt;/a&gt; es mi primera opción ahora mismo), entonces podría experimentar por su cuenta.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hicimos un viaje en familia a Colombia de casi 3 semanas, y salió a pedir de boca.
También fuimos unos dias a la sierra, y hasta hicimos 10 kilómetros de senderismo.
Definitivamente los niños ya están listos para viajar más, así que iremos planeando más viajes en los meses venideros, algunos más cortos y otros más largos.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Estuve un fin de semana frikeando con amigos.
Pura risoterapia con mucho juego de mesa y videojuegos a 4 jugadores.
Todos nos quedamos con ganas de más, así que deseando repetir.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;He apoyado a mi pareja en sus objetivos vitales, y ha realizado un progreso increíble en menos de un año.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Resumiendo, incluso aunque el año terminase hoy, creo que daría este propósito por cumplido; pero voy a seguir teniéndolo presente, debe ser una constante y no algo en lo que se trabaje sólo la mitad del año y te olvides la otra mitad.&lt;/p&gt;
&lt;h2 id="perder-peso"&gt;Perder peso ⚠&lt;/h2&gt;
&lt;p&gt;Mi objetivo de perder una talla de pantalón lo conseguí a principios del año: se me rompió el pantalón que usaba a diario y cuando fui a comprar uno nuevo el que me quedaba bien era de una talla menos.&lt;/p&gt;
&lt;p&gt;No obstante el camino no ha sido recto, ha habido temporadas en el año en las que he ganado bastante peso, y luego lo he soltado.
Tampoco he conseguido en lo que va de año el alcanzar mi peso mínimo histórico, aunque creo que tengo más músculo que nunca, así que el peso magro debe ser mayor.&lt;/p&gt;
&lt;p&gt;En cualquier caso, tengo que encontrar y mantener una rutina que me funcione, y evitar grandes saltos de peso.&lt;/p&gt;
&lt;p&gt;No podré considerar este propósito como exitoso si no termino el año con varios kilos menos de los que tenía al terminar 2024.&lt;/p&gt;
&lt;h2 id="blog"&gt;Blog ❌&lt;/h2&gt;
&lt;p&gt;Este propósito es fácil de comprobar que no se está cumpliendo: este es el tercer artículo de 2025, cuando debería ser al menos el séptimo.
Y la cosa es que llevo meses con dos artículos en borrador, pero nunca me siento a terminarlos, me distraigo con otros temas.&lt;/p&gt;
&lt;p&gt;Las distracciones vienen por dos flancos: por un lado creo que mi capacidad de concentración se ha mermado por el uso del teléfono móvil, y eso que no uso ninguna plataforma de contenidos cortos tipo TikTok o Instagram; pero por otro lado muchas veces inetnto enfocarme con el blog u otro asunto, sólo para ser interrumpido escasos minutos (¡o segundos!) después con alguna petición.&lt;/p&gt;
&lt;p&gt;Escribo estas líneas en verano, y con los niños en casa no es momento de enfocarse en grandes proyectos (véase el primer propósito), pero a partir de septiembre procuraré bloquear tiempo todos los dias y poner límites a las interrupciones para poder concentrarme.&lt;/p&gt;
&lt;p&gt;Aunque no vaya a cumplir el propósito de un artículo al mes, voy al menos procurar terminar el año con 12 artículos o más, distribuidos como de cualquier forma según el tiempo disponible lo permita.
Mi esperanza es ganar una inercia que continúe en 2026.&lt;/p&gt;
&lt;h2 id="nueva-casa"&gt;Nueva casa ✅&lt;/h2&gt;
&lt;p&gt;El proceso de reforma de la nueva casa ha ido acumulando retrasos, entre planificación, presupuestos y, lo más doloroso por la impotencia, burocracia.&lt;/p&gt;
&lt;p&gt;Personalmente puedo estar tranquilo de haberle dedicado todo el tiempo y esfuerzo que necesitaba por mi parte, de forma que los retrasos no me son imputables.&lt;/p&gt;
&lt;p&gt;Espero que la construcción comience dentro de poco, entonces tendré que estar bien encima para supervisar los trabajos.&lt;/p&gt;
&lt;h2 id="home-labbing-y-domotica"&gt;Home-labbing y domótica ⚠️&lt;/h2&gt;
&lt;p&gt;Dado que la reforma de la casa aún ni ha comenzado no he podido avanzar mucho en este aspecto.&lt;/p&gt;
&lt;p&gt;He dedicado bastante tiempo a investigar diferentes opciones, depurando la instalación que pretendo hacer.&lt;/p&gt;
&lt;p&gt;Voy a instalar &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt; en mi actual apartamento, aunque no tenga casi nada que automatizar, para irlo aprendiendo y jugar con él un poco.&lt;/p&gt;
&lt;h2 id="inteligencia-artificial"&gt;Inteligencia Artificial ⚠️&lt;/h2&gt;
&lt;p&gt;He seguido informándome de las novedades en IA (lo cual podría ser perfectamente un trabajo a tiempo completo), y experimentando con algunas cosas, pero menos de lo que me habría gustado.&lt;/p&gt;
&lt;p&gt;Hace más de un año que utilizo &lt;a href="https://venice.ai/chat?ref=6ytBRz"&gt;Venice.ai&lt;/a&gt; (enlace con &lt;em&gt;referral&lt;/em&gt;), tanto para consultas textuales como para generar imágenes.
Me gusta mucho que se basa en modelos de software libre, con una buena interfaz, pero sobre todo que es &lt;strong&gt;privado&lt;/strong&gt;, o sea que no registra conversaciones.
Puede usarse gratuitamente y sin registro, pero para los modelos más potentes hace falta la suscripción Pro.&lt;/p&gt;
&lt;p&gt;En local utilizo &lt;a href="https://ollama.com/"&gt;ollama&lt;/a&gt;, pero la verdad es que sólo ocasionalmente.
Mi propósito era encontrar flujos de trabajo que utilizase a diario, pero no es el caso a día de hoy.&lt;/p&gt;
&lt;p&gt;Para generar imágenes estuve probando &lt;a href="https://github.com/LykosAI/StabilityMatrix"&gt;StabilityMatrix&lt;/a&gt;, pero tuve dificultades para ejecutarlo con &lt;a href="https://github.com/netblue30/firejail/"&gt;firejail&lt;/a&gt;.
Tras varias semanas atascado decidí abandonarlo y centrarme en &lt;a href="https://www.comfy.org/"&gt;ComfyUI&lt;/a&gt;, que es de los entornos más potentes y creo que continuará siendo desarrollado en el futuro.
También tuve algunas dificultades debido a firejail, pero las conseguí superar y finalmente lo tengo funcionando.&lt;/p&gt;
&lt;p&gt;Tengo en mente un proyecto que automatice crear &lt;a href="https://es.wikipedia.org/wiki/Ajuste_fino_(aprendizaje_profundo)#Adaptaci%C3%B3n_de_bajo_rango"&gt;LoRAs&lt;/a&gt; de personas a partir de un conjunto de fotografias con caras reconocidas.
Mi idea es poder apuntarlo a mi colección fotográfica en Digikam o Immich, seleccionar una o más personas con caras reconocidas en la base de datos, y que genere un LoRA para esas personas extrayendo las caras de las fotografias para el entrenamiento.
Así después podría generar imágenes que usen a esas personas específicamente.&lt;/p&gt;
&lt;p&gt;Voy a procurar escribir artículos sobre todo esto, así me motivo y además progreso con el propósito del blog.&lt;/p&gt;</content><category term="Personal"/><category term="personal"/></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>Propósitos de Año Nuevo para 2025</title><link href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2025" rel="alternate"/><published>2025-01-06T00:00:00+00:00</published><updated>2025-02-06T10:22:59.288242+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2025-01-06:/es/personal/new-years-resolutions-2025</id><summary type="html">&lt;p&gt;Hace un año &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2024"&gt;publiqué mis propósitos para 2024&lt;/a&gt;.
Lo hice sin intención de convertirlo en una tradición anual, era más bien para desahogarme.
No obstante unos amigos me hicieron recientemente reflexionar sobre los propósitos que he cumplido y los que no (esa presión social que yo mismo buscaba en mi …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hace un año &lt;a href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2024"&gt;publiqué mis propósitos para 2024&lt;/a&gt;.
Lo hice sin intención de convertirlo en una tradición anual, era más bien para desahogarme.
No obstante unos amigos me hicieron recientemente reflexionar sobre los propósitos que he cumplido y los que no (esa presión social que yo mismo buscaba en mi publicación), así que voy a aprovechar para dejar esas reflexiones por escrito.&lt;/p&gt;
&lt;p&gt;Antes de pasar a la revisión de 2024 y a los propósitos de 2025 debo reconocer que el haber publicado mi lista de propósitos me ayudó a tenerlos en mente todo el año.
Incluso aunque muchos no los haya cumplido, los tenía presentes y era consciente de cuándo dejaba alguno de lado.&lt;/p&gt;
&lt;h1 id="lo-que-he-cumplido-en-2024"&gt;Lo que he cumplido en 2024&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enfocarme en mis hijos&lt;/strong&gt;: &lt;em&gt;éxito&lt;/em&gt;.
No hay mucho que añadir, si hay un propósito que tenía claro era éste.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amistades&lt;/strong&gt;: &lt;em&gt;éxito&lt;/em&gt;.
Siento que he recuperado o reforzado amistades que estaban en peligro, y no me preocupo tanto por las que parecen perdidas (¡aunque nunca sea tarde!).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimentar con IA&lt;/strong&gt;: &lt;em&gt;éxito parcial&lt;/em&gt;.
Monté un nuevo ordenador con una tarjeta gráfica potente, y he podido "jugar" con distintos modelos para distintas tareas: consultas textuales, generación y reconocimiento de imágenes, generación y análisis de código.
No he experimentado con algunos temas que me interesan, como &lt;a href="https://simonwillison.net/2023/Oct/23/embeddings/"&gt;embeddings&lt;/a&gt;,  &lt;a href="https://stable-diffusion-art.com/controlnet/"&gt;ControlNet&lt;/a&gt;, sistemas de &lt;a href="https://stable-diffusion-art.com/hunyuan-video/"&gt;generación de vídeo&lt;/a&gt; o &lt;a href="https://www.gladia.io/blog/best-open-source-speech-to-text-models"&gt;reconocimiento&lt;/a&gt; y &lt;a href="https://www.bentoml.com/blog/exploring-the-world-of-open-source-text-to-speech-models"&gt;generación de voz&lt;/a&gt;.
También me ha faltado integrarlas en mi flujo de trabajo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moderar esfuerzos ideológicos&lt;/strong&gt;: &lt;em&gt;éxito&lt;/em&gt;.
El preocuparme menos le ha venido muy bien a mi salud mental, aunque no puedo evitar sentirme triste de no poder debatir y hablar de según qué temas con la gente.
Siempre he sido intelectualmente muy curioso y activo y el guardarme mis ideas no me sienta bien.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redes sociales y mensajería&lt;/strong&gt;: &lt;em&gt;éxito parcial&lt;/em&gt;.
He reducido mi uso de redes sociales.
Las que consulto lo hago desde el ordenador, no tengo ninguna en el móvil.
A veces pasan días sin ver ninguna.
No obstante no he conseguido cumplir mi propósito de no mirar la mensajería durante el día ni desactivar las notificaciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="lo-que-no-he-cumplido-en-2024"&gt;Lo que no he cumplido en 2024&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Consultoría&lt;/strong&gt;: &lt;em&gt;fracaso&lt;/em&gt;.
Las trabas burocráticas me hicieron tomar la decisión de cerrar la empresa.
Quizá fue una racionalización post-hoc, pero también pensé que quizá no tenía que seguir haciendo lo mismo que siempre sólo porque se me dé bien.
Necesito encontrar el cruce entre mis habilidades y mis motivaciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Criptomonedas&lt;/strong&gt;: &lt;em&gt;fracaso&lt;/em&gt;.
No he colaborado con ningún proyecto, de hecho me he distanciado un poco del mundillo para enfocarme en los otros propósitos.
No obstante he colaborado con diferentes proyectos de software libre aunque no relacionados con criptomonedas, sobre todo algunos que utilizo personalmente a diario.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Más blog&lt;/strong&gt;: &lt;em&gt;rotundo fracaso&lt;/em&gt;.
Ni un sólo artículo en 2024 después de publicar el de los propósitos.
Y ha sido por falta de ideas, porque tengo muchas, las voy anotando y desarrollando entre mis notas.
Pero después no me siento a escribir artículos, aunque ya los tenga casi terminados en la cabeza.
Creo que mi barrera principal es el temor a ser juzgado, sobre todo si publico temas personales o ideológicos en vez de sólo técnicos.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="mis-propositos-para-2025"&gt;Mis propósitos para 2025&lt;/h1&gt;
&lt;h2 id="familia-y-amigos"&gt;Familia y amigos&lt;/h2&gt;
&lt;p&gt;Un éxito de 2024 que tengo intención de mantener este año y siguientes.&lt;/p&gt;
&lt;p&gt;Entre muchas otras cosas, quiero enseñar informática y programación a mis hijos.
Los ordenadores han sido una piedra angular en mi vida y quiero que ellos tengan al menos la oportunidad de conocerlos, ya veremos si les enganchan como a mí.&lt;/p&gt;
&lt;p&gt;También quiero empezar a viajar más con ellos ahora que el más pequeño ya no es un bebé.
El mundo es un lugar a la vez muy grande y muy pequeño, me gustaría que tuviesen una cultura global.&lt;/p&gt;
&lt;p&gt;Me gustaría tener la oportunidad al menos una vez al año de pasar unos días de frikeo con amigos.&lt;/p&gt;
&lt;h2 id="perder-peso"&gt;Perder peso&lt;/h2&gt;
&lt;p&gt;Quienes me conocen saben que gano peso con facilidad.
Hago levantamiento de peso y algo de cardio, y eso me mantiene si no delgado, al menos no muy gordo.
Pero bastan 2 semanas de navidades para ganar 5 o 6 kilos.&lt;/p&gt;
&lt;p&gt;Mi plan es hacer un esfuerzo consciente por regular mi dieta y perder peso de forma sana y progresiva durante el año.
Lo he hecho en el pasado, pero esta vez quiero bajar de mi anterior peso mínimo histórico.&lt;/p&gt;
&lt;p&gt;Consideraré este propósito un éxito si pierdo una talla de pantalón, o si se ve el más ligero atisbo de tableta de chocolate.
¡Ojo!, tener unos abdominales marcados es un esfuerzo tremendo que no estoy dispuesto a realizar, no nos volvamos locos.&lt;/p&gt;
&lt;h2 id="blog"&gt;Blog&lt;/h2&gt;
&lt;p&gt;Como expliqué antes, el blog ha estado abandonado durante 2024, y eso que tenía muchas ideas para artículos.&lt;/p&gt;
&lt;p&gt;Mi propósito es superar mis limitaciones y realmente escribir en el blog, tanto sobre temas técnicos como ideológicos.&lt;/p&gt;
&lt;p&gt;Por poner una métrica de éxito tangible, apuntemos a un mínimo de un artículo al mes, idealmente dos.&lt;/p&gt;
&lt;h2 id="nueva-casa"&gt;Nueva casa&lt;/h2&gt;
&lt;p&gt;El año pasado compramos nuestra primera casa, y necesita bastante reforma.
Eso no es un problema de por sí, es algo que sabíamos y buscábamos, para poder renovarla a nuestro gusto.&lt;/p&gt;
&lt;p&gt;Quiero estar disponible y enfocado para controlar el proceso y que quede lo mejor posible.&lt;/p&gt;
&lt;h2 id="home-labbing-y-domotica"&gt;Home-labbing y domótica&lt;/h2&gt;
&lt;p&gt;Relacionado con la nueva casa: pienso aprovechar para montar un nuevo servidor que reemplace a &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;mi actual&lt;/a&gt;, que ya se queda pequeño.&lt;/p&gt;
&lt;p&gt;También planeo instalar una variedad de sensores por la casa, y gestionar las automatizaciones con &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Quién sabe, lo mismo sale algún negocio de estas pasiones.
¡O al menos espero que dé para algunos buenos artículos en el blog! &lt;em&gt;guiño&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="inteligencia-artificial"&gt;Inteligencia Artificial&lt;/h2&gt;
&lt;p&gt;Me siguen fascinando los avances en IA, creo que con la tecnología actual ya es algo con un potencial enorme, lo que faltan son productos orientados al usuario final, y esos acabarán llegando.&lt;/p&gt;
&lt;p&gt;En 2024 pude experimentar lo que mi tiempo disponible me permitía, pero en 2025 quiero ir más allá y encontrar integraciones y flujos de trabajo que me sean útiles.&lt;/p&gt;
&lt;p&gt;Aunque no sea una prioridad inicial, me gustaría tener hardware en el nuevo servidor orientado a inferencia local.
Así podría ofrecer servicios de IA a la familia, con datos completamente privados.&lt;/p&gt;</content><category term="Personal"/><category term="personal"/></entry><entry><title>Propósitos de Año Nuevo para 2024</title><link href="https://blog.fidelramos.net/es/personal/new-years-resolutions-2024" rel="alternate"/><published>2024-01-01T00:00:00+00:00</published><updated>2024-01-01T18:52:43.349772+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2024-01-01:/es/personal/new-years-resolutions-2024</id><summary type="html">&lt;p&gt;Llevo varios meses reflexionando sobre la vida, quizá estoy viviendo mi particular crisis de la mediana edad.&lt;/p&gt;
&lt;p&gt;Este año 2024 quiero poner de mi parte para corregir aquello que esté en mi mano (y preocuparme menos por lo que no), de ahí esta lista de propósitos de Año Nuevo.&lt;/p&gt;
&lt;p&gt;Publicar …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Llevo varios meses reflexionando sobre la vida, quizá estoy viviendo mi particular crisis de la mediana edad.&lt;/p&gt;
&lt;p&gt;Este año 2024 quiero poner de mi parte para corregir aquello que esté en mi mano (y preocuparme menos por lo que no), de ahí esta lista de propósitos de Año Nuevo.&lt;/p&gt;
&lt;p&gt;Publicar estos propósitos tiene un propósito doble:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Que la gente entienda mis prioridades y las razones que las motivan.&lt;/li&gt;
&lt;li&gt;Mantener mi enfoque a lo largo del año gracias a la presión social.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="enfocarme-en-mis-hijos"&gt;Enfocarme en mis hijos&lt;/h2&gt;
&lt;p&gt;En el momento de escribir este artículo mis hijos tienen 5 años y 17 meses, y los quiero más que a nada, me alegran todos los días.&lt;/p&gt;
&lt;p&gt;Este último año ha sido difícil combinando mi último trabajo con el nacimiento de mi segundo hijo, mi salud mental se resintió al esforzarme en mantener mi productividad habitual.&lt;/p&gt;
&lt;p&gt;En este año 2024 mi enfoque será primero en mis hijos, y procuraré no sentirme culpable de lo que no me llegue el tiempo para hacer.&lt;/p&gt;
&lt;h2 id="amistades"&gt;Amistades&lt;/h2&gt;
&lt;p&gt;He ido perdiendo el contacto con diversos amigos, creo que fundamentalmente por dos razones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;La distancia, primero cuando viví en San Francisco, y ahora en Lisboa.
Con varios amigos intenté mantener el contacto, pero no reciprocaron.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Siento que algunos amigos me han dado de lado por mis ideologías.
No puedo estar seguro pero me entristece pensar que sea el caso.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Procuraré dedicar mis esfuerzos a las amistades que merecen la pena, quienes me demuestran que me respetan y que les importo tanto como ellos a mi.
Sin rencores con el resto, la vida cambia y es complicada para todos.&lt;/p&gt;
&lt;h2 id="consultoria"&gt;Consultoría&lt;/h2&gt;
&lt;p&gt;Este año organizaré mi servicio de consultoría y que se convierta en una fuente de ingresos.
Mi objetivo es ayudar a empresas y proyectos que se beneficien de mis 16 años de experiencia en desarrollo web.&lt;/p&gt;
&lt;p&gt;En las próximas semanas montaré una página web y buscaré mis primeros clientes.&lt;/p&gt;
&lt;h2 id="experimentar-con-ia"&gt;Experimentar con IA&lt;/h2&gt;
&lt;p&gt;Me interesan mucho los nuevos avances en Inteligencia Artificial, procuraré dedicarle tiempo a aprender y experimentar.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generación de imágenes y vídeos.
Tengo varios proyectos personales en mente, pero aparte quiero sentarme con mi hija y que ella aprenda a generar imágenes e interactuar con las IAs.
Creo que será algo importante para su futuro.&lt;/li&gt;
&lt;li&gt;LLMs (Large Language Models) para distintos temas: programación, chat, &lt;em&gt;embeddings&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Generación de voz.
Quiero clonar mi voz y la de mi familia con modelos locales.&lt;/li&gt;
&lt;li&gt;Traducciones.
Un primer objetivo es automatizar la traducción de los artículos de mi blog, con revisión manual.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="criptomonedas"&gt;Criptomonedas&lt;/h2&gt;
&lt;p&gt;Las criptomonedas me apasionan desde hace muchos años, pero nunca me he involucrado como desarrollador, cosa de la que me arrepiento.&lt;/p&gt;
&lt;p&gt;Este año quiero empezar a colaborar como programador en proyectos afines, sobre todo aquellos que se centran en criptomonedas como medios de pago.&lt;/p&gt;
&lt;p&gt;También haré más esfuerzos en usar criptomonedas en el día a día, inspirado por &lt;a href="https://cointr.ee/thedesertlynx"&gt;Joel Valenzuela&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="moderar-esfuerzos-ideologicos"&gt;Moderar esfuerzos ideológicos&lt;/h2&gt;
&lt;p&gt;Quienes me conocen desde hace tiempo saben que mi ideología ha cambiado radicalmente en los últimos 10 años.
Sospecho que muchos lo vean como una traición a mis principios, o el haberme convertido en otra persona.
A mi modo de ver es una evolución natural producto de educarme y reflexionar sobre temas a los que no había estado expuesto con anterioridad.&lt;/p&gt;
&lt;p&gt;Lo que no anticipaba era el esfuerzo mental y emocional de oponerse al zeitgeist hegemónico, es algo que me ha pasado factura en cuanto a felicidad y salud mental.
Sería fácil plegarme a la mayoría y volver al redil, evitar discusiones y peleas, pero creo que me sentiría mal conmigo mismo por la deshonestidad intelectual.&lt;/p&gt;
&lt;p&gt;Mi propósito es por un lado preocuparme menos sobre mi pureza, pero por otro ser firme cuando toque debatir.
Una cosa son las ideas que tenga y otra es que no quiera o pueda dedicarle mayores esfuerzos en sostenerlas en oposición a casi toda la sociedad.&lt;/p&gt;
&lt;p&gt;Sí que me enfocaré en educar a mis hijos en los valores que quiero ver en el mundo.&lt;/p&gt;
&lt;h2 id="redes-sociales-y-mensajeria"&gt;Redes sociales y mensajería&lt;/h2&gt;
&lt;p&gt;Hace ya tiempo que reduje mi uso de redes sociales considerablemente, eliminando aplicaciones de mi teléfono móvil por ejemplo, pero este año quiero ir un paso más allá para poder enfocarme de verdad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consultaré mensajes y redes sociales sólo dos veces al día: por la mañana con el café y por la noche cuando se acuesten mis hijos.&lt;/li&gt;
&lt;li&gt;Eliminaré absolutamente todas las notificaciones.
Si alguien me necesita urgentemente ya me llamará, si no puede esperar.&lt;/li&gt;
&lt;li&gt;Dedicaré las redes sociales sólo a los temas funcionales que me interesan (software libre, criptomonedas, IA), y reduciré los temas ideológicos.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mas-blog"&gt;Más blog&lt;/h2&gt;
&lt;p&gt;Quiero escribir más en mi blog, de todos los temas que me interesan, y también alguna entrada personal como esta.
Necesito dejar el perfeccionismo a un lado y escribir más rápido, sin pensármelo tanto.&lt;/p&gt;
&lt;p&gt;Para empezar con buen pie, esta entrada la escribí en 2 horas de reloj.&lt;/p&gt;
&lt;p&gt;Empiezo a pensar en lo que será mi legado (sí, definitivamente estoy en una crisis de mediana edad), y mi blog puede convertirse en una ventana a mi vida y mis intereses para mis hijos.&lt;/p&gt;</content><category term="Personal"/><category term="personal"/></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>Anuncio público: mi apuesta de BTC con tripu</title><link href="https://blog.fidelramos.net/es/cryptocurrency/btc-bet" rel="alternate"/><published>2023-05-06T00:00:00+00:00</published><updated>2023-06-03T08:16:48.640883+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2023-05-06:/es/cryptocurrency/btc-bet</id><summary type="html">&lt;p&gt;Haciendo eco a la apuesta sobre los futuros precios relativos entre Bitcoin y el dólar estadounidense que Balaji y Medlock cerraron públicamente el 2023-05-02
(&lt;a href="https://twitter.com/balajis/status/1653451860488130565"&gt;confirmación de Balaji&lt;/a&gt;;
&lt;a href="https://twitter.com/jdcmedlock/status/1653452817179508736"&gt;confirmación de Medlock&lt;/a&gt;),
hoy &lt;a href="https://tripu.info/"&gt;tripu&lt;/a&gt; y yo acordamos los siguientes términos para nuestra particular apuesta:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;El objeto de la apuesta es si Bitcoin …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;Haciendo eco a la apuesta sobre los futuros precios relativos entre Bitcoin y el dólar estadounidense que Balaji y Medlock cerraron públicamente el 2023-05-02
(&lt;a href="https://twitter.com/balajis/status/1653451860488130565"&gt;confirmación de Balaji&lt;/a&gt;;
&lt;a href="https://twitter.com/jdcmedlock/status/1653452817179508736"&gt;confirmación de Medlock&lt;/a&gt;),
hoy &lt;a href="https://tripu.info/"&gt;tripu&lt;/a&gt; y yo acordamos los siguientes términos para nuestra particular apuesta:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;El objeto de la apuesta es si Bitcoin (BTC) alcanza un precio de 1 millón en dólares estadounidenses al menos una vez en los próximos 90 días.&lt;/li&gt;
&lt;li&gt;Si al menos dos fuentes de confianza anuncian que BTC superó la cotización de $1 millón en cualquier punto antes de las &lt;a href="https://www.timeanddate.com/countdown/generic?iso=20230804T235959&amp;amp;p0=1440&amp;amp;msg=Fidel%2Ftripu+BTC+bet&amp;amp;font=sanserif"&gt;23:59:59 UTC del 2023-08-04&lt;/a&gt;, Fidel ganaría la apuesta. En caso contrario, tripu ganaría la apuesta.&lt;/li&gt;
&lt;li&gt;Consideramos fuentes fiables los medios habituales de noticias e información financiera: Google Finance, Yahoo Finance, Bloomberg, el New York Times, etc.&lt;/li&gt;
&lt;li&gt;Fidel apuesta 500€ (quinientos euros) en dinero fiat. tripu apuesta 0.019195752 BTC (500€ en BTC usando el &lt;a href="https://finance.yahoo.com/quote/BTC-EUR/history?period1=1683244800&amp;amp;period2=1683244800&amp;amp;interval=1d&amp;amp;filter=history&amp;amp;frequency=1d&amp;amp;includeAdjustedClose=true"&gt;precio de cierre de 26.170,33€ en 2023-05-05&lt;/a&gt;). Quién pierda la apuesta pagará su parte al ganador, no más tarde de un mes después de que se resuelva la apuesta.&lt;/li&gt;
&lt;li&gt;Tanto Fidel como tripu publican este texto por duplicado (excepto ediciones triviales, como intercambiar nuestros nombres) en nuestros respectivos blogs hoy (2023-05-06).&lt;/li&gt;
&lt;li&gt;Cada uno enlazará al artículo del otro, y a copias archivadas de ambos en &lt;code&gt;archive.org&lt;/code&gt; o &lt;code&gt;archive.ph&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Somos caballeros (y amigos) y por tanto consideramos suficientes estos términos.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Enlaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Artículo de tripu: &lt;a href="https://blog.tripu.info/btc-bet"&gt;https://blog.tripu.info/btc-bet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Este artículo en archive.org:&lt;a href="https://web.archive.org/web/20230507142041/https://blog.fidelramos.net/es/cryptocurrency/btc-bet"&gt;https://web.archive.org/web/20230507142041/https://blog.fidelramos.net/es/cryptocurrency/btc-bet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Artículo de tripu en archive.org: &lt;a href="https://web.archive.org/web/20230507134521/https://blog.tripu.info/btc-bet/"&gt;https://web.archive.org/web/20230507134521/https://blog.tripu.info/btc-bet/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Cryptocurrency"/><category term="criptomonedas"/><category term="bitcoin"/><category term="btc"/><category term="fiat"/><category term="dinero"/><category term="inflación"/></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>Mi proceso fotográfico con software libre</title><link href="https://blog.fidelramos.net/es/photography/photography-workflow" rel="alternate"/><published>2022-03-31T00:00:00+00:00</published><updated>2022-04-05T00:00:00+00:00</updated><author><name>Fidel Ramos</name></author><id>tag:blog.fidelramos.net,2022-03-31:/es/photography/photography-workflow</id><summary type="html">&lt;p&gt;En este artículo presento una guía paso a paso de mi proceso fotográfico. No voy a meterme
en todos los detalles de todos los programas que menciono, dado que ya tienen su propia
documentación y manuales, me limitaré a resaltar las operaciones que realizo.&lt;/p&gt;
&lt;p&gt;Mis objetivos principales son:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Control total …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;En este artículo presento una guía paso a paso de mi proceso fotográfico. No voy a meterme
en todos los detalles de todos los programas que menciono, dado que ya tienen su propia
documentación y manuales, me limitaré a resaltar las operaciones que realizo.&lt;/p&gt;
&lt;p&gt;Mis objetivos principales son:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Control total sobre todos mis datos, sin servicios externos.&lt;/li&gt;
&lt;li&gt;Uso exclusivo de software libre.&lt;/li&gt;
&lt;li&gt;Rapidez en las operaciones habituales: descarga de nuevas fotos, edición, post-procesado.&lt;/li&gt;
&lt;li&gt;Organizar la colección con metadatos.&lt;/li&gt;
&lt;li&gt;Sincronización y backup automáticos.&lt;/li&gt;
&lt;li&gt;(A futuro) Acceso y compartición transparente de las fotos.&lt;/li&gt;
&lt;/ol&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-descargar-fotos-con-digikam"&gt;1. Descargar fotos con Digikam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#2-revisar-con-geeqie"&gt;2. Revisar con Geeqie&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3-procesado-de-raw-con-rawtherapee"&gt;3. Procesado de RAW con RawTherapee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4-organizar-en-digikam"&gt;4. Organizar en Digikam&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#5-replicacion-con-syncthing"&gt;5. Replicación con Syncthing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#6-copia-de-seguridad-con-restic"&gt;6. Copia de seguridad con restic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#epilogo"&gt;Epílogo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#actualizaciones"&gt;Actualizaciones&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="1-descargar-fotos-con-digikam"&gt;1. Descargar fotos con Digikam&lt;/h2&gt;
&lt;p&gt;Uso el fantástico programa &lt;a href="https://www.digikam.org/"&gt;Digikam&lt;/a&gt; para organizar mi colección
de fotos (181708 fotos y 3127 vídeos en 2198 álbumes a fecha de marzo 2022). Descargo las
fotos de mis cámaras con Digikam porque tiene un filtro muy útil "Sólo elementos nuevos"
(&lt;em&gt;Only new items&lt;/em&gt;), de forma que cuando conecto la cámara sólo veo los nuevos contenidos.&lt;/p&gt;
&lt;p&gt;Tengo dos colecciones de fotos en Digikam: &lt;em&gt;Fotos&lt;/em&gt; y &lt;em&gt;Fotos por ordenar&lt;/em&gt;. La primera son
las fotos que ya están procesadas y organizadas, y se sincroniza con mi servidor casero;
la segunda es donde descargo las nuevas fotos para procesarlas y organizarlas, y luego las
muevo a &lt;em&gt;Fotos&lt;/em&gt; cuando están listas.&lt;/p&gt;
&lt;p&gt;La mayoría de mis fotos están clasificadas en álbumes por fecha, y Digikam también es de
ayuda para esto porque se pueden seleccionar las fotos a descargar por fecha, haciendo
click en el título de sección, luego uso la opción "Descargar seleccionadas" (&lt;em&gt;Download
Selected&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_import.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_import.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Importar desde cámara en Digikam&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Digikam entonces pregunta dónde descargarlas, y ahí directamente puedo crear un nuevo álbum:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_import_create_new_album.png" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_import_create_new_album.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Crear nuevo álbum en Digikam durante la importación&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Le pongo la categoría, descripción y fecha al álbum directamente, para que no se me olvide hacerlo después:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_new_album.png" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_new_album.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Ventana de nuevo álbum en Digikam&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="2-revisar-con-geeqie"&gt;2. Revisar con Geeqie&lt;/h2&gt;
&lt;p&gt;Cuando se han descargado todas las fotos cambio a &lt;a href="https://www.geeqie.org/"&gt;Geeqie&lt;/a&gt; para
verlas y borrar las malas (desenfocadas, borrosas, sobreexpuestas o subexpuestas, etc.) Lo
llevo usando desde que se llamaba GqView, hay muchos otros visores de imágenes pero ya
estoy acostumbrado a éste.&lt;/p&gt;
&lt;p&gt;En este punto necesito mencionar que en mis cámaras uso el modo JPEG+RAW, aunque siempre
postprocese el RAW y borre los JPEG creados por la cámara. La razón es que me gusta poder
hacer zoom al 100% para ver los detalles de las fotos y borrar las malas lo antes posible,
y las imágenes RAW normalmente tienen las previsualizaciones a tamaño reducido (por
ejemplo los ficheros RAW creados por mi Sony A7 III tienen una previsualización de
1616x1080 píxeles, lo que es pequeño para una pantalla 4K).&lt;/p&gt;
&lt;p&gt;Hay dos características imprescindibles para mí en esta fase:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Agrupar los ficheros para ver sólo los JPEG, pero borrar todos los ficheros de un grupo.&lt;/li&gt;
&lt;li&gt;Mantener el zoom y la posición de la vista cuando se cambia de imagen.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El agrupado de ficheros significa que por defecto Geeqie sólo muestra el fichero JPEG,
mientras que los ficheros RAW y potencialmente otros (por ejemplo &lt;em&gt;sidecars&lt;/em&gt; como XMP o
PP3) se ocultan, pero pueden verse expandiendo el grupo con un click. De esta forma cuando
se borra un grupo se borran todos los ficheros, no sólo el JPEG. Geeqie reconoce la
mayoría de ficheros &lt;em&gt;sidecar&lt;/em&gt;, pero si es necesario se puede configurar en las
&lt;em&gt;Preferencias&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/geeqie_sidecar_grouping.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/geeqie_sidecar_grouping.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Configuración del agrupado de sidecar en Geeqie&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;La otra característica que necesito es el poder hacer zoom al 100% en una foto, cambiar a
la foto siguiente o anterior para compararlas, y que la vista se mantenga en la misma
posición, es decir que no se resetee a la esquina superior izquierda o algo así como hacen
otros visores. Geeqie tiene este comportamiento por defecto, combinado con sus atajos de
teclado (&lt;em&gt;Z&lt;/em&gt; para zoom 100%, &lt;em&gt;X&lt;/em&gt; para zoom ajustado a la ventana, &lt;em&gt;AvPág&lt;/em&gt; y &lt;em&gt;RePág&lt;/em&gt; para
cambiar de imagen) me funciona muy bien.&lt;/p&gt;
&lt;p&gt;Quiero mencionar que soy consciente de que Digikam tiene una funcionalidad para ver las
fotos y compararlas por parejas (&lt;em&gt;Lightroom&lt;/em&gt; en inglés): se ve una imagen a la izquierda,
la siguiente en la derecha, y cuando se cambia el zoom o se mueve la vista ambos paneles
se mantienen en sincronía. Pinta perfecto pero tiene un gran inconveniente para mí: cuando
borro una foto no borra el fichero RAW asociado, lo deja huérfano. No he encontrado una
forma de resolver esto en Digikam, lo que puedo hacer es ejecutar un comando para borrar
todos los ficheros huérfanos, o echar las fotos sólo en formato RAW (pero entonces
perdería resolución de previsualización). Por esta razón sigo usando Geeqie.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_lightroom.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_lightroom.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Digikam Lightroom&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="3-procesado-de-raw-con-rawtherapee"&gt;3. Procesado de RAW con RawTherapee&lt;/h2&gt;
&lt;p&gt;Actualmente proceso la mayoría de mis fotos con &lt;a href="https://rawtherapee.com/"&gt;RawTherapee&lt;/a&gt;,
un fabuloso software libre que es a la vez fácil de usar y produce fotos de muy alta
calidad. En el pasado he usado también &lt;a href="https://www.darktable.org/"&gt;Darktable&lt;/a&gt;, pero
aunque tiene más funcionalidades me costaba más tiempo y esfuerzo lograr los resultados
que suelo buscar, sobre todo porque la reducción de ruido de RawTherapee funciona mucho
mejor.&lt;/p&gt;
&lt;p&gt;En cualquier caso, abro el directorio con el álbum en RawTherapee, aplico un &lt;a href="https://rawpedia.rawtherapee.com/Creating_processing_profiles_for_general_use"&gt;perfil
base&lt;/a&gt; a
todas las fotos y luego aplico perfiles parcialmente para aclarar (&lt;em&gt;sharpen&lt;/em&gt;) imágenes con
bajo ruido, aplicar una leve reducción de ruido a las imágenes con ISO intermedio y
reducción de ruido agresiva para aquellas con ISOs elevados. Uso el filtrado para primero
seleccionar sólo los ficheros RAW y luego aplico subfiltros por ISO.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_filter.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_filter.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Filtrado en RawTherapee&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_profiles.png" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_profiles.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Perfiles de procesado en RawTherapee&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Después aplico corrección a la distorsión de lente a todas las fotos. La detección
automática de lente me falla por alguna razón que desconozco, así que uso el proceso en
grupo (&lt;em&gt;Batch Edit&lt;/em&gt;), selecciono todas las fotos en el álbum y selecciono manualmente la
cámara y lente.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_batch_edit.png" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_batch_edit.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Correción de distorsión de lente en RawTherapee&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lo siguiente es revisar las fotos una por una, haciendo ajustes: exposición, balance de
blancos, recorte, etc. No voy a entrar en detalles porque es edición normal con
RawTherapee. Si te interesa saber más no dudes en enviarme un mensaje y consideraré hacer
un artículo o vídeo al respecto.&lt;/p&gt;
&lt;p&gt;Cuando todo está listo pongo las fotos en la cola (&lt;em&gt;Queue&lt;/em&gt;) y las exporto al mismo
directorio donde están los ficheros RAW (&lt;code&gt;%p1/%f&lt;/code&gt; como la plantilla en &lt;em&gt;Output
Location&lt;/em&gt;). Cuando necesito retocar alguna foto después del revelado (normalmente en GIMP)
entonces uso formato TIFF, dado que JPEG es un formato con pérdida y perdería calidad si
se vuelve a modificar.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_queue.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/rawtherapee_queue.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Cola en RawTherapee&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="4-organizar-en-digikam"&gt;4. Organizar en Digikam&lt;/h2&gt;
&lt;p&gt;Cuando RawTherapee ha terminado de guardar las fotos borro los JPEG generados por la
cámara, luego vuelvo a Digikam y refresco (&lt;em&gt;Refresh&lt;/em&gt;) el álbum para ver los nuevos
ficheros.&lt;/p&gt;
&lt;p&gt;Lo siguiente es agrupar las fotos por nombre, para ver sólo los JPEG y no los RAW. En el
álbum selecciono todos los ficheros (CTRL+A) y luego con un click derecho en cualquier
imagen &lt;em&gt;Agrupar&lt;/em&gt; &amp;rarr; &lt;em&gt;Agrupar seleccionados por nombre de fichero&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_group.jpg" title="Click para ver a pantalla completa"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_group.jpg" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Agrupar ficheros en Digikam: antes y después&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A continuación uso la &lt;a href="https://docs.kde.org/trunk5/en/digikam-doc/digikam/using-digikam.html#using-mainwindow-peopleview"&gt;detección de
caras&lt;/a&gt;
para que Digikam reconozca las caras de las personas en las fotos. Selecciono todas las
fotos (CTRL+A), click derecho y &lt;em&gt;Escanear caras&lt;/em&gt; (&lt;em&gt;Scan for Faces&lt;/em&gt;). Cuando el proceso
termina voy a la pestaña de &lt;em&gt;Personas&lt;/em&gt; (&lt;em&gt;People&lt;/em&gt;) en la izquierda para revisar los
resultados, aceptar los buenos y corregir los malos.&lt;/p&gt;
&lt;p&gt;Después reviso las etiquetas. La organización por etiquetas es algo personal, pero yo
aprovecho que las etiquetas en Digikam tienen una estructura jerárquica, de forma que por
ejemplo uso etiquetas para la localización como &lt;code&gt;place&lt;/code&gt; &amp;rarr; &lt;code&gt;Colombia&lt;/code&gt; &amp;rarr;
&lt;code&gt;Zapatoca&lt;/code&gt;. En este punto también me aseguro que todas las personas estén etiquetadas,
dado que el reconocimiento facial falla a veces, y me gusta tener etiquetado a todo el
mundo para poder filtrar más adelante de forma efectiva.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_tags.png"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_tags.png"&gt;&lt;figcaption&gt;Etiquetas en Digikam&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Para acelerar el etiquetado le asigno atajos de teclado a las etiquetas que uso más
habitualmente. Es sólo cuestión de editar las propiedades de una tag con un click derecho
y luego asignarle el atajo deseado:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/digikam_edit_tag.png"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/digikam_edit_tag.png"&gt;&lt;figcaption&gt;Editar etiqueta en Digikam&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;El último paso es puntuar las fotos con 1 a 5 estrellas. Hago doble click en la primera
foto del álbum, uso los atajos de teclado para asignarle la puntuación deseada (de CTRL+1
a CTRL+5, esos son atajos por defecto) y las teclas de cursor atrás y adelante para
cambiar de foto. El significado de las puntuaciones es también algo personal, pero esta es
la lógica que uso yo para mi colección:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;1 estrella&lt;/em&gt;: Mala calidad o duplicado. No merece la pena verse salvo que busque algo
  específico. Si le estuviese mostrando el álbum a alguien probablemente excluiría esta
  foto.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;2 estrellas&lt;/em&gt;: Mi puntuación por defecto en caso de duda. No tiene mala calidad y puede
  ser interesante, pero tampoco es fundamental para el álbum.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;3 estrellas&lt;/em&gt;: Una foto representativa del álbum en el que se encuentra, es decir que si
  le mostrase a alguien sólo las fotos de 3 o más estrellas vería lo mejor del álbum sin
  tener que aguantar demasiadas fotos.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;4 estrellas&lt;/em&gt;: Buena foto: enfocada, buena iluminación y composición. Si me puede
  interesar buscar una foto como independiente de su álbum eso significa que es de 4
  estrellas.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;5 estrellas&lt;/em&gt;: Gran foto, o bien por sus características artísticas y técnicas o porque
  tiene un significado especial para mí (buena foto de mi hija, por ejemplo). Foto de la
  que me siento orgulloso.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esta organización me permite luego filtrar mi colección para encontrar lo que quiera, por
ejemplo todas las fotos con todos los miembros de mi familia con 4 estrellas o más, y
quizá filtrar por fecha también.&lt;/p&gt;
&lt;p&gt;Cuando termino de organizar el álbum lo muevo de mi colección &lt;em&gt;Fotos por organizar&lt;/em&gt; a
&lt;em&gt;Fotos&lt;/em&gt; sólo arrastrándola en Digikam. Una vez movidas se replicarán automáticamente a mi
servidor casero.&lt;/p&gt;
&lt;h2 id="5-replicacion-con-syncthing"&gt;5. Replicación con Syncthing&lt;/h2&gt;
&lt;p&gt;Uso &lt;a href="https://syncthing.net/"&gt;Syncthing&lt;/a&gt; para replicar mis datos entre mis diferentes
dispositivos (ordenador, servidor, teléfono móvil). Funciona a la perfección, lo
recomiendo encarecidamente.&lt;/p&gt;
&lt;p&gt;Especificamente para mi proceso fotográfico sincronizo dos directorios entre mi ordenador
y mi servidor casero:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El directorio con las fotos ya organizadas.&lt;/li&gt;
&lt;li&gt;El directorio con las bases de datos de Digikam en formato SQLite. Lo tengo separado de
  la colección de fotos por motivos históricos, porque antes tenía las fotos en un disco
  duro magnético mientras que mi disco principal era un NVMe, de forma que el rendimiento
  era mucho mejor si las bases de datos estaban en este último.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Configuro ambos directorioes como "sólo enviar" (&lt;em&gt;send-only&lt;/em&gt;) en el ordenador, y "sólo
recibir" (&lt;em&gt;read-only&lt;/em&gt;) en el servidor:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/syncthing_send_only.png"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/syncthing_send_only.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Configuración de directorio en Syncthing&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;También configuro el control de versiones en el servidor, para tener facilidad de
recuperación en caso de borrados o alteraciones erróneas:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.fidelramos.net/es/images/photography_workflow/syncthing_file_versioning.png"&gt;
&lt;figure class="align-center"&gt;&lt;img src="https://blog.fidelramos.net/es/images/photography_workflow/syncthing_file_versioning.png" title="Click para ver a pantalla completa"&gt;&lt;figcaption&gt;Versionado de ficheros en Syncthing&lt;/figcaption&gt;&lt;/figure&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Mi servidor casero tiene 4 discos duros, uno de los cuales está configurado como paridad
usando &lt;a href="https://www.snapraid.it/"&gt;SnapRAID&lt;/a&gt; para tolerancia a errores.  Puedes encontrar
más detalles sobre la configuración de mi servidor casero en &lt;a href="https://blog.fidelramos.net/software/homelab"&gt;mi artículo sobre mi
homelab&lt;/a&gt; (en inglés).&lt;/p&gt;
&lt;h2 id="6-copia-de-seguridad-con-restic"&gt;6. Copia de seguridad con restic&lt;/h2&gt;
&lt;p&gt;Mi servidor casero hace una copia de seguridad incremental cada semana a
&lt;a href="https://wasabi.com/"&gt;Wasabi&lt;/a&gt; usando
&lt;a href="https://restic.readthedocs.io/en/stable/"&gt;restic&lt;/a&gt;. La copia de seguridad incluye mi
colección de fotos completa, además de cualquier otro dato personal que no sería
recuperable en caso de catástrofe. Puedes ver &lt;a href="https://gist.github.com/haplo/db12fc973122366ab1e8cb0d17afbd0c"&gt;mi &lt;em&gt;script&lt;/em&gt; de backup con
restic&lt;/a&gt; y ajustarlo a tus
necesidades. Uso &lt;a href="https://www.ansible.com/"&gt;Ansible&lt;/a&gt; para generar las listas de ficheros
incluidos y excluidos. El &lt;em&gt;script&lt;/em&gt; es ejecutado por cron cada semana, de noche.&lt;/p&gt;
&lt;p&gt;¿Por qué restic? Cuando estaba investigando las opciones disponibles leí muy buenas
opiniones que decían que hacía las cosas bien, y la verdad es que me ha funcionado sin
demasiados problemas. Si tuviese que implementarlo todo otra vez me gustaría investigar
&lt;a href="https://www.borgbackup.org/"&gt;Borg&lt;/a&gt; como alternativa, pero es posible que volviese a
elegir restic.&lt;/p&gt;
&lt;p&gt;¿Por qué Wasabi? Tiene una API compatible con S3 y de los precios más baratos que pude
encontrar, más que Backblaze, sobre todo contando conque no tiene costes para el envío o
recepción de datos.&lt;/p&gt;
&lt;p&gt;Por supuesto la &lt;a href="https://restic.readthedocs.io/en/stable/070_encryption.html"&gt;copia de seguridad va
cifrada&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="epilogo"&gt;Epílogo&lt;/h2&gt;
&lt;p&gt;Este proceso es la evolución de 20 años de fotografía digital. Espero que hayas podido
aprender algo nuevo que puedas aplicar a tu propio proceso fotográfico. Actualizaré este
artículo a medida que haga cambios a las herramientas que uso.&lt;/p&gt;
&lt;p&gt;Quizá te estés preguntando sobre el último de los objetivos que mencioné al principio del
artículo: &lt;em&gt;"acceso y compartición transparente de las fotos"&lt;/em&gt;. Estoy trabajando en la
creación de un software de galería web que dé acceso a mi colección fotográfica completa y
que permita navegar utilizando los metadatos de Digikam. El software se ejecutaría en mi
servidor casero, donde ya se alojan las fotos y se sincronizan todas las nuevas. Esto
significa que tras organizar las fotos en Digikam estarían disponibles para su acceso
desde cualquier dispositivo de mi elección manteniendo el control sobre mis datos y sin
tener que subirlos a ningún otro servicio. Pienso liberar el software con licencia libre
cuando sus funcionalidades básicas funcionen, sigue el blog por RSS o sígueme en redes
sociales si te interesa estar al tanto de su lanzamiento.&lt;/p&gt;
&lt;p&gt;Si tienes cualquier comentario o sugerencia me encantaría oírlo, no dudes en mencionarme o
enviarme un mensaje por redes sociales, o un email. Puedes ver mis datos de contacto en
&lt;a href="https://fidelramos.net/"&gt;mi página web&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="actualizaciones"&gt;Actualizaciones&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;2022-04-05: Corregido el nombre de RawTherapee. Gracias por los &lt;a href="https://news.ycombinator.com/item?id=30918149"&gt;desternillantes
  comentarios, Hacker News&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;2024-01-01: Incluido enlace al artículo del homelab.&lt;/li&gt;
&lt;/ul&gt;</content><category term="Photography"/><category term="fotografía"/><category term="software-libre"/></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>