<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Blog de Pledin</title><description>Blog personal de José Domingo Muñoz</description><link>https://www.josedomingo.org/</link><item><title>Uno de mis cursos en awesome-extremadura</title><link>https://www.josedomingo.org/microblog/2026/04/uno-de-mis-cursos-en-awesomeextremadura/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/04/uno-de-mis-cursos-en-awesomeextremadura/</guid><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/04/extremadura.svg&quot; alt=&quot; &quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GeiserX/awesome-extremadura&quot;&gt;awesome-extremadura&lt;/a&gt; es una selección de software open source que da soporte específico a Extremadura, sus municipios, universidades e instituciones. Me han escrito para informarme que han incluido en la selección uno de mis cursos sobre Docker que impartí hace unos años para el CPR de Badajoz: &lt;a href=&quot;https://josedom24.github.io/curso_docker_2022/&quot;&gt;Curso Docker CPR Badajoz&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Me ha hecho mucha ilusión. Gracias!!!&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>Feed RSS/Atom: Novedades del portal de educación de la Junta de Andalucía</title><link>https://www.josedomingo.org/microblog/2026/04/feed-rssatom-novedades-del-portal-de-educacion-de-la-junta-de-andalucia/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/04/feed-rssatom-novedades-del-portal-de-educacion-de-la-junta-de-andalucia/</guid><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/04/ja.png&quot; alt=&quot; &quot;&gt;&lt;/p&gt;
&lt;p&gt;Hace unos meses, posiblemente en la última actualización de la página de &lt;a href=&quot;https://www.juntadeandalucia.es/educacion/portales/novedades-portada&quot;&gt;&lt;strong&gt;Novedades del portal de educación de la Junta de Andalucía&lt;/strong&gt;&lt;/a&gt;, la opción de sindicación de contenidos mediante RSS desapareció y dejé de recibir actualizaciones en mi lector.&lt;/p&gt;
&lt;p&gt;Como no soy dado a revisar la página periódicamente, he desarrollado un pequeño script en Python que genera automáticamente el archivo XML del feed. Al alojarlo en GitHub Pages, puedo volver a acceder a él desde mi cliente RSS habitual.&lt;/p&gt;
&lt;p&gt;Gracias a GitHub Actions, el feed se actualiza automáticamente. Podéis suscribiros a las novedades accediendo a este &lt;a href=&quot;https://josedom24.github.io/educacion-novedades-rss/feed.xml&quot;&gt;link&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Para quienes tengan curiosidad o quieran ver el código, os dejo el &lt;a href=&quot;https://github.com/josedom24/educacion-novedades-rss&quot;&gt;repositorio&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>Migración de Pledin a Astro</title><link>https://www.josedomingo.org/2026/03/migracion-pledin-astro/</link><guid isPermaLink="true">https://www.josedomingo.org/2026/03/migracion-pledin-astro/</guid><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/04/astro.svg&quot; alt=&quot;pledin&quot;&gt;&lt;/p&gt;
&lt;p&gt;Desde septiembre de 2018, mi página &lt;strong&gt;Pledin - Plataforma Educativa Informática&lt;/strong&gt; ha sido una web estática generada por Jekyll, si quieres más información de como fue construida te invito a leer el artículo: &lt;a href=&quot;https://www.josedomingo.org/pledin/2018/09/bienvenidos-a-pledin30/&quot;&gt;Bienvenidos a PLEDIN 3.0&lt;/a&gt;. Después de estos años usando &lt;strong&gt;Jekyll&lt;/strong&gt; como generador de sitios estáticos para este blog, he decidido migrar a &lt;strong&gt;Astro&lt;/strong&gt;. En este artículo explico qué es Astro, por qué lo he elegido y el proceso que he llevado a cabo para esta migración.&lt;/p&gt;
&lt;h2&gt;¿Qué es Astro?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt; es un framework moderno para construir sitios web, especialmente pensado para sitios donde el contenido es lo más importante: blogs, documentación, portfolios...&lt;/p&gt;
&lt;p&gt;La filosofía de Astro es sencilla: generar páginas HTML simples y rápidas, enviando al navegador solo lo imprescindible. Mientras otros frameworks cargan grandes cantidades de JavaScript aunque no haga falta, Astro hace justo lo contrario.&lt;/p&gt;
&lt;p&gt;Algunas de sus características más interesantes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;/strong&gt;: puedes usar componentes de React, Vue, Svelte... o simplemente los componentes propios de Astro, que tienen una sintaxis muy parecida al HTML de toda la vida.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gestión de contenido integrada&lt;/strong&gt;: permite organizar los ficheros Markdown de forma estructurada, con validación automática de los metadatos de cada página.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensible&lt;/strong&gt;: dispone de un sistema de plugins oficial para añadir funcionalidades como generación de sitemap, feed RSS, soporte para MDX, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rendimiento&lt;/strong&gt;: genera sitios extremadamente rápidos gracias a la eliminación de JavaScript innecesario.&lt;/li&gt;
&lt;/ul&gt;
&lt;!--more--&gt;
&lt;h2&gt;¿Por qué migrar de Jekyll a Astro?&lt;/h2&gt;
&lt;p&gt;Jekyll ha sido una herramienta excelente durante años, pero tiene algunas limitaciones que con el tiempo se hacen evidentes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requiere Ruby y sus dependencias, lo que puede ser un problema en entornos modernos.&lt;/li&gt;
&lt;li&gt;El sistema de plantillas Liquid, aunque funcional, es limitado comparado con componentes modernos.&lt;/li&gt;
&lt;li&gt;La velocidad de build con muchos posts es lenta.&lt;/li&gt;
&lt;li&gt;La integración con herramientas modernas de frontend es complicada.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Antes de pasarme a Astro, mi web funcionaba con Jekyll y la plantilla &lt;strong&gt;Minimal Mistakes&lt;/strong&gt;, una de las más populares del ecosistema Jekyll. Con el tiempo empezaron a aparecer warnings en cada build, una señal de que mantener el sitio en pie iba a exigir cada vez más esfuerzo.&lt;/p&gt;
&lt;p&gt;Los warnings eran de dos tipos. Por un lado, una &lt;strong&gt;deprecación en Ruby&lt;/strong&gt;: alguna dependencia del proyecto usaba una API interna que ya había cambiado en versiones modernas. No rompía nada de momento, pero indicaba que el stack no estaba actualizado, y que una actualización de Ruby podía acabar rompiendo el build.&lt;/p&gt;
&lt;p&gt;Por otro lado, una larga lista de warnings relacionados con &lt;strong&gt;Sass&lt;/strong&gt; y la evolución de Dart Sass hacia versiones más estrictas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deprecación de &lt;code&gt;@import&lt;/code&gt;&lt;/strong&gt;: la plantilla cargaba sus estilos mediante reglas &lt;code&gt;@import&lt;/code&gt; encadenadas. Esta forma de importar archivos Sass desaparecerá en Dart Sass 3.0.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecación del operador &lt;code&gt;/&lt;/code&gt; para divisiones&lt;/strong&gt;: varias partes del código usaban &lt;code&gt;/&lt;/code&gt; directamente para hacer operaciones matemáticas en Sass, algo eliminado en favor de &lt;code&gt;math.div()&lt;/code&gt; o &lt;code&gt;calc()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecación de funciones globales de color&lt;/strong&gt;: la plantilla usaba funciones como &lt;code&gt;mix()&lt;/code&gt;, &lt;code&gt;red()&lt;/code&gt;, &lt;code&gt;green()&lt;/code&gt; o &lt;code&gt;blue()&lt;/code&gt; en su forma global, que también están marcadas para desaparecer en Dart Sass 3.0.0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deprecación de &lt;code&gt;mixed-decls&lt;/code&gt;&lt;/strong&gt;: Sass va a cambiar cómo maneja propiedades CSS mezcladas con reglas anidadas, algo que afecta a varios archivos internos de la plantilla.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frente a esto, Astro ofrece una experiencia de desarrollo moderna basada en JavaScript y TypeScript, con un ecosistema muy activo y una filosofía que encaja perfectamente con lo que necesitaba: generar HTML estático de forma eficiente, sin cargar JavaScript innecesario al navegador.&lt;/p&gt;
&lt;h2&gt;El proceso de migración&lt;/h2&gt;
&lt;p&gt;Debo mencionar que empecé probando distintas plantillas de astro como &lt;strong&gt;Starlight&lt;/strong&gt;. Sin embargo, después de varios intentos de hacerlo encajar en lo que necesitaba (www es un blog, y ese tema está pensado para la documentación), volví a comenzar de nuevo sin utilizar ninguna plantilla, fui construyendo desde 0, los distintos elementos de la página:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Componentes&lt;/strong&gt;: cabecera, pie de página, tarjetas de post, sistema de navegación...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layouts&lt;/strong&gt;: un layout base y layouts específicos para la portada, los posts y las páginas estáticas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Páginas&lt;/strong&gt;: índice, página de post individual, sección de etiquetas...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estilos&lt;/strong&gt;: sistema de variables CSS, tipografía, modo oscuro...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Durante todo este proceso usé la IA, principalmente Claude, como asistente. El flujo habitual era: yo definía qué quería construir, Claude proponía una implementación, y yo la revisaba, ajustaba y la integraba en el proyecto. Útil especialmente para no atascarse en detalles de sintaxis de Astro o en patrones que aún no conocía bien.&lt;/p&gt;
&lt;h2&gt;Estructura del proyecto&lt;/h2&gt;
&lt;p&gt;El sitio se ha organizado como un &lt;strong&gt;monorepo&lt;/strong&gt; con tres aplicaciones independientes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;www&lt;/strong&gt; → Blog personal (&lt;a href=&quot;https://www.josedomingo.org&quot;&gt;www.josedomingo.org&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;plataforma&lt;/strong&gt; → Catálogo de cursos (&lt;a href=&quot;https://plataforma.josedomingo.org&quot;&gt;plataforma.josedomingo.org&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fp&lt;/strong&gt; → Módulos de Formación Profesional (&lt;a href=&quot;https://fp.josedomingo.org&quot;&gt;fp.josedomingo.org&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;www-astro/
├── apps/
│   ├── www/          # Blog personal
│   ├── plataforma/   # Catálogo de cursos
│   └── fp/           # Módulos FP
└── packages/
    └── ui/           # Componentes y layouts compartidos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puedes ver el código completo en el &lt;a href=&quot;https://github.com/josedom24/www-astro&quot;&gt;repositorio de GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Durante el desarrollo de la migración se han tenido que ir solucionando distintos problemas que enumeramos a continuación:&lt;/p&gt;
&lt;h3&gt;Componentes y layouts compartidos&lt;/h3&gt;
&lt;p&gt;Una de las ventajas de usar Astro es poder reutilizar código entre las tres aplicaciones. Para ello se ha creado el paquete &lt;code&gt;@pledin/ui&lt;/code&gt; que contiene dos tipos de elementos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layouts&lt;/strong&gt;: son las plantillas que definen la estructura general de cada tipo de página. Por ejemplo, &lt;code&gt;BaseLayout&lt;/code&gt; define la cabecera, el menú de navegación y el pie de página común a todo el sitio. &lt;code&gt;BlogLayout&lt;/code&gt; añade además una barra lateral con el índice del blog, y &lt;code&gt;DocLayout&lt;/code&gt; está pensado para las páginas de documentación de los módulos FP, con navegación lateral y tabla de contenidos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Componentes&lt;/strong&gt;: son piezas reutilizables más pequeñas que se insertan dentro de los layouts o las páginas. Por ejemplo, &lt;code&gt;PostCard&lt;/code&gt; muestra la vista previa de un post del blog, &lt;code&gt;TagList&lt;/code&gt; muestra las etiquetas de un artículo, o &lt;code&gt;PostNav&lt;/code&gt; muestra los enlaces al post anterior y siguiente.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Manteniendo las URLs&lt;/h3&gt;
&lt;p&gt;Uno de los requisitos más importantes era no romper las URLs existentes, ya que llevan años indexadas y referenciadas. En la gran mayoría de los casos las URLs se han mantenido exactamente igual. Para los pocos casos donde no fue posible, se añadieron redirecciones en el fichero &lt;code&gt;.htaccess&lt;/code&gt; del servidor Apache.&lt;/p&gt;
&lt;h3&gt;Limpieza de directivas Liquid y Kramdown en los archivos Markdown&lt;/h3&gt;
&lt;p&gt;Otro de los trabajos previos a la migración fue revisar los archivos Markdown para eliminar directivas que son específicas de Jekyll y que Astro no entiende, por ejemplo, &lt;strong&gt;&lt;code&gt;{: .center}&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Esta es una sintaxis propia de &lt;strong&gt;Kramdown&lt;/strong&gt;, el procesador de Markdown que usa Jekyll. Permite aplicar atributos HTML directamente sobre un elemento, en este caso una clase CSS.&lt;/p&gt;
&lt;p&gt;En Astro el procesador de Markdown es diferente (Remark), y esta sintaxis simplemente no existe. Había que eliminarla o sustituirla por la alternativa correspondiente.&lt;/p&gt;
&lt;p&gt;En resumen, parte del trabajo de migración no fue solo mover archivos, sino limpiar los Markdown de todas estas dependencias con el ecosistema Jekyll, que en muchos casos estaban repartidas por decenas de archivos.&lt;/p&gt;
&lt;h3&gt;Conversión automática de bloques notice&lt;/h3&gt;
&lt;p&gt;En Jekyll, los bloques de aviso se construían con una combinación de etiquetas Liquid y HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-liquid&quot;&gt;{% capture notice-text %}
## Título del aviso
Contenido del aviso.
{% endcapture %}
&amp;lt;div class=&amp;quot;notice--info&amp;quot;&amp;gt;{{ notice-text | markdownify }}&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En Astro, la forma equivalente es usando directivas de Remark, mucho más limpias:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;:::tip[Título del aviso]
Contenido del aviso.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Como este patrón se repetía en decenas de archivos, escribimos un script Python que recorría todos los Markdown del proyecto, detectaba estos bloques y los convertía automáticamente, traduciendo además las clases CSS de Minimal Mistakes (&lt;code&gt;notice--info&lt;/code&gt;, &lt;code&gt;notice--warning&lt;/code&gt;, &lt;code&gt;notice--danger&lt;/code&gt;...) a sus tipos de directiva correspondientes en Remark. Con un solo comando, todos los archivos quedaban migrados sin intervención manual.&lt;/p&gt;
&lt;h3&gt;El frontmatter de los ficheros Markdown&lt;/h3&gt;
&lt;p&gt;Una de las buenas noticias de la migración es que el contenido apenas ha necesitado cambios. La mayoría de los ficheros Markdown se han podido reutilizar tal cual, con dos pequeños ajustes en el blog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Se ha añadido el campo &lt;code&gt;date&lt;/code&gt; de forma explícita.&lt;/li&gt;
&lt;li&gt;El campo &lt;code&gt;permalink&lt;/code&gt; se ha renombrado a &lt;code&gt;slug&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;En las otras dos aplicaciones (plataforma y fp) no ha sido necesario tocar ningún fichero de contenido.&lt;/p&gt;
&lt;h3&gt;Comentarios&lt;/h3&gt;
&lt;p&gt;En la versión anterior del sitio los comentarios funcionaban con &lt;a href=&quot;https://www.josedomingo.org/pledin/2021/05/comentarios-pledin-staticman/&quot;&gt;Staticman&lt;/a&gt;, una herramienta que permite añadir comentarios a sitios estáticos guardándolos directamente en el repositorio Git como ficheros YAML o JSON. Era una solución elegante para un sitio sin backend, pero requería mantener un servidor propio con la aplicación Staticman funcionando.&lt;/p&gt;
&lt;p&gt;Al migrar a Astro, todos los comentarios existentes se han conservado convirtiéndolos a ficheros JSON, uno por post. Cada fichero contiene un array con los comentarios del post, incluyendo nombre, fecha, mensaje y opcionalmente la URL y el email del autor.&lt;/p&gt;
&lt;p&gt;Para mostrarlos se ha creado un componente &lt;code&gt;Comments.astro&lt;/code&gt; que recibe esos comentarios como prop y los renderiza. El mensaje se procesa con &lt;code&gt;marked&lt;/code&gt; para interpretar el Markdown que algunos comentarios podían contener. Para el avatar, si el comentario incluye email se usa &lt;a href=&quot;https://gravatar.com&quot;&gt;Gravatar&lt;/a&gt; para obtener la imagen del usuario; si no, se muestra la inicial del nombre sobre un fondo de color.&lt;/p&gt;
&lt;p&gt;Para los comentarios nuevos se ha adoptado &lt;a href=&quot;https://giscus.app&quot;&gt;Giscus&lt;/a&gt;, una solución que almacena los comentarios en las &lt;strong&gt;Discussions de GitHub&lt;/strong&gt;. Cada post del blog tiene su propia discusión asociada en el repositorio, y Giscus se encarga de cargarla y mostrarla mediante un widget embebido.&lt;/p&gt;
&lt;p&gt;Las ventajas respecto a Staticman son claras: no requiere ningún servidor propio, se autentica con GitHub, y el mantenimiento es prácticamente nulo. El único requisito es que el repositorio sea público y tenga las Discussions activadas.&lt;/p&gt;
&lt;p&gt;En la página de cada post aparecen primero los comentarios históricos migrados desde Staticman y a continuación el widget de Giscus para nuevos comentarios, de forma que el histórico queda preservado y la sección sigue siendo funcional.&lt;/p&gt;
&lt;h3&gt;Búsqueda con Pagefind&lt;/h3&gt;
&lt;p&gt;Una de las funcionalidades que quería mantener en la migración era el buscador. En Jekyll lo tenía resuelto con un plugin, pero en Astro necesitaba una solución que funcionara con sitios estáticos, sin ningún servidor backend.&lt;/p&gt;
&lt;p&gt;La solución elegida fue &lt;a href=&quot;https://pagefind.app&quot;&gt;Pagefind&lt;/a&gt;, una librería de búsqueda estática que funciona de la siguiente manera: tras el build de Astro, se ejecuta Pagefind sobre el directorio &lt;code&gt;dist/&lt;/code&gt; generado, indexa todo el contenido HTML y genera un índice de búsqueda en formato binario optimizado. Ese índice se sirve junto con el resto de ficheros estáticos, y la búsqueda se realiza completamente en el navegador sin ninguna petición a un servidor.&lt;/p&gt;
&lt;p&gt;La integración en el proceso de build es muy sencilla. En el &lt;code&gt;package.json&lt;/code&gt; de cada aplicación, el script de build encadena Astro y Pagefind:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;astro build &amp;amp;&amp;amp; npx pagefind --site dist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para mostrar el buscador en la interfaz se usa el componente oficial &lt;code&gt;PagefindUI&lt;/code&gt;, que se integra directamente en el layout base. En desarrollo el buscador no funciona porque el índice no existe hasta que se hace un build — es el único inconveniente, pero es perfectamente asumible.&lt;/p&gt;
&lt;p&gt;El resultado es un buscador rápido, sin dependencias externas, que funciona igual en los tres sitios del monorepo: &lt;code&gt;www&lt;/code&gt;, &lt;code&gt;plataforma&lt;/code&gt; y &lt;code&gt;fp&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Revistas Libres de Software Libre&lt;/h3&gt;
&lt;p&gt;Aprovechando la migración, se ha rescatado y renovado una sección que llevaba años bastante abandonada: la colección de &lt;strong&gt;Revistas Libres de Software Libre&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Esta sección recoge revistas digitales gratuitas sobre Software Libre, Linux, educación y tecnología. Muchas de ellas ya no están activas, pero forman parte de la historia del movimiento del software libre hispanohablante: publicaciones como TuxInfo, ATIX, Papirux o Begins que nacieron de comunidades locales de Argentina, Bolivia, Chile, Colombia, Cuba o México, y que durante años fueron una referencia para sus lectores.&lt;/p&gt;
&lt;p&gt;La lista se ha reorganizado en dos secciones: &lt;strong&gt;revistas activas&lt;/strong&gt;, con su último número publicado actualizado, y un &lt;strong&gt;archivo histórico&lt;/strong&gt; con las que ya no publican, indicando el número de ejemplares y los años en que estuvieron en activo.&lt;/p&gt;
&lt;p&gt;La implementación técnica es sencilla: toda la información está centralizada en un fichero &lt;code&gt;revistas.json&lt;/code&gt; que la página de Astro lee directamente y renderiza en forma de tarjetas. Cada tarjeta incluye la portada de la revista, una descripción, el último número disponible y enlaces para acceder a la colección y a la página web original. Añadir o actualizar una revista es tan simple como editar ese fichero JSON.&lt;/p&gt;
&lt;p&gt;Los PDFs de todas las revistas están alojados en un servidor Nextcloud propio, garantizando que los enlaces no dejen de funcionar aunque las páginas originales desaparezcan — algo que ya ha ocurrido con varias de ellas.&lt;/p&gt;
&lt;h2&gt;Desarrollo y despliegue&lt;/h2&gt;
&lt;h3&gt;Servidor de desarrollo&lt;/h3&gt;
&lt;p&gt;Durante el desarrollo, Astro incluye un servidor web local que recarga automáticamente el navegador cada vez que se modifica un fichero. Para arrancar cada aplicación:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev:www
npm run dev:plataforma
npm run dev:fp
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Proceso de build y despliegue&lt;/h3&gt;
&lt;p&gt;Cuando el sitio está listo para publicar, Astro genera todos los ficheros HTML estáticos mediante el proceso de build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build:www
npm run build:plataforma
npm run build:fp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Para automatizar el despliegue se ha creado un script que realiza tres acciones en orden: hace un commit con los cambios, los sube a GitHub, construye la aplicación en local y finalmente sincroniza los ficheros generados con el servidor mediante &lt;code&gt;rsync&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Migración a Astro 6&lt;/h3&gt;
&lt;p&gt;Poco después de terminar la migración de Jekyll a Astro 5, se publicó &lt;strong&gt;Astro 6&lt;/strong&gt;. El proceso de actualización fue sencillo, aunque requirió resolver algunos detalles.&lt;/p&gt;
&lt;p&gt;Astro 6 requiere Node.js 22.12.0 o superior. Para actualizar en Debian:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://zod.dev&quot;&gt;Zod&lt;/a&gt; es una librería de validación de esquemas para TypeScript. En Astro se usa para definir y validar la estructura del frontmatter de los ficheros Markdown: qué campos son obligatorios, qué tipo tiene cada uno, cuáles son opcionales... &lt;code&gt;z&lt;/code&gt; es el objeto principal de Zod a partir del cual se construyen todos los esquemas: &lt;code&gt;z.string()&lt;/code&gt;, &lt;code&gt;z.boolean()&lt;/code&gt;, &lt;code&gt;z.array()&lt;/code&gt;...&lt;/p&gt;
&lt;p&gt;En Astro 6, &lt;code&gt;z&lt;/code&gt; ya no se importa desde &lt;code&gt;astro:content&lt;/code&gt; sino desde &lt;code&gt;astro/zod&lt;/code&gt;. El cambio afecta a los ficheros &lt;code&gt;content.config.ts&lt;/code&gt; de las tres aplicaciones:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;- import { defineCollection, z } from &amp;#39;astro:content&amp;#39;;
+ import { defineCollection } from &amp;#39;astro:content&amp;#39;;
+ import { z } from &amp;#39;astro/zod&amp;#39;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Los schemas en sí no necesitaron ningún cambio, ya que son simples (&lt;code&gt;z.string()&lt;/code&gt;, &lt;code&gt;z.boolean()&lt;/code&gt;, &lt;code&gt;z.array()&lt;/code&gt;...) y son totalmente compatibles con Zod 4.&lt;/p&gt;
&lt;p&gt;Astro dispone de un comando oficial que detecta y actualiza automáticamente todas las integraciones:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd apps/www &amp;amp;&amp;amp; npx @astrojs/upgrade
cd ../plataforma &amp;amp;&amp;amp; npx @astrojs/upgrade
cd ../fp &amp;amp;&amp;amp; npx @astrojs/upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;También hay que actualizar la versión de Astro declarada en &lt;code&gt;packages/ui/package.json&lt;/code&gt;, que al ser un monorepo actúa como versión de referencia para todo el workspace. Tras hacer el cambio, hay que limpiar e instalar de nuevo las dependencias:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rm -rf node_modules
npm install
npx astro --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusiones&lt;/h2&gt;
&lt;p&gt;La migración de Jekyll a Astro ha sido un proceso largo pero satisfactorio. El resultado es un sitio más moderno, más rápido y mucho más fácil de mantener y extender.&lt;/p&gt;
&lt;p&gt;Desde el punto de vista técnico, los cambios más significativos han sido:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Velocidad de build&lt;/strong&gt;: el tiempo de construcción se ha reducido drásticamente. Lo que antes tardaba varios minutos en Jekyll ahora se resuelve en segundos en local.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mantenibilidad&lt;/strong&gt;: prescindir de Ruby y sus dependencias y trabajar con un stack basado en JavaScript/TypeScript, simplifica enormemente el mantenimiento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reutilización de código&lt;/strong&gt;: la organización en monorepo con un paquete &lt;code&gt;@pledin/ui&lt;/code&gt; compartido entre las tres aplicaciones evita duplicar componentes y estilos, y garantiza una experiencia coherente en los tres sitios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buscador integrado&lt;/strong&gt;: gracias a &lt;a href=&quot;https://pagefind.app&quot;&gt;Pagefind&lt;/a&gt;, los tres sitios disponen ahora de un buscador estático que funciona sin ningún servidor backend, generado automáticamente tras cada build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Desde el punto de vista del proceso, la experiencia de usar IA como asistente durante el desarrollo ha sido muy positiva. No como sustituto del criterio propio, sino como herramienta para avanzar más rápido en los detalles de implementación, resolver dudas de sintaxis y explorar alternativas sin tener que detenerse a buscar en la documentación en cada paso.&lt;/p&gt;
&lt;p&gt;El código fuente del proyecto está disponible en &lt;a href=&quot;https://github.com/josedom24/www-astro&quot;&gt;GitHub&lt;/a&gt; por si puede ser de utilidad para alguien que esté pensando en hacer una migración similar.&lt;/p&gt;
&lt;h2&gt;Galería Pledin 3.0&lt;/h2&gt;
&lt;p&gt;Dejo por aquí algunas capturas de pantalla de las páginas web antes de la migración.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/www.png&quot; alt=&quot;pledin&quot;&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/blog.png&quot; alt=&quot;pledin&quot;&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/microblog.png&quot; alt=&quot;pledin&quot;&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/revistas.png&quot; alt=&quot;pledin&quot;&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/plataforma.png&quot; alt=&quot;pledin&quot;&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/fp.png&quot; alt=&quot;pledin&quot;&gt;&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Seguridad y control de acceso en una VPN tailscale/headscale: ACLs y Tags</title><link>https://www.josedomingo.org/2026/03/vpn-mesh-headscale-seguridad-control-acceso/</link><guid isPermaLink="true">https://www.josedomingo.org/2026/03/vpn-mesh-headscale-seguridad-control-acceso/</guid><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/headscale3.png&quot; alt=&quot;vpn&quot;&gt;&lt;/p&gt;
&lt;p&gt;Hasta ahora, nuestra red mesh ha funcionado bajo una confianza total: cualquier dispositivo que uníamos a nuestra red podía comunicarse con los demás. Sin embargo, en entornos educativos o profesionales, necesitamos aplicar el principio de &lt;strong&gt;mínimo privilegio&lt;/strong&gt;. En este artículo aprenderemos a usar las &lt;strong&gt;ACLs&lt;/strong&gt; y los &lt;strong&gt;Tags&lt;/strong&gt; para crear una red donde los alumnos puedan acceder a los recursos del aula, pero estén aislados entre sí.&lt;/p&gt;
&lt;h2&gt;Conceptos claves: ACLs y Tags&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Las &lt;strong&gt;ACLs (Listas de Control de Acceso)&lt;/strong&gt; son un conjunto de reglas en formato JSON (o HuJSON) que definen quién puede hablar con quién. Si no hay una regla que permita la conexión, Headscale la denegará por defecto.&lt;/li&gt;
&lt;li&gt;Los &lt;strong&gt;Tags&lt;/strong&gt; (etiquetas) nos permiten agrupar dispositivos por su función en lugar de por su nombre o usuario. Por ejemplo, podemos etiquetar un equipo como &lt;code&gt;tag:servidor&lt;/code&gt; y otros como &lt;code&gt;tag:alumno&lt;/code&gt;. Cuando un dispositivo se registra con un Tag, deja de &amp;quot;pertenecer&amp;quot; al usuario que lo registró y pasa a ser propiedad del sistema, lo que facilita la gestión de permisos globales.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Caso práctico: El Aula Aislada&lt;/h2&gt;
&lt;p&gt;Para entender los conceptos de seguridad usando ACLs y Tags vamos a ver un ejemplo concreto. en este ejemplo vamos a estudiar dos aproximaciones distintas para tener un conjunto de clientes (en nuestro caso alumnos) que acceden a una red local (en nuestro caso una red de un aula). Los objetivos a cumplir son los siguientes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Los alumnos se conectan a la VPN.&lt;/li&gt;
&lt;li&gt;Existe un &lt;strong&gt;Router&lt;/strong&gt; que anuncia la red local del aula (&lt;code&gt;192.168.100.0/24&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Los alumnos &lt;strong&gt;pueden&lt;/strong&gt; acceder a los equipos de la red local.&lt;/li&gt;
&lt;li&gt;Los alumnos &lt;strong&gt;NO pueden&lt;/strong&gt; verse ni hacerse ping entre ellos.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Para conseguir dichos objetivos, vamos a estudiar dos aproximaciones distintas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Estrategia 1: Red Unificada con Control por Etiquetas (Tags)&lt;/li&gt;
&lt;li&gt;Estrategia 2: Segmentación por Usuarios (Namespaces Aislados)&lt;/li&gt;
&lt;/ul&gt;
&lt;!--more--&gt;

&lt;h2&gt;Estrategia 1: Red unificada con control por Etiquetas (Tags)&lt;/h2&gt;
&lt;p&gt;Esta estrategia se basa en mantener a todos los nodos dentro de un mismo espacio compartido (el mismo usuario), pero diferenciando sus capacidades mediante el uso de &lt;strong&gt;etiquetas lógicas&lt;/strong&gt;. Al asignar un &lt;code&gt;tag:alumno&lt;/code&gt; o &lt;code&gt;tag:router&lt;/code&gt;, el motor de seguridad de Headscale deja de aplicar permisos basados en quién inició sesión y pasa a aplicar reglas estrictas definidas en el archivo de política: si una regla no autoriza explícitamente que un &amp;quot;alumno&amp;quot; hable con otro, el sistema bloquea esa comunicación de forma nativa. Es la opción más escalable y profesional, ya que permite gestionar cientos de dispositivos con apenas un par de líneas de configuración centralizada, manteniendo un control granular sobre puertos y protocolos.&lt;/p&gt;
&lt;p&gt;Lo primero que tenemos que hacer es activar la configuración de las ACLs, para ello en el fichero de configuración indicamos el fichero donde vamos a escribir nuestras ACLs, para ello en tu &lt;code&gt;config.yaml&lt;/code&gt;, añadimos el parámetro &lt;code&gt;path&lt;/code&gt; y reiniciamos el servicio:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;policy:
  mode: file
  path: /etc/headscale/acl.hujson
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Headscale utiliza un formato llamado &lt;strong&gt;HuJSON&lt;/strong&gt; (JSON con comentarios y comas finales permitidas), lo que facilita mucho su mantenimiento. El archivo de políticas se divide en bloques lógicos que el motor de Headscale procesa de arriba hacia abajo para decidir si permite o bloquea un paquete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Grupos (&lt;code&gt;groups&lt;/code&gt;): Permiten agrupar &lt;strong&gt;usuarios reales&lt;/strong&gt; bajo un alias. Es muy útil para no repetir nombres de personas en las reglas.&lt;/li&gt;
&lt;li&gt;Propietarios de Etiquetas (&lt;code&gt;tagOwners&lt;/code&gt;): Esta es una sección de &lt;strong&gt;seguridad crítica&lt;/strong&gt;. Define qué usuarios tienen permiso para asignar qué etiquetas (&lt;code&gt;tags&lt;/code&gt;) a sus dispositivos. Sin esto, cualquier usuario podría etiquetarse como &amp;quot;servidor&amp;quot; para saltarse las reglas.&lt;/li&gt;
&lt;li&gt;Reglas de Acceso (&lt;code&gt;acls&lt;/code&gt;): Es el corazón del archivo. Cada regla define una &lt;strong&gt;acción&lt;/strong&gt;, un &lt;strong&gt;origen&lt;/strong&gt;, y un &lt;strong&gt;destino&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Action:&lt;/strong&gt; Actualmente solo existe &lt;code&gt;accept&lt;/code&gt;. Todo lo que no esté explícitamente aceptado, se deniega (&lt;strong&gt;Deny por defecto&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Src (Source):&lt;/strong&gt; Quién origina la conexión. Puede ser un usuario, un grupo, un tag, una IP o &lt;code&gt;*&lt;/code&gt; (todos).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dst (Destination):&lt;/strong&gt; A qué se intenta acceder. Se define como &lt;code&gt;objetivo:puerto&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El funcionamiento general del sistema de seguridad es el siguiente:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Denegación implícita:&lt;/strong&gt; Si no escribes ninguna regla, ningún nodo puede hablar con nadie.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unidireccionalidad:&lt;/strong&gt; Las reglas definen quién inicia la conexión. Si permites que &lt;code&gt;A&lt;/code&gt; conecte con &lt;code&gt;B&lt;/code&gt;, Tailscale gestiona automáticamente el tráfico de retorno, pero &lt;code&gt;B&lt;/code&gt; no podrá iniciar una nueva conexión hacia &lt;code&gt;A&lt;/code&gt; a menos que exista otra regla.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluación de Tags:&lt;/strong&gt; Cuando un dispositivo tiene un Tag, las reglas basadas en su &amp;quot;usuario original&amp;quot; dejan de aplicarse. El Tag tiene prioridad absoluta para el control de tráfico.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A continuación ya podemos escribir nuestras políticas de seguridad, para ello creamos el archivo &lt;code&gt;acl.hujson&lt;/code&gt; en nuestra carpeta de configuración de Headscale (si estamos usando docker como en los artículos anteriores este fichero sería &lt;code&gt;config/acl.hujson&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  // Definimos los grupos y etiquetas
  &amp;quot;groups&amp;quot;: {
    &amp;quot;group:profesores&amp;quot;: [&amp;quot;admin&amp;quot;]
  },

  &amp;quot;tagOwners&amp;quot;: {
    &amp;quot;tag:router-aula&amp;quot;: [&amp;quot;group:profesores&amp;quot;],
    &amp;quot;tag:alumno&amp;quot;: [&amp;quot;group:profesores&amp;quot;]
  },

  &amp;quot;acls&amp;quot;: [
    // 1. Permitir que los alumnos accedan a la red local anunciada por el router
    {
      &amp;quot;action&amp;quot;: &amp;quot;accept&amp;quot;,
      &amp;quot;src&amp;quot;:    [&amp;quot;tag:alumno&amp;quot;],
      &amp;quot;dst&amp;quot;:    [&amp;quot;192.168.100.0/24:*&amp;quot;]
    },
    // 2. Permitir que el profesor tenga acceso total (opcional)
    {
      &amp;quot;action&amp;quot;: &amp;quot;accept&amp;quot;,
      &amp;quot;src&amp;quot;:    [&amp;quot;group:profesores&amp;quot;],
      &amp;quot;dst&amp;quot;:    [&amp;quot;*:*&amp;quot;]
    }
    // NOTA: Al no haber una regla que permita &amp;quot;src: tag:alumno&amp;quot; a &amp;quot;dst: tag:alumno&amp;quot;,
    // el tráfico entre alumnos queda bloqueado automáticamente.
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta política de seguridad utiliza un modelo de &lt;strong&gt;confianza cero (Zero Trust)&lt;/strong&gt;. Siguiendo la sintaxis de Headscale, cada bloque cumple una función específica para garantizar que los alumnos estén aislados entre sí pero tengan acceso a los recursos necesarios.&lt;/p&gt;
&lt;p&gt;Aquí tienes la explicación detallada de cada sección:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Definición de Roles (&lt;code&gt;groups&lt;/code&gt;):&lt;/strong&gt; Se crea un grupo llamado &lt;code&gt;group:profesores&lt;/code&gt; que incluye al usuario &lt;code&gt;admin&lt;/code&gt;. Esto permite asignar permisos a un conjunto de personas sin tener que escribir sus nombres uno por uno en cada regla.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control de Etiquetas (&lt;code&gt;tagOwners&lt;/code&gt;):&lt;/strong&gt; Esta es la capa de seguridad de identidad. Establece que &lt;strong&gt;solo&lt;/strong&gt; los miembros de &lt;code&gt;group:profesores&lt;/code&gt; tienen autoridad para asignar las etiquetas &lt;code&gt;tag:router-aula&lt;/code&gt; y &lt;code&gt;tag:alumno&lt;/code&gt; a los dispositivos. Esto evita que un alumno malintencionado intente cambiarse de etiqueta para obtener más privilegios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Acceso Restringido para Alumnos (&lt;code&gt;acls&lt;/code&gt; - Regla 1):&lt;/strong&gt; Define que cualquier dispositivo etiquetado como &lt;code&gt;tag:alumno&lt;/code&gt; puede iniciar conexiones hacia la subred física del aula (&lt;code&gt;192.168.100.0/24&lt;/code&gt;) en cualquier puerto (&lt;code&gt;*&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privilegios de Administración (&lt;code&gt;acls&lt;/code&gt; - Regla 2):&lt;/strong&gt; Se otorga al &lt;code&gt;group:profesores&lt;/code&gt; acceso total a cualquier destino (&lt;code&gt;*:*&lt;/code&gt;). Esto asegura que el personal docente pueda gestionar todos los nodos y recursos de la VPN sin restricciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento Automático (Deny implícito):&lt;/strong&gt; Es la parte más importante del funcionamiento. Al no existir una regla que diga que &lt;code&gt;tag:alumno&lt;/code&gt; puede conectar con &lt;code&gt;tag:alumno&lt;/code&gt;, el tráfico entre los dispositivos de los estudiantes es &lt;strong&gt;bloqueado por defecto&lt;/strong&gt;. Aunque estén en la misma VPN, son invisibles entre sí.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tráfico Unidireccional:&lt;/strong&gt; Las reglas definen quién puede &amp;quot;abrir&amp;quot; la conexión. Los alumnos pueden acceder a la red local, pero los equipos de la red local no pueden iniciar conexiones hacia los alumnos (a menos que añadas una regla inversa).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para que las reglas se apliquen, debemos usar los tags al conectar los equipos:&lt;/p&gt;
&lt;h3&gt;En el Router de la red local&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org \
  --advertise-routes=192.168.100.0/24 \
  --advertise-tags=tag:router-aula
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recuerda que en el servidor headscale habrá que aceptar la ruta anunciada, como estudiamos en el post: &lt;a href=&quot;https://www.josedomingo.org/pledin/2026/02/vpn-mesh-headscale-rutas-dns/&quot;&gt;Configuración del enrutamiento y DNS en una VPN tailscale/headscale&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;En el equipo de cada Alumno&lt;/h3&gt;
&lt;p&gt;Para simplificar, puedes generar una &lt;strong&gt;Pre-Auth Key&lt;/strong&gt; etiquetada para que los alumnos no tengan que hacer nada manual:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale preauthkeys create --user vpn1 --reusable --tag tag:alumno
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El alumno solo tendrá que ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org --authkey hskey-auth-XXXXX
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Resultado final&lt;/h3&gt;
&lt;p&gt;El alumno ahora tiene una interfaz &lt;code&gt;tailscale0&lt;/code&gt; que le permite llegar a &lt;code&gt;192.168.100.50&lt;/code&gt; (un equipo del aula), pero si intenta hacer ping a la IP de la VPN de su compañero de al lado, el tráfico será descartado silenciosamente por el motor de filtrado de Tailscale.&lt;/p&gt;
&lt;h2&gt;Estrategia 2: Segmentación por usuarios (Namespaces Aislados)&lt;/h2&gt;
&lt;p&gt;Esta estrategia aprovecha la arquitectura nativa de Headscale para crear &lt;strong&gt;compartimentos estancos&lt;/strong&gt; mediante la creación de un usuario independiente para cada entidad (por ejemplo, uno por cada alumno). En este modelo, el aislamiento es el estado por defecto: los nodos de diferentes usuarios no pueden verse entre sí porque pertenecen a redes lógicas totalmente separadas. Para permitir el acceso a los recursos comunes, como el router del aula, se definen reglas de visibilidad transversales en las ACL que &amp;quot;perforan&amp;quot; estos muros de forma controlada. Es una solución muy intuitiva y robusta para escenarios donde se busca un &lt;strong&gt;aislamiento total y sencillo&lt;/strong&gt;, ya que evita que un error en el etiquetado de un nodo pueda exponerlo accidentalmente al resto de la malla.&lt;/p&gt;
&lt;p&gt;En este escenario, crearíamos &lt;code&gt;alumno1&lt;/code&gt;, &lt;code&gt;alumno2&lt;/code&gt;, &lt;code&gt;alumno3&lt;/code&gt;, etc. El &lt;strong&gt;Router&lt;/strong&gt; de la red local (donde están los recursos) se quedaría en un usuario administrativo, por ejemplo &lt;code&gt;infra&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale users create infra
docker exec headscale headscale users create alumno1
docker exec headscale headscale users create alumno2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A continuación, registramos el Router en el usuario &lt;code&gt;infra&lt;/code&gt;, por lo tanto en el router (en el nodo que hace de pasarela a la red local (&lt;code&gt;192.168.100.0/24&lt;/code&gt;)) ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org --advertise-routes=192.168.100.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recuerda que en el servidor Headscale habrá que aceptar la ruta anunciada, como estudiamos en el post: &lt;a href=&quot;https://www.josedomingo.org/pledin/2026/02/vpn-mesh-headscale-rutas-dns/&quot;&gt;Configuración del enrutamiento y DNS en una VPN tailscale/headscale&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Configuramos la ACL para &amp;quot;romper&amp;quot; el aislamiento. Como cada alumno está en un usuario distinto, ahora mismo no pueden ver el Router (porque el Router está en &lt;code&gt;infra&lt;/code&gt;). Necesitamos una regla en &lt;code&gt;acl.hujson&lt;/code&gt; que permita este cruce:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;acls&amp;quot;: [
    // Permitir que cualquier usuario (alumnos) acceda a la subred del router
    {
      &amp;quot;action&amp;quot;: &amp;quot;accept&amp;quot;,
      &amp;quot;src&amp;quot;:    [&amp;quot;*&amp;quot;], 
      &amp;quot;dst&amp;quot;:    [&amp;quot;192.168.100.0/24:*&amp;quot;]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Al usar &lt;code&gt;src: [&amp;quot;*&amp;quot;]&lt;/code&gt;, permitimos que todos los usuarios de Headscale lleguen a esa red, pero como no hay ninguna regla que diga que &lt;code&gt;alumno1&lt;/code&gt; puede hablar con &lt;code&gt;alumno2&lt;/code&gt;, el aislamiento entre ellos se mantiene intacto.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Por último, cada usuario registra sus propios dispositivos usando el registro interactivo o usando una &lt;strong&gt;Pre-Auth Key&lt;/strong&gt; y aceptando las rutas. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Para el alumno 1
sudo tailscale up --accept-routes --login-server https://vpn.example.org --authkey hskey-auth-XXXXX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recuerda que en los clientes linux tenemos que aceptar las reglas de encaminamiento con el parámetro &lt;code&gt;--accept-routes&lt;/code&gt;, como estudiamos en el post: &lt;a href=&quot;https://www.josedomingo.org/pledin/2026/02/vpn-mesh-headscale-rutas-dns/&quot;&gt;Configuración del enrutamiento y DNS en una VPN tailscale/headscale&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Resultado final&lt;/h3&gt;
&lt;p&gt;Al finalizar esta configuración, cada dispositivo se encuentra en una burbuja lógica definida por su usuario (Namespace). Un alumno conectado como &lt;code&gt;alumno1&lt;/code&gt; podrá alcanzar cualquier servicio dentro de la red del aula &lt;code&gt;192.168.100.0/24&lt;/code&gt; gracias a la regla de &amp;quot;cruce&amp;quot; que hemos definido en las ACL. Sin embargo, dado que no existe ninguna política que autorice el tráfico entre usuarios distintos, el aislamiento es absoluto: la red mesh se comporta como una estrella donde el centro son los recursos compartidos y las puntas son nodos totalmente independientes.&lt;/p&gt;
&lt;h2&gt;Conclusiones&lt;/h2&gt;
&lt;p&gt;Hemos explorado dos formas de alcanzar un mismo objetivo: &lt;strong&gt;seguridad y aislamiento&lt;/strong&gt;. Aunque ambas son eficaces, la elección dependerá de la complejidad y el propósito de tu red:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Característica&lt;/th&gt;
&lt;th&gt;Estrategia 1: Tags&lt;/th&gt;
&lt;th&gt;Estrategia 2: Usuarios (Namespaces)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Escalabilidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Alta. Ideal para cientos de nodos.&lt;/td&gt;
&lt;td&gt;Media. Tedioso si hay muchos usuarios.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Granular (por puertos y protocolos).&lt;/td&gt;
&lt;td&gt;Grueso (aislamiento total por defecto).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gestión&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Centralizada en un solo archivo JSON.&lt;/td&gt;
&lt;td&gt;Descentralizada en la base de datos.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uso ideal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Entornos profesionales y corporativos.&lt;/td&gt;
&lt;td&gt;Entornos pequeños o laboratorios rápidos.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;La &lt;strong&gt;Estrategia 1 (Tags)&lt;/strong&gt; es la que más se acerca al modelo &lt;strong&gt;Zero Trust&lt;/strong&gt; moderno, ya que permite definir políticas basadas en la función del dispositivo y no en quién es su dueño. Es la opción que ofrece mayor flexibilidad a largo plazo, permitiéndote, por ejemplo, que un alumno acceda al servidor web del aula (puerto 80) pero no al SSH del router (puerto 22) con una simple modificación en las ACL.&lt;/p&gt;
&lt;p&gt;Por otro lado, la &lt;strong&gt;Estrategia 2 (Usuarios)&lt;/strong&gt; es perfecta por su robustez ante errores humanos; es casi imposible que un alumno acceda accidentalmente a otro dispositivo si están en usuarios distintos, lo que la convierte en una opción muy segura para configuraciones rápidas donde no queremos mantener un archivo de políticas complejo.&lt;/p&gt;
&lt;p&gt;Independientemente del camino elegido, Headscale demuestra ser una herramienta extremadamente potente para gestionar la seguridad de red sin la rigidez de los firewalls tradicionales.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Nuevo editor Zed</title><link>https://www.josedomingo.org/microblog/2026/03/editor-zed/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/03/editor-zed/</guid><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/zed.jpg&quot; alt=&quot; &quot;&gt;&lt;/p&gt;
&lt;p&gt;En los últimos días estoy usando un nuevo editor de texto llamado &lt;strong&gt;ZED&lt;/strong&gt; y la experiencia está siendo bastante interesante. ZED es un editor de código construido desde cero en &lt;strong&gt;Rust&lt;/strong&gt;, lo que lo hace increíblemente rápido y ligero. Arranca en milisegundos y no consume apenas memoria, algo que se nota mucho si vienes de editores más pesados.&lt;/p&gt;
&lt;p&gt;Algunas de las características que más me están gustando:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alto rendimiento.&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Colaboración en tiempo real.&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IA integrada.&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimalista pero potente.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensiones para aportar más funcionalidades.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://zed.dev/&quot;&gt;Página web&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>Próximo curso de Kubernetes</title><link>https://www.josedomingo.org/microblog/2026/03/curso-kubernetes/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/03/curso-kubernetes/</guid><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/curso_kubernetes.png&quot;  alt=&quot;&quot; /&gt;

&lt;p&gt;Quinta edición del curso: &lt;strong&gt;Introducción a Kubernetes&lt;/strong&gt; para profesores de la Junta de Andalucía. &lt;a href=&quot;https://www.juntadeandalucia.es/educacion/portales/web/cep-castilleja-cuesta/detalle-actividad?c_ediactfor=264128FP023&amp;redirect=/educacion/portales/web/cep-castilleja-cuesta&quot;&gt;Más información&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>NoSubscription.org: Herramientas propias, no alquiladas</title><link>https://www.josedomingo.org/microblog/2026/03/nosuscription/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/03/nosuscription/</guid><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/03/nosuscription.png&quot;  alt=&quot;&quot; /&gt;


&lt;p&gt;&lt;strong&gt;NoSubscription.org&lt;/strong&gt; es un directorio y manifiesto para profesionales que rechazan el modelo de suscripción mensual. Su objetivo es ayudar a freelancers y empresas a recuperar la propiedad de su flujo de trabajo.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;¿Para qué sirve?&lt;/strong&gt;: Para localizar software profesional sin cuotas recurrentes, eliminando costes fijos y el control de las grandes corporaciones SaaS.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;¿Qué encontrarás?&lt;/strong&gt;: Un catálogo verificado de herramientas divididas en:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One-time purchase:&lt;/strong&gt; Pago único con licencia perpetua.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open source:&lt;/strong&gt; Código abierto y soberanía técnica.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free tools:&lt;/strong&gt; Alternativas gratuitas de alta calidad.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;El Manifiesto:&lt;/strong&gt; &lt;em&gt;&amp;quot;Creemos que las personas deben ser dueñas de sus herramientas, no alquilarlas. Las suscripciones agotan los recursos de los profesionales.&amp;quot;&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>Dominando journalctl: Filtra logs como un pro con Unidades y Tags</title><link>https://www.josedomingo.org/microblog/2026/03/journalctl/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2026/03/journalctl/</guid><pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;journalctl&lt;/code&gt; es la herramienta de línea de comandos que permite consultar y administrar de forma centralizada todos los registros (logs) del sistema y de los servicios gestionados por &lt;strong&gt;systemd&lt;/strong&gt; en Linux.&lt;/p&gt;
&lt;p&gt;En &lt;code&gt;journalctl&lt;/code&gt;, una etiqueta o &lt;strong&gt;tag&lt;/strong&gt; (técnicamente llamado &lt;code&gt;SYSLOG_IDENTIFIER&lt;/code&gt;) es el nombre breve que identifica al programa o servicio que ha generado un mensaje, permitiendo filtrar rápidamente los logs por su procedencia.&lt;/p&gt;
&lt;p&gt;Podemos leer los dos logs de dos formas distintas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;journalctl -u&lt;/code&gt; (Unit):&lt;/strong&gt; Se usa para filtrar por una &lt;strong&gt;unidad de systemd&lt;/strong&gt; (normalmente archivos &lt;code&gt;.service&lt;/code&gt;). Es ideal cuando quieres ver todo lo que ha pasado con un demonio específico (arranque, paradas y errores), como por ejemplo &lt;code&gt;apache2&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt; o &lt;code&gt;nginx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;journalctl -t&lt;/code&gt; (Tag/Identifier):&lt;/strong&gt; Se usa para filtrar por el &lt;strong&gt;identificador de syslog&lt;/strong&gt; (&lt;code&gt;SYSLOG_IDENTIFIER&lt;/code&gt;). Es ideal para localizar mensajes enviados por scripts personalizados, comandos manuales (usando &lt;code&gt;logger&lt;/code&gt;) o aplicaciones que no tienen un servicio propio pero &amp;quot;firman&amp;quot; sus logs con un nombre.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para ver qué etiquetas (&lt;strong&gt;SYSLOG_IDENTIFIER&lt;/strong&gt;) tienes disponibles y cuáles puedes usar para filtrar, el comando más directo y limpio es el siguiente:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo journalctl -F SYSLOG_IDENTIFIER
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>microblog</category></item><item><title>Configuración del enrutamiento y DNS en una VPN tailscale/headscale</title><link>https://www.josedomingo.org/2026/02/vpn-mesh-headscale-rutas-dns/</link><guid isPermaLink="true">https://www.josedomingo.org/2026/02/vpn-mesh-headscale-rutas-dns/</guid><pubDate>Tue, 24 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/02/headscale2.png&quot; alt=&quot;vpn&quot;&gt;&lt;/p&gt;
&lt;p&gt;En el post anterior, &lt;a href=&quot;https://www.josedomingo.org/pledin/2026/02/vpn-mesh-headscale/&quot;&gt;Construcción de VPN mesh con tailscale/headscale&lt;/a&gt;, logramos conectar varios dispositivos mediante una VPN mesh.&lt;/p&gt;
&lt;p&gt;En este artículo, profundizaremos en tres capacidades críticas que transforman una red privada virtual en una solución de conectividad integral:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subnet Routers:&lt;/strong&gt; Implementaremos el concepto de &lt;strong&gt;VPN Site-to-Site&lt;/strong&gt;, permitiendo que nodos específicos actúen como &lt;em&gt;gateways&lt;/em&gt; para exponer subredes completas (LAN) a la malla, integrando así dispositivos sin capacidad de instalar software (como impresoras, servidores NAS,...).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exit Nodes:&lt;/strong&gt; Configuraremos nodos de salida para centralizar el tráfico de internet de la red mesh, permitiendo una navegación segura y controlada a través de un punto de confianza (similar a una VPN de acceso remoto tradicional).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split DNS y Global Nameservers:&lt;/strong&gt; Optimizaremos la resolución de nombres dentro de la red mediante la integración de DNS internos, facilitando el acceso a servicios mediante FQDNs en lugar de direcciones IP estáticas.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Subnet Router&lt;/h2&gt;
&lt;p&gt;Un &lt;strong&gt;Subnet Router&lt;/strong&gt; es un nodo de nuestra red que actúa como pasarela, permitiendo que el resto de los dispositivos de la VPN &amp;quot;salten&amp;quot; a una red local física (por ejemplo, la red de tu oficina o de tu casa). Cualquier nodo Linux en el que ya tengas Tailscale funcionando puede servir como Subnet Router.&lt;/p&gt;
&lt;p&gt;El escenario desde el que partimos es el siguiente:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list
ID | Hostname | Name    | MachineKey | NodeKey | User | Tags | IP addresses                  | Ephemeral | Last seen           | Expiration          | Connected | Expired
1  |  nodo1   | nodo1   | [qatKd]    | [fMe1N] | vpn1 |      | 100.64.0.1, fd7a:115c:a1e0::1 | false     | 2026-02-13 18:09:45 | N/A                 | online    | no     
2  |  nodo2   | nodo2   | [G1VUT]    | [9wBm+] | vpn1 |      | 100.64.0.2, fd7a:115c:a1e0::2 | false     | 2026-02-13 18:30:17 | 0001-01-01 00:00:00 | online    | no     
3  |  nodo3   | nodo3   | [asFdw]    | [8qDba] | vpn1 |      | 100.64.0.3, fd7a:115c:a1e0::3 | false     | 2026-02-13 18:35:17 | 0001-01-01 00:00:00 | online    | no     
&lt;/code&gt;&lt;/pre&gt;
&lt;!--more--&gt;

&lt;p&gt;Vamos a suponer que el &lt;code&gt;nodo2&lt;/code&gt; es el &lt;strong&gt;Subnet Router&lt;/strong&gt;. Para configurar este nodo lo primero es habilitar el reenvío de paquetes en ese nodo, para ello activamos el bit de forwarding en &lt;code&gt;nodo2&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &amp;#39;net.ipv4.ip_forward = 1&amp;#39; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Anuncio de las rutas de encaminamiento&lt;/h3&gt;
&lt;p&gt;A continuación, el &lt;code&gt;nodo2&lt;/code&gt; tiene que &lt;strong&gt;anunciar la ruta&lt;/strong&gt;, suponemos que la red local a la que nos da acceso el &lt;code&gt;nodo2&lt;/code&gt; es la &lt;code&gt;192.168.18.0/24&lt;/code&gt;. Para anunciar la ruta de encaminamiento que permita a los otros nodos acceder a dicha red, ejecutamos en &lt;code&gt;nodo2&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org --advertise-routes=192.168.18.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si el nodo ya estaba iniciado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale set --advertise-routes=192.168.18.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Podemos comprobar que el nodo está anunciando nuevas rutas desde el servidor Headscale, ejecutando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list-routes
ID | Hostname | Approved        | Available       | Serving (Primary)
2  | nodo2    | 192.168.18.0/24 | 192.168.18.0/24 |   
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Aprobación de las rutas de encaminamiento&lt;/h3&gt;
&lt;p&gt;Por seguridad, Headscale no permite que un nodo &amp;quot;inyecte&amp;quot; rutas en la red mesh sin permiso del administrador. Tenemos dos formas de aprobar la ruta anunciada: de forma manual o de forma automática.&lt;/p&gt;
&lt;p&gt;La &lt;strong&gt;forma automática&lt;/strong&gt; está pensada cuando tenemos pensado añadir muchos nodos o redes. Headscale permite definir reglas de &amp;quot;auto-aprobación&amp;quot; en su configuración, modificando el archivo de configuración &lt;code&gt;config.yml&lt;/code&gt; como nos indica la &lt;a href=&quot;https://headscale.net/stable/ref/routes/#automatically-approve-routes-of-a-subnet-router&quot;&gt;documentación&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;En nuestro caso estudiaremos la &lt;strong&gt;aprobación manual&lt;/strong&gt;. Desde el servidor Headscale, ejecutamos la siguiente instrucción para habilitar la ruta que se está anunciando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes approve-routes --identifier 2 --routes 192.168.18.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si volvemos a ver las rutas desde el servidor, comprobamos que se ha aprobado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list-routes
ID | Hostname | Approved        | Available       | Serving (Primary)
2  | nodo2    | 192.168.18.0/24 | 192.168.18.0/24 | 192.168.18.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Configuración de la ruta de encaminamiento en el cliente&lt;/h3&gt;
&lt;p&gt;Una vez que la ruta está aprobada en el servidor, los clientes (nodos) de la VPN ya saben que esa red existe, pero los clientes Linux son precavidos y &lt;strong&gt;no&lt;/strong&gt; la añadirán a su tabla de rutas local a menos que se lo pidas. Por ejemplo en el &lt;code&gt;nodo1&lt;/code&gt; ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up    --login-server=https://vpn.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si el cliente ya está iniciado, podemos ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale set --accept-routes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En dispositivos en otros sistemas operativos, este paso no suele ser necesario, ya que el cliente oficial acepta las rutas del servidor automáticamente.&lt;/p&gt;
&lt;h3&gt;Verificación de la conectividad&lt;/h3&gt;
&lt;p&gt;Desde el nodo &lt;code&gt;nodo1&lt;/code&gt; hacemos ping a un dispositivo de la red local que &lt;strong&gt;no&lt;/strong&gt; tenga Tailscale instalado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ping 192.168.18.1  # La IP del router de tu casa
64 bytes from 192.168.18.1: icmp_seq=1 ttl=63 time=50.6 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Veamos donde se declara la ruta de encaminamiento. Tailscale crea una nueva tabla de encaminamiento para introducir las rutas que necesita, para ver las tablas de encaminamiento ejecutamos: &lt;code&gt;ip rule show&lt;/code&gt;. Entre varias tablas que encontramos, tenemos que tener en cuenta que la tabla de encaminamiento por defecto se llama &lt;strong&gt;main&lt;/strong&gt; y la tabla de encaminamiento de Tailscale es la llamada &lt;strong&gt;52&lt;/strong&gt;. Por lo tanto si vemos las rutas de esta tabla, encontramos la ruta de encaminamiento que hemos aceptado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip route show table 52
... 
192.168.18.0/24 dev tailscale0 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Exit Node&lt;/h2&gt;
&lt;p&gt;Por defecto, Tailscale solo encamina a través de la VPN el tráfico destinado a otros nodos de tu red. El resto del tráfico (tu navegación por internet) sale por la conexión local del dispositivo en el que estás.&lt;/p&gt;
&lt;p&gt;Al configurar un &lt;strong&gt;Exit Node&lt;/strong&gt;, puedes indicar a tus dispositivos que &lt;strong&gt;todo&lt;/strong&gt; su tráfico de internet pase primero por un nodo específico de la VPN antes de salir a la red pública. Esto es ideal para:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Navegar de forma segura desde redes Wi-Fi públicas sospechosas.&lt;/li&gt;
&lt;li&gt;Acceder a servicios que solo permiten IPs de un país determinado (si tienes el Exit Node allí).&lt;/li&gt;
&lt;li&gt;Centralizar el filtrado de tráfico en un solo punto.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Anuncio del exit node&lt;/h3&gt;
&lt;p&gt;En nuestro ejemplo, vamos a configurar como &lt;strong&gt;exit node&lt;/strong&gt; nuestro nodo &lt;code&gt;nodo3&lt;/code&gt;. Lo primero activaremos el bit de forwarding en ese nodo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &amp;#39;net.ipv4.ip_forward = 1&amp;#39; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Una vez preparado, le indicamos que se anuncie como nodo de salida:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org --advertise-exit-node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si el nodo ya está funcionando, podemos ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale set --advertise-exit-node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Podemos comprobar que el nodo está anunciando nuevas rutas desde el servidor Headscale, ejecutando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list-routes
ID | Hostname | Approved        | Available       | Serving (Primary)
2  | nodo2    | 192.168.18.0/24 | 192.168.18.0/24 | 192.168.18.0/24
3  | nodo3    | 0.0.0.0/0       | 0.0.0.0/0       |         
   |          | ::/0            | ::/0            |     
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Aprobación del exit node&lt;/h3&gt;
&lt;p&gt;Al igual que con los subnet routers, ahora debemos aprobar la ruta por defecto desde el servidor Headscale. A aunque podemos hacerlo de &lt;a href=&quot;https://headscale.net/stable/ref/routes/#automatically-approve-an-exit-node-with-auto-approvers&quot;&gt;forma automática&lt;/a&gt;, vamos a ver el procedimiento manual. Para ello ejecutamos desde el servidor Headscale:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes approve-routes --identifier 3 --routes 0.0.0.0/0,::/0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y volvemos a comprobar las rutas aprobadas:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list-routes
ID | Hostname | Approved        | Available       | Serving (Primary)
2  | nodo2    | 192.168.18.0/24 | 192.168.18.0/24 | 192.168.18.0/24
3  | nodo3    | 0.0.0.0/0       | 0.0.0.0/0       | 0.0.0.0/0        
   |          | ::/0            | ::/0            | ::/0     
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cómo usar el Exit Node desde los clientes&lt;/h3&gt;
&lt;p&gt;Una vez aprobado, el nodo de salida  &lt;strong&gt;no se usa automáticamente&lt;/strong&gt;. Cada cliente debe elegir activarlo. Para empezar a navegar a través del nodo de salida, ejecutamos en el &lt;code&gt;nodo1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --exit-node=nodo3 --login-server https://vpn.example.org 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si el nodo ya está funcionando, ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale set --exit-node nodo3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En las aplicaciones oficiales de otros sistemas operativos, aparecerá una nueva opción en el menú llamada &lt;strong&gt;&amp;quot;Exit Node&amp;quot;&lt;/strong&gt;. Al pulsarla, verás una lista de los nodos disponibles que has aprobado en el paso anterior. Solo tienes que seleccionar uno.&lt;/p&gt;
&lt;h3&gt;Verificación del nodo de salida&lt;/h3&gt;
&lt;p&gt;Para comprobar que realmente estás saliendo a internet a través de tu nodo de confianza, puedes usar cualquier servicio de geolocalización de IPs desde tu navegador o CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl ifconfig.me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La dirección IP devuelta debería ser la IP pública de tu &lt;strong&gt;Exit Node&lt;/strong&gt;, no la de la red a la que estás conectado físicamente.
Además podemos ver que se ha creado una ruta por defecto en la tabla de encaminamiento &lt;strong&gt;52&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip route show table 52
default dev tailscale0 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DNS y MagicDNS&lt;/h2&gt;
&lt;p&gt;Hasta ahora, nos hemos comunicado entre nodos usando direcciones IP (como &lt;code&gt;100.64.0.1&lt;/code&gt;). Pero, ¿no sería mejor acceder a nuestro servidor simplemente escribiendo su nombre? Aquí es donde entra &lt;strong&gt;MagicDNS&lt;/strong&gt; y la configuración de DNS en Headscale.&lt;/p&gt;
&lt;p&gt;MagicDNS es una función que registra automáticamente los nombres de tus dispositivos en un servidor DNS interno gestionado por Headscale. Si tienes un nodo llamado &lt;code&gt;servidor&lt;/code&gt;, MagicDNS te permitirá hacerle un ping o acceder a su web usando un nombre de dominio.&lt;/p&gt;
&lt;h3&gt;Configuración del DNS en Headscale&lt;/h3&gt;
&lt;p&gt;Para que esto funcione, debemos editar el archivo de configuración de nuestro servidor Headscale (&lt;code&gt;config/config.yaml&lt;/code&gt;). Busca la sección &lt;code&gt;dns_config&lt;/code&gt; y ajústala:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;dns_config:
  # El dominio base para tu red mesh
  magic_dns: true
  base_domain: vpn.example.org

  # Servidores DNS que usarán los nodos para resolver internet
  nameservers:
    - 1.1.1.1
    - 1.0.0.1
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;magic_dns: true&lt;/strong&gt;: Activa la resolución automática de nombres de nodos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;base_domain&lt;/strong&gt;: Es el nombre de dominio que vamos a usar. En nuestro caso &lt;code&gt;vpn.example.org&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nameservers&lt;/strong&gt;: Servidores DNS que se usan para resolver otros dominios.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tras cambiar el fichero, recuerda reiniciar el contenedor:
&lt;code&gt;docker-compose restart headscale&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Con esta configuración tenemos un servidor DNS en la dirección &lt;code&gt;100.100.100.100&lt;/code&gt; que resolverá los nombres de nuestros nodos de la VPN usando el dominio configurado y que funciona como DNS forward para resolver otros nombres.&lt;/p&gt;
&lt;p&gt;Podemos comprobar la configuración del servidor DNS de uno de nuestro nodos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cat /etc/resolv.conf

nameserver 100.100.100.100
search vpn.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y podemos acceder a los nodos usando su nombre FQDN o hostname, ya que tenemos configurado el parámetro &lt;code&gt;search&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ping nodo2
PING nodo2.vpn.example.org (100.64.0.2) 56(84) bytes of data.
64 bytes from nodo2.vpn.example.org (100.64.0.2): icmp_seq=1 ttl=64 time=17.7 ms

ping nodo2.vpn.example.org
PING nodo2.vpn.example.org (100.64.0.2) 56(84) bytes of data.
64 bytes from nodo2.vpn.example.org (100.64.0.2): icmp_seq=1 ttl=64 time=17.7 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Split DNS (DNS dividido)&lt;/h3&gt;
&lt;p&gt;Esta es una de las funciones más potentes. Imagina que tienes un servidor DNS en tu red local que solo resuelve dominios internos (ej: &lt;code&gt;*.josedomingo.org&lt;/code&gt;). Puedes decirle a Headscale que &lt;strong&gt;solo&lt;/strong&gt; las consultas para ese dominio se envíen a ese servidor.&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;Split DNS&lt;/strong&gt; es la capacidad de Tailscale/Headscale para actuar como un &amp;quot;semáforo&amp;quot; de peticiones DNS. En lugar de enviar todas tus consultas a un único servidor DNS, el cliente decide a quién preguntar basándose en el &lt;strong&gt;dominio&lt;/strong&gt; de la dirección que intentas resolver.&lt;/p&gt;
&lt;p&gt;En nuestro ejemplo, vamos a configurar un Split DNS para que las consultas de &lt;code&gt;josedomingo.org&lt;/code&gt; vayan al servidor &lt;code&gt;192.168.18.3&lt;/code&gt;, Para ello en la configuración de Headscale, en el fichero &lt;code&gt;config.yaml&lt;/code&gt;, en la sección &lt;code&gt;dns_config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;dns_config:
...
  split:
    josedomingo.org:
      - 192.168.18.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recuerda que para que el cliente pueda alcanzar la IP &lt;code&gt;192.168.18.3&lt;/code&gt; (que es el servidor DNS), esa IP debe estar anunciada por algún &lt;strong&gt;Subnet Router&lt;/strong&gt; en tu red y aprobada en Headscale.&lt;/p&gt;
&lt;p&gt;Para comprobar el funcionamiento, podemos hacer una consulta desde el &lt;code&gt;nodo1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;dig home.josedomingo.org +short
192.168.18.3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;La integración de &lt;strong&gt;Subnet Routers&lt;/strong&gt;, &lt;strong&gt;Exit Nodes&lt;/strong&gt; y &lt;strong&gt;Split DNS&lt;/strong&gt; transforma una red mesh básica en una infraestructura de red corporativa completa y eficiente. Esta arquitectura permite consolidar topologías &lt;strong&gt;Site-to-Site&lt;/strong&gt; para integrar dispositivos sin agente, centralizar el tráfico de salida bajo políticas de seguridad unificadas y optimizar la resolución de nombres mediante una gestión inteligente de consultas DNS. En conjunto, estas capacidades proporcionan una abstracción de capa 3 que garantiza una conectividad transparente, segura y escalable, eliminando la complejidad del enrutamiento tradicional en entornos híbridos.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Construcción de VPN mesh con tailscale/headscale</title><link>https://www.josedomingo.org/2026/02/vpn-mesh-headscale/</link><guid isPermaLink="true">https://www.josedomingo.org/2026/02/vpn-mesh-headscale/</guid><pubDate>Sat, 14 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2026/02/headscale.png&quot; alt=&quot;vpn&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tailscale&lt;/strong&gt; es una VPN de &lt;strong&gt;red en malla (mesh)&lt;/strong&gt; basada en el protocolo &lt;strong&gt;WireGuard&lt;/strong&gt;. A diferencia de las VPN tradicionales (donde todo el tráfico pasa por un servidor central), en una red mesh los dispositivos establecen conexiones directas &lt;strong&gt;punto a punto ()&lt;/strong&gt; entre sí. Para lograr esto, utiliza técnicas de &lt;strong&gt;NAT Traversal&lt;/strong&gt;, permitiendo que los nodos se comuniquen aunque estén detrás de firewalls o routers sin necesidad de abrir puertos manualmente.&lt;/p&gt;
&lt;p&gt;La arquitectura de Tailscale se divide en dos capas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Control Plane:&lt;/strong&gt; Es el &amp;quot;cerebro&amp;quot; de la red. No toca tus datos; su función es autenticar usuarios, intercambiar las llaves públicas de cifrado y distribuir la tabla de rutas a todos los nodos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Plane:&lt;/strong&gt; Es el tráfico real cifrado que fluye directamente entre los dispositivos una vez que el Control Plane los ha &amp;quot;presentado&amp;quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tailscale opera bajo un modelo &lt;strong&gt;Open Core&lt;/strong&gt;: el software del cliente (lo que instalas en tus dispositivos) es de código abierto, pero el servidor de coordinación (Control Plane) es software propietario alojado en su nube.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Headscale&lt;/strong&gt; es la alternativa libre y &lt;strong&gt;autoalojable (self-hosted)&lt;/strong&gt; al Control Plane oficial. Es un proyecto de código abierto que implementa las mismas APIs, permitiéndote:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mantener la soberanía total sobre tus metadatos y la gestión de la red.&lt;/li&gt;
&lt;li&gt;Utilizar los &lt;strong&gt;clientes oficiales de Tailscale&lt;/strong&gt; simplemente apuntándolos a tu propia instancia.&lt;/li&gt;
&lt;li&gt;Eliminar los límites de usuarios o dispositivos de los planes comerciales.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Instalación de Headscale&lt;/h2&gt;
&lt;p&gt;Para que Headscale pueda coordinar las conexiones entre todos tus dispositivos, necesitamos alojarlo en una máquina que actúe como punto de encuentro permanente. A diferencia de los nodos de la VPN, que pueden entrar y salir de la red o cambiar de ubicación, el servidor de Headscale debe ser el ancla de la infraestructura.&lt;/p&gt;
&lt;h3&gt;Características de la máquina&lt;/h3&gt;
&lt;p&gt;El servidor donde instales Headscale (normalmente un VPS o un servidor dedicado) debe cumplir con tres condiciones fundamentales para garantizar la estabilidad de la red:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accesibilidad Global:&lt;/strong&gt; La máquina debe tener una &lt;strong&gt;IP pública estática&lt;/strong&gt; o un nombre de dominio (FQDN) asociado. Esto permite que cualquier nodo, esté donde esté, pueda localizar el Control Plane para solicitar instrucciones de conexión.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seguridad mediante HTTPS:&lt;/strong&gt; Es obligatorio que el tráfico entre los clientes y Headscale viaje cifrado. Necesitarás un certificado SSL (puedes usar Let&amp;#39;s Encrypt) para que la comunicación se realice de forma segura a través del puerto &lt;strong&gt;443&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disponibilidad (Uptime):&lt;/strong&gt; Si el servidor de Headscale se apaga, los nodos nuevos no podrán unirse y los existentes no podrán actualizar sus tablas de rutas si hay cambios en la red.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para una configuración básica y funcional, no es necesario complicar el firewall. Según la &lt;a href=&quot;https://headscale.net/stable/setup/requirements/&quot;&gt;documentación oficial de requisitos de Headscale&lt;/a&gt;, solo necesitas asegurar la apertura de los siguientes puertos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;443/tcp&lt;/strong&gt;:Tráfico de control (API) y autenticación mediante &lt;strong&gt;HTTPS&lt;/strong&gt;. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;80/tcp&lt;/strong&gt;: Generalmente usado para la validación de certificados (Let&amp;#39;s Encrypt).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Headscale es extremadamente ligero. Para una red pequeña o mediana, una máquina con &lt;strong&gt;1 vCPU y 1 GB de RAM&lt;/strong&gt; es más que suficiente para gestionar el panel de control.&lt;/p&gt;
&lt;!--more--&gt;

&lt;p&gt;En la página anterior puedes encontrar distintos métodos de instalación, en este artículo utilizaremos un contenedor Docker para realizar la instalación, para ello y siguiendo la documentación &lt;a href=&quot;https://headscale.net/stable/setup/install/container/&quot;&gt;Running headscale in a container&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ./headscale/{config,lib}
cd headscale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bajamos el fichero de configuración por defecto según la versión con la que vamos a trabajar, podemos encontrar las instrucciones para descargarlo en la &lt;a href=&quot;https://headscale.net/stable/ref/configuration/&quot;&gt;documentación&lt;/a&gt;. En este caso usamos la versión 0.28.0:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd config
curl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.28.0-beta.2/config-example.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A continuación modificamos la configuración para que el servidor escuche en todos las direcciones, para ello en el fichero &lt;code&gt;config/config.yaml&lt;/code&gt; modificamos el siguiente parámetro de la siguiente forma:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;listen_addr: 0.0.0.0:8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ahora creamos el fichero &lt;code&gt;docker-compose.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;services:
  headscale:
    image: docker.io/headscale/headscale:${HEADSCALE_VERSION}
    restart: unless-stopped
    container_name: headscale
    read_only: true
    tmpfs:
      - /var/run/headscale
    ports:
      - &amp;quot;127.0.0.1:8080:8080&amp;quot;
      - &amp;quot;127.0.0.1:9090:9090&amp;quot;
    volumes:
      - ${HEADSCALE_PATH}/config:/etc/headscale:ro
      - ${HEADSCALE_PATH}/lib:/var/lib/headscale
    command: serve
    healthcheck:
        test: [&amp;quot;CMD&amp;quot;, &amp;quot;headscale&amp;quot;, &amp;quot;health&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y el fichero &lt;code&gt;.env&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# La versión de headscale que deseas utilizar
HEADSCALE_VERSION=0.28.0

# La ruta absoluta a tu directorio de headscale
HEADSCALE_PATH=.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Iniciamos el contenedor y comprobamos que funciona:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose up -d
Starting headscale ... done

curl http://127.0.0.1:8080/health
{&amp;quot;status&amp;quot;:&amp;quot;pass&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ahora configuramos el proxy inverso, para ello tenemos ejemplos de configuración en &lt;a href=&quot;https://headscale.net/stable/ref/integration/reverse-proxy/&quot;&gt;Running headscale behind a reverse proxy&lt;/a&gt;. Y comprobamos que funciona:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl https://vpn.example.org/health
{&amp;quot;status&amp;quot;:&amp;quot;pass&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finalmente cambiamos el parámetro de la configuración para indicar la url de acceso, para ello en el fichero &lt;code&gt;config/config.yaml&lt;/code&gt; modificamos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;server_url: https://vpn.example.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reiniciamos el contenedor:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Creación de usuarios&lt;/h2&gt;
&lt;p&gt;En Headscale, un &lt;strong&gt;usuario&lt;/strong&gt; es una entidad lógica que agrupa un conjunto de dispositivos.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Los dispositivos que pertenecen al mismo usuario pueden verse entre sí de forma predeterminada.&lt;/li&gt;
&lt;li&gt;Sirve para aislar entornos: puedes tener un usuario para tus dispositivos personales y otro para servidores de trabajo, manteniendo las redes separadas dentro de la misma instancia de Headscale.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No puedes registrar ningún nodo (dispositivo) en Headscale si no existe al menos un usuario al que asignárselo. Para crear tu primer usuario (por ejemplo, &amp;quot;mi-red&amp;quot;), utiliza el siguiente comando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale users create vpn1
User created

docker exec headscale headscale users list
ID | Name | Username | Email | Created            
1  |      | vpn1     |       | 2026-02-13 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Podemos eliminar un usuario ejecutando:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale users destroy vpn1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En este caso se realiza una limpieza total de los recursos asociados a esa identidad:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Eliminación de Nodos&lt;/strong&gt;: Todos los dispositivos (nodos) registrados bajo ese usuario son expulsados de la red inmediatamente.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revocación de Claves&lt;/strong&gt;: Las claves de cifrado y los certificados asociados a esos nodos quedan invalidados.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Registro de Nodos: conectando tus dispositivos&lt;/h2&gt;
&lt;p&gt;Una vez que el servidor está listo y el usuario creado, el siguiente paso es registrar los dispositivos (nodos). Mientras que &lt;strong&gt;Headscale&lt;/strong&gt; opera exclusivamente en el &lt;strong&gt;Control Plane&lt;/strong&gt; (gestionando la lógica de red y la distribución de llaves), la conectividad efectiva requiere el despliegue del cliente oficial de &lt;strong&gt;Tailscale&lt;/strong&gt; en cada nodo. Este cliente actúa en el &lt;strong&gt;Data Plane&lt;/strong&gt;, integrándose con el stack de red del sistema operativo a través de una interfaz virtual &lt;strong&gt;TUN&lt;/strong&gt; y gestionando el cifrado y encapsulamiento de paquetes mediante &lt;strong&gt;WireGuard&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Puedes instalar el cliente de Tailscale en prácticamente cualquier sistema operativo moderno (Windows, macOS, Android, iOS, etc.), cuyas descargas están disponibles en la &lt;a href=&quot;https://tailscale.com/download&quot;&gt;página oficial de Tailscale&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Para este artículo, nos centraremos en &lt;strong&gt;Linux&lt;/strong&gt;, donde la instalación es tan sencilla como ejecutar su script automatizado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -fsSL https://tailscale.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Una vez que el cliente de Tailscale está instalado en el nodo, debemos vincularlo al &lt;strong&gt;Control Plane&lt;/strong&gt; de Headscale. Existen dos &lt;strong&gt;métodos de registro&lt;/strong&gt; principales para realizar esta tarea.&lt;/p&gt;
&lt;h3&gt;Método interactivo&lt;/h3&gt;
&lt;p&gt;Este es el método estándar para estaciones de trabajo o dispositivos con intervención humana. El flujo se basa en un desafío-respuesta manual:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Solicitud de registro:&lt;/strong&gt; En el nodo cliente, se inicia la conexión apuntando a nuestra instancia:&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desafío:&lt;/strong&gt; El cliente no podrá autenticarse automáticamente y devolverá una URL de registro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autorización:&lt;/strong&gt; Al abrir esa URL, Headscale mostrará un comando preformateado que incluye la &lt;code&gt;nodekey&lt;/code&gt;. Debes ejecutar ese comando en el &lt;strong&gt;servidor Headscale&lt;/strong&gt; para validar el nodo y asignarlo a un usuario:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Veamos un ejemplo:&lt;/p&gt;
&lt;p&gt;Desde el cliente solicitamos el registro:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org

To authenticate, visit:

        https://vpn.example.org/register/STqfQR4wnKr-eerDXam3_RYi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Abrimos el enlace en un navegador web y nos dará la instrucción (que incluye el &lt;code&gt;nodekey&lt;/code&gt;) que tenemos que ejecutar en Headscale para autorizar el dispositivo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;headscale nodes register --key STqfQR4wnKr-eerDXam3_RYi --user USERNAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En nuestro caso, ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes register --key STqfQR4wnKr-eerDXam3_RYi --user vpn1
Node registered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En headscale podemos ver los nodos registrados:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list
ID | Hostname | Name    | MachineKey | NodeKey | User | Tags | IP addresses                  | Ephemeral | Last seen           | Expiration | Connected | Expired
1  |  nodo1   | nodo1   | [qatKd]    | [fMe1N] | vpn1 |      | 100.64.0.1, fd7a:115c:a1e0::1 | false     | 2026-02-13 18:09:45 | N/A        | online    | no     ``` 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y podemos comprobar que el cliente tiene la ip asignada en la VPN:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip addr show tailscale0
2: tailscale0: &amp;lt;POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP&amp;gt; mtu 1280 qdisc pfifo_fast state UNKNOWN group default qlen 500
    link/none 
    inet 100.64.0.1/32 scope global tailscale0
       valid_lft forever preferred_lft forever
    inet6 fe80::c26f:e628:12cb:72c9/64 scope link stable-privacy 
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Método no interactivo&lt;/h3&gt;
&lt;p&gt;Para despliegues automatizados, contenedores o servidores donde no queremos realizar una validación manual, Headscale permite el uso de &lt;strong&gt;Pre-Auth Keys&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Generación de la clave:&lt;/strong&gt; Primero, generamos un token de autorización en el servidor Headscale asociado a un usuario específico:&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registro automático:&lt;/strong&gt; Usamos esa clave en el nodo cliente para completar el registro en un solo paso, sin necesidad de confirmación manual en el servidor:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Veamos un ejemplo:&lt;/p&gt;
&lt;p&gt;En primer lugar generamos la clave en el Control Plane:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale preauthkeys create --user 1 --reusable
hskey-auth-Db1RF5n7gSk3-NzIu0vDgXi8yewFjq5w4rczR-uZD-J_RfYnUDSjCmrgRHKXnGaXj_HK3jJfvnhk0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con el parámetro &lt;code&gt;--reusable&lt;/code&gt; la clave se puede usar varias veces, si no se pone, la clave solo se puede usar una vez. &lt;/p&gt;
&lt;p&gt;Podemos usar el parámetro &lt;code&gt;--expiration 24h&lt;/code&gt; para que la clave expire en 24 horas (por defecto, solo dura una hora), o &lt;code&gt;--ephemeral&lt;/code&gt; para que la clave expire cuando el dispositivo se desconecte.&lt;/p&gt;
&lt;p&gt;Con el parámetro &lt;code&gt;--user&lt;/code&gt; debemos indicar el ID del usuario.&lt;/p&gt;
&lt;p&gt;Y luego la usamos para conectar el cliente:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo tailscale up --login-server https://vpn.example.org --authkey hskey-auth-Db1RF5n7gSk3-NzIu0vDgXi8yewFjq5w4rczR-uZD-J_RfYnUDSjCmrgRHKXnGaXj_HK3jJfvnhk0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y comprobamos que el cliente se ha conectado:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec headscale headscale nodes list
ID | Hostname | Name    | MachineKey | NodeKey | User | Tags | IP addresses                  | Ephemeral | Last seen           | Expiration          | Connected | Expired
1  |  nodo1   | nodo1   | [qatKd]    | [fMe1N] | vpn1 |      | 100.64.0.1, fd7a:115c:a1e0::1 | false     | 2026-02-13 18:09:45 | N/A                 | online    | no     
2  |  nodo2   | nodo2   | [G1VUT]    | [9wBm+] | vpn1 |      | 100.64.0.2, fd7a:115c:a1e0::2 | false     | 2026-02-13 18:30:17 | 0001-01-01 00:00:00 | online    | no     
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Comprobación de la conectividad&lt;/h2&gt;
&lt;p&gt;Con los nodos registrados y visibles en el listado de Headscale, la red mesh ya está construida. Ahora debemos verificar que el tráfico fluye correctamente entre ellos utilizando las herramientas que proporciona el ecosistema de Tailscale.&lt;/p&gt;
&lt;h3&gt;El comando &lt;code&gt;status&lt;/code&gt; en el cliente&lt;/h3&gt;
&lt;p&gt;Desde cualquier nodo (por ejemplo, desde &lt;code&gt;nodo1&lt;/code&gt;), podemos verificar cómo ve este a sus &amp;quot;pares&amp;quot; (peers) en la red:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale status

100.64.0.1  nodo1  vpn1  linux  -                      
100.64.0.2  nodo2  vpn1  linux  idle, tx 468 rx 348 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este comando te mostrará una lista de los demás dispositivos de tu usuario, su dirección IP interna y las características de la conexión.&lt;/p&gt;
&lt;h3&gt;Prueba de latencia con &lt;code&gt;tailscale ping&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Aunque el &lt;code&gt;ping&lt;/code&gt; tradicional funciona, Tailscale ofrece una herramienta especializada que nos indica cómo se está realizando la conexión:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;tailscale ping nodo2
pong from nodo2 (100.64.0.2) via DERP(mad) in 777ms
pong from nodo2 (100.64.0.2) via DERP(mad) in 18ms
pong from nodo2 (100.64.0.2) via DERP(mad) in 16ms
pong from nodo2 (100.64.0.2) via 143.47.53.108:41641 in 13ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A diferencia del ping estándar, este comando te informará si ha logrado establecer una &lt;strong&gt;conexión directa P2P&lt;/strong&gt; (atravesando con éxito el NAT) o si está usando un servidor intermedio. Es la prueba definitiva de que el &lt;strong&gt;Data Plane&lt;/strong&gt; está operando de forma óptima.&lt;/p&gt;
&lt;p&gt;Cualquier servicio que levantes en el puerto 80, 22 o cualquier otro en &lt;code&gt;nodo2&lt;/code&gt; será accesible desde &lt;code&gt;nodo1&lt;/code&gt; (y viceversa) de forma totalmente cifrada y privada, como si estuvieran conectados al mismo switch físico.&lt;/p&gt;
&lt;h2&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;La implementación de &lt;strong&gt;Headscale&lt;/strong&gt; como plano de control (&lt;em&gt;Control Plane&lt;/em&gt;) independiente permite explotar todo el potencial del protocolo &lt;strong&gt;WireGuard&lt;/strong&gt; sin las limitaciones de un modelo SaaS propietario. Al desacoplar la gestión de identidades y la distribución de rutas de la infraestructura de Tailscale, hemos consolidado una red mesh con &lt;strong&gt;topología dinámica&lt;/strong&gt; capaz de garantizar comunicaciones &lt;strong&gt;P2P cifradas&lt;/strong&gt; con una sobrecarga (&lt;em&gt;overhead&lt;/em&gt;) mínima. Esta arquitectura no solo optimiza el rendimiento mediante la negociación directa de túneles tras entornos de NAT complejo, sino que establece un entorno de &lt;strong&gt;Zero Trust&lt;/strong&gt; donde la soberanía de los metadatos y la integridad del tráfico permanecen bajo control absoluto del administrador.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Cursos de virtualización con KVM/libvirt</title><link>https://www.josedomingo.org/2025/12/curso-kvm/</link><guid isPermaLink="true">https://www.josedomingo.org/2025/12/curso-kvm/</guid><pubDate>Sat, 27 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/12/kvm.png&quot; alt=&quot;kvm&quot;&gt;&lt;/p&gt;
&lt;p&gt;Este año también he tenido la oportunidad de &lt;strong&gt;desarrollar y publicar en OpenWebinars dos cursos centrados en la virtualización con KVM y libvirt&lt;/strong&gt;, abordando tanto una &lt;strong&gt;introducción accesible para principiantes&lt;/strong&gt; como una &lt;strong&gt;profundización orientada a un uso más avanzado y profesional&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Ambos cursos forman parte de un itinerario pensado para entender la virtualización en sistemas Linux desde una perspectiva práctica, progresiva y alineada con el uso real en servidores y entornos de producción.&lt;/p&gt;
&lt;h2&gt;¿Qué es la virtualización con KVM y libvirt?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;KVM (Kernel-based Virtual Machine)&lt;/strong&gt; es la tecnología de virtualización integrada en el kernel de Linux que permite convertirlo en un hipervisor de tipo 1. Gracias a KVM, un sistema Linux puede ejecutar máquinas virtuales con un rendimiento muy cercano al nativo, aprovechando las extensiones de virtualización por hardware (Intel VT-x / AMD-V).&lt;/p&gt;
&lt;p&gt;Por su parte, &lt;strong&gt;libvirt&lt;/strong&gt; es una capa de abstracción que facilita la gestión de máquinas virtuales y otros recursos de virtualización. Proporciona una API unificada y herramientas tanto gráficas como de línea de comandos para administrar hipervisores como KVM de forma coherente y segura.&lt;/p&gt;
&lt;p&gt;En conjunto, &lt;strong&gt;KVM + libvirt&lt;/strong&gt; constituyen una de las soluciones de virtualización más utilizadas en entornos Linux, tanto en laboratorios como en infraestructuras profesionales.&lt;/p&gt;
&lt;h2&gt;Cursos publicados&lt;/h2&gt;
&lt;p&gt;Para cubrir distintos niveles de experiencia, he preparado &lt;strong&gt;dos cursos complementarios&lt;/strong&gt;, publicados en OpenWebinars y acompañados de material práctico en GitHub.&lt;/p&gt;
&lt;h3&gt;Curso 1: Introducción a la virtualización con KVM/libvirt usando virt-manager&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://plataforma.josedomingo.org/pledin/cursos/kvm1&quot;&gt;Accede al curso en Pledin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio del curso: &lt;a href=&quot;https://github.com/josedom24/curso_kvm_ow/blob/main/curso1&quot;&gt;https://github.com/josedom24/curso_kvm_ow/blob/main/curso1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Este primer curso está &lt;strong&gt;orientado a principiantes&lt;/strong&gt; que desean iniciarse en la virtualización sobre Linux sin necesidad de conocimientos previos avanzados.&lt;/p&gt;
&lt;p&gt;Características principales del curso:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Introducción a los conceptos básicos de virtualización&lt;/li&gt;
&lt;li&gt;Instalación y configuración de KVM y libvirt&lt;/li&gt;
&lt;li&gt;Uso de &lt;strong&gt;virt-manager&lt;/strong&gt; como herramienta gráfica&lt;/li&gt;
&lt;li&gt;Creación y gestión básica de máquinas virtuales&lt;/li&gt;
&lt;li&gt;Redes y almacenamiento a nivel introductorio&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es un curso ideal para &lt;strong&gt;romper la barrera de entrada&lt;/strong&gt; y comenzar a trabajar con máquinas virtuales de forma visual e intuitiva.&lt;/p&gt;
&lt;h3&gt;Curso 2: Profundización en la virtualización con KVM/libvirt&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://plataforma.josedomingo.org/pledin/cursos/kvm2&quot;&gt;Accede al curso en Pledin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio del curso: &lt;a href=&quot;https://github.com/josedom24/curso_kvm_ow/blob/main/curso2&quot;&gt;https://github.com/josedom24/curso_kvm_ow/blob/main/curso2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El segundo curso está pensado para quienes ya conocen los fundamentos y desean &lt;strong&gt;profundizar en la virtualización usando herramientas de línea de comandos&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;En este curso se trabaja, entre otros aspectos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gestión avanzada de máquinas virtuales con &lt;code&gt;virsh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Definición y modificación de dominios mediante XML&lt;/li&gt;
&lt;li&gt;Redes virtuales avanzadas&lt;/li&gt;
&lt;li&gt;Gestión de almacenamiento con libvirt&lt;/li&gt;
&lt;li&gt;Automatización y administración más cercana a entornos de servidor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Este enfoque resulta especialmente útil para &lt;strong&gt;administradores de sistemas&lt;/strong&gt;, laboratorios avanzados o escenarios donde no se dispone de entorno gráfico.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Cursos de preparación para obtener las certificaciones Python PCEP y PCAP</title><link>https://www.josedomingo.org/2025/12/curso-python-pcep-pcap/</link><guid isPermaLink="true">https://www.josedomingo.org/2025/12/curso-python-pcep-pcap/</guid><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/12/python.png&quot; alt=&quot;pcep-pacp&quot;&gt;&lt;/p&gt;
&lt;p&gt;Este año he tenido la oportunidad de preparar y publicar &lt;strong&gt;dos cursos orientados a la obtención de certificaciones oficiales de Python&lt;/strong&gt;, pensados tanto para personas que se inician en el lenguaje como para quienes desean consolidar sus conocimientos y dar un paso más en su formación.&lt;/p&gt;
&lt;p&gt;Siguiendo la misma filosofía que en otros cursos que he desarrollado, el objetivo principal ha sido &lt;strong&gt;ofrecer un itinerario claro, progresivo y muy práctico&lt;/strong&gt;, alineado con los contenidos oficiales de los exámenes y acompañado de abundantes ejemplos y ejercicios.&lt;/p&gt;
&lt;h2&gt;Certificaciones PCEP y PCAP&lt;/h2&gt;
&lt;p&gt;Las certificaciones &lt;strong&gt;PCEP&lt;/strong&gt; y &lt;strong&gt;PCAP&lt;/strong&gt; forman parte del itinerario oficial de certificación en Python y están ampliamente reconocidas como una forma sólida de validar conocimientos en este lenguaje.&lt;/p&gt;
&lt;h3&gt;PCEP – Certified Entry-Level Python Programmer&lt;/h3&gt;
&lt;p&gt;La certificación &lt;strong&gt;PCEP&lt;/strong&gt; está orientada a personas que comienzan a programar en Python. Evalúa los &lt;strong&gt;fundamentos del lenguaje&lt;/strong&gt;, tales como:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sintaxis básica y semántica&lt;/li&gt;
&lt;li&gt;Tipos de datos fundamentales&lt;/li&gt;
&lt;li&gt;Variables y operadores&lt;/li&gt;
&lt;li&gt;Control de flujo (&lt;code&gt;if&lt;/code&gt;, bucles)&lt;/li&gt;
&lt;li&gt;Funciones básicas&lt;/li&gt;
&lt;li&gt;Manejo elemental de errores&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es una excelente puerta de entrada para quien quiere demostrar que domina los conceptos esenciales de Python y que está preparado para seguir avanzando.&lt;/p&gt;
&lt;h3&gt;PCAP – Certified Associate Python Programmer&lt;/h3&gt;
&lt;p&gt;La certificación &lt;strong&gt;PCAP&lt;/strong&gt; da un paso más y está dirigida a programadores con cierta experiencia previa en Python. En este nivel se profundiza en aspectos como:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Programación estructurada y modular&lt;/li&gt;
&lt;li&gt;Colecciones y estructuras de datos avanzadas&lt;/li&gt;
&lt;li&gt;Manejo de excepciones&lt;/li&gt;
&lt;li&gt;Programación orientada a objetos&lt;/li&gt;
&lt;li&gt;Uso más avanzado de funciones y módulos&lt;/li&gt;
&lt;li&gt;Buenas prácticas en Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Superar esta certificación demuestra una &lt;strong&gt;comprensión sólida y práctica del lenguaje&lt;/strong&gt;, adecuada para entornos profesionales o académicos más exigentes.&lt;/p&gt;
&lt;h2&gt;Cursos publicados&lt;/h2&gt;
&lt;p&gt;Para cubrir ambos niveles he desarrollado &lt;strong&gt;dos cursos completos de preparación&lt;/strong&gt;, con contenidos teóricos, ejemplos explicados paso a paso y ejercicios específicos orientados al examen.&lt;/p&gt;
&lt;h3&gt;Curso de preparación para el examen &lt;strong&gt;PCEP&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://plataforma.josedomingo.org/pledin/cursos/python_pcep&quot;&gt;Accede al curso en Pledin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio del curso: &lt;a href=&quot;https://github.com/josedom24/python_pcep_pcap/tree/main/PCEP&quot;&gt;https://github.com/josedom24/python_pcep_pcap/tree/main/PCEP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio de ejercicios: &lt;a href=&quot;https://github.com/josedom24/ejercicios_python_pcep&quot;&gt;https://github.com/josedom24/ejercicios_python_pcep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Curso en OpenWebinars: &lt;a href=&quot;https://openwebinars.net/cursos/certificacion-python-pcep/&quot;&gt;https://openwebinars.net/cursos/certificacion-python-pcep/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Este curso está pensado para quienes se inician en Python y quieren una preparación guiada, clara y práctica para afrontar el examen PCEP con garantías.&lt;/p&gt;
&lt;h3&gt;Curso de preparación para el examen &lt;strong&gt;PCAP&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://plataforma.josedomingo.org/pledin/cursos/python_pcap&quot;&gt;Accede al curso en Pledin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio del curso: &lt;a href=&quot;https://github.com/josedom24/python_pcep_pcap/tree/main/PCAP&quot;&gt;https://github.com/josedom24/python_pcep_pcap/tree/main/PCAP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Repositorio de ejercicios: &lt;a href=&quot;https://github.com/josedom24/ejercicios_python_pcap&quot;&gt;https://github.com/josedom24/ejercicios_python_pcap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Curso en OpenWebinars: &lt;a href=&quot;https://openwebinars.net/cursos/certificacion-python-pcap/&quot;&gt;https://openwebinars.net/cursos/certificacion-python-pcap/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;En este segundo curso se profundiza en Python, con un enfoque más cercano al uso real del lenguaje y a los requisitos del examen PCAP.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Historia de Linux</title><link>https://www.josedomingo.org/microblog/2025/10/historia-linux/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2025/10/historia-linux/</guid><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Historia de Linux desde los años 60 hasta el día de hoy.&lt;/p&gt;
&lt;a href=&quot;/pledin/assets/2025/10/linux.jpg&quot;&gt;
  &lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/10/linux.jpg&quot; width=&quot;400&quot; alt=&quot;Historia de Linux&quot; /&gt;
&lt;/a&gt;</content:encoded><category>microblog</category></item><item><title>Contenedores Docker para la creación de páginas web estáticas usando Jekyll</title><link>https://www.josedomingo.org/2025/10/docker-jekyll/</link><guid isPermaLink="true">https://www.josedomingo.org/2025/10/docker-jekyll/</guid><pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/10/docker-jekyll.png&quot; alt=&quot;docker&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.josedomingo.org/pledin/2018/09/bienvenidos-a-pledin30/&quot;&gt;Como ya saben&lt;/a&gt;, mis páginas web son &lt;strong&gt;páginas estáticas&lt;/strong&gt; que genero utilizando &lt;strong&gt;Jekyll&lt;/strong&gt;, una herramienta muy popular en el ecosistema de Ruby. Para quienes no estén familiarizados, &lt;strong&gt;Jekyll&lt;/strong&gt; es un generador de sitios estáticos que toma contenido escrito en Markdown o HTML y lo convierte en un sitio web completo, listo para ser servido en cualquier servidor web. Entre sus principales ventajas se encuentran la simplicidad, la velocidad de carga de las páginas generadas, la facilidad para trabajar con plantillas y layouts, y la posibilidad de mantener el contenido separado del diseño.&lt;/p&gt;
&lt;p&gt;Hasta ahora, en mi servidor tenía instalado &lt;strong&gt;Ruby&lt;/strong&gt; y &lt;strong&gt;Jekyll&lt;/strong&gt; directamente, lo que me permitía generar mis sitios sin problemas. Sin embargo, al manejar &lt;strong&gt;distintos proyectos Jekyll&lt;/strong&gt;, este enfoque puede traer ciertos inconvenientes: diferentes proyectos pueden requerir &lt;strong&gt;versiones distintas de gemas&lt;/strong&gt;, que son librerías o paquetes de Ruby que Jekyll y sus extensiones utilizan para funcionar correctamente. Además, las actualizaciones de Ruby, Jekyll o de las gemas siempre pueden complicarse, generando conflictos o problemas de compatibilidad.&lt;/p&gt;
&lt;p&gt;En este artículo vamos a presentar una alternativa más &lt;strong&gt;flexible y ordenada&lt;/strong&gt;: la construcción de una &lt;strong&gt;imagen Docker con Jekyll ya instalado&lt;/strong&gt;, que nos permitirá crear distintos contenedores para generar cada uno de nuestros proyectos de manera aislada.&lt;/p&gt;
&lt;p&gt;Este enfoque tiene varias ventajas: garantiza que cada proyecto use la versión exacta de Jekyll y de sus gemas, evita conflictos entre proyectos, simplifica el proceso de actualización y facilita la portabilidad del entorno de desarrollo o generación de sitios, ya que basta con tener Docker instalado para ponerlo en marcha.&lt;/p&gt;
&lt;!--more--&gt;

&lt;h2&gt;Construyendo la imagen Docker para Jekyll&lt;/h2&gt;
&lt;p&gt;Para gestionar múltiples proyectos Jekyll de forma aislada, construimos una imagen basada en &lt;strong&gt;Ruby 3.2&lt;/strong&gt; que incluya Jekyll y Bundler (herramienta de gestión de dependencias (&lt;strong&gt;gemas&lt;/strong&gt;) para proyectos Ruby). Esta imagen servirá como base para todos nuestros contenedores.&lt;/p&gt;
&lt;h3&gt;Dockerfile explicado&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM ruby:3.2

ENV LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    TZ=UTC

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    build-essential \
    nodejs \
    git \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

WORKDIR /srv/jekyll

RUN gem install jekyll bundler

COPY build-site.sh /usr/local/bin/build-site.sh
RUN chmod +x /usr/local/bin/build-site.sh
COPY install-gem.sh /usr/local/bin/install-gem.sh
RUN chmod +x /usr/local/bin/install-gem.sh

CMD [&amp;quot;bash&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta imagen se crea a partir de la imagen base &lt;code&gt;ruby:3.2&lt;/code&gt; para asegurar compatibilidad con Jekyll, instalamos los paquetes necesarios para construir gemas y manejar proyectos Jekyll. Lo más importante,  instalamos &lt;strong&gt;Jekyll y Bundler&lt;/strong&gt;. En esta imagen copiamos dos scripts bash que utilizaremos para gestionar nuestros proyectos Jekyll: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;install-gem.sh&lt;/code&gt;: Será el encargado de instalar las gemas a partir del fichero &lt;code&gt;Gemfile&lt;/code&gt; del proyecto Jekyll. Se ejecuta sólo una vez, cuando se crea el contenedor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build-site.sh&lt;/code&gt;: Lo ejecutaremos cada vez que que queramos generar nuestra página estática a partir del proyecto Jekyll.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hay que tener en cuenta que el contenedor que vamos a crear tendrá dos volúmenes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El &lt;strong&gt;directorio del proyecto&lt;/strong&gt;: que será la ruta al repositorio donde se encuentra el proyecto Jekyll.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;directorio de salida&lt;/strong&gt;: que será la ruta del directorio donde hay que guardar el contenido estático generado.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;De manera gráfica sería:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/10/docker-jekyll2.png&quot; alt=&quot;docker&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Gestión de contenedores con &lt;code&gt;run.sh&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Para simplificar la creación y actualización de contenedores, usamos un &lt;strong&gt;wrapper bash&lt;/strong&gt; llamado &lt;code&gt;run.sh&lt;/code&gt;. Cuando usemos este script tendremos que indicar 3 parámetros en la línea de comandos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;El &lt;strong&gt;nombre del contenedor&lt;/strong&gt;: de esta manera podremos tener distintos contenedores para gestionar distintos proyectos Jekyll, cada uno de ellos podrá requerir dependencias distintas.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;directorio del proyecto&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;directorio de salida&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El script hace las siguientes operaciones:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Si no existe la imagen, la genera a partir del fichero &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si el contenedor ya existe, se arranca si estaba parado y se ejecuta &lt;code&gt;build-site.sh&lt;/code&gt;. Es decir, se genera la página estática.&lt;/li&gt;
&lt;li&gt;Si no existe el contenedor, se crea un contenedor con el nombre indicado y los volúmenes del proyecto y del HTML, se ejecuta &lt;code&gt;install-gem.sh&lt;/code&gt; y luego &lt;code&gt;build-site.sh&lt;/code&gt;. Es decir, se crea el contenedor, se instalan las gemas a partir del fichero &lt;code&gt;Gemfile&lt;/code&gt; del proyecto y se genera, por primera vez, la página estática.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Para mantener el contenedor activo se ejecuta &lt;code&gt;bash -c &amp;quot;tail -f /dev/null&amp;quot;&lt;/code&gt;, permitiendo ejecutar comandos adicionales mediante &lt;code&gt;docker exec&lt;/code&gt; sin detenerlo.&lt;/p&gt;
&lt;p&gt;El contenido del fichero &lt;code&gt;run.sh&lt;/code&gt; es:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
set -e

if [ $# -lt 3 ]; then
  echo &amp;quot;Uso: $0 &amp;lt;nombre-contenedor&amp;gt; &amp;lt;ruta-repositorio-jekyll&amp;gt; &amp;lt;ruta-salida-html&amp;gt;&amp;quot;
  exit 1
fi

CONTAINER_NAME=&amp;quot;$1&amp;quot;
REPO_DIR=$(realpath &amp;quot;$2&amp;quot;)
OUT_DIR=$(realpath &amp;quot;$3&amp;quot;)

docker image inspect docker-jekyll &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || \
  docker build -t docker-jekyll .

if docker ps -a --format &amp;#39;{{.Names}}&amp;#39; | grep -q &amp;quot;^$CONTAINER_NAME\$&amp;quot;; then
  echo &amp;quot;Contenedor existente &amp;#39;$CONTAINER_NAME&amp;#39;, ejecutando build-site.sh...&amp;quot;
  if ! docker ps --format &amp;#39;{{.Names}}&amp;#39; | grep -q &amp;quot;^$CONTAINER_NAME\$&amp;quot;; then
    docker start &amp;quot;$CONTAINER_NAME&amp;quot;
  fi
  docker exec -i &amp;quot;$CONTAINER_NAME&amp;quot; /usr/local/bin/build-site.sh &amp;quot;$REPO_DIR&amp;quot; &amp;quot;$OUT_DIR&amp;quot;
else
  echo &amp;quot;Creando contenedor &amp;#39;$CONTAINER_NAME&amp;#39; y ejecutando install-gem.sh...&amp;quot;
  docker run -dit --name &amp;quot;$CONTAINER_NAME&amp;quot; \
    -v &amp;quot;$REPO_DIR&amp;quot;:&amp;quot;$REPO_DIR&amp;quot; \
    -v &amp;quot;$OUT_DIR&amp;quot;:&amp;quot;$OUT_DIR&amp;quot; \
    docker-jekyll \
    bash -c &amp;quot;tail -f /dev/null&amp;quot;
  docker exec -i &amp;quot;$CONTAINER_NAME&amp;quot; /usr/local/bin/install-gem.sh &amp;quot;$REPO_DIR&amp;quot; 
  docker exec -i &amp;quot;$CONTAINER_NAME&amp;quot; /usr/local/bin/build-site.sh &amp;quot;$REPO_DIR&amp;quot; &amp;quot;$OUT_DIR&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Scripts para gestionar proyectos&lt;/h2&gt;
&lt;p&gt;Sólo nos queda explicar el funcionamiento de los dos script que hemos copiado en la imagen y nos ayudan a gestionar nuestros proyectos Jekyll.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;install-gem.sh&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Este script se encarga de instalar las gemas que cada proyecto Jekyll necesita según su &lt;code&gt;Gemfile&lt;/code&gt;. Solo se ejecuta, como hemos visto, cuando se crea el contenedor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
set -e

if [ $# -lt 1 ]; then
  echo &amp;quot;Uso: $0 &amp;lt;ruta-repositorio-jekyll&amp;gt;&amp;quot;
  exit 1
fi

SRC_DIR=&amp;quot;$1&amp;quot;

echo &amp;quot;Instalando Gemfile de: $SRC_DIR&amp;quot;

cd &amp;quot;$SRC_DIR&amp;quot;

if [ -f &amp;quot;Gemfile&amp;quot; ]; then
  bundle install
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;build-site.sh&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Este script genera el HTML estático del proyecto Jekyll en el directorio de salida. Se ejecuta &lt;strong&gt;cada vez que queremos generar la página&lt;/strong&gt;. Permite mantener separados el código fuente y el resultado generado, usando un volumen para la salida HTML.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;#!/bin/bash
set -e

if [ $# -lt 2 ]; then
  echo &amp;quot;Uso: $0 &amp;lt;ruta-repositorio-jekyll&amp;gt; &amp;lt;ruta-salida-html&amp;gt;&amp;quot;
  exit 1
fi

SRC_DIR=&amp;quot;$1&amp;quot;
DEST_DIR=&amp;quot;$2&amp;quot;

echo &amp;quot;Usando repositorio en: $SRC_DIR&amp;quot;
echo &amp;quot;Generando HTML en: $DEST_DIR&amp;quot;

cd &amp;quot;$SRC_DIR&amp;quot;

bundle exec jekyll build -d &amp;quot;$DEST_DIR&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ejemplo de uso&lt;/h2&gt;
&lt;p&gt;Supongamos que tenemos un proyecto Jekyll llamado &lt;code&gt;proyecto-jekyll&lt;/code&gt; y queremos generar el HTML resultante en el directorio &lt;code&gt;html&lt;/code&gt;. Además, queremos crear un contenedor llamado &lt;code&gt;contenedor-jekyll&lt;/code&gt; para gestionar este proyecto de forma aislada.&lt;/p&gt;
&lt;p&gt;Con el script &lt;code&gt;run.sh&lt;/code&gt; que hemos definido, el proceso es muy sencillo. Basta con ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./run.sh contenedor-jekyll ./proyecto-jekyll ./html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El comportamiento será el siguiente:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Creación de la imagen Docker (si no existe):&lt;/strong&gt;
El script comprobará si la imagen &lt;code&gt;docker-jekyll&lt;/code&gt; ya existe. Si no, la construirá a partir del &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Creación del contenedor (si no existe):&lt;/strong&gt;
Si el contenedor &lt;code&gt;contenedor-jekyll&lt;/code&gt; no existe, se creará, montando los volúmenes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./proyecto-jekyll&lt;/code&gt; dentro del contenedor, para acceder al código fuente.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./html&lt;/code&gt; dentro del contenedor, para guardar el HTML generado.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A continuación se ejecutará &lt;code&gt;install-gem.sh&lt;/code&gt; para instalar las gemas necesarias según el &lt;code&gt;Gemfile&lt;/code&gt; del proyecto.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generación del sitio:&lt;/strong&gt;
Finalmente, se ejecutará &lt;code&gt;build-site.sh&lt;/code&gt;, generando el HTML en el directorio &lt;code&gt;./html&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Contenedor activo para futuras ejecuciones:&lt;/strong&gt;
El contenedor permanecerá en ejecución en segundo plano, lo que permite generar de nuevo el sitio simplemente volviendo a ejecutar &lt;code&gt;run.sh&lt;/code&gt; o utilizando &lt;code&gt;docker exec&lt;/code&gt; para ejecutar comandos adicionales dentro del contenedor.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Después de ejecutar el comando, el directorio &lt;code&gt;./html&lt;/code&gt; contendrá todo el sitio estático generado por Jekyll, listo para ser servido por cualquier servidor web. Además, el contenedor &lt;code&gt;contenedor-jekyll&lt;/code&gt; estará disponible para regenerar el sitio cuando hagamos cambios en el proyecto.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls html
# index.html  about.html  css/  assets/ ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este enfoque permite gestionar múltiples proyectos Jekyll con distintas versiones de gemas sin que haya conflictos entre ellos, manteniendo cada proyecto completamente aislado dentro de su propio contenedor Docker.&lt;/p&gt;
&lt;h2&gt;Ventajas de este enfoque&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento completo&lt;/strong&gt;: cada proyecto Jekyll tiene su propio contenedor con gemas específicas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evita conflictos de versión&lt;/strong&gt;: distintas gemas o versiones de Jekyll no interfieren entre proyectos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portabilidad&lt;/strong&gt;: basta con Docker para levantar el entorno, sin necesidad de instalar Ruby ni Jekyll en el host.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reutilización&lt;/strong&gt;: la misma imagen sirve para cualquier proyecto Jekyll futuro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibilidad&lt;/strong&gt;: puedes mantener contenedores “vivos” listos para actualizar o regenerar sitios en cualquier momento.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Trucos y buenas prácticas&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regenerar el sitio tras cambios:&lt;/strong&gt;
Si modificas el contenido del proyecto Jekyll, basta con ejecutar de nuevo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./run.sh contenedor-jekyll ./proyecto-jekyll ./html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esto ejecutará &lt;code&gt;build-site.sh&lt;/code&gt; dentro del contenedor, actualizando el HTML.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Actualizar gemas o dependencias:&lt;/strong&gt;
Si quieres instalar nuevas gemas o actualizar las existentes, puedes ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -i contenedor-jekyll /usr/local/bin/install-gem.sh ./proyecto-jekyll
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eliminar contenedores antiguos:&lt;/strong&gt;
Para borrar un contenedor sin perder los datos generados en los volúmenes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker rm -f contenedor-jekyll
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inspeccionar o ejecutar comandos adicionales:&lt;/strong&gt;
Puedes abrir una shell dentro del contenedor:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker exec -it contenedor-jekyll bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusiones&lt;/h2&gt;
&lt;p&gt;El uso de &lt;strong&gt;contenedores Docker&lt;/strong&gt; para gestionar proyectos Jekyll ofrece una solución flexible, ordenada y reproducible. Cada proyecto puede mantenerse aislado, evitando conflictos de versiones de gemas o problemas de compatibilidad con Ruby y Jekyll. Además, la portabilidad y la facilidad de regenerar sitios estáticos hacen que este enfoque sea ideal tanto para desarrollo como para despliegue. Con esta metodología, se logra un flujo de trabajo más limpio, seguro y escalable, permitiendo concentrarse en el contenido y el diseño de las páginas, sin preocuparse por el entorno subyacente. Puedes encontrar los ficheros listos para probarlo en el siguiente &lt;a href=&quot;https://github.com/josedom24/docker-jekyll&quot;&gt;repositorio de GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Habilitar IP Forwarding en Debian 13</title><link>https://www.josedomingo.org/microblog/2025/10/ip-forward-debian13/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2025/10/ip-forward-debian13/</guid><pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;En &lt;strong&gt;Debian 13 (Trixie)&lt;/strong&gt; el &lt;em&gt;IP forwarding&lt;/em&gt; ya no se configura en &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;, sino en un archivo bajo &lt;code&gt;/etc/sysctl.d/&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Crea &lt;code&gt;/etc/sysctl.d/99-forward.conf&lt;/code&gt; con:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;net.ipv4.ip_forward=1  
net.ipv6.conf.all.forwarding=1  
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Aplica y reinicia servicios:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo sysctl --system
sudo systemctl restart systemd-sysctl
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Con esto el reenvío queda habilitado de forma persistente.&lt;/p&gt;
</content:encoded><category>microblog</category></item><item><title>Creación de un box para vagrant</title><link>https://www.josedomingo.org/2025/09/creacion-box-vagrant/</link><guid isPermaLink="true">https://www.josedomingo.org/2025/09/creacion-box-vagrant/</guid><pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Vagrant es una herramienta que facilita la creación y gestión de entornos de desarrollo virtualizados de forma reproducible. Su funcionamiento se basa en el uso de &lt;strong&gt;box&lt;/strong&gt;, imágenes preconfiguradas que sirven como punto de partida para desplegar máquinas virtuales de manera rápida y coherente. Estas box pueden estar disponibles para distintos &lt;strong&gt;proveedores&lt;/strong&gt; de virtualización —como VirtualBox, VMware o libvirt, entre otros—, lo que permite a los desarrolladores trabajar con la tecnología que mejor se adapte a sus necesidades.&lt;/p&gt;
&lt;p&gt;En mi caso, utilizo &lt;strong&gt;libvirt&lt;/strong&gt; como proveedor, lo que me permite crear y administrar máquinas virtuales sobre &lt;strong&gt;KVM&lt;/strong&gt; con un buen rendimiento. Sin embargo, en los últimos días he revisado el &lt;strong&gt;Vagrant Box Registry&lt;/strong&gt;, el repositorio oficial de box disponibles públicamente, y me he dado cuenta que todavía no existen box oficiales de &lt;strong&gt;Debian 13&lt;/strong&gt;, la nueva versión estable de la distribución.&lt;/p&gt;
&lt;p&gt;Por esta razón, en esta entrada del blog vamos a presentar el proceso completo que he seguido de &lt;strong&gt;creación de una box personalizada de Debian 13&lt;/strong&gt; para Vagrant, de forma que podamos disponer de un entorno base actualizado y listo para ser usado en nuestros proyectos.&lt;/p&gt;
&lt;h2&gt;Creación de la imagen base&lt;/h2&gt;
&lt;p&gt;Mi primer enfoque fue crear una imagen base de Debian 13 desde cero en KVM, con la idea de construir la box partiendo de una instalación mínima totalmente limpia. Sin embargo, en la práctica me encontré con varios problemas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Resolución de nombres y configuración de red:&lt;/strong&gt; la máquina no conseguía resolver nombres de dominio de forma estable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gestión de interfaces de red:&lt;/strong&gt; al parecer, Vagrant espera que la red se gestione mediante &lt;code&gt;ifupdown&lt;/code&gt; y &lt;code&gt;dhclient&lt;/code&gt;, pero incluso después de instalarlos y ajustar la configuración, la red no funcionaba de forma fiable en mis pruebas. Las interfaces conectadas a una red con un servidor DHCP funcionaban sin problemas, pero al conectarlas a redes privadas con direccionamiento estáticos, la configuración no se hacía de forma correcta.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Después de dedicarle un tiempo a resolver estos inconvenientes sin éxito, decidí cambiar de estrategia para simplificar el proceso y garantizar la compatibilidad con Vagrant.&lt;/p&gt;
&lt;p&gt;El nuevo enfoque consiste en crear la máquina virtual directamente con Vagrant a partir de la box oficial &lt;strong&gt;&lt;code&gt;debian/bookworm64&lt;/code&gt;&lt;/strong&gt;, y posteriormente:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Actualizar el sistema&lt;/strong&gt; desde Debian 12 (Bookworm) a &lt;strong&gt;Debian 13&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generalizar la máquina&lt;/strong&gt; eliminando datos temporales y preparando el entorno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convertir esta máquina actualizada en una nueva box&lt;/strong&gt; que servirá como base para futuros proyectos.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;De esta forma, aprovecho una box oficial y probada como punto de partida, pero obtengo al final una imagen totalmente actualizada a Debian 13 y lista para empaquetar.&lt;/p&gt;
&lt;!--more--&gt;

&lt;h2&gt;Actualización de la máquina base&lt;/h2&gt;
&lt;p&gt;Utilizando un &lt;code&gt;Vagrantfile&lt;/code&gt; con el siguiente contenido:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(&amp;quot;2&amp;quot;) do |config|
    config.vm.box = &amp;quot;debian/bookworm64&amp;quot;
    config.vm.hostname=&amp;quot;maquina_base&amp;quot;
    config.vm.synced_folder &amp;quot;.&amp;quot;, &amp;quot;/vagrant&amp;quot;, disabled: true
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creamos una nueva máquina con &lt;strong&gt;Debian12&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant up
vagrant ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A continuación vamos a hacer la actualización a &lt;strong&gt;Debian 13&lt;/strong&gt;, para ello actualizamos el sistema actual, ejecutando como &lt;code&gt;root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A continuación, modificamos los repositorios de paquetes, para ello en el fichero &lt;code&gt;/etc/apt/source.list&lt;/code&gt; cambiamos la palabra &lt;code&gt;bookworm&lt;/code&gt; a &lt;code&gt;trixie&lt;/code&gt; que es el nombre de la nueva versión de debian. Y a continuación actualizamos a la nueva versión:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -y
apt full-upgrade
apt autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reiniciamos la máquina y seguimos trabajando en ella.&lt;/p&gt;
&lt;h2&gt;Generación de la máquina base&lt;/h2&gt;
&lt;p&gt;Ya tenemos &lt;strong&gt;Debian 13&lt;/strong&gt; instalado en nuestra máquina base. A continuación, vamos a ejecutar distintas operaciones para preparar la máquina para convertirla en un nuevo box de Vagrant. En primer lugar podemos instalar los paquetes de las utilidades que queremos tener en nuestras máquinas, en mi caso sólo voy a instalar &lt;code&gt;curl&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install curl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A continuación, voy a borrar la clave pública que se ha inyectado en la creación de la máquina base, y la voy a sustituir por la &lt;strong&gt;clave pública de Vagrant&lt;/strong&gt;. De esta manera cuando se creen nuevas máquinas se detectará que tienen una clave insegura y serán sustituidas por una nueva clave pública que se genera, junto a la clave privada, en la creación de las máquinas. Para ello, con el usuario &lt;code&gt;vagrant&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://raw.githubusercontent.com/hashicorp/vagrant/master/keys/vagrant.pub -o /home/vagrant/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El siguiente paso es realizar una limpieza de la máquina, para que cuando la convirtamos a box tenga el menor tamaño posible, para ello ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt clean
sudo rm -rf /tmp/*
sudo rm -rf /var/tmp/*
sudo dd if=/dev/zero of=/EMPTY bs=1M
sudo rm -f /EMPTY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El tamaño virtual del disco de la máquina que hemos creado es de 100Gb, con el comando &lt;code&gt;dd&lt;/code&gt; creamos un fichero que ocupará todo el disco lleno de 0, que finalmente borraremos. Este paso, cuya ejecución es muy lenta, no es obligatorio, pero nos permite que posteriormente en el paso de compresión de la imagen consigamos que el box sea muy liviano ya que el espacio libre del disco esté lleno de facilita la compresión.&lt;/p&gt;
&lt;p&gt;Por último eliminamos el historial del usuario &lt;code&gt;vagrant&lt;/code&gt;y del &lt;code&gt;root&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;history -c
rm -f ~/.bash_history
unset HISTFILE
sudo su -
history -c
unset HISTFILE
rm -f /root/.bash_history
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Por último podemos apagar la máquina para continuar con el proceso de conversión.&lt;/p&gt;
&lt;h2&gt;Convertir la máquina a un nuevo box&lt;/h2&gt;
&lt;p&gt;Suponemos que la máquina que hemos creado tiene un disco que corresponde al fichero &lt;code&gt;debian12.qcow2&lt;/code&gt;. En primer lugar vamos comprimir el disco utilizando la utilizada &lt;code&gt;qemu-img&lt;/code&gt;. El fichero resultante lo tenemos que llamar &lt;code&gt;box.img&lt;/code&gt;, para ello nos dirigimos al directorio donde esta guardado el fichero correspondiente al disco (normalmente &lt;code&gt;/var/lib/libvirt/images&lt;/code&gt;, aunque puede estar en otro directorio) y como &lt;code&gt;root&lt;/code&gt; ejecutamos:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;qemu-img convert -c -O qcow2 debian13.qcow2 box.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La opción &lt;code&gt;-c&lt;/code&gt; indica la operación de compresión que se va a realizar.&lt;/p&gt;
&lt;p&gt;El box que vamos a crear es un fichero comprimido que contiene 3 ficheros: la imagen de la máquina que se debe llamar &lt;code&gt;box.img&lt;/code&gt;, un fichero de metadatos llamado &lt;code&gt;metadata.json&lt;/code&gt; donde hay información del proveedor, del formato de la imagen base y su tamaño y un fichero &lt;code&gt;Vagrantfile&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Basado en la &lt;a href=&quot;https://github.com/vagrant-libvirt/vagrant-libvirt/tree/main/example_box&quot;&gt;documentación&lt;/a&gt; del plugin &lt;code&gt;vagrant-libvirt&lt;/code&gt;, el contenido del fichero &lt;code&gt;metadata.json&lt;/code&gt; es:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;provider&amp;quot;     : &amp;quot;libvirt&amp;quot;,
  &amp;quot;format&amp;quot;       : &amp;quot;qcow2&amp;quot;,
  &amp;quot;virtual_size&amp;quot; : 100
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y el contenido del fichero &lt;code&gt;Vagrantfile&lt;/code&gt; es:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
# frozen_string_literal: true

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(&amp;quot;2&amp;quot;) do |config|

  # Example configuration of new VM..
  #
  #config.vm.define :test_vm do |test_vm|
    # Box name
    #
    #test_vm.vm.box = &amp;quot;centos64&amp;quot;

    # Domain Specific Options
    #
    # See README for more info.
    #
    #test_vm.vm.provider :libvirt do |domain|
    #  domain.memory = 2048
    #  domain.cpus = 2
    #end

    # Interfaces for VM
    #
    # Networking features in the form of `config.vm.network`
    #
    #test_vm.vm.network :private_network, :ip =&amp;gt; &amp;#39;10.20.30.40&amp;#39;
    #test_vm.vm.network :public_network, :ip =&amp;gt; &amp;#39;10.20.30.41&amp;#39;
  #end

  # Options for Libvirt Vagrant provider.
  config.vm.provider :libvirt do |libvirt|

    # A hypervisor name to access. Different drivers can be specified, but
    # this version of provider creates KVM machines only. Some examples of
    # drivers are KVM (QEMU hardware accelerated), QEMU (QEMU emulated),
    # Xen (Xen hypervisor), lxc (Linux Containers),
    # esx (VMware ESX), vmwarews (VMware Workstation) and more. Refer to
    # documentation for available drivers (http://libvirt.org/drivers.html).
    libvirt.driver = &amp;quot;kvm&amp;quot;

    # The name of the server, where Libvirtd is running.
    # libvirt.host = &amp;quot;localhost&amp;quot;

    # If use ssh tunnel to connect to Libvirt.
    libvirt.connect_via_ssh = false

    # The username and password to access Libvirt. Password is not used when
    # connecting via ssh.
    libvirt.username = &amp;quot;root&amp;quot;
    #libvirt.password = &amp;quot;secret&amp;quot;

    # Libvirt storage pool name, where box image and instance snapshots will
    # be stored.
    libvirt.storage_pool_name = &amp;quot;default&amp;quot;

    # Set a prefix for the machines that&amp;#39;s different than the project dir name.
    #libvirt.default_prefix = &amp;#39;&amp;#39;
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Una vez que tenemos los tres ficheros, lo comprimimos para crear nuestro box que hemos llamado &lt;code&gt;debian13-libvirt.box&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar cvzf debian13-libvirt.box metadata.json Vagrantfile box.img
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Probando nuestro nuevo box&lt;/h2&gt;
&lt;p&gt;Ya podemos utiliza nuestro box en nuestro equipo local, para ello tenemos que añadir el box a nuestro repositorio, ejecutando:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant box add debian13-libvirt debian13-libvirt.box --provider=libvirt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y crear un fichero &lt;code&gt;Vagrantfile&lt;/code&gt; para crear una máquina virtual, podemos ejecutar:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant init debian13-libvirt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Distribuyendo nuestro box&lt;/h2&gt;
&lt;p&gt;Si queremos que otros usuarios puedan usar nuestro box (por ejemplo, mis alumnos) es necesario subirlo al catálogo oficial de boxes de vagrant. Para ello tenemos que crear una cuenta en el catálogo en la página &lt;a href=&quot;https://portal.cloud.hashicorp.com/vagrant/discover&quot;&gt;https://portal.cloud.hashicorp.com/vagrant/discover&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Una vez logueado escogemos la opción &lt;strong&gt;Vagrant Registry&lt;/strong&gt; y creamos un nuevo registro usando el botón &lt;strong&gt;Create a Box Registry&lt;/strong&gt;, donde indicaremos un nombre y una descripción:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant1.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Una vez creado el registro, accedemos a él y creamos un nuevo box usando el botón &lt;code&gt;Create box&lt;/code&gt;. En la primera pantalla indicaremos el nombre, que en mi caso será &lt;code&gt;josedom24/debian13&lt;/code&gt;, indicaremos si es público o privado y podremos poner una descripción:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant2.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;En la siguiente pantalla podremos indicar la versión del box y una descripción de la misma:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant3.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Continuamos, y en la nueva pantalla pondremos el proveedor, en nuestro caso &lt;code&gt;libvirt&lt;/code&gt;, podremos subir el fichero &lt;code&gt;.box&lt;/code&gt; que hemos creado e indicar la arquitectura:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant4.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Se sube la imagen:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant5png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Y por último, podemos comprobar que tenemos dado de alta nuestro nuevo box en el registro:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/09/vagrant6.png&quot; alt=&quot;vagrant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Ahora cualquier usuario podría usar nuestro box, ejecutando:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant init josedom24/debian13 --box-version 13.0.1
vagrant up
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusiones&lt;/h2&gt;
&lt;p&gt;La creación de un box personalizado para Vagrant puede parecer un proceso complejo al inicio, pero siguiendo una estrategia adecuada es posible obtener imágenes estables y adaptadas a nuestras necesidades. En este caso, partimos de la box oficial de Debian 12, la actualizamos a Debian 13 y la preparamos para su uso como nueva box, lista para automatizar entornos de desarrollo sobre KVM/libvirt.&lt;/p&gt;
&lt;p&gt;Además, vimos cómo publicar el resultado en el &lt;strong&gt;Vagrant Box Registry&lt;/strong&gt;, lo que permite reutilizarlo fácilmente en distintos proyectos o compartirlo con la comunidad. Con ello disponemos de una base moderna, coherente y mantenible que simplifica enormemente el despliegue de máquinas virtuales en entornos de desarrollo.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Publicado Debian 13 Trixie</title><link>https://www.josedomingo.org/microblog/2025/08/publicado-debian13/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2025/08/publicado-debian13/</guid><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hoy, 9 de agosto, se ha publicado la última versión de mi distribución favorita: &lt;strong&gt;Debian 13 Trixie&lt;/strong&gt;. Habrá que pensar en ir actualizando. Os dejo las &lt;a href=&quot;https://www.debian.org/releases/trixie/release-notes/index.es.html&quot;&gt;Notas de publicación&lt;/a&gt;.&lt;/p&gt;
&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/08/debian13.png&quot; alt=&quot; &quot; /&gt;</content:encoded><category>microblog</category></item><item><title>El problema del enrutamiento asimétrico</title><link>https://www.josedomingo.org/2025/06/enrutamiento asimetrico/</link><guid isPermaLink="true">https://www.josedomingo.org/2025/06/enrutamiento asimetrico/</guid><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;De forma general, hablamos de &lt;strong&gt;enrutamiento asimétrico&lt;/strong&gt; cuando se da la situación donde los paquetes de ida y vuelta entre dos dispositivos no siguen el mismo camino. En el enrutamiento asimétrico, el paquete de ida toma un camino y la respuesta toma otro &lt;strong&gt;distinto&lt;/strong&gt;.
Recientemente me he encontrado un escenario concreto donde se presenta. Lo podemos ver gráficamente en el siguiente esquema:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/06/asimetrico1.png&quot; alt=&quot;enrutamiento&quot;&gt;&lt;/p&gt;
&lt;p&gt;No puedo acceder a un servidor web que tengo en mi red local porque no tengo acceso a la configuración del router y no puedo configurar las reglas DNAT para abrir los puertos de navegación Web.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/06/asimetrico2.png&quot; alt=&quot;enrutamiento&quot;&gt;&lt;/p&gt;
&lt;p&gt;La &lt;strong&gt;solución&lt;/strong&gt; pasa por evitar el acceso directo al router local. En su lugar, accedemos a una máquina remota (VPS) con dirección IP pública. Esta máquina actúa como router intermedio, ya que le configuramos una regla DNAT que redirige los paquetes al servidor web local, conectado a la VPS a través de una VPN. Es decir, los paquetes entrantes llegan al servidor web por la interfaz de la VPN (tun0).&lt;/p&gt;
&lt;p&gt;El &lt;strong&gt;problema&lt;/strong&gt;: La ruta por defecto del servidor web está configurada para enviar el tráfico saliente a través del router físico local, no por la VPN. Como resultado, la respuesta no vuelve por el mismo camino, y el cliente no puede acceder correctamente al servidor web. Esto ocurre porque el router local no puede realizar el cambio de dirección de origen (SNAT) para paquetes que no vio llegar. Lo vemos claramente en este esquema:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/06/asimetrico3.png&quot; alt=&quot;enrutamiento&quot;&gt;&lt;/p&gt;
&lt;p&gt;Lo deseable sería que el &lt;strong&gt;tráfico de respuesta también saliera por la VPN&lt;/strong&gt;, manteniendo la simetría en la ruta. Es decir, que tanto los paquetes de ida como los de vuelta pasen por el túnel VPN, como se muestra aquí:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/06/asimetrico4.png&quot; alt=&quot;enrutamiento&quot;&gt;&lt;/p&gt;
&lt;p&gt;Veamos dos posibles soluciones a este problema:&lt;/p&gt;
&lt;!--more--&gt;

&lt;h2&gt;Primera solución:  SNAT en la interfaz VPN&lt;/h2&gt;
&lt;p&gt;Cuando un cliente accede desde Internet a la &lt;strong&gt;IP pública del VPS&lt;/strong&gt;, este reenvía el tráfico por la VPN al Servidor Web (192.168.100.2). Pero el servidor &lt;strong&gt;responde a través de su ruta por defecto&lt;/strong&gt; (salida local, 192.168.0.1). Esto rompe la conexión porque:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;La IP de origen del cliente (pública) no puede ser alcanzada desde el servidor a través de su red local.&lt;/li&gt;
&lt;li&gt;El firewall/NAT intermedio puede bloquear la respuesta porque no corresponde al flujo original.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para solucionar esto, en el VPS aplicamos &lt;strong&gt;Source NAT (SNAT)&lt;/strong&gt; sobre el tráfico que se reenvía al servidor. Es decir, &lt;strong&gt;modificamos la IP de origen del paquete antes de enviarlo por la VPN&lt;/strong&gt;, de modo que el servidor web &lt;strong&gt;ve como origen la IP de la VPN del VPS&lt;/strong&gt;, en lugar de la IP real del cliente.&lt;/p&gt;
&lt;p&gt;De esta forma, el servidor responde por la misma VPN (&lt;code&gt;tun0&lt;/code&gt;), y la respuesta llega correctamente al cliente.&lt;/p&gt;
&lt;p&gt;En este caso en la VPN tendríamos dos reglas NAT:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Una &lt;strong&gt;Destination NAT (DNAT)&lt;/strong&gt;: para redirigir el tráfico entrante (puertos 80 y 443) al servidor web local por la VPN.&lt;/li&gt;
&lt;li&gt;Otra &lt;strong&gt;SNAT&lt;/strong&gt;: para modificar la IP de origen del cliente por la IP VPN del VPS, asegurando así que el servidor devuelva la respuesta por la misma VPN.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;iptables -A PREROUTING -i eth0 -p tcp -m multiport --dports 80,443 -j DNAT --to-destination 192.168.100.2
iptables -A POSTROUTING -s 192.168.100.0/24 -o tun0 -j MASQUERADE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El principal problema de esta solución es que al reescribe la dirección IP de origen del paquete para que parezca que viene del VPS, el Servidor Web no concoe la dirección IP del cliente real y esto puede causar problemas secundarios: el servidor web no puede registrar en los log las IP reales de los clientes, no puede aplicar restricciones por la IP de origen,...&lt;/p&gt;
&lt;h2&gt;Segunda solución: Políticas de enrutamiento (Policy-Based Routing - PBR)&lt;/h2&gt;
&lt;p&gt;Para solucionar el problema de que le Servidor Web no conoce la dirección IP de los clientes reales, vamos a explorar otra solución. En este caso, asumimos que hemos borrado la regla SNAT que explicamos en el punto anterior.&lt;/p&gt;
&lt;p&gt;Por defecto, Linux enruta según la tabla de rutas global (basada en destino). Pero con las políticas de enrutamiento, podemos definir rutas de este estilo: &lt;strong&gt;&amp;quot;Si el tráfico viene desde cierto origen o llega por cierta interfaz, usa una tabla de rutas distinta.&amp;quot;&lt;/strong&gt; De forma concreta en nuestro ejemplo: &lt;strong&gt;&amp;quot;Si el tráfico viene de la VPN (192.168.100.0/24), responde por la VPN, no por la ruta por defecto.&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Esta configuración se tiene que realizar en el Servidor Web. Veamos los pasos que tenemos que seguir para conseguirlo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Vamos a crear una nueva tabla de enrutamiento que llamaremos &lt;strong&gt;vpnroute&lt;/strong&gt;. Para ello editamos el fichero &lt;code&gt;/etc/iproute2/rt_tables&lt;/code&gt; y añadimos la siguiente línea:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;100 vpnroute
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Añadimos una ruta por defecto para esta tabla.Indicamos que para la tabla &lt;code&gt;vpnroute&lt;/code&gt;, la salida por defecto es la interfaz VPN &lt;code&gt;tun0&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip route add default dev tun0 table vpnroute
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Añadimos una regla de enrutamiento condicional. Configuramos la siguiente regla: &lt;strong&gt;Si un paquete viene desde la IP de la VPN, usa la tabla &lt;code&gt;vpnroute&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip rule add from 192.168.100.2 lookup vpnroute
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verificamos la configuración:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ip rule
...
32765:    from 192.168.98.2 lookup vpnroute

ip route show table vpnroute
default dev tun0 scope link 
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ya estaría funcionando, pero esta configuración no es persistente. Para hacer la persistente si estamos usando la configuración de red con la herramienta &lt;strong&gt;ifupdown&lt;/strong&gt;, podríamos hacer un script ejecutable que guardaríamos en el directorio&lt;code&gt;/etc/network/if-up.d/&lt;/code&gt; para que se ejecutase al levantar la interfaz. El fichero se puede llamar &lt;code&gt;/etc/network/if-up.d/policy-routing-vpn&lt;/code&gt; con el siguiente contenido:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;#!/bin/sh

# Solo ejecutar si la interfaz es tun0
[ &amp;quot;$IFACE&amp;quot; = &amp;quot;tun0&amp;quot; ] || exit 0

# Añadir ruta por defecto en la tabla &amp;#39;vpnroute&amp;#39;
ip route add default dev tun0 table vpnroute 2&amp;gt;/dev/null

# Añadir regla de enrutamiento si no existe
ip rule list | grep -q &amp;quot;from 192.168.100.2 lookup vpnroute&amp;quot; || \
  ip rule add from 192.168.100.2 lookup vpnroute

exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y finalmente, le damos permiso de ejecución:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chmod +x /etc/network/if-up.d/policy-routing-vpn
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Aunque podríamos explorar otras soluciones, como instalar un &lt;strong&gt;proxy inverso&lt;/strong&gt; en el VPS o modificar directamente la &lt;strong&gt;ruta por defecto&lt;/strong&gt; del servidor web (lo cual podría afectar otras comunicaciones), con esta descripción queda claro cuáles son las &lt;strong&gt;dos soluciones principales&lt;/strong&gt; al problema del enrutamiento asimétrico en este escenario concreto.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Por un lado, la solución basada en &lt;strong&gt;SNAT&lt;/strong&gt; es sencilla de implementar y permite resolver el problema de forma rápida, aunque con la &lt;strong&gt;limitación de perder la IP real del cliente&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Por otro, la solución mediante &lt;strong&gt;enrutamiento por políticas (policy-based routing)&lt;/strong&gt; es más limpia y preserva la información del cliente, pero requiere mayor control sobre el sistema y una configuración algo más avanzada.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Elegir una u otra dependerá de las necesidades específicas del entorno: si prima la simplicidad o si es esencial mantener la trazabilidad de los clientes.&lt;/p&gt;
&lt;p&gt;En definitiva, entender cómo funciona el enrutamiento asimétrico y cómo corregirlo es fundamental en redes modernas, especialmente cuando combinamos &lt;strong&gt;VPNs, NAT y servicios web expuestos&lt;/strong&gt; desde redes locales a través de máquinas intermedias como VPS.&lt;/p&gt;
</content:encoded><category>blog</category></item><item><title>Mi última camiseta...</title><link>https://www.josedomingo.org/microblog/2025/06/camiseta-linux/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2025/06/camiseta-linux/</guid><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/06/camiseta_linux.jpg&quot; alt=&quot; &quot; /&gt;</content:encoded><category>microblog</category></item><item><title>Me ha encantado... muchas gracias</title><link>https://www.josedomingo.org/microblog/2025/05/regalo/</link><guid isPermaLink="true">https://www.josedomingo.org/microblog/2025/05/regalo/</guid><pubDate>Sun, 25 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://www.josedomingo.org/pledin/assets/2025/05/regalo.jpg&quot; alt=&quot; &quot; /&gt;</content:encoded><category>microblog</category></item></channel></rss>