# escribano.dev (contenido completo) > Blog técnico sobre programación, self-hosting y soberanía digital. Contenido íntegro de todos los posts en Markdown, listados del más reciente al más antiguo, por idioma. Cada post también está disponible como archivo Markdown independiente en la misma URL con sufijo `.md` (por ejemplo https://escribano.dev/es/blog/006-homelab-vpn-access.md). ## Blog completo (es) ### [Cinco LLMs de pesos abiertos explicados como Caballeros del Zodiaco](https://escribano.dev/es/blog/021-llm-caballeros-zodiaco/) > En el recreo nadie se ponía de acuerdo sobre cuál era el mejor caballero, y con los modelos abiertos pasa igual. Una chuleta con los cinco protagonistas de Los Caballeros del Zodiaco para orientarte. Cuando decides no quedarte con los modelos frontera y empiezas a bucear en el catálogo de pesos abiertos, te encuentras con una auténtica jungla. Hay docenas de modelos, todos publican pesos, y ninguno lidera en todo. La pregunta "¿cuál es el mejor modelo abierto?" se queda sin respuesta limpia. Me pasó lo mismo con las distribuciones de Linux. Cuando me inicié en 2003, probé varias e incluso FreeBSD en busca de la "perfecta". No existía Ubuntu todavía (llegó en 2004, y es lo que hoy diríamos que es la distro para el que se inicia). Antes estaban Debian, Red Hat, Mandrake, Slackware, Gentoo, SUSE: cada una con su comunidad y sus guerras santas. Preguntar cuál era la mejor terminaba siempre en *flamewars*, porque la respuesta dependía de para qué la quisieras. Y hay una sensación aún más antigua a la que me remite esto. TVE empezó a emitir la serie en 1991, y en el recreo cada uno tenía su caballero favorito; el mío era el del Dragón. Nadie preguntaba en serio cuál de los protagonistas era "el mejor": la respuesta dependía de qué buscas en un personaje. Con los pesos abiertos me pasa igual: la respuesta no es universal, sino situacional. Y como me sirvió la serie para no olvidar quién era quién en la API, dejo aquí el mapeo. Es una chuleta, no un benchmark; el mapa real sigue siendo el uso que le das tú. ## El mapeo | Caballero | Proveedor | Rasgo que recuerdo | |---|---|---| | Seiya (Pegaso) | Qwen | Versátil en todos los tamaños; vale para todo sin destacar en nada | | Shiryu (Dragón) | GLM | De solo texto a multimodal de verdad | | Hyoga (Cisne) | Kimi | Frío y preciso en código; caro de mantener | | Ikki (Fénix) | DeepSeek | Apariciones disruptivas: razonamiento, caché y precios | | Shun (Andrómeda) | MiniMax | El débil aparente que a veces sorprende | ## Seiya (Caballero de Pegaso) → Qwen Seiya es el protagonista de la serie, pero no destaca en nada concreto: está en todas las sagas sin ser el especialista de ninguna. Qwen tiene una dinámica parecida. La familia cubre un rango enorme de tamaños, desde modelos pensados para correr en máquinas modestas hasta los de centro de datos, con checkpoints generales, de código y multimodales. Si tu máquina no puede con los modelos grandes, la apuesta razonable es Qwen3.8-27B: pequeño de verdad, descargable, con licencia Apache 2.0 y suficiente para probar. Rara vez es el mejor en un solo eje, pero sirve para casi todo. Un aviso práctico: con tantas versiones, el nombre no siempre dice qué estás usando. Los pesos abiertos publicados son los de **Qwen3.8-Flash-Next** (125B de parámetros totales, 6B activos), mientras que el modelo de producción que sirve la API como `qwen3.8-flash` llega con un millón de tokens de contexto por defecto y herramientas integradas. Alias y pesos no son exactamente el mismo modelo: lee el ID real antes de confiar en el nombre. ## Shiryu (Caballero del Dragón) → GLM En la saga del Santuario, Shiryu se ciega a sí mismo para no ver el Escudo de Medusa de Argol de Perseo, que petrificaba a quien lo mirara. La vista le vuelve al despertar el séptimo sentido durante esa misma saga, la vuelve a perder contra Krishna de Crisaor en la saga de Poseidón y no la recupera definitivamente hasta el arco de Hades, al despertar el octavo sentido. Esa primera escena, la del sacrificio voluntario, es la que me marcó y la que explica por qué era mi favorito. GLM pasó por un ciclo parecido. Las versiones anteriores de la serie GLM-5 no eran multimodales, y **GLM-5.3-Flash** (finales de agosto de 2026) llegó como el primer modelo multimodal nativo de la serie (texto e imagen), con pesos MIT en Hugging Face, 320B de parámetros totales, 18B activos y un millón de tokens de contexto. Ojo con dos matices: no es un modelo pequeño, y el GLM-5.3 grande (el *flagship*) no es multimodal. La recuperación de la "vista", en cualquier caso, es el rasgo que lo diferencia. ## Hyoga (Caballero del Cisne) → Kimi Hyoga, discípulo de Camus, pelea con hielo: frío y preciso. Kimi me da una sensación parecida: fiable en código, pero caro de mantener a pleno rendimiento. Cursor basó sus Composer 2 y 2.5 en Kimi K2.5 (en Composer 2.5, el 85 % del cómputo es entrenamiento propio de Cursor). **Kimi K2.7 Code** (12 de junio de 2026) es mucho más barato y lo he usado para tareas de infra. **Kimi K3** (API el 16 de julio de 2026, pesos el 27 de julio, con licencia propia que no es MIT) sale caro y no es herramienta de día a día: lo dejo para cuando me sobra cuota justo antes de que se reinicie el límite de uso de mi suscripción de OpenCode Go, y aprovecho para pasarle revisiones de código en busca de vulnerabilidades. ## Ikki (Caballero del Fénix) → DeepSeek Ikki tiene la costumbre de aparecer en el momento justo y cambiar el signo de la pelea. DeepSeek hace lo mismo con sus modelos: fue de los primeros en poner el razonamiento en el mapa de los pesos abiertos, y después ha seguido moviendo el tablero con cambios de precio y de caché. El 10 de septiembre de 2026 publicó **V4.1 Flash**, con pesos MIT, y los alias antiguos (`deepseek-v4-flash`) pasaron a apuntar a la nueva versión. Su caché en horas valle (las franjas de menor demanda, con tarifa reducida; en horas punta cuesta el doble) se queda en 0,003 $ por millón de tokens, muy por debajo de la competencia. En un bucle agéntico, donde el contexto vuelve una y otra vez, eso puede decidir la factura más que el precio de entrada. ## Shun (Caballero de Andrómeda) → MiniMax Shun parece el más débil por pacifista: esquiva pelear siempre que puede y usa su cadena más para defenderse que para atacar, pero cuando la cosa se pone seria, responde. En mi experiencia, MiniMax es de los proveedores que menos se mencionan, pero puede ser muy útil en contextos concretos; me da la impresión de que sobre todo en refactors con mucho contexto. Los datos al menos no lo contradicen: **MiniMax M3** (1 de junio de 2026) trae ventana de un millón de tokens (con un mínimo garantizado de 512K en la API). No es un modelo pequeño: 428B de parámetros totales y unos 23B activos. Es multimodal nativo, usa una arquitectura de atención dispersa ([MSA](https://arxiv.org/abs/2606.13392)) y sale con licencia propia, no MIT. Y además es barato. Para refactors o análisis de logs con mucho contexto, MiniMax es la cadena de Andrómeda: la que aparece de repente y lo sujeta todo. ## Cierre Esto es una chuleta, no un benchmark. Si quieres números, la [guía rápida](/es/blog/018-open-weight-models-api/) y el post sobre [caché y runtimes](/es/blog/019-open-models-cache-agents/) están ahí. Aquí solo quería un mapa que cupiera en la cabeza. *Nota: los nombres y personajes de Saint Seiya son propiedad de Toei Animation y Masami Kurumada. Este post es una analogía original; no utiliza imágenes ni material protegido.* ---------------------------------------- ### [Modelos abiertos por API: caché, agentes y tres caballos de batalla](https://escribano.dev/es/blog/019-open-models-cache-agents/) > En bucles agénticos el precio de lectura de caché decide la factura, y en acierto DeepSeek V4.1 Flash y MiMo-V2.6-Flash cuestan prácticamente lo mismo. Los tres workhorses del día a día con herramientas: DeepSeek por defecto, GLM-5.3-Flash como segunda ruta y MiMo para salida barata en valle y multimodal. Mi [guía rápida de modelos de pesos abiertos](/blog/018-open-weight-models-api/) dejó la tabla sin cifras y un reparto en prueba. Aquí lo que en una guía rápida no cabe: caché, dónde enchufo cada modelo y los tres que hoy uso (o contrasto) en bucles con herramientas. *Precios de API comprobados el 12 de octubre de 2026, salvo que se indique otra fuente o fecha.* ## La caché es donde se decide el coste En un bucle agéntico el contexto vuelve en cada turno: system prompt, herramientas, trozos del repo. Por eso miro primero el precio de **lectura de caché**. DeepSeek V4.1 Flash (`deepseek-flash`) en **valle** cobra \$0.003 por millón en hit y \$0.15 en miss ([pricing DeepSeek](https://api-docs.deepseek.com/quick_start/pricing)). MiMo-V2.6-Flash cobra \$0.0028 en hit y \$0.14 en miss ([ficha MiMo](https://mimo.mi.com/models/en-US/mimo-v2.6-flash)). En acierto, los dos cuestan prácticamente lo mismo; MiMo, que no entraba en aquella guía, queda a la par. Para ver el rango, la [tarifa de Moonshot](https://platform.moonshot.ai/docs/pricing/chat) para Kimi K3 es \$0.30 en hit y \$3.00 en miss. Qwen3.8-Max, MiniMax-M3 y el resto de tablas los dejo para contexto enorme o salida de razonamiento; en esa guía están los nombres. La tarifa de caché de **GLM-5.3-Flash** no la he mirado para este post. La salida suele pesar más que la entrada. Lo enlazo con [The Bitter Lesson](/blog/017-bitter-lesson/): más cómputo al decidir, más factura. En valle, DeepSeek pide \$0.60 por millón de salida; MiMo, \$0.28 en la ficha. En **punta**, DeepSeek sube a hit \$0.006, miss \$0.30 y salida \$1.20 por millón (lunes a viernes 01:00–04:00 y 06:00–10:00 UTC, fuera festivos en China; el resto valle). Si quieres apagar razonamiento en API, DeepSeek V4.1 Flash admite `extra_body` `{"thinking": {"type": "disabled"}}` ([Thinking Mode DeepSeek](https://api-docs.deepseek.com/guides/thinking_mode)). MiMo acepta `{"thinking": {"type": "disabled"}}` en su API compatible con OpenAI ([ficha MiMo](https://mimo.mi.com/models/en-US/mimo-v2.6-flash)). En GLM-5.3-Flash no hay interruptor: según la [guía de thinking de Z.AI](https://docs.z.ai/guides/capabilities/thinking-mode.md), `thinking.type` solo admite `enabled`, y la [guía del modelo](https://docs.z.ai/guides/vlm/glm-5.3-flash.md) recomienda `reasoning_effort: max`. ## Tres caballos de batalla Uso tres checkpoints con pesos en Hugging Face: **DeepSeek V4.1 Flash**, **GLM-5.3-Flash** y **MiMo-V2.6-Flash**. Los tres declaran ventana de **1M tokens** ([guía de GLM](https://docs.z.ai/guides/vlm/glm-5.3-flash.md), [ficha de MiMo](https://mimo.mi.com/models/en-US/mimo-v2.6-flash), [pricing de DeepSeek](https://api-docs.deepseek.com/quick_start/pricing)) y los tres son MIT: DeepSeek y GLM según mi [guía rápida](/blog/018-open-weight-models-api/), MiMo en su [README](https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Flash-MOPD). No sustituyen a Kimi K2.7 Code ni a Qwen3.8-Max en backend pesado; cubren el día a día con herramientas en OpenClaw, Moltis u OpenCode. **DeepSeek V4.1 Flash** (`deepseek-flash`) es mi primera opción en texto y código. La API la he visto en más sitios que las otras dos, responde antes en mi uso y puedo apuntar a valle o evitar punta. En **Moltis** la dejo por defecto. Si en la misma llamada necesito audio o vídeo, cambio de modelo. **GLM-5.3-Flash** es mi segunda ruta en OpenClaw (`zai/*`, `tool_stream: true` por defecto; lo cortas con `params.tool_stream: false`, [proveedor Z.AI en OpenClaw](https://docs.openclaw.ai/providers/zai)) o cuando el flujo es visual. En Moltis la alterno cuando quiero variar o probar Z.AI. En OpenCode a veces cae o va lento; DeepSeek y MiMo me han parecido más fiables. Sigo contrastándolo en mis repos. **MiMo-V2.6-Flash** lo elijo cuando quiero salida barata en valle y multimodal en la misma API; en programación me va bien. En bucles agénticos largos dentro de Moltis todavía no lo he metido. - Serie V2.6 anunciada el **22 de septiembre de 2026** ([changelog MiMo](https://mimo.mi.com/static/docs/updates/model.md)); repo `MiMo-V2.6-Flash-RL` creado el **21** en Hugging Face (metadatos). - MoE 309B totales, 15B activados ([README MOPD](https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Flash-MOPD)). - Entrada texto, imagen, vídeo y audio; salida texto. `https://api.xiaomimimo.com/v1`, modelo `mimo-v2.6-flash`, o OpenRouter si el agente no trae Xiaomi. ## Dónde lo enchufo **Moltis** concentra lo que más uso: DeepSeek, Z.AI, OpenRouter, endpoint OpenAI-compatible y MiMo por OpenRouter o base URL de Xiaomi. MiniMax y Moonshot también están. Las herramientas de fichero (`Read`, `Write`, `Edit`, `Glob`, `Grep`) copian esquemas de Claude Code. En **OpenCode** es donde comparo proveedores con calma; la parte de specs y artefactos va en el [post sobre SDD](/blog/015-spec-driven-development-iag/). La fiabilidad de GLM frente a DeepSeek y MiMo se nota más ahí. **OpenClaw** usa `provider/model`, tiene `--local-model-lean` para modelos locales lentos y, con GLM y `zai/*`, trae `tool_stream` activado. En OpenCode tiro de OpenRouter como plan B si cae la ruta principal. Por defecto sigo con **DeepSeek**. **GLM** lo reservo para segunda ruta o flujos visuales, y **MiMo** para programar cuando me importa la salida barata. A MiMo aún no lo he metido en bucles largos en Moltis; es lo primero que revisaré. ---------------------------------------- ### [Go domina mi homelab: catálogo de apps self-hosted](https://escribano.dev/es/blog/020-homelab-go-stack-catalog/) > Catálogo con lenguaje, tamaño de imagen y RAM en reposo de las 24 apps del homelab: 13 en Go, 11 de las 15 de infraestructura. El binario autocontenido y el consumo bajo de memoria explican por qué Go domina la capa que se administra a diario, mientras que la media pesada (Jellyfin, Immich) se queda en otros lenguajes. El homelab tiene tres máquinas. kvothe (Ubuntu, Docker) acumula la media y las utilidades; kilvin (Fedora, Podman y quadlets) lleva la infraestructura y la identidad; elodin (QNAP) guarda datos y copias. Si pongo los contenedores de las dos primeras en una tabla y los ordeno por tamaño, sale un patrón: los servicios ligeros de infraestructura están escritos en Go, y los pesados de media, en otros lenguajes. Este post es ese catálogo: lenguaje, tamaño de imagen y RAM de cada pieza que corre hoy. No es exhaustivo ni lo pretende; con esas tres columnas el patrón se ve solo. La [serie homelab](/blog/006-homelab-vpn-access/) ya cubrió acceso, DNS, [proxy](/blog/008-caddy-reverse-proxy-pihole/) e [identidad](/blog/009-homelab-identity/); aquí toca la foto completa del stack una vez resueltas esas capas. ## Go en la infraestructura Go se ha convertido en uno de los lenguajes dominantes en las herramientas de infraestructura moderna. Varios de los proyectos que definieron la última década de operaciones están escritos en Go: | Categoría | Proyectos | |-----------|-----------| | Contenedores y orquestación | Docker, Podman, containerd, Kubernetes, k3s, Helm, Kustomize | | Red y proxy | Caddy, Traefik, CoreDNS | | Observabilidad | Prometheus, Grafana, InfluxDB, Telegraf, Loki | | Bases de datos y almacenamiento | etcd, CockroachDB, SeaweedFS, rclone, restic | | Infraestructura como código | Terraform, OpenTofu, Pulumi, Terragrunt, Packer | | Herramientas de desarrollo | Hugo, Dagger, gh (GitHub CLI) | | Aplicaciones self-hosted | Syncthing, Zitadel, Forgejo, Memos, FileBrowser, PhotoPrism, Vikunja, Navidrome | Un detalle sobre InfluxDB: la versión 3 está reescrita en Rust; las 1.x y 2.x siguen en Go. Contenedores, orquestación, observabilidad e infraestructura como código comparten perfil: software que corre siempre, en muchos sitios, con poco mantenimiento y un binario que despliegas sin pensarlo. Ahí encaja Go. El contraejemplo también existe: nginx, Redis y PostgreSQL están escritos en C y dominan su hueco con una madurez que Go no ha desbancado. Go se ha impuesto sobre todo en la capa de operaciones; en las bases de datos clásicas y los servidores web, manda C. ## El dato: Go por mayoría y por capa Mi homelab es una versión reducida de ese patrón. De las veinticuatro aplicaciones del catálogo, trece están escritas en Go: algo más de la mitad. En la capa de infraestructura (proxy, DNS, identidad, git, métricas, dashboards) son once de quince. En media y documentos el patrón se invierte: de las nueve aplicaciones de ese grupo, solo dos (Memos y FileBrowser) son Go. Cuento como aplicación cada servicio con una función propia; quedan fuera las bases de datos, los contenedores de desarrollo y CI, y los proyectos en marcha. Ese filtro favorece a Go, y lo digo sin rodeos: las tres bases de datos del catálogo (MariaDB, Firebird y Postgres) están en C o C++, así que excluirlas sube el porcentaje. Es una decisión de método; si las contara, el reparto sería menos favorable. La tendencia se mantiene igual: cuanto más cerca del mantenimiento diario, más Go; cuanto más cerca de servir gigas de fotos y películas, menos. ## El catálogo Las imágenes son las que reportan `docker images` y `podman images` (tamaño de capas, una arquitectura); la RAM es la de `docker stats` y `podman stats` en reposo, redondeada. Algunos servicios (Caddy, Memos, FileBrowser, Prometheus, Grafana, Vaultwarden y Whisper) corren en ambas máquinas; en esos casos doy el rango. rclone lo uso bajo demanda para las copias; al no ser un servicio residente, queda fuera del catálogo. ### Infraestructura y plataforma | Aplicación | Lenguaje | Imagen | RAM | Para qué | |------------|----------|--------|-----|----------| | node-exporter | Go | 26 MB | 17 MiB | Métricas del host | | DuckDNS | shell | 35 MB | 5 MiB | DNS dinámico | | oauth2-proxy | Go | 36 MB | 5 MiB | Forward auth | | Syncthing | Go | 43 MB | 26 MiB | Sincronización de archivos | | Caddy | Go | 59–63 MB | 12–14 MiB | Reverse proxy y TLS | | Authelia | Go | 78 MB | 17 MiB | Forward auth y 2FA | | Tailscale | Go | 112 MB | 43 MiB | Mesh y acceso | | Forgejo | Go | 178 MB | 203 MiB | Git self-hosted | | Zitadel | Go | 179 MB | 81 MiB | Identidad (IdP OIDC) | | Portainer | Go | 184 MB | 19 MiB | Gestión de contenedores | | Homepage | TypeScript | 266 MB | 115 MiB | Dashboard | | Prometheus | Go | 296–423 MB | 90–93 MiB | Métricas | | Glances | Python | 502 MB | 90 MiB | Monitor del sistema | | wg-easy | Node | 535 MB | 18 MiB | WireGuard con panel | | Grafana | Go | 555 MB–1,07 GB | 109–156 MiB | Dashboards | ### Aplicaciones, media y documentos | Aplicación | Lenguaje | Imagen | RAM | Para qué | |------------|----------|--------|-----|----------| | Memos | Go | 66–69 MB | 12 MiB | Notas rápidas | | aMule | C++ | 126 MB | 129 MiB | Red P2P eD2k/Kad | | FileBrowser | Go | 192–219 MB | 24–35 MiB | Archivos desde el navegador | | Vaultwarden | Rust | 243–249 MB | 10–14 MiB | Gestor de contraseñas | | BookStack | PHP | 401 MB | 12 MiB | Documentación y wiki | | Whisper | Python | 606–614 MB | 13–315 MiB | Transcripción (STT) | | Piper | C++ | 877 MB | 650 MiB | Síntesis de voz (TTS) | | Jellyfin | C# | 1,56 GB | 1,0 GiB | Servidor multimedia | | Immich | TypeScript (+ Python) | 3,5 GB | 385 MiB | Fotos | Las bases de datos que acompañan a esas aplicaciones (MariaDB, Firebird y Postgres) no cuentan como aplicaciones; son infraestructura de apoyo y están todas fuera de Go. ## Por qué Go encaja en el homelab La explicación empieza en el binario. Go compila normalmente a un binario autocontenido: el runtime de Go va incluido en el propio ejecutable, así que no hace falta instalar un runtime externo como Python, Java o .NET. Evitando CGO puedes producir binarios estáticos, aunque no todos los binarios Go lo son ni están libres de dependencias del sistema. Ese modelo reduce las dependencias que pueden desincronizarse entre la máquina y el contenedor. En el homelab eso se traduce en imágenes de decenas de megas: Caddy 63 MB, node-exporter 26 MB, Syncthing 43 MB, Authelia 78 MB. Las que superan el gigabyte en mi catálogo son todas de media (Jellyfin 1,56 GB, Immich 3,5 GB). La RAM en reposo sigue la misma lógica. Caddy se queda en 12–14 MiB, Memos en 12 MiB, Authelia en 17 MiB y Vaultwarden (Rust) en 14 MiB. Whisper salta de 13 MiB en reposo a más de 300 MiB cuando transcribe, Piper (TTS) se planta en 650 MiB, el servidor de Immich arranca en 385 MiB y Jellyfin ronda el gigabyte. En un mini PC de bajo consumo, esa diferencia se nota en cuántos servicios caben sin hacer swap y en la factura eléctrica. Yo lo tengo medido: kvothe tiene 16 GB de RAM y de normal quedan libres unos 12 GB, con Jellyfin y buena parte de los servicios del catálogo funcionando. Con ese margen se puede seguir añadiendo servicios sin miedo a saturar la RAM, apuntando a los ligeros siempre que se pueda. La tercera pieza es el mantenimiento. Menos piezas móviles significan menos cosas que rompen al actualizar. Que eso se traduzca en actualizaciones sin drama depende más de los contenedores que de Go: aislar el servicio es lo que permite actualizar y revertir cuando toca. Ahora bien, cuando el contenedor depende de un binario único (como pasa con Go y Rust), la imagen es mucho más pequeña y las actualizaciones, más rápidas. ## Tres matices al patrón Go no lo explica todo. Hay tres matices que conviene tener presentes. El primero: Go y "ligero" no siempre van juntos. Grafana (Go) pesa más de medio gigabyte, Prometheus (Go) entre 296 y 423 MB, y Forgejo (Go) se queda en 203 MiB de RAM pese a ser un único binario. Las herramientas de observabilidad empaquetan el frontend y los plugins, y un forge de git arrastra mucho más que el runtime. Go reduce el backend; lo que decidas empaquetar encima sigue pesando lo suyo. El segundo: la compacidad no es exclusiva de Go. Vaultwarden, escrito en Rust, genera una imagen de 243 MB y arranca en 14 MiB, en línea con los servicios Go del extremo ligero. Rust comparte el motivo (binario autocontenido, imágenes pequeñas y buen perfil de consumo en servicios como este), así que el sitio de Go en este eje no es permanente. El tercero: la media. Jellyfin (C#) domina el extremo pesado y no tiene un equivalente en Go igual de completo para vídeo. Con Immich (TypeScript y Python) el argumento es más débil: existe PhotoPrism, también en Go, con un enfoque y un flujo de trabajo distintos a los que busco. Me decanté por Immich porque es multiusuario, aunque en la práctica soy yo solo; me queda en la lista probar PhotoPrism o incluso Ente (que también tiene el servidor en Go) en algún momento del futuro. El criterio de fondo es la fricción operativa: cuando una pieza pide más RAM y más atención que su alternativa, se nota en la tabla; cuando no, se queda. ## Cierre En el [marco RPS](/blog/002-rps-principle/), Go ocupa el sitio de la piedra: una pieza que funciona, con poco que romper. Para infraestructura personal, donde lo que decide es el mantenimiento, la piedra gana. El catálogo repite el patrón. Once de los quince servicios de infraestructura están escritos en Go. Los más ligeros (node-exporter, Caddy, Authelia, Syncthing, Tailscale) se quedan en imágenes de decenas de megas y unas decenas de MiB de RAM en reposo; otros como Forgejo, Zitadel, Prometheus o Grafana se alejan bastante de ese perfil. Los gigabytes y la RAM alta se concentran en la media: Jellyfin (1,56 GB, 1 GiB) e Immich (3,5 GB, 385 MiB). El lenguaje es de las pocas decisiones que no se cambian con un reverse proxy delante, así que conviene mirarlo antes de desplegar. En la capa que administro a mano, Go gana por mayoría; en la de media, depende de lo que haya disponible en cada ecosistema. Este catálogo es también una instantánea de lo que significa la [soberanía digital](/blog/005-digital-sovereignty/) en la práctica: quince servicios en hardware propio, cada uno con su coste de mantenimiento a la vista y una salida despejada si algo deja de encajar. ---------------------------------------- ### [S3 ya no es (solo) Amazon: alternativas, precios y el coste de recuperar](https://escribano.dev/es/blog/018-s3-compatible-object-storage/) > La API de S3 lleva congelada desde 2006 y es el idioma común del storage. MinIO, la referencia self-hosted, quedó archivada en 2026; el relevo lo toman Ceph RGW (madurez), SeaweedFS (el subconjunto S3 más amplio) y Garage (ligero y geodistribuido). En la factura, la trampa está en recuperar: un TB de Deep Archive cuesta más de 100 dólares de vuelta, mientras R2 y B2 no cobran salida a internet. Abre la documentación de cualquier herramienta que toque almacenamiento: rclone, restic, Kopia, Duplicati, Cyberduck, PhotoPrism, Forgejo. En todas aparece la misma expresión, *S3-compatible*, en la página de configuración. Lo mismo en la web de cualquier proveedor de object storage que haya surgido en la última década. Hay una API que se convirtió en el idioma por defecto del sector, y la forma en que llegó ahí dice mucho de cómo se ganan los estándares. La pregunta de trivial suele formularse al revés: ¿cuál fue el primer servicio de AWS? La respuesta instintiva es S3 y falla. Amazon SQS abrió beta a finales de 2004, año y medio antes. S3 se lanzó el 14 de marzo de 2006, fue el primero en alcanzar disponibilidad general y EC2 llegó más adelante ese mismo año. S3 fue el segundo servicio en existir y el que bautizó a todos los demás. ## Una API congelada en el tiempo El anuncio original ocupaba un párrafo: *storage for the Internet*. El sistema arrancó con aproximadamente un petabyte de capacidad repartido en unos 400 nodos de almacenamiento, con objetos limitados a 5 GB y un precio de 15 centavos por gigabyte al mes. La consola web no existió hasta 2010: durante los primeros años, usar S3 significaba escribir código o instalar una herramienta de terceros. Veinte años después, la versión de la API que figura en la documentación sigue siendo `2006-03-01`. En el post de aniversario, AWS lo subraya: el código que escribiste contra S3 en 2006 funciona hoy sin cambios. El servicio creció por adición (objetos de hasta 50 TB, clases de almacenamiento, más de 500 billones de objetos) mientras el vocabulario básico se quedaba quieto. Esa quietud es la que hizo posible todo lo demás. ## Protocolo y producto comparten nombre Cuando alguien dice S3 puede estar nombrando dos cosas. El producto: la plataforma de AWS, con sus funciones exclusivas (lifecycle hacia capas frías, replicación, notificaciones de eventos, Object Lock, integración fina con IAM). El protocolo: el vocabulario HTTP que habla cualquier cliente (buckets, objetos, PUT/GET/DELETE, subidas multiparte, firma SigV4). El *S3-compatible* de las webs significa, en la práctica, hablar el protocolo con más o menos producto encima. Ahí está el detalle que conviene mirar antes de elegir: los clientes que usan el núcleo del vocabulario funcionan contra casi cualquier backend, mientras que los que dependen de funciones del producto solo funcionan contra AWS y contra los pocos proveedores que las replican. Algunos ejemplos de esa periferia que se cae fuera: * Backblaze B2 reduce las ACL a dos valores (privado o público) a nivel de bucket, no soporta tagging de objetos ni configuración de websites. * Hetzner mantiene el subconjunto más conservador: sin replicación, sin website, cifrado solo con SSE-C. * Cloudflare R2 deja fuera las notificaciones estilo Lambda y compensa con extensiones propias (buckets públicos, bindings de Workers). Un cliente como rclone (o restic, que solo necesita PUT, GET y LIST con su formato encima) apenas toca el núcleo. Por eso funciona en todas partes. Un script que se apoya en replicación o en event notifications ya está jugando en la cancha de AWS, aunque el endpoint sea otro. ## MinIO: la referencia se detuvo Durante una década, si querías hablar S3 sin nube, la respuesta era una: MinIO. Un binario en Go que exponía la API completa sobre discos propios. La referencia para pruebas, desarrollo y homelabs, y la demostración de que el protocolo podía ejecutarse sin AWS de por medio. Esa referencia envejeció de golpe. El proyecto pasó a AGPLv3 en 2021, con aviso explícito de lo que implicaba ofrecer el servicio en red con código modificado. En diciembre de 2025 entró en modo mantenimiento: sin características nuevas, sin pull requests, seguridad crítica evaluada caso a caso. En abril de 2026 el repositorio quedó archivado y en solo lectura. La edición comunitaria se distribuye solo como código fuente (via `go install`), las imágenes Docker oficiales se retiraron y la compañía redirige a AIStor, su producto comercial en ediciones Free y Enterprise. Existe un fork mantenido por Chainguard, también en modo mantenimiento, y el propio README de SeaweedFS recomienda RustFS (una reescritura en Rust con licencia Apache 2.0 y formato en disco compatible byte a byte) para quien necesite migrar una instalación MinIO tal cual. La lección pesa más que el proyecto. La comunidad trató a MinIO como la piedra del ecosistema S3-compatible, y la piedra resultó ser un producto con dueño que, llegado el momento, reservó el camino comercial para sí. La API sobrevivió. Los binarios, ya se ve. ## El relevo: Ceph, SeaweedFS y Garage El hueco lo cubren proyectos con perfiles distintos y años de rodaje a sus espaldas. Con una advertencia previa: *S3-compatible* es un espectro, no un sello binario. Ninguno de los tres implementa la API completa, y cada uno publica el suyo. Garage mantiene una página pública de compatibilidad, endpoint por endpoint, donde todo lo no implementado responde con un honesto `501 Not Implemented`. El veterano es Ceph RGW, la puerta de objetos de Ceph: una década y media de producción en OpenStack y plataformas similares, respaldo de la Ceph Foundation, Red Hat e IBM, con Object Lock, cifrado, IAM y multisitio. Su documentación habla de un subconjunto amplio de la API de Amazon. El precio de tanta madurez es la operación: un clúster RADOS con sus OSD exige una escala de mantenimiento que está lejos de una Raspberry Pi. SeaweedFS ocupa el centro. Go, Apache 2.0, 34.000 estrellas, ritmo de versiones semanal y el subconjunto S3 más completo del grupo: el gateway implementa 73 operaciones de bucket y objeto, IAM, STS, versionado, Object Lock con retención y legal hold, lifecycle, tagging y CORS, con una suite de compatibilidad en CI. Su nota honesta está en el propio README: MinIO seguía de cerca la API de AWS y SeaweedFS sigue avanzando hacia ese nivel. La arquitectura pide más piezas que un solo binario (master, volume, filer y gateway), aunque `weed mini` arranca todo en un comando para montajes pequeños. Garage apuesta por lo contrario. Rust, AGPL, un binario ligero sin dependencias externas, diseñado para clústeres geodistribuidos pequeños: tres nodos en tres casas distintas replicando datos sobre Internet. Deuxfleurs lo lleva en producción desde su primera versión en 2020. El subconjunto S3 es el más conservador del trío: sin versionado (en camino), sin Object Lock, lifecycle parcial y cifrado solo con SSE-C. Para restic, rclone o servir webs estáticas sobra; para una plataforma que dependa del versionado, corta. | Proyecto | Licencia | Fortaleza | Aviso | |----------|----------|-----------|-------| | Ceph RGW | LGPL2.1 | Madurez y multisitio a gran escala | Operación exigente | | SeaweedFS | Apache 2.0 | El subconjunto S3 más amplio | Más piezas que mantener | | Garage | AGPL | Ligero y geodistribuido | Sin versionado ni Object Lock | | RustFS | Apache 2.0 | Caída directa desde MinIO | 1.0 GA (septiembre 2026) | En la órbita cercana quedan dos opciones recientes: RustFS, que alcanzó su 1.0 GA el 16 de septiembre de 2026 después de más de un año de iteración pública, y VersityGW, un gateway sin estado en Go (Apache 2.0) que expone S3 sobre un sistema de ficheros POSIX, Azure u otro backend S3, con pruebas de conformidad contra los SDK oficiales de AWS. ## Las facturas: almacenar barato, recuperar caro B2 y R2 compiten con AWS por otro eje: el precio, y en concreto por la línea de la factura que aparece cuando pides los datos de vuelta. AWS publica el almacenamiento por clases, y ahí está la primera trampa. La clase Standard no cobra recuperación (los 0,023 dólares por GB y mes, unos 23 dólares por TB, son el coste de guardar). Las clases que abaratan el almacenamiento meten un peaje en la puerta de salida: | Clase de AWS | Almacenar ($/GB·mes) | Recuperar ($/GB) | Estancia mínima | |--------------|----------------------|------------------|-----------------| | S3 Standard | 0,023 | gratis | ninguna | | Standard-IA | 0,0125 | 0,01 | 30 días | | One Zone-IA | 0,01 | 0,01 | 30 días | | Glacier Instant Retrieval | 0,004 | 0,03 | 90 días | | Glacier Flexible | 0,0036 | 0,01 (bulk: gratis) | 90 días | | Glacier Deep Archive | 0,00099 | 0,02 (bulk: 0,0025) | 180 días | La letra pequeña acompaña: estancia mínima facturada si retiras antes de tiempo, tamaño mínimo facturable de 128 KB en las clases IA (mil millones de ficheros de 4 KB se cobran como si pesaran 128 GB), tarifas por petición encima, y un archivo restaurado de Glacier que factura como copia temporal en Standard mientras lo descargas. A eso se suma el tráfico hacia Internet: 0,09 dólares por GB en AWS (los primeros 100 GB al mes van gratis), en B2 gratis hasta un límite de tres veces el volumen medio almacenado al mes (después, 0,01 dólares por GB) y en R2 gratis sin condiciones. El ejemplo que hace visible la diferencia: restaurar un TB guardado en Deep Archive por la vía estándar cuesta unos 20 dólares de recuperación, la copia temporal en Standard (menos de un dólar al día), las peticiones y unos 85 más de salida hacia Internet si el destino final es tu casa. Más de cien dólares por traer de vuelta lo que cuesta cerca de un dólar al mes guardar. El mismo TB desde B2 entra gratis dentro de la regla del 3x (unos 10 dólares por encima) y desde R2 no paga salida; 10 dólares solo si estaba en su clase Infrequent Access, la única de las tres que mantiene un peaje de recuperación, anunciado y contenido. Comparativa directa (por TB y mes, tarifas publicadas): | | AWS S3 (Standard) | B2 | R2 (Standard) | |---|-------------------|----|---------------| | Almacenar | ≈ 23,5 $ | 6,95 $ | ≈ 15 $ | | Salida a Internet | 0,09 $/GB | gratis hasta 3x, luego 0,01 $/GB | gratis | | Capas frías con peaje | 0,01 a 0,03 $/GB | sin capas frías | solo IA: 0,01 $/GB | | Peticiones | 0,005 $/1k escrituras, 0,0004 $/1k lecturas | gratis (clase D a 0,004 $/10k tras 2.500/día) | 4,50 $/M escritura, 0,36 $/M lectura | Y el paquete plano europeo queda fuera de la comparación por estructura: Hetzner Object Storage vende una cuota base con almacenamiento y tráfico incluidos, datos en Falkenstein, Nuremberg y Helsinki, a cambio del subconjunto S3 más conservador del mercado (sin replicación, sin website, solo SSE-C), pensado para backups y servidores de ficheros. El catálogo real es más largo (Wasabi, DigitalOcean Spaces, Ceph RGW para quien quiera montarse el suyo), pero entre estos cubres el abanico de modelos: archivado barato, egreso cero, paquete europeo y autohospedado. ## El mismo binario, otro endpoint La promesa práctica del estándar se ve en una línea: ```bash aws s3 ls s3://mi-bucket --endpoint-url https://.r2.cloudflarestorage.com ``` El mismo CLI que usa contra AWS funciona contra R2, B2 o Hetzner añadiendo un flag. Con rclone el cambio es similar: al configurar un *remote* de tipo `s3` solo cambian endpoint, región y claves; el resto del pipeline (scripts, cifrado, cron) queda intacto. El esfuerzo de aprender una herramienta se paga en todos los backends a la vez, que es exactamente lo que un estándar promete. ## Piedra, papel y tijeras El [marco RPS](/blog/002-rps-principle/) ordena las opciones por el nivel de complejidad que estás dispuesto a mantener: | | Enfoque | |--|---------| | **Piedra** | Un bucket en B2 o en R2, restic o rclone, las claves en el gestor de contraseñas. Una copia remota que se puede restaurar. O Garage en un nodo propio si los datos pueden vivir en casa. | | **Papel** | Dos proveedores con la misma herramienta, scripts parametrizados por endpoint y restauraciones probadas de vez en cuando. O SeaweedFS en tres nodos para llevar el almacenamiento a tu hardware sin perder vocabulario. | | **Tijeras** | Ceph RGW con multisitio, lifecycle hacia capas frías, CDN delante del bucket y automatización completa del ciclo de vida. | El eje puente/ancla (que [ya analicé con el cloud-first](/blog/004-cloud-first-problem/)) encaja aquí con claridad. Hablar S3 es el puente: cambiar de proveedor se reduce a cambiar endpoint y claves, en cualquier dirección. El ancla aparece en tres sitios. Acoplarte a funciones del producto que solo AWS tiene (replicación con configuración fina, Object Lock, IAM) te ata al proveedor. Encadenar la arquitectura a la factura de egreso del hyperscaler te ata a la plataforma, y es justo el ancla que R2 convirtió en argumento comercial. Y el peaje de recuperación de las capas frías es el ancla silenciosa de la factura: te ata a las condiciones del proveedor en el peor momento posible, el día que necesitas los datos. La [soberanía digital](/blog/005-digital-sovereignty/) tiene aquí una versión práctica: los datos en Garage o Hetzner pesan distinto que en un bucket de AWS, con el mismo vocabulario por medio. ## El lenguaje común La historia tiene una ironía agradable. Amazon convirtió su API en el estándar de facto y, durante años, eso funcionó como puerta de entrada a una plataforma enorme. MinIO montó su posición sobre la parte libre de ese estándar y, al girar hacia el producto comercial, dejó el hueco que hoy ocupan Garage, SeaweedFS y Ceph. El protocolo sobrevivió a los dos: al congelar el vocabulario y crecer alrededor, quedó libre de copiarse, y cada proveedor lo habla con su propio acento. Los datos pueden vivir en tres casas distintas (Garage), en un clúster serio (SeaweedFS, Ceph), en un archivador barato (B2) o detrás de una red que no cobra por la salida (R2), y las herramientas siguen siendo las mismas. La elección se descompone en dos preguntas: con qué protocolo hablan tus herramientas (S3, casi seguro) y cuánto te costará traer los datos de vuelta el día que los necesites. La primera decide con qué backends puedes trabajar. La segunda decide cuánto te cuesta cambiar de idea. *Nota: mientras este post estaba en revisión, RustFS alcanzó su versión 1.0 GA (16 de septiembre de 2026). Gracias a Lorenzo de [atareao.es](https://atareao.es/) por el aviso.* ---------------------------------------- ### [Modelos de pesos abiertos por API: guía rápida](https://escribano.dev/es/blog/018-open-weight-models-api/) > Guía de siete modelos de pesos abiertos por API y cuándo elegir cada uno: Qwen3.8-Flash-Next para volumen barato con herramientas, DeepSeek V4.1 Flash por caché y bucles agénticos, GLM-5.3-Flash multimodal, MiniMax M3 y Kimi K3 para un millón de tokens, Kimi K2.7 Code para coding de largo horizonte y Qwen3.8-Max para specs. Todos con pesos publicados: cambiar de proveedor reduce el lock-in. Siete modelos de pesos abiertos que se consumen por API: qué hace bien cada uno y cuándo elegirlo. Verifiqué precios el 20 de septiembre de 2026; caducan rápido (uno ya lo hizo mientras escribía), así que la tabla va sin cifras. Todos tienen los pesos publicados: puedes descargarlos y, con la infraestructura adecuada, ejecutarlos tú mismo o buscar otro proveedor de inferencia. Cambiar de proveedor no es plug-and-play (suelen cambiar URL, ID del modelo y parámetros), pero reduce una dependencia importante: si mañana suben los precios o retiran una API, los pesos siguen disponibles. No elimina todas las dependencias, pero cambia bastante la relación. Es la capa de cognición de la que hablé en el [post sobre soberanía digital](/blog/005-digital-sovereignty/). ## La tabla | Modelo | Cuándo usarlo | |---|---| | Qwen3.8-Flash-Next | Volumen alto con herramientas y entrada multimodal; la mejor relación calidad-precio de la lista. Es el checkpoint que sirve la API `qwen3.8-flash` | | DeepSeek V4.1 Flash | Agentes con muchas herramientas e iteraciones (OpenClaw, Hermes, Moltis): de los mejores números agénticos de la lista y la caché más barata con diferencia, que en esos bucles puede dominar la factura. ID de API: `deepseek-flash` (los alias `deepseek-v4-flash` redirigen). Licencia MIT | | GLM-5.3-Flash | Multimodal de verdad (imagen, vídeo y ficheros) con licencia MIT | | MiniMax M3 | Refactors grandes a bajo coste con tests o revisión detrás: ventana de hasta un millón de tokens (entrada más salida; la API de MiniMax lo contabiliza así) mediante su arquitectura de atención dispersa [MSA](https://github.com/MiniMax-AI/MSA) (el contexto largo sigue siendo usable), multimodal nativo y razonamiento adaptable que se activa solo cuando hace falta | | Kimi K2.7 Code | Coding de largo horizonte e ingeniería de sistemas: completa tareas de principio a fin con pocos tokens de razonamiento. Caro, para cuando la tarea lo justifica | | Qwen3.8-Max | Redactar la spec y descomponer el trabajo (la implementación la hacen los modelos baratos); también muy bien puntuado en Code Arena: WebDev. El snapshot de la API va un paso por delante de los pesos descargables (`Qwen/Qwen3.8-2.4T-A95B`) | | Kimi K3 | Deep research y tareas de largo horizonte: contrasta cientos de fuentes con ventana de un millón de tokens (90,4 en BrowseComp en una evaluación con 1M de contexto sin compactación, una cifra comparable a modelos cerrados de primera línea). Razona siempre, graduable en tres niveles; caro, para cuando la tarea lo justifica | ## En la práctica Llevo varios meses con Cursor y estoy contento con Composer 2.5, sobre todo por la velocidad cuando va en modo fast. En la práctica es un Kimi K2.5, y me ha ido muy bien tanto para programación como para infraestructura de homelab (Docker, Podman, Quadlet y similares). Por eso ahora pruebo Kimi K2.7 Code en [OpenCode](https://opencode.ai/): quiero ver si la familia Kimi mantiene ese nivel fuera del ecosistema Cursor. Me acabo de suscribir a OpenCode Go y pienso usar Cursor, OpenCode y [OpenRouter](https://openrouter.ai/) en paralelo un tiempo en side projects. OpenRouter, de pago por uso, me sirve como plan B. En el trabajo sigo con modelos cerrados; en tiempo libre calibro abiertos por si algún día hace falta llevarlos a producción, sobre todo si siguen subiendo los precios de inferencia (Anthropic es el caso más claro). Los abiertos también pueden encarecerse, pero el lock-in del proveedor es menor, y ese es el motivo de esta guía. El reparto que tengo en mente, aún en prueba: tres workhorses en agentes tipo OpenClaw y Moltis (Hermes aún no lo he probado): DeepSeek V4.1 Flash por coste y caché, GLM-5.3-Flash cuando el flujo es multimodal o va por Z.AI, y MiMo-V2.6-Flash cuando quiero ventana de un millón de tokens con la misma liga de precio. En el día a día sigo priorizando DeepSeek; el detalle de caché y runtimes lo desarrollo en el post que sigue a esta guía. Qwen3.8-Max para redactar specs en [OpenSpec](https://github.com/Fission-AI/OpenSpec). GLM-5.3-Flash también quiero contrastarlo en frontend: Z.AI lo vende fuerte en coding visual y reproducción de interfaces. Kimi K2.7 Code lo reservo para backend (sobre todo Go) e ingeniería de sistemas: Quadlets, integración con Tailscale o Zitadel y tareas parecidas. Kimi K3 lo reservo para revisiones de código importantes: razona siempre y el precio se nota. Con Qwen3.8-Max tuve una lección más brusca: en unas horas quemé la mitad de la cuota mensual de OpenCode Go, en buena parte porque dejé crecer el contexto sin vigilarlo. Vigilar el contexto importa también en OpenSpec, aunque ahí el trabajo es más acotado. ## Lo que no caduca Los precios cambian. GLM-5.3-Flash costaba la mitad hace dos semanas y la promoción se acabó el 9 de septiembre sin aviso; DeepSeek publicó V4.1 Flash el 10 de septiembre con tarifa nueva. En diciembre estas cifras no coincidirán. ---------------------------------------- ### [The Bitter Lesson: el conocimiento humano como techo](https://escribano.dev/es/blog/017-bitter-lesson/) > La lección amarga de Sutton (2019): a largo plazo ganan los métodos generales que escalan cómputo (búsqueda y aprendizaje) frente a incrustar conocimiento humano del dominio. AlphaGo usó partidas humanas como rampa; AlphaGo Zero las descartó y a los 40 días las superaba 89-11. El conocimiento humano funciona como prior: acelera el arranque pero techa la curva en un óptimo con forma humana. Lo conocí escuchando Inteligencia Artificial Semanal ([Spotify](https://open.spotify.com/show/48stHRcIdLoDi8WPK8JJWq), [YouTube](https://www.youtube.com/channel/UC4ewDq9cCPd9eqA88M6yvEQ), [iVoox](https://www.ivoox.com/podcast-inteligencia-artificial-semanal_sq_f12364979_1.html)). No busqué el paper. El autor suele decir, en tono humorístico, que se considera *suttonista del séptimo día*: alineado con Sutton, con *The Bitter Lesson* y con el aprendizaje por refuerzo. El podcast volvía al ensayo con naturalidad, como quien nombra una herramienta que ya da por sentada, y en algún episodio la frase encajó: el conocimiento humano, el que nos parece lo más valioso que podemos meter en un sistema, puede ser el atajo y el techo a la vez. En marzo de 2019, [Richard Sutton](http://www.incompleteideas.net/IncIdeas/BitterLesson.html) condensó setenta años de investigación en unas pocas páginas. El título, *The Bitter Lesson*, avisa de que la conclusión no gusta. La idea es simple de enunciar y difícil de aceptar: a largo plazo ganan los métodos generales que aprovechan cómputo (búsqueda y aprendizaje). El conocimiento humano del dominio ayuda al principio, satisface a quien lo pone, y más tarde aplana la curva. A veces, la estorba. Ese aplanamiento es lo que me interesa. No el eslogan de "más GPUs". La forma de la curva: subir más rápido con lo que ya sabemos los humanos, y acercarse de forma asintótica a un techo con forma humana. ## Qué dice el ensayo Sutton parte de una observación histórica, no de una predicción de laboratorio. La mayor parte de la investigación en IA se ha hecho como si el cómputo disponible fuera constante. Si el presupuesto de cálculo no va a crecer, meter conocimiento humano es casi la única palanca. En un plazo un poco más largo que un proyecto típico, el coste por unidad de cómputo cae y aparece muchísimo más cálculo. Entonces la palanca que importa es otra. Las dos estrategias no tienen por qué enemistarse. En la práctica lo hacen. El tiempo que pasas formalizando cómo crees que piensas es tiempo que no pasas en métodos que escalan cuando mañana haya diez veces más cómputo. Hay compromisos psicológicos: has invertido en un enfoque y quieres que gane. Y el enfoque "conocimiento humano" tiende a complicar los métodos de un modo que los hace peores para absorber más cálculo. Sutton resume la lección en cuatro observaciones: 1. Los investigadores han intentado, una y otra vez, construir conocimiento dentro de los agentes. 2. Eso siempre ayuda a corto plazo, y resulta personalmente satisfactorio. 3. A largo plazo llega a una meseta e incluso inhibe el progreso. 4. El salto de verdad llega por la vía opuesta: escalar cómputo mediante búsqueda y aprendizaje. El éxito posterior sabe amargo porque gana frente a un enfoque que el campo prefería: el centrado en el humano, el que "entiende" el dominio como lo entendemos nosotros. La frase que deja el ensayo, y que el podcast me dejó dando vueltas, es esta: queremos agentes que puedan descubrir como nosotros, no agentes que contengan lo que ya hemos descubierto. ## Por qué es amarga La amargura no es técnica. Es profesional y un poco identitaria. Si tu oficio consiste en meter en el sistema cómo funciona el ajedrez, el go, la voz o la visión (las aperturas, los fonemas, los cilindros generalizados, las features que "tienen sentido"), el sistema es una extensión de tu comprensión. Cuando un método más bruto, con más búsqueda o más datos, te adelanta, la derrota no es solo de un paper. Es de una idea de qué cuenta como inteligencia. Sutton lo cuenta sin adorno en el ajedrez. En 1997, Deep Blue venció a Kasparov con búsqueda masiva y profunda. Gran parte de la comunidad que había trabajado en comprender la estructura especial del juego no fue buena perdedora. Dijeron que la fuerza bruta había ganado esta vez, que no era una estrategia general, y que de todos modos no era así como juegan las personas. Querían que ganaran los métodos con input humano. Ese "queríamos que ganara lo nuestro" es la parte amarga. El campo tarda en digerir que el avance haya llegado por el camino que menos se parecía a nosotros. ## Los ejemplos del ensayo Sutton no inventa un patrón. Lo señala en cuatro frentes donde el campo ya había pagado el mismo ciclo. | Dominio | Atajo humano | Lo que acabó escalando | |---------|--------------|------------------------| | Ajedrez | Comprensión de la estructura del juego, menos búsqueda | Búsqueda profunda a gran escala (Deep Blue, 1997) | | Go | Evitar la búsqueda con conocimiento y particularidades del tablero | Búsqueda más aprendizaje por autojuego, veinte años después | | Voz | Palabras, fonemas, tracto vocal (competición DARPA, años 70) | Métodos estadísticos (HMM) y, más tarde, deep learning | | Visión | Bordes, cilindros generalizados, features SIFT | Redes que usan poco más que convolución e invarianzas | En reconocimiento de voz el ciclo es especialmente limpio. Un lado llevaba conocimiento humano del lenguaje y del aparato fonador. El otro hacía más cómputo con modelos estadísticos. Ganaron los estadísticos. Durante décadas, el procesamiento del lenguaje se fue desplazando en esa dirección. El deep learning fue el siguiente paso del mismo movimiento: todavía menos conocimiento embebido, todavía más cálculo y más datos. En visión ocurrió lo mismo con otro vocabulario. Durante años se diseñaron primitivas que imitaban cómo creíamos que veíamos. Hoy esas primitivas están fuera del camino crítico. Queda un sesgo inductivo mínimo (convolución, cierta invariancia) y el resto lo encuentra el entrenamiento. El go, en el ensayo, es el ajedrez con veinte años de retraso. El campo gastó un esfuerzo enorme en no buscar, en aprovechar lo que los humanos ya sabían del juego. Cuando la búsqueda y el autojuego se aplicaron a escala, ese esfuerzo quedó irrelevante o peor. Sutton no entra en el detalle de las versiones de AlphaGo. El detalle es el que vuelve concreta la asíntota. ## La curva: atajo y techo El conocimiento humano no es ruido. Es un **prior**: la creencia previa con la que partes, antes de ver datos nuevos. En estadística actualizas esa creencia con lo que observas. En IA el mecanismo es el mismo, más informal: lo que el sistema ya asume condiciona lo que puede aprender después. Un prior bueno te coloca antes en un sitio decente. Si alimentas la red con partidas de profesionales, no empieza en el movimiento aleatorio: ya "cree", en algún sentido, que las aperturas y las buenas formas humanas son el sitio razonable. Un prior que describe cómo jugamos, hablamos o vemos los humanos también te atrae hacia ese sitio cuando el sistema podría irse más lejos. La forma, en crudo: | | Conocimiento humano incrustado | Búsqueda + aprendizaje | |--|-------------------------------|-------------------------| | Arranque | Subes rápido: imitas a los expertos | Empiezas cerca de lo aleatorio | | Asíntota | Un techo con forma humana (estilos, features, sesgos) | La marca el cómputo y el espacio del problema | | Efecto secundario | Métodos más complejos, peores para absorber más cálculo | Coste, y menos control sobre *qué* ha encontrado | "Techo con forma humana" no significa "Elo de un profesional". AlphaGo, el que sí usó partidas humanas, ya era sobrehumano: le ganó a Lee Sedol. El techo no es "no puedes superar a las personas". Es la cuenca de atracción de lo que las personas ya exploraron y de cómo describieron el problema. El sistema aprende a ser un humano muy bueno, o un humano muy bueno con un poco de búsqueda extra. Le cuesta salir de esa cuenca. Eso es lo asintótico. El atajo te acerca antes a un óptimo local que se parece a nosotros. El método que parte de menos (reglas, búsqueda, aprendizaje) tarda más en parecer inteligente y puede pasar de largo ese óptimo. ## AlphaGo y AlphaGo Zero En 2016, [AlphaGo](https://www.nature.com/articles/nature16961) se convirtió en el primer programa que derrotaba a un campeón mundial de go. La receta mezclaba las dos estrategias. Redes neuronales entrenadas primero con aprendizaje supervisado sobre partidas de aficionados y profesionales, después con aprendizaje por refuerzo; y, en tiempo de partida, una búsqueda en árbol (MCTS) guiada por esas redes. El conocimiento humano era la rampa: miles de juegos que ya existían, un estilo, un repertorio de aperturas y de "buenas formas". Funcionó. Fan Hui, Lee Sedol, más tarde Ke Jie. El atajo cumplió. En pocos años el go pasó de "demasiado grande para la búsqueda" a problema resuelto por encima del techo humano. En 2017, DeepMind publicó [AlphaGo Zero](https://deepmind.google/blog/alphago-zero-starting-from-scratch/): mismo juego, otra rampa. Sin partidas humanas. Sin features de dominio más allá de las piedras en el tablero. Sin la red de política y la de valor por separado. Una sola red, búsqueda más simple, y un bucle en el que el programa es su propio profesor. Empieza en juego casi aleatorio. Juega contra sí mismo. Actualiza la red para predecir sus propias jugadas y el ganador. Vuelve a buscar. Repite. El paper en *Nature* se titula, sin disimulo, [Mastering the game of Go without human knowledge](https://www.nature.com/articles/nature24270). Las cifras del propio equipo: * A los tres días de autojuego, AlphaGo Zero ganó 100-0 a la versión publicada que había vencido a Lee Sedol (AlphaGo Lee). * A los 21 días alcanzó el nivel de AlphaGo Master, el que había barrido a los mejores humanos. * A los 40 días superaba a todas las versiones anteriores. En un match de 100 partidas, el Zero de 40 días ganó a Master 89-11. Zero no solo llegó más alto. En tres días de autojuego había dejado atrás al AlphaGo entrenado con personas, y después siguió subiendo. El paper lo dice con la palabra que importa aquí: mejor *rendimiento asintótico* que entrenar con datos de expertos. Demis Hassabis lo resumió después: Zero ya no estaba sujeto a los límites del conocimiento humano. Eso se vio en el tablero. Zero redescubrió joseki conocidos y luego se los saltó. Encontró jugadas que a un profesional le parecían feas y que, vistas con el resultado, eran mejores. El go humano es una biblioteca de siglos. Resultó ser una biblioteca incompleta. Entrenar contra esa biblioteca te mete en ella. Entrenar contra ti mismo te deja salir. Hay un matiz que conviene no esconder. Zero no partía de la nada metafísica. Partía de las reglas del go, que son conocimiento humano, y de un algoritmo de búsqueda que también lo es. "Sin conocimiento humano" en el paper significa sin partidas, sin consejos de estilo y sin features de dominio. El entorno sigue escrito por personas. El contenido de la mente del agente, no. Ese matiz no debilita la lección. La refuerza. Lo que Sutton pide dejar fuera es el catálogo de lo que ya encontramos (las jugadas buenas, las formas, los fonemas, los bordes). Lo que pide dejar dentro son los meta-métodos con los que un agente puede encontrar ese catálogo, y otro más grande, cuando haya más cómputo. ## Aprendizaje por refuerzo Sutton no habla de un aprendizaje cualquiera. El que escala junto a la búsqueda es, en la práctica, el **aprendizaje por refuerzo** (*reinforcement learning*): el sistema prueba, recibe una señal de cómo le fue (ganó, perdió, acertó) y ajusta lo que hará la próxima vez. Nadie le dicta la jugada correcta. Le dicen, tarde o temprano, si el resultado valió. Imitar a un humano es otra cosa. El aprendizaje supervisado parte de etiquetas que ya trajimos nosotros: esta posición se juega así porque un profesional lo hizo. Es un prior con forma de demostración. El refuerzo parte de una recompensa. Puede descubrir una línea que ningún profesional jugó, si esa línea gana. AlphaGo usó las dos en secuencia: primero imitar partidas, después refuerzo. Zero se saltó la imitación. Por eso el paper puede hablar de mejor rendimiento asintótico: el prior humano no estaba ahí para techar su forma de jugar. En 2026 el mismo gesto se reconoce con otros nombres. Preentrenar un modelo con texto de internet es imitar a escala (un prior humano enorme). Luego a menudo llega una fase de refuerzo: el modelo genera, se evalúa el resultado (un problema con respuesta comprobable, un juez, una señal de preferencia) y se actualiza. La lección amarga no pide un algoritmo concreto. Pide no confundir la biblioteca de ejemplos humanos con la capacidad de mejorar a partir de lo que ocurre. ## Emerger En Inteligencia Artificial Semanal se oye mucho el verbo **emerger**. Al añadir potencia de cálculo (más parámetros, más datos, más autojuego, más búsqueda en inferencia) aparecen capacidades que nadie escribió como regla. A veces las esperabas: jugar mejor al go. A veces no: una jugada que parece fea; una habilidad que no era el objetivo del entrenamiento. Sutton lo describe sin usar la palabra. Si metes en el agente cómo crees que piensas, el techo es esa teoría. Si metes métodos que aprovechan más cómputo, el sistema puede encontrar complejidad que no habías nombrado. El comportamiento nuevo no se añade a mano. Aparece. Zero redescubriendo joseki y luego saltándoselos es emergencia en un tablero medible. En los modelos grandes el patrón se parece: al crecer el cómputo, el sistema hace algo que la versión diez veces más pequeña no hacía, o lo hace de un modo que nadie anticipó. Hay debate sobre si el salto es brusco o una curva suave que medíamos mal. El punto útil no cambia. Lo sorprendente es la consecuencia de no haber construido el catálogo dentro. Emerger es el nombre que el podcast le pone a la apuesta de Sutton: queremos agentes que descubran, y el descubrimiento, visto desde fuera, parece que surgió de la nada. ## Dos métodos que escalan Sutton nombra dos familias que siguen siendo útiles cuando el cálculo disponible se vuelve enorme: **búsqueda** y **aprendizaje**. La búsqueda gasta cómputo en el momento de decidir: más profundidad, más simulaciones, más candidatos. El aprendizaje que tiene en mente es experiencia convertida en mejores decisiones, el bucle del refuerzo, más que copiar etiquetas. Las dos convierten "mañana habrá más cálculo" en "mañana el mismo método será mejor", sin que tengas que reescribir a mano la teoría del dominio. Y, si el método escala de verdad, lo que aparece después es lo que el podcast llama emerger. El conocimiento incrustado no escala así. Una feature de go bien pensada en 2014 no se vuelve diez veces mejor porque el cluster sea diez veces mayor. A menudo se vuelve un estorbo: el sistema tiene que negociar con ella en lugar de usar el presupuesto extra para buscar o para aprender. La segunda conclusión del ensayo es más seca. El contenido real de las mentes es irremediablemente complejo. Deberíamos dejar de buscar maneras simples de pensar ese contenido (el espacio, los objetos, los agentes múltiples, las simetrías) para meterlas en el agente como si fueran la esencia. Eso es el mundo, arbitrario y sin fondo. Lo que hay que construir son métodos capaces de encontrar y aproximar esa complejidad. La búsqueda de las aproximaciones buenas debería hacerla el método, no nosotros. ## Por qué importa ahora En 2019 el ensayo se leía como un recuento: ajedrez, voz, visión, go. En 2026 se lee como el supuesto de trabajo de los laboratorios. El coste por unidad de cómputo ha seguido cayendo, y el cómputo que un entrenamiento o una inferencia puede tragarse ha seguido subiendo. Imitar texto a escala, luego refuerzo, y gastar más cálculo por respuesta son, con otros nombres, las dos familias de Sutton. Cada vez que un modelo "razona" más tiempo y mejora, estás viendo búsqueda. Cada vez que un entrenamiento largo desplaza a un sistema lleno de reglas, estás viendo aprendizaje. Y cada vez que, al subir parámetros o cómputo, aparece una capacidad que nadie había puesto en el diseño, el podcast diría que ha emergido. El ciclo del ensayo no se ha cerrado. Sigue siendo tentador construir cómo creemos que pensamos: arquitecturas cognitivas a mano, curricula que imitan el colegio, features que "tienen sentido", recetas de expertos inyectadas como si fueran el destino del sistema y no su primer peldaño. Ayuda. Es satisfactorio. Publica bien. Y dibuja una asíntota. El go lo dejó medido. AlphaGo, con partidas humanas, llegó antes a un sitio que ya era sobrehumano. AlphaGo Zero, sin esas partidas, tardó un poco en parecer alguien y luego pasó de largo el sitio al que las personas (y el sistema entrenado con personas) habían llegado. El conocimiento humano fue un acelerador y un imán. El imán es el que techa. Sutton no pide despreciar lo que sabemos. Pide no confundirlo con el contenido que el agente debería llevar dentro. Las reglas, la evaluación, el criterio de si algo importa: eso puede seguir siendo nuestro. El catálogo de soluciones, el "así se juega", el "así se ve", el "así se habla", es un óptimo local que se siente como llegada. Lo amargo es identitario: nos gusta ser la fuente. La apuesta del ensayo es otra. Meter en el sistema la capacidad de descubrir, no la biblioteca de lo ya descubierto. El podcast me lo dejó dicho de pasada, entre la broma del suttonista del séptimo día y el verbo emerger. El tablero de Zero, con 100-0, lo dejó escrito en resultado. ---------------------------------------- ### [Ctrl+J: el salto de línea que las TUIs de agentes no te cuentan](https://escribano.dev/es/blog/016-ctrl-j-terminal-newline/) > Ctrl+J es el byte LF (ASCII 10), por eso es el único atajo universal para insertar salto de línea en TUIs de agentes como kiro-cli y Cursor Agent. Enter envía porque es CR (Ctrl+M), y Shift+Enter depende de que el emulador reporte el modificador. En la terminal integrada de VS Code y derivados, Ctrl+J se queda el workbench (togglePanel): soluciones con commandsToSkipShell o sendSequence. Quieres un prompt de varias líneas en `kiro-cli` o en la TUI de `agent`: una lista de requisitos, un diff explicado, contexto que no cabe en una frase. Pulsas Enter y se envía a medias. **Ctrl+J** inserta la línea en casi cualquier terminal, incluido tmux. Hasta que lo pulsas dentro de la terminal integrada de VS Code (o de un derivado como Cursor o Kiro IDE) y desaparece el panel. Este post es corto a propósito. Complementa el [mapa de herramientas IAG](/blog/015-spec-driven-development-iag/) con un detalle de teclado que esas CLIs documentan mal. ## El truco Enter envía. Ctrl+J inserta el salto de línea. | Herramienta | Nueva línea (siempre) | Otras, si el terminal las reporta | |-------------|----------------------|-----------------------------------| | OpenCode | Shift+Enter / Ctrl+Enter (la app las intercepta) | (ninguna) | | Kiro CLI | Ctrl+J | Shift+Enter, Alt+Enter; `/editor` abre `$EDITOR` | | Cursor Agent (`agent`) | Ctrl+J; `\` y luego Enter | Shift+Enter; Option+Enter tras `/setup-terminal` | Ctrl+J no es un atajo inventado por Kiro. Es el carácter de control que el terminal lleva décadas emitiendo. **No hay otro `Ctrl+letra` que haga lo mismo**: J es la décima letra del alfabeto latino, y Ctrl+J es el único byte ASCII 10 (LF). Ctrl+M es CR (el Enter que envía). Ctrl+I es Tab. No existe un «Ctrl+K pero para newline». Lo que sí existe son **otras combinaciones que la TUI interpreta como “nueva línea”** sin emitir LF: | Combinación | Qué viaja por el cable | Dónde funciona de serie | |-------------|------------------------|-------------------------| | Ctrl+J | LF (`\n`, ASCII 10) | Cualquier terminal real; en VS Code el workbench se lo queda | | `\` + Enter | barra invertida y luego CR; la TUI lo traduce a newline | Cursor Agent, también en tmux y en la terminal de VS Code | | Shift+Enter | secuencia con modificador (no es LF) | iTerm2, Ghostty, Kitty, Warp, Zed | | Alt+Enter / Option+Enter | suele ser ESC+CR u otra secuencia | Terminal.app, Ghostty; VS Code solo tras configurar | Shift+Enter y Alt+Enter **no son Ctrl+J con otra etiqueta**. Dependen de que el emulador distinga el modificador. En gnome-terminal, en muchos IDEs y dentro de tmux, Shift+Enter llega como un Enter normal y envía el prompt. Por eso las docs de Cursor y Kiro llaman a Ctrl+J (y, en Agent, a `\`+Enter) la opción universal. Kiro documenta `/settings terminal` para auto-configurar Shift+Enter. Cursor documenta `/setup-terminal`. Ambos acaban escribiendo un keymap del emulador (en VS Code, un `sendSequence`) para que esa combinación deje de ser un Enter disfrazado. ## Por qué funciona Un `Ctrl+letra` no es magia del sistema operativo. En el modelo teletype / VT100, **Ctrl más una letra produce el byte `letra & 0x1F`**: el carácter de control correspondiente. | Atajo | Carácter | ASCII | Equivalente | |-------|----------|-------|-------------| | Ctrl+J | LF | 10 | `\n` | | Ctrl+M | CR | 13 | Enter | | Ctrl+I | HT | 9 | Tab | | Ctrl+[ | ESC | 27 | Escape | | Ctrl+D | EOT | 4 | EOF / cerrar input | | Ctrl+L | FF | 12 | Limpiar pantalla (a menudo) | | Ctrl+H | BS | 8 | Backspace (a veces) | Enter es **CR** (Ctrl+M). Ctrl+J es **LF**. Kiro y Agent, en la TUI, tratan LF como newline: si el editor se come Ctrl+J antes, no llega nada al input. OpenCode intercepta Shift+Enter a nivel de aplicación y no depende de ese byte. Hay tres capas, y cada una puede quedarse el evento: ``` Teclado → terminal (bytes) → TUI (interpretación) ``` En un emulador de verdad (WezTerm, Alacritty, iTerm2, GNOME Terminal) las tres capas colaboran y Ctrl+J es LF. En la terminal integrada de un IDE la capa del medio es el *workbench*: decide si el atajo es suyo o del proceso que corre dentro. Linux y macOS se comportan igual en terminal real. En Windows, Windows Terminal + WSL, Git Bash o SSH también. PowerShell, cmd y cualquier campo de texto de una GUI **no** siguen estas reglas: ahí Ctrl+J no es LF. ## El conflicto en VS Code (y derivados) En Visual Studio Code, **Ctrl+J** (Cmd+J en macOS) está ligado a `workbench.action.togglePanel`: mostrar u ocultar el panel inferior. Cursor, Kiro IDE y el resto de forks heredan el mapa de teclas. Si ejecutas `kiro-cli` o `agent` **dentro** de esa terminal, Ctrl+J no llega al TUI. Llega al workbench. El panel se pliega. En macOS el atajo del panel suele ser **Cmd+J**, así que **Ctrl+J puede funcionar** en la terminal integrada. El roce feo es Linux y Windows, donde Ctrl+J *es* el toggle del panel. No hay un segundo `Ctrl+…` de fábrica que inserte LF y deje Ctrl+J para el panel. En **Cursor Agent** no hace falta: escribe `\` y pulsa Enter. Lo he comprobado en la terminal integrada de Cursor: inserta la línea, no oculta el panel y no toca el keymap. (La documentación de Agent lo da también por bueno en tmux y por SSH.) En **Kiro**, sin remapear, queda `/editor` (abre `$EDITOR`) o pegar texto ya multilínea. Si quieres Shift+Enter en esa terminal, no es gratis: hay que configurar el IDE (`/setup-terminal` en Agent, `/settings terminal` en Kiro), que acaba en un `sendSequence`. El resto de esta sección es eso: recuperar el LF de Ctrl+J, o mandar el byte 10 con otra tecla. ### Dejar que Ctrl+J llegue al TUI VS Code intercepta una lista de comandos cuando la terminal tiene el foco (`terminal.integrated.commandsToSkipShell`). `workbench.action.togglePanel` está en esa lista. Si lo sacas, Ctrl+J vuelve a ser LF para el proceso: ```json { "terminal.integrated.commandsToSkipShell": [ "-workbench.action.togglePanel" ] } ``` El panel sigue ocultándose con ``Ctrl+` `` (el atajo de siempre de la terminal). Solo cedes Ctrl+J mientras el cursor está en la terminal. ### Enviar LF con otra tecla Si no quieres tocar el toggle, manda el byte a mano. `\u000a` es LF (el mismo 10 que Ctrl+J). **No uses `\u000d`**: eso es CR, el Enter que envía el prompt. ```json { "key": "ctrl+enter", "command": "workbench.action.terminal.sendSequence", "args": { "text": "\u000a" }, "when": "terminalFocus" } ``` Elige una combinación que el IDE no reserve ya. Ctrl+Enter a veces está ocupado; Alt+J o Ctrl+Shift+J suelen estar libres. Misma receta en Cursor y Kiro IDE: el comando se llama igual. También puedes reasignar Ctrl+J **solo con la terminal enfocada** a ese `sendSequence`. Entonces Ctrl+J en el editor sigue plegando el panel, y en la terminal inserta la línea. Sigue siendo un cambio de keymap; es el más preciso si vives en el IDE. ### No pelear con el IDE Si `\`+Enter no te basta o estás en Kiro: abre la TUI en WezTerm, Alacritty o el emulador que ya uses. Ahí Ctrl+J es LF y no hay panel que ocultar. Pegar texto multilínea desde el portapapeles también funciona en casi todas las TUIs. La TUI del agente y el chat del sidebar de Cursor no son la misma superficie. Este conflicto solo aparece cuando el agente corre **en la terminal integrada**. ## Cierre Enter envía porque es CR. Ctrl+J inserta una línea porque es LF: no hay otro `Ctrl+letra` equivalente. Shift+Enter y Alt+Enter son atajos de la TUI, no del terminal, y fallan donde el emulador no reporta el modificador. En Agent, `\`+Enter cubre ese hueco sin pelear con VS Code. En el resto, o le devuelves el LF al TUI, o escribes el prompt donde el editor no pisa el teclado. ---------------------------------------- ### [SDD en la era IAG: Kiro, OpenSpec y cuánta estructura necesitas](https://escribano.dev/es/blog/015-spec-driven-development-iag/) > Cuando el agente escribe el código, el problema es gobernar lo que hace. Vibe-coding es piedra degenerada; SDD (Spec-Driven Development) es papel: specs versionadas en el repo como contrato con el agente. Mapa de madurez de cuatro niveles y comparativa de herramientas: AGENTS.md como constitución mínima, OpenSpec con deltas para brownfield, Kiro con trazabilidad EARS y los compromisos de cada uno. Cuando el agente escribe el código, el problema deja de ser "¿qué herramienta uso?" y pasa a ser "¿cómo gobierno lo que el agente hace?" Cursor, Claude Code, Kiro, [OpenCode](https://opencode.ai/) y docenas de CLI más resuelven la superficie: IDE o terminal, modelo A o modelo B. Pero si el flujo es abrir un chat, describir la feature en prosa y aceptar el diff sin más, estás en **vibe-coding**: código plausible que deriva de la intención, requisitos enterrados en un historial que cerrarás mañana y ningún artefacto que sobreviva al contexto de la sesión. El hilo es el marco: por qué las specs versionadas importan en la era IAG, cómo se comparan los enfoques, y dónde encaja cada uno en el [principio RPS](/blog/002-rps-principle/). ## El problema del vibe-coding Vibe-coding funciona hasta que deja de funcionar. El agente genera código que compila, pasa los tests que le pediste y encaja con el estilo del repo. Tres semanas después no recuerdas por qué eligió esa abstracción, el requisito original vivía en el mensaje 47 del chat y el siguiente agente (o tú, con memoria fresca) reinterpreta la intención desde cero. En el marco RPS, vibe-coding es piedra degenerada: tiene la forma de la piedra (cero ceremonia, máxima velocidad aparente) pero sin sus virtudes. Piedra consciente asume reversibilidad alta y coste del fallo bajo. Vibe-coding en un módulo con datos sensibles, en código que vivirá años o en un dominio regulado es improvisación con apariencia de pragmatismo, no piedra consciente. Los síntomas son predecibles: * **Drift**: el código se aleja de lo que pediste sin que nadie lo note hasta la revisión, o hasta producción. * **Contexto perdido**: cerrar el chat es borrar la spec. * **Código plausible, no correcto**: el agente optimiza para "parecer bien", no para cumplir invariantes que nunca quedaron escritos. * **Alucinaciones silenciosas**: APIs inventadas, dependencias que no existen, configuraciones que "deberían" funcionar. Ninguno de estos problemas es culpa del modelo. Son síntomas de ausencia de acuerdo explícito entre humano y agente antes de implementar. ## Qué es SDD **Spec-Driven Development (SDD)** pone la especificación como artefacto versionado en el repositorio (no en el historial del chat) y la trata como contrato entre humanos y agente. La spec dice qué debe hacer el sistema, bajo qué restricciones y qué no debe hacer. El agente implementa; los humanos revisan contra la spec, no solo contra el diff. En un banco o en una empresa con datos regulados, SDD es papel auténtico en RPS: pagas estructura inicial (specs, revisiones, gates) a cambio de control del riesgo. El riesgo de verdad es filtración de datos a APIs externas, alucinaciones en producción o decisiones que nadie puede auditar seis meses después, no que el código quede feo. Tampoco es "siempre escribir un PRD de veinte páginas". Es elegir cuánta estructura necesitas según equipo, dominio y coste del fallo. ### Niveles de madurez | Nivel | Nombre | Idea | |-------|--------|------| | 0 | Vibe-coding | Prompt en chat; requisitos en historial perdido | | 1 | Spec-first ad hoc | PRD suelto, notas en Markdown, Cursor Plan Mode, OpenCode en modo Plan | | 2 | Spec-anchored | Spec Kit, Kiro, OpenSpec, specs en repo, agente las sigue | | 3 | Spec-as-source | Specs como fuente de verdad viva (deltas, archivo, trazabilidad requisito→código) | La mayoría de equipos no necesitan el nivel 3 desde el día uno. Pero confundir el nivel 0 con piedra consciente es el error habitual. ## La constitución mínima: `AGENTS.md` y reglas del proyecto Antes de frameworks formales existe un escalón mínimo: decirle al agente cómo comportarse en este repo. [`AGENTS.md`](https://agents.md/) es un convenio emergente, Markdown en la raíz del proyecto con convenciones, límites y contexto que cualquier agente debería leer antes de tocar código. Es portable entre Cursor, Claude Code, Copilot, OpenCode y la mayoría de herramientas que respetan instrucciones de proyecto. En Cursor, el equivalente práctico son las [reglas de proyecto](https://docs.cursor.com/context/rules) (`.cursor/rules/`): archivos con convenciones de este blog, por ejemplo, o restricciones de estilo. No sustituyen una spec de feature, pero evitan que cada sesión empiece de cero. En RPS es piedra mínima: casi cero ceremonia, máximo efecto por línea escrita. No tiene workflow formal ni deltas ni trazabilidad. Pero es un puente: mañana cambias de IDE o de agente y la constitución del proyecto viaja contigo. Este repositorio aún no tiene `AGENTS.md`; usa reglas de Cursor. Adoptar `AGENTS.md` sería el siguiente paso natural, misma intención, más portabilidad. ## OpenCode: el agente que arranca en piedra mínima Conviene separar frameworks SDD (OpenSpec, Kiro) de agentes que ejecutan el trabajo. [OpenCode](https://opencode.ai/) es un agente open source (licencia MIT) pensado para terminal, con extensiones de escritorio e IDE. No sustituye un framework de specs, pero encaja en este post porque incorpora varios hábitos de gobernanza mínima sin ceremonia extra. **`/init`** analiza el proyecto y genera un `AGENTS.md` en la raíz. Es el puente más directo entre "constitución del repo" y herramienta: no escribes el archivo a mano desde cero; lo revisas, lo ajustas y lo versionas. Misma intención que las reglas de Cursor, con un artefacto portable que otros agentes también pueden leer. **Modos Build y Plan** (conmutables con Tab en la TUI) separan análisis de ejecución. En Plan, el agente es de solo lectura: explora el código, propone enfoques, no toca ficheros. En Build, escribe y ejecuta comandos. No es SDD completo (no hay deltas ni trazabilidad requisito→código), pero es nivel 1 honesto: piensa antes de mutar. Si cierras la sesión sin exportar el plan, vuelves al vibe-coding con pasos extra. **Agnóstico de proveedor**: más de setenta modelos vía API (Anthropic, OpenAI, Google, GitHub Copilot) o inferencia local con Ollama. En RPS es piedra con puente fuerte: pagas poca fricción operativa y eliges tú quién procesa el código. La ancla depende del proveedor de modelo que conectes, no del agente en sí. OpenCode no almacena tu código ni tu contexto en sus servidores, relevante cuando el perímetro importa, aunque eso no sustituye criterio sobre qué mandas al modelo. Conecta con [soberanía digital](/blog/005-digital-sovereignty/): el agente puede ser puente; el modelo cerrado en la nube sigue siendo decisión tuya. OpenSpec, Kiro o Spec Kit definen qué debe hacer el sistema. OpenCode (como Cursor o Claude Code) es quien lo implementa. La gobernanza viene de los artefactos del repo (`AGENTS.md`, specs, tests), no del nombre del agente. ## OpenSpec: papel con puente fuerte [OpenSpec](https://github.com/Fission-AI/OpenSpec) es un framework agnóstico de herramienta para SDD orientado a brownfield y cambios incrementales. Las specs viven en `openspec/specs/`; los cambios propuestos, en `openspec/changes/` con deltas explícitos: ADDED, MODIFIED, REMOVED. La idea central: antes de implementar, el agente (o tú) propone un cambio estructurado. Revisas la spec del delta, la apruebas, y entonces se implementa. Después archivas el cambio y la spec base se actualiza. Por qué encaja en RPS como papel: * Pagas estructura a cambio de alineación y menos sorpresas. * Los artefactos son Markdown en tu repo, no en un SaaS propietario. * Funciona con Cursor, Claude Code, Copilot u OpenCode sin vendor lock-in del framework. Puente fuerte: las specs son tuyas; el agente es intercambiable. Ancla baja: dependes de la disciplina de seguir el workflow, no de un proveedor concreto. OpenSpec brilla cuando el proyecto ya existe, los cambios son frecuentes y necesitas que el agente no reinvente requisitos en cada sesión. Comandos como `/opsx:propose` o `/opsx:apply` aceleran el ciclo; la documentación oficial cubre el detalle, aquí importa el criterio, no la sintaxis. ## Kiro SDD: papel con trazabilidad, ancla en ecosistema [Kiro](https://kiro.dev/) lleva SDD más lejos: workflow estructurado requirements → design → tasks, notación [EARS](https://alistairmavin.com/ears/) para requisitos, specs en `.kiro/specs/` y hooks que validan antes y después de que el agente ejecute herramientas. Donde OpenSpec es agnóstico y ligero, Kiro apuesta por trazabilidad requisito→diseño→tarea→código y por integración profunda en IDE y CLI propios. Para equipos regulados o proyectos donde "¿quién pidió esto y por qué?" no es pregunta retórica, esa trazabilidad es papel de verdad, no teatro. El coste en RPS: * Ancla en el ecosistema Kiro/AWS/Bedrock: no es que las specs no sean tuyas, sino que el workflow completo asume sus herramientas. * Papel/tijeras según contexto: papel si el dominio exige auditoría; tijeras si adoptas hooks, multi-agente y metodología completa en un homelab de una persona. Kiro no compite con OpenSpec en "cuál es mejor". Compiten en cuánta estructura institucional necesitas y cuánta ancla aceptas a cambio. ## Kiro vs OpenSpec | Dimensión | Kiro SDD | OpenSpec | |-----------|----------|----------| | Integración | IDE + CLI propios | Agnóstico (Cursor, Claude Code, OpenCode, Copilot…) | | Formato | EARS, `.kiro/specs/` | Deltas en `openspec/specs/` y `openspec/changes/` | | Fuerte en | Trazabilidad requisito→código, hooks, equipos regulados | Brownfield, cambios incrementales, sin vendor lock-in | | Ancla | Ecosistema Kiro/AWS/modelos | Baja, specs en tu repo, cualquier agente | | RPS | Papel/tijeras | Papel con puente fuerte | ## Otros nombres en el mapa **GitHub Spec Kit**: referencia de GitHub para SDD con más ceremonia que OpenSpec; útil en greenfield con equipos que ya viven en GitHub. Papel con puente (Markdown en repo), algo más de ancla en el ecosistema GitHub. **Cursor Plan Mode**: SDD ligero sin framework extra: planificas antes de ejecutar, el agente sigue el plan en la misma sesión. Piedra/papel según disciplina: si exportas el plan a un archivo versionado, se acerca al nivel 1; si muere al cerrar el chat, es vibe-coding con pasos extra. **OpenCode**: ya tratado arriba: agente OSS, no framework SDD. Destaca por `/init` → `AGENTS.md`, modo Plan de solo lectura y agnosticismo de modelo. Piedra operativa con puentes; la profundidad de herramienta (IDE vs CLI, comparativa con Cursor) queda para el post del panorama de agentes. **BMAD, Tessl, Antigravity**: metodologías multi-agente o comerciales con más peso. Tijeras en RPS: máximo potencial metodológico, riesgo de overkill si eres una persona y el dominio no lo pide. Mención honesta sin review en profundidad: no los he usado en producción. ## SDD en el marco RPS Volver a la triada: | Enfoque | RPS | Cuándo tiene sentido | |---------|-----|----------------------| | **Vibe-coding** | Piedra degenerada | Prototipos desechables, spikes de una hora, reversibilidad real | | **`AGENTS.md` / reglas** | Piedra mínima | Constitución del proyecto; no sustituye spec por feature | | **OpenCode (Plan + `/init`)** | Piedra / nivel 1 | Análisis antes de escribir; genera `AGENTS.md`; agente intercambiable | | **OpenSpec / Spec Kit** | Papel | Brownfield, equipo pequeño, cambios frecuentes con agente | | **Kiro SDD** | Papel/tijeras | Regulación, trazabilidad, hooks antes de comandos destructivos | | **BMAD y similares** | Tijeras | Equipos grandes, metodología explícita, dominio complejo | La pregunta útil es qué estrategia estoy usando y si el contexto lo justifica, no cuál framework tiene más estrellas. Un equipo de banca que procesa PII y usa modelos en la nube sin SDD no está en piedra; está en piedra degenerada con consecuencias legales. Un desarrollador solo que escribe Spec Kit completo para un script de 40 líneas no está en papel; está en falso papel. ### Puentes y anclas en SDD | Decisión | Tipo | Por qué | |----------|------|---------| | Specs en Markdown en el repo | Puente | Cualquier agente, cualquier IDE, diff en PR | | Deltas ADDED/MODIFIED/REMOVED (OpenSpec) | Puente | Cambios incrementales auditables | | Hooks PreToolUse/PostToolUse (Kiro) | Puente | Validación antes de ejecutar; reduce sorpresas | | OpenCode `/init` → `AGENTS.md` versionado | Puente | Constitución portable; cualquier agente la lee | | OpenCode modo Plan (solo lectura) | Puente | Análisis sin mutación; no sustituye spec de feature | | Spec en Confluence + código en Git | Ancla | El agente no ve la spec; drift garantizado | | Workflow que solo funciona en un IDE | Ancla | Cambiar herramienta implica rehacer el proceso | | Vibe-coding con datos reales en el prompt | Ancla | El contexto sale del perímetro; irreversible | Conecta con [soberanía digital](/blog/005-digital-sovereignty/): puedes tener specs impecables y seguir filtrando datos si mandas el contenido del repo a un modelo cerrado sin criterio. SDD gobierna la intención; la soberanía cognitiva gobierna el perímetro. ## Cuándo no hace falta SDD SDD es papel, y papel tiene coste: no es obligatorio. Quédate en vibe-coding (piedra consciente, no degenerada) cuando: * El artefacto se tira en horas o días. * Nadie más mantendrá el código. * El fallo es barato y reversible. * Estás explorando, no construyendo. Sube a `AGENTS.md` o reglas cuando: * El proyecto vivirá semanas o meses. * Vuelves al repo con agentes distintos. * Hay convenciones que el agente rompe en cada sesión. Sube a OpenSpec o equivalente cuando: * El código ya existe y los cambios se acumulan. * Necesitas que el agente proponga deltas antes de implementar. * Varias personas (o varias sesiones) deben compartir la misma intención. Sube a Kiro o metodología pesada cuando: * Hay requisitos de auditoría o regulación. * Los comandos del agente pueden ser destructivos. * El coste de una alucinación en producción es inaceptable. ## Lo que no resuelve SDD SDD no sustituye revisión humana. No garantiza que el agente siga la spec, solo que tienes contra qué medir el incumplimiento. No elimina la necesidad de tests, CI ni criterios de aceptación. Y no elige el modelo por ti: un spec excelente con un agente sin acceso a ella es decoración. Lo apalancado es saber cuándo la spec merece el esfuerzo, y cuándo es ruido. ## Qué viene después Este post nombra la gobernanza: vibe-coding como piedra degenerada, SDD como papel, y el espectro entre `AGENTS.md` y frameworks completos. Otros posts de la serie IAG bajarán al panorama de herramientas (Cursor, Kiro, Claude Code, OpenCode, IDE vs CLI) y a casos operativos en homelab (compose → quadlet, SSO, diagnóstico con agente en terminal), siempre con el mismo criterio: no recetas por recetas, sino saber qué estás pagando cuando eliges. Pagas estructura a cambio de alineación cuando el agente escribe el código y necesitas que la intención sobreviva al chat. ---------------------------------------- ### [Identidad soberana en el homelab: Zitadel y el espectro RPS](https://escribano.dev/es/blog/009-homelab-identity/) > Identidad de aplicación en el homelab en el marco RPS: auth básica como piedra mínima, Authelia y forward auth para pocos usuarios, Keycloak y Authentik como papel, y Zitadel como tijeras (IdP cloud-native en Go, ligero y multi-tenant). Diferencia clave: forward auth pregunta al portero del proxy, un IdP habla OIDC contigo. En el [post sobre reverse proxy](/blog/008-caddy-reverse-proxy-pihole/) la pila ya tenía acceso, nombres, TLS y enrutamiento HTTP. Pero `https://grafana.lan` sin login es una puerta abierta en la LAN (y a veces fuera de ella si algo falló en el perímetro). La cuarta capa responde "¿quién eres?": identidad de aplicación, OIDC, forward auth. La VPN del [post 006](/blog/006-homelab-vpn-access/) responde "¿estás en mi red?"; el DNS del [007](/blog/007-sslip-io-homelab-dns/), "¿cómo se llama?". Si las mezclas, montas un IdP cuando el problema era llegar al NAS, o abres Grafana a internet cuando bastaba un túnel. Parto del [principio RPS](/blog/002-rps-principle/) y del eje puente/ancla aplicados a identidad en homelab. El caso real es Zitadel (recién empezando, sin fingir años de producción) y el hilo sigue el de [soberanía digital](/blog/005-digital-sovereignty/) y [cloud-first](/blog/004-cloud-first-problem/). ## La pregunta después del proxy Con nombre, certificado y enrutado, la tentación es proteger "ya" todos los paneles. Auth básica en Caddy, un contenedor de Authelia, o de golpe un IdP con organizaciones, proyectos y políticas. Las tres cierran *un* login, y cada una asume un problema distinto. Authelia y OAuth2-Proxy hacen **forward auth**: el proxy pregunta a un servicio si la petición puede pasar. Un portero en el borde, sin directorio central de aplicaciones. Keycloak, Authentik y Zitadel son un **IdP**: las apps hablan OIDC o SAML contigo. Esa diferencia importa más que el nombre del producto. En un homelab personal el orden sigue siendo el de la serie: 1. **Acceso a la red**: [Tailscale, WireGuard u OpenVPN](/blog/006-homelab-vpn-access/) 2. **Resolución de nombres**: [sslip.io](/blog/007-sslip-io-homelab-dns/), MagicDNS o Pi-hole 3. **Terminación TLS y enrutado HTTP**: [Caddy](/blog/008-caddy-reverse-proxy-pihole/) 4. **Identidad de aplicación** ← aquí Saltar a la 4 sin las anteriores es tijeras antes de tiempo: identidad de plataforma para un servicio que aún no tiene nombre estable. ## Dos dimensiones | Dimensión | Pregunta | |-----------|----------| | RPS | ¿Cuánto inviertes en la arquitectura de identidad? | | Puente / ancla | ¿Cuánta opcionalidad conservas para migrar mañana? | Una misma decisión puede ser piedra estratégica y ancla fuerte. Social login como único factor es el ejemplo habitual: cero operación hoy, el factor de identidad no es tuyo mañana. ## Espectro RPS: herramientas self-hosted ### Piedra: proteger servicios, ya Auth básica en Caddy o nginx (`htpasswd`) es la piedra mínima: un fichero, un realm, y el navegador pide usuario y contraseña. Sirve para un panel que solo usas tú. No escala a varias apps con SSO, el 2FA es pobre o inexistente, y el día que compartas el homelab con alguien empiezas a odiar la lista de claves. **Authelia** (y OAuth2-Proxy) suben un peldaño sin convertirte en administrador de un IdP. Forward auth y 2FA delante del proxy. El caso honesto es el homelab personal, pocos usuarios, y servicios que no hablan OIDC de verdad (o que no te importa que no lo hablen). Si Grafana, el NAS y Home Assistant solo necesitan que no entre cualquiera en la LAN, Authelia suele ser suficiente. ### Papel: IdP serio, riesgo controlado **Keycloak** es el papel corporativo: Java, pesado, predecible, referencia de industria. Lo eliges cuando el fallo es caro (varios usuarios, apps que ya esperan OIDC, alguien más depende del login) y aceptas operar un servidor que no es ligero. **Authentik** ocupa el mismo hueco estratégico con otro equilibrio: IdP completo, DX más moderna, stack heterogéneo (Python y servicios auxiliares). Más cómodo de vivir en un homelab o un equipo pequeño que Keycloak; sigue siendo papel. Montas directorio, flujos, y te responsabilizas de que no se caiga. Misma estrategia RPS. Distinto precio en RAM, en curva de aprendizaje y en cuánto te recuerda a un producto enterprise. ### Tijeras: identidad como plataforma **Zitadel** nace como IdP cloud-native: event sourcing, multi-tenant por diseño, escrito en Go. En homelab eso se nota en el footprint (más ligero que Keycloak, más compacto que Authentik) y en el modelo mental: organizaciones, proyectos, aplicaciones, políticas. Grafana queda como un cliente más. Tiene sentido cuando la identidad *es* el núcleo: varias apps OIDC de verdad, un SSO que quieres poseer, o un proyecto (una PWA, un SaaS propio) que va a vivir años con esos tokens. El riesgo es el de siempre con tijeras: overengineering si eres una persona y tres servicios detrás de forward auth. ### Falso papel Montar Keycloak o Authentik cuando Authelia bastaba. Montar Zitadel cuando el problema era un `htpasswd`. Se parece a papel (hay un IdP, hay un panel, hay documentación densa) pero pagas complejidad que el contexto no pide. El mismo patrón que [cloud-first](/blog/004-cloud-first-problem/) en miniatura: la herramienta "seria" por defecto. ## Anclas gestionadas: por profundidad No todo el mundo self-hostea el IdP. El espectro managed es otra escala; mejor no mezclarla con la anterior: ``` Menos profunda ──────────────────────────────► Más profunda Social login BaaS auth Cloud IdP Hyperscaler IAM (Google/GitHub) (Firebase, (Cognito, Auth0, (IAM + stack cloud) Supabase) Clerk) ``` | Capa | RPS típico | Reversibilidad | |------|------------|----------------| | Social login solo | Piedra táctica | Ancla en el proveedor social | | Firebase / Supabase Auth | Piedra (MVP) | Ancla media según el acoplamiento | | Cognito, Auth0, Clerk | Papel (auth sin operarlo) | Ancla fuerte | | IAM del hyperscaler | Papel enterprise | Ancla máxima | Supabase puede ser puente si te quedas en Auth y Postgres estándar; ancla si el producto entero vive ahí. Firebase ancla antes. Cognito y Auth0 te quitan la operación y te venden la salida cara. ## Social login "Entra con Google" y nada más es piedra pragmática: cero operación, los usuarios ya existen. También es ancla: no controlas el factor de identidad. Si Google cierra la API, cambia el consentimiento o te bloquea el proyecto, el login se cae y no hay directorio al que volver. El matiz que me interesa es otro. Un IdP propio (Zitadel, Keycloak, Authentik) con Google como *identity broker*. Las apps hablan OIDC contigo; tú federas con Google. Puente parcial: mañana quitas Google y dejas usuarios locales, o añades otro IdP, sin reescribir cada cliente. El ancla se queda en el broker, no en cada aplicación. ## Mi experiencia con Zitadel Lo digo sin postureo: recién empezando. Primeras impresiones. Monté Zitadel detrás de Tailscale, con MagicDNS para nombres y acceso. El perímetro sigue siendo la tailnet del [post 006](/blog/006-homelab-vpn-access/); el IdP no está abierto a internet. La doble ancla: * **Tailscale** coordina la red: puente operativo, ancla en el plano de control salvo que pases a Headscale. * **Zitadel** guarda usuarios y emite OIDC en una instancia mía: puente si te quedas en estándares abiertos. Ancla consciente. La alternativa que evito es abrir todo a internet y delegar el login en Cognito "porque es lo profesional". La configuración es conceptualmente densa. Organizaciones, proyectos, aplicaciones, URIs de redirección, quién es el issuer. Un `htpasswd` no te prepara para ese mapa. Los agentes IAG en CLI recortan la primera configuración (el compose, el Caddyfile, el error tonto de Postgres) y dejan intacto el modelo mental. El agente te deja un YAML que arranca; entender por qué una app no recibe el claim que esperabas sigue tocando hacerlo tú. En [SDD](/blog/015-spec-driven-development-iag/) hablo de esa misma distancia entre artefacto y criterio. El caso operativo, el agente de verdad contra el homelab, da para otro post. Lo que noté enseguida: Go se nota. Zitadel ocupa menos que Keycloak (Java) y menos que Authentik (Python y acompañantes). En un mini PC que ya corre Pi-hole y Caddy, se agradece. Yo empezaría por la [documentación de Zitadel con Caddy](https://zitadel.com/docs/self-hosting/manage/reverseproxy/caddy) y por la pila que ya tienes. Los dominios siguen la misma progresión que en el [post 008](/blog/008-caddy-reverse-proxy-pihole/): sslip.io con certificado autofirmado para probar, un DynDNS si hace falta, dominio real y Let's Encrypt cuando el nombre merezca quedarse. Pi-hole y las renovaciones ACME ya están cubiertas ahí; el IdP no inventa otra física de red. ## Cuándo usar qué Auth básica o Authelia basta cuando: * Eres tú, o un hogar, y los servicios no hablan OIDC. * El problema es que no entre cualquiera, no federar cinco aplicaciones. * Aún no tienes claro si el homelab se queda. Keycloak o Authentik cuando: * Ya hay varias apps OIDC y el login compartido es el problema real. * Quieres un IdP que mucha gente ha operado antes, con el coste de peso (Keycloak) o de stack (Authentik). Zitadel cuando: * La identidad es parte del sistema que estás construyendo, no un candado delante de Grafana. * Te importa el footprint y aceptas un modelo más de plataforma que de portero. * Puedes vivir con primeras impresiones: la herramienta es joven *en tu stack*, no en el mercado. Quédate en social login o en un BaaS cuando el producto aún es un MVP y el coste de operar un IdP supera al de un ancla que ya nombraste. ## Limitaciones honestas * Un IdP no sustituye el perímetro. Zitadel sin VPN (o con el 443 abierto a internet) es otra superficie, no más soberanía. * OIDC mal configurado es peor que `htpasswd`. Un redirect URI laxo o un cliente público mal entendido abre más de lo que cierra. * Tijeras se siente bien el primer fin de semana. Tres meses después, si solo tienes tres servicios, Authelia te habría dejado más tiempo para el hobby. * Hablo desde el primer montaje, no desde años operando esto en serio. ## Lo que viene después Con esta capa, la pila que empecé en [acceso](/blog/006-homelab-vpn-access/) queda nombrada: red, nombres, TLS, identidad. Cada una responde a una pregunta distinta. Si las mezclas, usas Caddy para autenticar usuarios o Tailscale para sustituir OIDC. El hilo siguiente se aleja del rack: cómo gobiernas lo que escribe el agente ([SDD](/blog/015-spec-driven-development-iag/)) y, más adelante, cómo ese mismo agente te ayuda a operar el homelab sin sustituir el criterio. Zitadel, Keycloak o Authelia solo tienen sentido en tu contexto. La pregunta útil es qué estrategia estás jugando y qué anclas aceptas. ---------------------------------------- ### [Reverse proxy en el homelab: Caddy, TLS y cuando Pi-hole se interpone](https://escribano.dev/es/blog/008-caddy-reverse-proxy-pihole/) > Caddy termina TLS y enruta HTTP por hostname en el homelab con configuración mínima. Al mezclarlo con Pi-hole aparecen las fricciones reales: split DNS para resolver distinto desde LAN e internet, rebind protection que bloquea las IPs privadas de sslip.io, y renovaciones ACME que fallan si el proxy consulta al resolver interno. Incluye Caddyfile de ejemplo y diagnóstico con dig. En el [post sobre sslip.io](/blog/007-sslip-io-homelab-dns/) la capa de nombres quedó resuelta de forma mínima: hostnames que apuntan a una IP sin comprar dominio ni montar BIND. Pero un nombre no sirve de mucho si cada servicio sigue escuchando en su puerto (`:3000`, `:8123`, `:9000`) y el navegador sigue gritando por certificados autofirmados. La siguiente capa es terminar TLS y enrutar HTTP: un reverse proxy que recibe en el 443, presenta un certificado válido y reparte el tráfico según el hostname. Caddy, nginx y Traefik hacen eso; este post se centra en Caddy porque encaja con el perfil del homelab personal (TLS automático, configuración legible, footprint modesto) y porque es donde aparecen las fricciones reales al mezclarlo con un resolver local como Pi-hole. El hilo es el marco: qué problema resuelve el proxy, qué rompe cuando añades DNS interno, y cómo encaja en el [principio RPS](/blog/002-rps-principle/). ## La pila hasta aquí Recuerda el orden que venimos construyendo: 1. **Acceso a la red**: [Tailscale, WireGuard u OpenVPN](/blog/006-homelab-vpn-access/) 2. **Resolución de nombres**: [sslip.io como puente](/blog/007-sslip-io-homelab-dns/), MagicDNS, o Pi-hole 3. **Terminación TLS y enrutado HTTP** ← aquí 4. **Identidad de aplicación**: OIDC, forward auth; [Zitadel y el espectro RPS](/blog/009-homelab-identity/) El proxy no sustituye la VPN ni el DNS. Asume que ya sabes llegar a la red y resolver el nombre. Su trabajo es que `https://grafana.192-168-1-47.sslip.io` llegue al contenedor correcto con un certificado que el navegador acepte. ## Qué hace un reverse proxy (y qué no) Un reverse proxy escucha en los puertos estándar (80 y 443) y reenvía el tráfico a servicios internos según reglas: hostname, path, headers. En el homelab eso traduce: | Sin proxy | Con proxy | |-----------|-----------| | `192.168.1.47:3000` | `https://grafana.lan` | | Certificado por servicio (o ninguno) | Un certificado en el borde | | Recordar puertos | Recordar nombres | Lo que no hace: autenticar usuarios de aplicación (eso es la capa 4) ni resolver nombres DNS (eso es la capa 2). Confundir capas lleva a montar OAuth en Caddy cuando el problema era solo enrutar, o a exponer servicios sin perímetro cuando aún no tienes VPN. ## Caddy en el marco RPS | Solución | RPS | Qué pagas | Qué ganas | |----------|-----|-----------|-----------| | Acceso directo por puerto | Piedra mínima | Superficie expuesta, sin TLS unificado | Cero proxy | | **Caddy** | Piedra operativa | Aprender su DSL; acoplamiento al ecosistema Caddy | TLS automático, config declarativa, bajo mantenimiento | | **nginx** | Piedra / papel | Más config manual; Certbot aparte | Máximo control, referencia universal | | **Traefik** | Piedra operativa | Etiquetas Docker, modelo mental de routers | Descubrimiento automático en stacks containerizados | | **Cloudflare Tunnel** | Piedra operativa | Ancla en Cloudflare | Sin abrir puertos; TLS gestionado fuera | Caddy es piedra operativa en el sentido bueno: resuelve TLS + routing con poca ceremonia. El coste es una ancla blanda en el ecosistema (migrar a nginx no es imposible, pero reescribes configs) y la tentación de usar features propietarias de Caddy (`forward_auth`, módulos) sin evaluar reversibilidad. Para un homelab de una persona que ya eligió simplicidad operativa en la capa VPN, Caddy es la continuación honesta. ## TLS: HTTP-01, sslip.io y la LAN privada En el post anterior vimos que Let's Encrypt con [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) funciona con `203-0-113-7.sslip.io` si la IP pública es alcanzable en el puerto 80. Caddy lo automatiza: declaras el hostname, Caddy obtiene y renueva el certificado. Con IPs privadas (`192.168.x.x`) la cosa cambia. Let's Encrypt no puede llegar a tu LAN desde internet. Opciones reales: * Certificado autofirmado: rápido, el navegador protesta. * CA interna: más trabajo, viable en red controlada. * DNS-01 con wildcard: tijeras; sslip.io lo documenta, no es el camino por defecto. * Estar dentro de la VPN/tailnet con TLS gestionado por Tailscale, ya cubierto en el [post de acceso](/blog/006-homelab-vpn-access/). Caddy automatiza el baile cuando las condiciones se cumplen; la física de la red sigue ahí. ### Subdominios con sslip.io Un patrón habitual: `grafana.192-168-1-47.sslip.io` apunta a la misma IP que `192-168-1-47.sslip.io`; Caddy enruta por hostname al backend correcto. Eso permite varios servicios detrás de un solo proxy sin Pi-hole ni dominio propio, mientras la IP no cambie y aceptes la estética del nombre. ### Ejemplo mínimo de `Caddyfile` Un bloque por hostname ilustra el patrón: TLS automático, backend en la LAN. ```caddyfile { # Opcional: email para avisos de Let's Encrypt email admin@example.com } grafana.192-168-1-47.sslip.io { reverse_proxy 192.168.1.47:3000 } homeassistant.192-168-1-47.sslip.io { reverse_proxy 192.168.1.48:8123 } ``` Caddy escucha en 80 y 443, obtiene certificados para cada hostname y reenvía al puerto interno. Si la IP del proxy es `192.168.1.47` y los backends viven en otras máquinas, cambia las IPs de `reverse_proxy`: el hostname sslip.io sigue apuntando a la IP del proxy, no a la del backend. Con IP privada y sin salida pública al 80, añade `tls internal` dentro del bloque para forzar certificado autofirmado de Caddy (útil en LAN, inútil para visitantes desde internet). ## Pi-hole entra en escena sslip.io resuelve nombres sin infraestructura propia. Pi-hole (con Unbound detrás) es el salto a DNS soberano en la LAN: registros arbitrarios (`grafana.homelab`, `nas.lan`), bloqueo de ads, control total del resolver. El problema: mezclar sslip.io con Pi-hole sin criterio es falsa piedra. Dos resolvers, reglas contradictorias, y dolores de cabeza que solo aparecen cuando montas el proxy. ### Split DNS Split DNS significa que la misma pregunta DNS tiene respuesta distinta según quién pregunta: | Consulta | Desde internet | Desde la LAN (Pi-hole) | |----------|----------------|------------------------| | `app.midominio.com` | IP pública del router | IP interna del proxy (`192.168.1.47`) | Sin split DNS, un dispositivo en casa puede resolver tu dominio a la IP pública, salir al router, hacer hairpin NAT y fallar, o ir por un camino lento e impredecible. Con split DNS, la LAN resuelve directo a la IP interna. Caddy necesita que el cliente y el proxy estén de acuerdo en qué hostname usa cada servicio. Si Pi-hole devuelve una cosa y el resolver público otra, el proxy emite certificados para un nombre que desde dentro no resuelve igual. ### Rebind protection Pi-hole incluye protección contra [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding): bloquea respuestas que apuntan a IPs privadas para dominios públicos. Es una defensa legítima. Pero sslip.io devuelve IPs privadas por diseño (`192-168-1-47.sslip.io` → `192.168.1.47`). Pi-hole puede bloquear esas respuestas. Síntoma: el nombre resuelve fuera de casa y falla dentro, o al revés. Solución: whitelist de dominios de confianza (`sslip.io`, `nip.io`) o desactivar rebind protection para esos casos, consciente del trade-off de seguridad. ### Renovaciones ACME con Pi-hole Cuando Caddy renueva un certificado con HTTP-01, Let's Encrypt consulta DNS público y conecta a tu IP pública en el puerto 80. Si Pi-hole intercepta todas las consultas DNS (incluidas las de la propia máquina del proxy) y devuelve la IP interna para tu dominio, la renovación puede fallar: LE intenta llegar a `192.168.x.x` desde internet. Patrones que funcionan: * **Local DNS records** en Pi-hole para el dominio público apuntando a la IP interna solo para clientes LAN, más regla de firewall que permita al proxy responder en el 80 desde fuera. * **DNS condicional**: el proxy usa resolver upstream distinto al de los clientes para las consultas ACME. * **DNS-01** en lugar de HTTP-01 (más complejo, evita el problema del hairpin). No hay una receta única; depende de si tienes IP pública, CG-NAT, o solo acceso por tailnet. Antes de culpar a Caddy, diagnostica con `dig` (@Pi-hole vs @8.8.8.8). ## Orden de arranque y dependencias Otra fricción silenciosa: Caddy arranca y quiere renovar certificados; Pi-hole aún no está listo; el proxy resuelve mal y entra en bucle de reintentos. O al revés: Pi-hole depende de un contenedor que Caddy aún no enruta. Reglas prácticas: * Pi-hole y el proxy en hosts estables (IP fija o DHCP reservation). * `depends_on` en Docker no garantiza que el servicio esté *listo*; healthchecks si la pila es frágil. * Documentar qué servicio debe arrancar primero en un apagado largo, no confiar en la memoria. ## En el marco RPS: puentes y anclas | Decisión | Tipo | Por qué | |----------|------|---------| | Caddy con `Caddyfile` versionado | Puente | Migrar a nginx es trabajo, no rehacer la topología | | Certificados Let's Encrypt en Caddy | Puente | Estándar; otro cliente ACME puede sustituirlo | | Pi-hole como único resolver en la LAN | Piedra / ancla | Control y fricción operativa a cambio de soberanía DNS | | Mezclar sslip.io + Pi-hole + dominio propio sin mapa | Falsa piedra | Tres fuentes de verdad, debugging infernal | | Cloudflare Tunnel como único borde | Ancla | TLS y routing delegados; difícil volver a self-hosted puro | Conecta con [soberanía digital](/blog/005-digital-sovereignty/): el proxy puede estar en tu mini PC y los certificados en Let's Encrypt, pero si Pi-hole filtra mal una renovación, pierdes TLS y la cadena de confianza entera se resiente. ## Cuándo usar qué Caddy + sslip.io basta cuando: * Estás probando la pila antes de comprar dominio. * Tienes IP pública y pocos servicios. * Accedes sobre todo desde la tailnet con TLS de Tailscale para lo crítico. Añade Pi-hole (o Unbound) cuando: * Necesitas docenas de nombres locales arbitrarios (`*.homelab`). * Quieres bloqueo de ads en toda la LAN. * Tienes dominio propio y necesitas split DNS serio. Pasa a dominio propio + Caddy cuando: * El servicio es público o semi-profesional. * sslip.io ya te parece frágil o feo para compartir. * Necesitas estabilidad de nombre independiente de la IP. ## Limitaciones honestas * Caddy no arregla CG-NAT. Sin IP pública alcanzable, HTTP-01 desde internet no funciona; necesitas tailnet, túnel, o DNS-01. * Un proxy mal configurado es peor que ninguno. Superficie de ataque centralizada; un error en `Caddyfile` expone todo. * Pi-hole + proxy + sslip.io + dominio real puede coexistir, pero exige un mapa explícito de qué resolver responde qué para quién. * Let's Encrypt tiene límites de tasa. No pruebes renovaciones en bucle contra producción; usa staging o espera (cinco renovaciones fallidas por semana y empiezas a tener problemas). ## Lo que viene después Con nombre, certificado y enrutamiento HTTP resueltos, la pregunta siguiente es quién puede hacer login: Grafana, tu PWA, el panel de administración. Ahí entra la capa de identidad (OIDC, forward auth, Zitadel frente a Authelia frente a "auth básica y listo") en el [post 009](/blog/009-homelab-identity/). El proxy termina TLS; el DNS nombra; la VPN te mete en la red. Sin un mapa de qué resolver responde qué, las tres se pisan. Cada capa en su sitio, anclas conscientes. ---------------------------------------- ### [Nombres sin comprar dominio: sslip.io en el homelab](https://escribano.dev/es/blog/007-sslip-io-homelab-dns/) > sslip.io y nip.io embeben la IP en el hostname: 192-168-0-42.sslip.io resuelve a 192.168.0.42, sin registrar nada. Sirve para nombrar servicios del homelab sin dominio y, con IP pública, conseguir TLS de Let's Encrypt vía HTTP-01. Piedra operativa con una ancla blanda: dependes de un DNS público ajeno. En el [post sobre acceso al homelab](/blog/006-homelab-vpn-access/) la primera capa era llegar a la red: Tailscale, WireGuard u OpenVPN. Pero entrar en la LAN no responde la siguiente pregunta: ¿cómo llamas a un servicio sin memorizar IPs? `192.168.1.47:3000` funciona hasta que no funciona. Cambia la IP del contenedor, añades un segundo servicio o quieres un certificado TLS válido y de repente necesitas nombres. Editar `/etc/hosts` en cada dispositivo es piedra de la peor clase: rápido para una prueba, insostenible para una pila entera. Hablo de la capa intermedia que muchos homelabs saltan: DNS pragmático con [sslip.io](https://sslip.io/) (y su predecesor conceptual, [nip.io](https://nip.io/)), nombres que resuelven a la IP que llevan escrita en el hostname. ## Qué es sslip.io [sslip.io](https://sslip.io/) es un servicio DNS público con una regla simple: si el hostname contiene una dirección IP codificada con guiones, la resolución devuelve esa IP. | Hostname | Resuelve a | |----------|------------| | `192-168-0-42.sslip.io` | `192.168.0.42` | | `app.192-168-0-42.sslip.io` | `192.168.0.42` | | `203-0-113-7.sslip.io` | `203.0.113.7` (IP pública de ejemplo) | No registras nada. No configuras zona DNS. El nombre es el registro. nip.io sigue el mismo principio; hoy ambos servicios están unificados bajo el proyecto mantenido por [cunnie](https://github.com/cunnie/sslip.io). Para desarrollo local, demos o un homelab que aún no tiene dominio propio, eso elimina una fricción enorme: cualquier máquina con salida DNS puede resolver el nombre sin tocar tu router ni montar un servidor BIND. ## Por qué importa en la pila del homelab Recuerda el orden que planteé en el post de VPN: 1. Acceso a la red 2. **Resolución de nombres** ← aquí 3. Terminación TLS y reverse proxy 4. Identidad de aplicación sslip.io cubre el paso 2 de forma mínima. No sustituye un DNS interno completo (no tienes registros `A` arbitrarios como `grafana.lan`) pero sí resuelve el caso *"quiero un hostname que apunte a esta IP concreta"* sin ceremonia. ### Lo que Tailscale ya te da (y no te da) Si usas Tailscale, **MagicDNS** resuelve nombres dentro de la tailnet (`minipc.tail12345.ts.net`) sin sslip.io. Eso es piedra operativa con ancla en la coordinación de Tailscale; ya lo traté en el post anterior. sslip.io entra cuando: * Quieres nombres dentro de la LAN sin depender de la tailnet (otros dispositivos, VLAN de invitados, CI en la misma red). * Necesitas un hostname que Let's Encrypt reconozca vía HTTP-01 en una IP pública (o en un servicio alcanzable desde fuera). * Estás probando un reverse proxy o un PaaS self-hosted (Coolify, CapRover…) antes de comprar dominio. MagicDNS y sslip.io no compiten; resuelven capas distintas. Puedes usar ambos. ## TLS: el motivo por el que mucha gente llega aquí Un certificado TLS válido exige que la autoridad de certificación verifique que controlas el nombre. Con un dominio normal, Caddy o Certbot lo resuelven en minutos. Sin dominio, el callejón sin salida habitual era certificado autofirmado, y el navegador gritando. Con sslip.io y una IP pública alcanzable en el puerto 80, el desafío [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) funciona: Caddy (o nginx, o Traefik) sirve en `203-0-113-7.sslip.io`, Let's Encrypt comprueba el token y emite el certificado. Es el caso de uso que la propia documentación de sslip.io destaca. Con IPs privadas (`192.168.x.x`) la cosa cambia: Let's Encrypt no puede llegar a tu LAN desde internet. Ahí sslip.io sigue resolviendo nombres en la red local, pero el certificado tendrá que ser autofirmado, de una CA interna, o conseguirse por otra vía, por ejemplo, estando dentro de la VPN o usando un wildcard con DNS-01 y un servidor autoritativo propio, que es tijeras de verdad y merece su propio post. No voy a detallar el baile del wildcard `*.external-ip.sslip.io` con DNS-01; la [documentación del proyecto](https://github.com/cunnie/sslip.io/blob/master/docs/wildcard.md) lo explica. Basta saber que existe y que no es el camino por defecto. ## En el marco RPS | Enfoque | RPS | Qué pagas | Qué ganas | |---------|-----|-----------|-----------| | `/etc/hosts` manual | Piedra mínima | Editar cada máquina | Cero infra | | **sslip.io / nip.io** | Piedra operativa | Dependencia de DNS público ajeno | Nombres al instante, TLS posible con IP pública | | **Pi-hole / Unbound local** | Piedra / papel | Operar y mantener el resolver | Nombres locales (`*.homelab`), bloqueo de ads, control | | **Dominio propio + DNS** | Papel | Dinero, gestión de zona, renovaciones | Marca, portabilidad, soberanía real | | **MagicDNS (Tailscale)** | Piedra operativa | Ancla en coordinación | Cero config dentro de la tailnet | sslip.io es piedra operativa en el sentido bueno: resuelve hoy el problema de nombres sin exigir dominio ni servidor DNS propio. El coste es una ancla blanda: dependes de que `sslip.io` siga resolviendo y de que tu homelab no necesite registros DNS que el esquema no permite. Un puente hacia algo que funcione mientras decides si merece la pena un dominio o un Pi-hole, no soberanía DNS. ### Puentes y anclas | Decisión | Tipo | Por qué | |----------|------|---------| | Hostname `servicio.IP.sslip.io` en configs versionadas | Puente | Mañana cambias a dominio propio; el patrón es explícito | | Autoridad DNS de sslip.io operada por tercero | Ancla | Si el servicio desaparece o cambia, tus nombres dejan de resolver | | [Servidor DNS sslip.io self-hosted](https://github.com/cunnie/sslip.io) | Puente | Misma lógica, infraestructura tuya | | Mezclar sslip.io con Pi-hole sin criterio | Falsa piedra | Dos resolvers, split DNS, dolores de cabeza, de eso va el siguiente post | Conecta con [soberanía digital](/blog/005-digital-sovereignty/): la capa de datos puede estar en tu NAS y el resolver de nombres en un servicio público que ni siquiera es tuyo. Consciente está bien; invisible es trampa. ## Cuándo usar sslip.io Tiene sentido cuando: * Estás montando el homelab y aún no quieres comprar dominio. * Necesitas probar HTTPS con Let's Encrypt en una IP pública sin DynDNS. * Un servicio exige hostname en lugar de IP (OAuth, cookies, WebSockets) y es una prueba temporal. * Quieres documentar o compartir una demo sin tocar DNS de nadie. Pasa a otra cosa cuando: * El servicio es público y profesional, merece dominio propio. * Necesitas docenas de nombres locales arbitrarios, Pi-hole o Unbound. * El resolver externo es inaceptable por política, DNS interno o dominio self-hosted. * Ya tienes Tailscale y solo accedes desde la tailnet, MagicDNS puede bastar. ## Limitaciones honestas * No es un DNS genérico. No creas `db.internal` apuntando a otra IP; el nombre debe llevar la IP embebida (salvo trucos avanzados con wildcards). * Dependencia externa. No es Cloudflare, pero tampoco es tuyo. * Privacidad. Cada resolución sale a internet hacia el resolver de sslip.io; en una LAN aislada puede importar. * Estética y estabilidad. `grafana.192-168-1-47.sslip.io` no es un nombre de producto; si la IP cambia, el hostname cambia. ## Lo que viene después sslip.io nombra la capa de resolución mínima. El siguiente paso natural en esta pila es terminar TLS y enrutar HTTP (reverse proxy con Caddy u otro) y ahí aparecen las fricciones reales cuando mezclas resolver local (Pi-hole) con proxy: split DNS, rebind protection, órdenes de arranque. Ese es otro post. Después vendrá identidad de aplicación (OIDC, forward auth) cuando los servicios ya tengan nombre y certificado. sslip.io es piedra operativa con ancla consciente: un puente que te deja avanzar sin fingir que ya tienes soberanía DNS cuando solo tienes un truco elegante con guiones. ---------------------------------------- ### [Acceso al homelab: Tailscale, WireGuard y OpenVPN](https://escribano.dev/es/blog/006-homelab-vpn-access/) > El acceso remoto es la primera capa del homelab, antes de DNS, proxy o identidad. OpenVPN es papel legacy, WireGuard manual es piedra con control total, Tailscale es piedra operativa con ancla consciente (el plano de coordinación es suyo) y Headscale, tijeras con puente. Con CG-NAT de por medio, la continuidad del hobby pesa más que la pureza arquitectónica. En los posts sobre [cloud-first](/blog/004-cloud-first-problem/) y [soberanía digital](/blog/005-digital-sovereignty/) hablé de datos, infraestructura y cognición. Pero antes de hablar de DNS, reverse proxy o identidad hay una pregunta más básica: ¿cómo llegas a tu red desde fuera? Un homelab sin acceso remoto controlado es una colección de servicios en una LAN que solo existe cuando estás en casa. La VPN (o su equivalente moderno) es la primera capa práctica de soberanía de red: decides quién entra, por qué túnel y bajo qué coordinación. No escribo esto para decir que debes exponer servicios a internet con `port forward` y rezar. Escribo para situar OpenVPN, WireGuard y Tailscale en el [marco RPS](/blog/002-rps-principle/) y en el eje puente/ancla, con la misma pregunta de siempre: *¿qué estás pagando cuando eliges?* ## La pregunta antes del login Es tentador empezar el homelab por identidad, montar un IdP, federar Google, proteger Grafana con OIDC. Pero la identidad responde a *"¿quién eres?"*; la VPN responde a *"¿estás en mi red?"*. Son capas distintas. Confundirlas lleva a montar Zitadel o Keycloak cuando el problema real era llegar al NAS desde el móvil sin abrir puertos. O a abrir el 443 de todo a internet cuando bastaba un túnel privado. En un homelab personal, el orden sensato suele ser: 1. **Acceso a la red** (este post) 2. **Resolución de nombres** (DNS interno; un post posterior) 3. **Terminación TLS y enrutado HTTP** (reverse proxy) 4. **Identidad de aplicación** (IdP, forward auth; ver [issue sobre Zitadel](https://github.com/escribanoruben/escribano.dev/issues/30)) Saltarse el paso 1 y publicar servicios directamente es piedra de la peor clase: rápido hoy, superficie de ataque mañana. ## Dos dimensiones otra vez Como en identidad o en cloud, conviene separar: | Dimensión | Pregunta | |-----------|----------| | RPS | ¿Cuánta infraestructura de red montas y operas tú? | | Puente / ancla | ¿Puedes cambiar de solución sin rehacer toda tu topología? | Una VPN puede ser piedra operativa (poca fricción al configurar) y ancla fuerte: si el plano de coordinación no es tuyo. Tailscale es el ejemplo habitual. ## OpenVPN: papel histórico OpenVPN fue durante años la respuesta por defecto al acceso remoto self-hosted. Certificados X.509, topologías tun/tap, archivos `.ovpn`, renovaciones, a veces un servidor pesado y configuraciones que nadie quiere tocar seis meses después. En el marco RPS es papel legacy: pagas complejidad inicial y mantenimiento continuo a cambio de un protocolo maduro, ampliamente soportado y entendido por firewalls corporativos. No es elegante. Pero durante mucho tiempo fue realista cuando el fallo (no poder entrar al servidor de backup) era caro. Hoy sigue apareciendo por inercia: tutoriales antiguos, routers con cliente integrado, empresas que estandarizaron en él hace una década. Mantenerlo por costumbre cuando WireGuard o un mesh moderno cubren tu caso es el mismo patrón que cloud-first: pagar complejidad que el problema ya no pide. OpenVPN self-hosted en tu VPS o en casa suele ser puente si controlas certificados y configuración: mañana puedes migrar clientes. Pero el coste operativo es alto para un homelab de una persona. ## WireGuard: piedra moderna en el protocolo WireGuard cambió el punto de partida. Diseño mínimo, criptografía actual, implementación en el kernel Linux, código pequeño (escrito en C, con herramientas de usuario en Go) y rendimiento excelente en hardware modesto. En RPS es piedra en el sentido bueno: hace una cosa, la hace bien, con poco ruido. No te da MagicDNS, panel de administración ni identidad de dispositivos; te da túneles punto a punto o hub-and-spoke con claves estáticas o rotación que tú diseñas. Ventajas en homelab: * Footprint mínimo; corre en una Raspberry o en un contenedor sin drama. * Configuración explícita: sabes qué peer es qué máquina. * **Puente fuerte**: protocolo abierto, implementaciones múltiples, sin vendor detrás del túnel en sí. Coste: * Tú gestionas IP internas, DNS (si quieres nombres en lugar de IPs), alta disponibilidad y onboarding de cada dispositivo nuevo. * Sin capa de coordinación, escalar a diez dispositivos es tedioso; a treinta, ya es un mini-proyecto. WireGuard puro es la opción honesta cuando quieres máximo control y mínima dependencia, y aceptas pagar fricción en cada cliente nuevo. ## Tailscale: piedra operativa, ancla en coordinación Tailscale construye una red mesh sobre WireGuard. Instalas el cliente, autenticas el dispositivo con tu identidad (Google, GitHub, Microsoft, etc.), y de repente tus máquinas se ven entre sí (con MagicDNS, ACLs opcionales, subnet routers, exit nodes) sin abrir puertos en el router. ### La gran ventaja: sin tocar el router La ventaja operativa que más peso tiene en un homelab real no es técnica sino humana: no hace falta redirigir puertos en el router. Durante años los tutoriales de homelab asumían que sabías entrar en la interfaz del router, crear una regla NAT, apuntar el 443 o el 1194 a la IP de tu servidor y rezar para que tu IP pública no cambiara. Hoy esa operativa es poco familiar para mucha gente, sobre todo para quien llega por primera vez con una Raspberry Pi o un mini PC y quiere ver si el hobby encaja antes de invertir meses. Pero el problema va más allá de la falta de práctica. Hoy no basta con redirigir puertos. Una parte enorme de conexiones domésticas (fibra y móvil, en España y en buena parte de Europa) está detrás de CG-NAT (*Carrier-Grade NAT*): tu router no tiene una IPv4 pública propia; comparte dirección con otros clientes del operador. Desde fuera no hay forma fiable de alcanzar tu homelab aunque configures el port forward perfecto. La salida clásica es llamar a tu compañía y pedir que te saquen del CG-NAT, o que te asignen IPv4 pública. A veces funciona; a veces implica cambiar de tarifa, esperar, discutir con soporte de primer nivel que no sabe lo que es CG-NAT, o descubrir que tu operador simplemente no lo ofrece. Es una fricción desmedida para alguien que solo quiere consultar su NAS desde el tren. Tailscale (y en general un mesh que negocia salida por relé o por hole punching) evita ese callejón sin entrar. No sustituye la soberanía de tener IP pública; elimina el prerrequisito de tenerla para que el homelab sea útil. ### Continuidad del hobby frente a pureza arquitectónica Personalmente prefiero aceptar la ancla de coordinación de Tailscale a poner en riesgo la continuidad del homelab, sobre todo para recién llegados. Un homelab al que no puedes acceder desde cualquier sitio pierde la mitad de su utilidad: deja de ser tu nube personal y pasa a ser un aparato que solo existe cuando estás en el salón. La curva de abandono es predecible: frustración, meses sin tocarlo, y esa Raspberry Pi o mini PC acaba en un cajón, hardware sano, proyecto muerto. Eso no invalida WireGuard manual ni Headscale cuando tengas el contexto y la paciencia. Pero exigir port forwarding o salir del CG-NAT antes de que el hobby demuestre valor es pedir pureza arquitectónica a cambio de probabilidad real de fracaso. En el marco RPS, Tailscale es piedra operativa con ancla consciente; abandonar el homelab por no poder entrar es piedra mal entendida: ahorraste dependencia y perdiste el sistema entero. ### MagicDNS, HTTPS y una pila más corta La segunda gran ventaja aparece cuando empiezas a nombrar y servir cosas, no solo a conectarte. MagicDNS resuelve los nombres de tus máquinas dentro de la tailnet (`minipc.tail12345.ts.net`, por ejemplo) sin montar DynDNS, DuckDNS ni un cron que actualice tu IP pública cada vez que el router reinicia. Para acceso entre dispositivos de tu red privada mesh, ese problema desaparece. Y en cierto grado desaparece también la ceremonia de Certbot, Let's Encrypt y renovaciones a mano. Tailscale puede emitir certificados TLS válidos para los nombres de tu tailnet (con Let's Encrypt por debajo, pero gestionado por ellos) de forma que tus servicios responden en HTTPS sin que tú operes la PKI. Combinado con Tailscale Serve (servir HTTP/HTTPS desde el propio nodo), la fricción baja otro escalón. Eso abre casos que en un homelab clásico parecían overkill montar solo para probar. Por ejemplo, desplegar una PWA en un servidor de casa e instalarla en el navegador del móvil o del portátil sin pelear con certificados autofirmados. Los navegadores exigen un origen seguro para tratar una web como instalable: hace falta HTTPS y un dominio reconocible. No vale la IP de la LAN (`192.168.1.50`), ni HTTP plano, ni el aviso "tu conexión no es privada" que el usuario medio no sabe aceptar. Con un nombre `*.ts.net` y TLS gestionado, ese requisito se cumple dentro de la tailnet con poca fricción. El dominio y el certificado los presta Tailscale, no tú: comodidad operativa (otra cara de la ancla). El día que quieras un dominio propio en internet público, un reverse proxy con Caddy y Let's Encrypt propio seguirá teniendo sentido; de eso hablaré en un post posterior. Pero para servicios solo tuyos, alcanzables desde tus dispositivos, la combinación MagicDNS + HTTPS de tailnet evita una pila entera que durante años fue el ritual de iniciación del homelab. Para un homelab personal es piedra operativa: el problema de "quiero entrar desde el móvil" se resuelve en minutos. No montas PKI. No editas `wg0.conf` a mano por cada portátil. Pero el plano de coordinación (quién emite las claves, quién mantiene el mapa de la red) vive en servidores de Tailscale Inc., salvo que uses un plan que te permita self-host del control plane (no es el caso típico del homelab gratuito). Eso es ancla consciente: * Dependes de su servicio de coordinación y de sus políticas. * MagicDNS y los nombres `*.ts.net` son convenientes (y evitan DynDNS y, en la tailnet, buena parte del baile con Certbot), pero no son tu DNS soberano; son un atajo que funciona mientras aceptes el modelo. * La autenticación inicial del dispositivo suele pasar por un proveedor social o SSO externo, otra capa de delegación que conviene nombrar. No lo digo para demonizar Tailscale. Lo uso en mi propio homelab como capa de acceso antes de montar identidad de aplicación con Zitadel. Es la decisión correcta cuando el coste de no tener acceso remoto supera al coste de delegar coordinación. Pero es ancla: conviene saberlo. ## Headscale: tijeras con puente Headscale es una reimplementación abierta del servidor de coordinación de Tailscale. Misma idea (mesh sobre WireGuard), pero tú operas el plano de control. En RPS es tijeras: máximo potencial de soberanía dentro del modelo mesh (sin depender de los servidores de coordinación de Tailscale Inc.), a cambio de más complejidad y más riesgo operativo. Actualizas el servidor, depuras por qué un nodo no se registra, te responsabilizas de backups y disponibilidad. Es construir tu propia plataforma de coordinación manteniendo el protocolo y el cliente que ya conoces. En reversibilidad sigue siendo puente: el túnel sigue siendo WireGuard estándar; recuperas independencia del SaaS sin abandonar el modelo mental del mesh. Pero el precio en tiempo y atención es de tijeras, no de papel. Tiene sentido cuando la ancla de Tailscale deja de ser aceptable (por política, jurisdicción o principio) y ya validaste que el mesh te aporta valor. Saltar a Headscale el primer día, sin haber tenido acceso remoto estable, suele ser tijeras mal aplicadas: complejidad de plataforma propia antes de que el hobby demuestre para qué sirve. ## Comparación en una tabla | Solución | RPS típico | Reversibilidad | Perfil homelab | |----------|------------|----------------|----------------| | **OpenVPN** | Papel legacy | Puente si es tuyo | Compatibilidad, inercia, mucho mantenimiento | | **WireGuard** (manual) | Piedra | Puente fuerte | Control total, más fricción por dispositivo | | **Tailscale** | Piedra operativa | Ancla en coordinación | Acceso rápido, MagicDNS, poca ops | | **Headscale** | Tijeras | Puente | Mesh con plano de control propio; más ops, más soberanía | ## Puentes, anclas y exposición pública Abrir puertos en el router (443 de Plex, 8123 de Home Assistant, lo que sea) es exposición directa, no una VPN. Y en muchos hogares ni siquiera es viable sin salir antes del CG-NAT. A veces se combina con Cloudflare Tunnel u otros relays; el patrón es el mismo: tu servicio queda alcanzable desde internet sin que el visitante pase por tu red privada. Eso puede ser piedra legítima para un servicio concreto. Pero como estrategia por defecto para todo el homelab reproduce el cloud-first en miniatura: comodidad ahora, superficie de ataque y dependencia de terceros (DNS, CDN, certificados) después. Una VPN o mesh privado no sustituye al reverse proxy ni al IdP. Te da perímetro: primero entras a la red, luego resuelves nombres, luego autenticas aplicaciones. ## Cuándo subir de piedra a papel (o a tijeras) Quédate en WireGuard manual o Tailscale cuando: * Eres una persona o un hogar, pocos dispositivos, sin requisitos de compliance. * El problema es llegar a casa, no segmentar una empresa. Pasa a Headscale (tijeras) cuando: * La ancla de coordinación de Tailscale deja de ser aceptable y quieres el mesh sin el SaaS. * Ya tienes acceso remoto funcionando y el siguiente paso es soberanía del plano de control, no aprender VPN desde cero. * Aceptas operar, actualizar y depurar el servidor de coordinación tú mismo. Reserva otras tijeras (topologías híbridas serias, múltiples sitios, routing entre VLANs, exit nodes con política, integración con identidad corporativa) cuando el homelab deja de ser hobby y se convierte en infraestructura que otros dependen. ## Lo que viene después Este post nombra la capa de acceso. Los siguientes bajarán al resto de la pila que prometí en [cloud-first](/blog/004-cloud-first-problem/): DNS, reverse proxy e identidad. El acceso con Tailscale (o WireGuard) no resuelve *"¿cómo se llama este servicio?"* ni *"¿quién puede hacer login en Grafana?"*; solo responde *"¿estoy en la red donde vive Grafana?"*. La pregunta es quién coordina tu red, qué anclas aceptas y cuándo la piedra de un mesh gestionado es más honesta que las tijeras de un IdP que aún no necesitas. ---------------------------------------- ### [Qué es soberanía digital](https://escribano.dev/es/blog/005-digital-sovereignty/) > La soberanía digital son tres capas: datos (dónde viven y si puedes exportarlos), infraestructura (quién ejecuta tu software) y cognición (qué modelo de IA procesa tu información). Con la IA se añade un espectro de decisión: API cerrada, API de pesos abiertos o inferencia local. La clave está en el coste de abandonar cada tecnología, no solo en el de entrada. # Qué es soberanía digital Como adelanté en el [post sobre cloud-first](/blog/004-cloud-first-problem/), para mí la soberanía digital consiste en que podamos decidir sobre datos, infraestructura y modelos de IA, sin que un proveedor, una decisión pasada o la inercia del mercado lo hagan por nosotros. No pretendo escribir un manifiesto anti-nube. Me interesa algo mucho más práctico: entender qué partes de nuestro stack controlamos realmente, cuáles hemos delegado y, sobre todo, si esa delegación tiene marcha atrás. A menudo el debate sobre soberanía digital se centra en la jurisdicción de los datos, la regulación o la dependencia tecnológica entre países. Pero aquí quiero hablar de un aspecto más cercano al día a día de quienes desarrollamos software: la capacidad de revisar una decisión cuando las circunstancias cambian. Me gusta pensar la soberanía digital como tres capas complementarias. Dos de ellas, datos e infraestructura, las conocemos de sobra desde siempre: * **Datos.** Dónde se almacenan, quién puede acceder a ellos y si puedes exportarlos o eliminarlos sin depender de un tercero. * **Infraestructura.** Quién ejecuta el software, con qué herramientas y cuánto costaría migrarlo a otro lugar. Pero ahora, ante el auge de la IA generativa, hay una tercera capa: * **Cognición.** Qué modelos de IA procesan tu información y hasta qué punto puedes cambiar de modelo, de proveedor o incluso ejecutarlo tú mismo. Puedes tener un VPS propio, ejecutar software libre y administrar tus propios servicios. Pero si cada documento, cada conversación y cada fragmento de código termina procesándose exclusivamente por un modelo cerrado, tu soberanía sigue siendo parcial. En los artículos anteriores ([presentación](/blog/001-introduction/) y [cloud-first](/blog/004-cloud-first-problem/)) el software libre, los formatos abiertos y el self-hosting constituyen la base. Pero autoalojar un servicio no garantiza soberanía si, por ejemplo, tu servicio requiere una API de terceros que no controlas y que te costaría cambiar si mañana dejara de estar disponible en tu país o cambiara de precio. Como comentaba al hablar del [principio RPS](/blog/002-rps-principle/), las decisiones que preservan la capacidad de cambiar son **puentes**; las que dificultan abandonar una tecnología son **anclas**. El software libre y el self-hosting suelen construir puentes en la infraestructura. Los modelos de pesos abiertos pueden hacer exactamente lo mismo en la capa cognitiva. ## Soberanía cognitiva La llegada de la IA añade una nueva pregunta: ¿quién ejecuta realmente el modelo que procesa tu información? Los modelos cerrados como GPT, Claude o Gemini ofrecen capacidades muy potentes, pero el usuario depende completamente del proveedor: no es posible ejecutar esos mismos modelos por cuenta propia ni trasladarlos a otro operador independiente. Los modelos de pesos abiertos funcionan de otra manera. Si trabajas con familias como Qwen, Gemma, DeepSeek o Kimi, puedes utilizar la API del desarrollador original, recurrir a distintos proveedores de inferencia o, si tu hardware lo permite, ejecutar exactamente el mismo modelo en tu propia infraestructura. El modelo permanece; cambia quién lo ejecuta. Elegir un modelo de pesos abiertos mantiene abiertas más opciones para el futuro: hoy puedes consumir una API externa y mañana decidir mover esa misma carga a un servidor propio sin tener que rehacer toda tu integración. Un pequeño inciso: algunas licencias de modelos de pesos abiertos incluyen restricciones comerciales, pero suelen apuntar a uso a escala (un SaaS, una empresa con cientos de usuarios) más que al uso personal. Aun así, conviene revisar cada licencia: no todas siguen ese patrón. La soberanía cognitiva no exige montar un servidor con varias GPUs desde el primer día. Existe un espectro razonable de decisiones: 1. API de un modelo cerrado. 2. API de un modelo de pesos abiertos. 3. Inferencia local (modelo de pesos abiertos). El punto uno nos ancla a un modelo concreto que no controlamos. El dos mantiene el mismo modelo, pero permite cambiar de proveedor o llevarlo a casa. El tres ofrece el máximo control, a cambio de más coste operativo. Cuando evaluamos una tecnología solemos fijarnos en el coste de entrada: cuánto tardaremos en ponerla en marcha, cuánto cuesta al mes o qué curva de aprendizaje tiene. Sin embargo, pocas veces pensamos en el coste de abandonarla. Migrar una base de datos, sustituir un proveedor de correo o reemplazar un modelo de IA puede requerir semanas o meses de trabajo. Cuanto mayor sea ese coste, menor será nuestra capacidad real para decidir en el futuro. En cualquier caso, siempre podemos optar por soluciones híbridas, como por ejemplo usar un modelo local para la mayoría de las peticiones y reservar los modelos frontera para casos puntuales donde se necesite potencia extra. ## Mi experiencia con la inferencia local Mi experiencia en inferencia local es limitada, pero me ha servido para calibrar expectativas. Con **Gemma 4** (4B) y **SmolLM3** (3B) sobre CPU vía Ollama, los modelos pequeños son usables, aunque lentos; una GPU cambia por completo la experiencia. No hace falta hardware potente para empezar: sin GPU ya puedes experimentar y construir herramientas personales; con una de gama media muchos modelos abiertos bastan para desarrollo, redacción o automatización. Para tareas más exigentes (especialmente flujos agénticos) he optado por un punto intermedio del espectro: **modelos de pesos abiertos consumidos vía API**, concretamente **`glm-4.7-flash`** y **`deepseek-v4-flash`** a través de Ollama Cloud. Son modelos que, en principio, podría ejecutar en hardware propio tipo Mac Mini si tuviera el equipo. Hoy no lo tengo, así que uso la API: sigo delegando la inferencia a un tercero. El puente no es la soberanía del uso actual, sino la puerta de salida: si mañana tengo acceso a una máquina con suficiente memoria unificada (o con una GPU de 24 GB de VRAM), puedo descargar los mismos pesos y migrar la inferencia casi sin tocar la integración. Esa es la diferencia entre un puente y una ancla. ## Cuándo tiene sentido un modelo cerrado La soberanía digital no implica rechazar los modelos cerrados. Siguen siendo la mejor opción cuando ofrecen capacidades que todavía no existen en el ecosistema de pesos abiertos, cuando el coste de equivocarse es elevado o cuando se trata de un uso puntual. El problema no es utilizarlos, sino que se conviertan en la opción por defecto sin haber evaluado si una alternativa más abierta sería suficiente. La misma crítica que puede hacerse al *cloud-first* puede hacerse también al *LLM-first*: delegar por costumbre en lugar de hacerlo por necesidad. No se trata de autoalojarlo todo, ni de evitar cualquier servicio comercial, sino de conservar la capacidad de elegir. El software libre, el self-hosting y los modelos de pesos abiertos reducen el coste de cambiar; los modelos cerrados y la nube grande seguirán resolviendo muchos problemas mejor que una infraestructura doméstica. La cuestión no es prohibirse usarlos, sino evitar que la comodidad inicial termine convirtiéndose en una dependencia permanente. Si te ha gustado el post (o discrepas en algo), no dudes en comentar. ---------------------------------------- ### [Tales Vault System: aleatorio primero, memorable después](https://escribano.dev/es/blog/011-tales-vault-system/) > Tales Vault System (tale) invierte el flujo de las passphrases: primero letras aleatorias de una CSPRNG (20 caracteres, ~85 bits), después frases mnemotécnicas derivadas con plantilla fija por cada bloque de 4 letras. Solo minúsculas para teclear y dictar mejor, diccionarios en y es embebidos en un binario Go offline. Recuperas la contraseña concatenando las iniciales de cada frase. En el [post anterior](/blog/010-password-entropy-length/) vimos que la entropía solo cuenta cuando la aleatoriedad es real. El gestor de contraseñas resuelve el 99% de los casos. Pero para la clave maestra, el arranque del portátil o las pocas contraseñas que necesitas memorizar sin depender del portapapeles, la pregunta sigue en pie: ¿cómo generas entropía sin caer en el sesgo de elegir palabras que "suenan bien"? **[Tales Vault System](https://github.com/escribanoruben/tale)** (repositorio [`tale`](https://github.com/escribanoruben/tale)) invierte el flujo habitual. Primero la contraseña (CSPRNG). Después la historia mnemotécnica, derivada con reglas fijas. Tú no eliges las letras; en el modo interactivo solo eliges entre candidatas válidas si quieres personalizar la frase. ## El problema que no resuelve un gestor Un gestor guarda cien contraseñas. Pero alguien tiene que recordar la clave que abre el gestor. Y a veces necesitas teclear una contraseña en un entorno donde pegar desde el portapapeles no es opción. Las soluciones habituales fallan por el mismo motivo: | Enfoque | Flujo | Problema | |---------|-------|----------| | **"Mi frase favorita"** | Frase → contraseña | Patrones humanos, entropía baja | | **Leetspeak** | Palabra + sustituciones | Cracking moderno lo anticipa | | **xkcd sin dados** | Cuatro palabras que eliges | Distribución sesgada por letra inicial | | **Diceware** | Palabras aleatorias → frase | Funciona; ver [diceware.dmuth.org](https://diceware.dmuth.org/) | Diceware es sólido (y [diceware.dmuth.org](https://diceware.dmuth.org/) es una implementación excelente que incluso enlaza la viñeta de xkcd en su FAQ). Tales Vault apunta a otro equilibrio: secuencia corta de letras aleatorias (más bits por carácter tecleado) con frases gramaticales que ayudan a recuperarla. ## Cómo funciona ``` [CSPRNG] → contraseña (20 chars, alfabeto del idioma) ↓ bloques de 4: jnra | ngtb | ... ↓ sustantivo + adjetivo + verbo + sustantivo (por bloque) ↓ contraseña + frases + reglas de recuperación ``` Ejemplo real generado con `tale generate --language es`: ``` Password: jnrangtbtljnlbiqnhqm Mnemonic phrases: [jnra] jefe nuevo recordar año [ngtb] noche grande tener bebé [tljn] tiempo llamado jugar niño [lbiq] lugar bueno intentar queso [nhqm] nombre humano querer mujer Entropy: ~85.0 bits ``` Cada palabra aporta su inicial normalizada (`árbol` → `a`, `ñ` excluida del alfabeto español). Concatenas las iniciales de cada bloque y recuperas la contraseña. ## Principios de diseño | Principio | Detalle | |-----------|---------| | **Aleatoriedad primero** | La clave nace del generador criptográfico; la frase es derivada, nunca al revés | | **Alfabeto restringido** | Las 20 letras iniciales más frecuentes por idioma, maximiza cobertura del diccionario | | **Plantilla fija** | `sustantivo → adjetivo → verbo → sustantivo` por bloque de 4 letras | | **Sin elección de letras** | El usuario no inventa iniciales; el TUI solo permite elegir entre palabras válidas | | **Solo minúsculas** | Sin `Shift` ni alternancia de capas; más rápido en móvil y más claro al dictar | | **Local-first** | Listas embebidas en el binario (`go:embed`); sin red en tiempo de ejecución | | **Binario único** | ~3 MB con diccionarios EN + ES (~13k palabras curadas) | El alfabeto restringido reduce algo la entropía por carácter respecto a las 26 minúsculas, pero compensa con longitud: 20 caracteres sobre ~20 letras dan ~85 bits, suficiente para una clave maestra o pocas contraseñas críticas memorizadas. En español, la distribución desigual de palabras por letra inicial (que en el post anterior explicamos como sesgo humano) aquí se gestiona de forma explícita: el alfabeto se calcula a partir del corpus, y `tale stats` muestra la cobertura real por letra (`a`: 360 palabras, `q`: 48). ## Solo minúsculas Tales Vault genera contraseñas en minúsculas a propósito. Responde a los escenarios donde más duele teclear una clave que memorizas tú. En el [post anterior](/blog/010-password-entropy-length/) ya vimos el coste de `Shift`, símbolos y mayúsculas en teclado físico. Aquí la decisión es de producto: - **Un golpe menos por carácter.** Cada mayúscula exige `Shift` o cambio de capa. En veinte caracteres, evitar mayúsculas elimina hasta veinte pulsaciones extra y el riesgo de pulsar la tecla equivocada. - **Móvil.** En pantalla táctil, alternar mayúsculas y minúsculas ralentiza mucho la entrada. Una clave de solo minúsculas se teclea en la capa por defecto, sin saltar entre modos. - **Dictado.** Si transmites la contraseña en voz alta (copia de seguridad con alguien de confianza, repaso mientras la memorizas) no tienes que aclarar "ese es mayúscula" ni deletrear con ambigüedad. `jnrangtbtljnlbiqnhqm` se lee letra a letra, siempre igual. La entropía no se sacrifica en vano: el alfabeto tiene ~20 letras y la longitud por defecto es 20 caracteres (~85 bits). Más longitud, un solo caso, compensación deliberada frente a mezclar mayúsculas, números y símbolos que en la práctica nadie teclea bien. ## El comando `tale` El repositorio publica un binario Go llamado `tale`. Instalación mínima: ```bash git clone https://github.com/escribanoruben/tale cd tale make build # o: go build -o tale ./cmd/tale ``` ### Generación automática ```bash ./tale generate --language es ./tale generate --language en --length 16 ``` Modo aleatorio completo: contraseña, frases, entropía estimada y reglas de recuperación en stdout. Sin almacenar secretos en disco. ### Modo interactivo (TUI) ```bash ./tale tui --language es ./tale tui --language en --password abcdefghijklmnopqrst ``` Interfaz en terminal ([Bubble Tea](https://github.com/charmbracelet/bubbletea)) con selector tipo rodillo: por cada letra del bloque, eliges entre palabras cuya inicial coincide. Útil si quieres una frase más personal sin tocar la entropía de las letras, esas ya están fijadas. ### Estadísticas del diccionario ```bash ./tale stats ``` Muestra alfabeto, palabras por letra y parte del discurso. Herramienta de transparencia: puedes verificar que el idioma tiene cobertura antes de confiar en el generador. ### Idiomas | Idioma | Código | Alfabeto (20 letras) | |--------|--------|----------------------| | Inglés | `en` | `abcdefghilmnoprstuvw` | | Español | `es` | `abcdefghijlmnopqrstv` | También admite idiomas personalizados bajo `~/.tale/` (`tale language new`, `tale language init`). Los built-in cargan listas embebidas; los custom requieren rellenar los TSV correspondientes. ## Local-first y [soberanía digital](/blog/005-digital-sovereignty/) Las listas de palabras viajan dentro del binario. No hay llamadas a APIs, no hay "genera tu contraseña en la nube". En un contexto donde mandar secretos a terceros es inaceptable (o simplemente donde quieres auditar qué palabras puede proponer el sistema) eso importa. En el [marco RPS](/blog/002-rps-principle/), Tales Vault es piedra pragmática: un binario, cero ceremonia de despliegue, máxima transparencia sobre el diccionario. No pretende ser una plataforma de identidad ni un gestor completo. ## Límites honestos - No sustituye un gestor de contraseñas. Está pensado para pocas claves que memorizas tú. - **Entropía algo menor** que 20 minúsculas puras por el alfabeto recortado, documentada en la salida. - **Cobertura del diccionario.** Bloques sin candidatas válidas se regeneran; en español algunas letras tienen menos palabras que otras. - **Modo TUI y sesgo.** Si eliges siempre la palabra "más bonita" entre candidatas, introduces sesgo en la memorabilidad, no en las letras, pero la frase deja de ser la que el scoring automático habría elegido. - **Licencia del proyecto:** TBD en el repositorio; las fuentes de las listas tienen licencias propias (ver `internal/worddata/README.md`). ## Diceware y [diceware.dmuth.org](https://diceware.dmuth.org/) Antes de hablar de `tale`, conviene situar Diceware bien hecho. [diceware.dmuth.org](https://diceware.dmuth.org/), de Douglas Muth, es la referencia web que más gente conoce: tiras virtuales de dados, lista de palabras de la [EFF](https://www.eff.org/dice), passphrase generada en el navegador sin enviar nada al servidor. Cuatro palabras aleatorias ≈ 44 bits; seis, ≈ 66. Las palabras *son* la contraseña: las recuerdas y las tecleas tal cual. Casos donde Diceware brilla (el propio sitio los enumera): Smart TV, teclados incómodos, ordenador ajeno sin gestor instalado. Escribir `correct horse battery staple` es más lento que veinte letras seguidas, pero cada palabra es un chunk mental reconocible. Tales Vault System no compete con eso; resuelve otro perfil: | | [diceware.dmuth.org](https://diceware.dmuth.org/) | Tales Vault (`tale`) | |---|---------------------------------------------------|----------------------| | **Qué memorizas** | Las palabras aleatorias | Frases mnemotécnicas; la clave son las iniciales | | **Qué tecleas** | Palabras completas con espacios (o sin ellos) | Solo minúsculas, sin `Shift` ni alternar capas | | **Flujo** | Palabras aleatorias → passphrase | Letras aleatorias (CSPRNG) → frase derivada | | **Idiomas** | Lista EFF en inglés | Inglés y español embebidos; alfabeto por idioma | | **Entorno** | Web (JS local) o fuente en GitHub | Binario Go offline (`go:embed`) | | **Entropía típica** | ~44 bits (4 palabras) / ~66 (6) | ~85 bits (20 letras, alfabeto ~20) | Si quieres passphrase de palabras enteras y te encaja el ritmo de Diceware, usa Diceware (de verdad). Si necesitas más entropía por tecla, solo minúsculas, recuperación por frases con plantilla gramatical y soporte ES nativo, ahí está `tale`. ## Distinción frente a otros enfoques | Sistema | Quién elige | Entropía | |---------|-------------|----------| | **Tales Vault** | CSPRNG → frase derivada | Alta (~85 bits, letras uniformes) | | **Diceware** ([dmuth.org](https://diceware.dmuth.org/)) | Dados/PRNG → palabras | Alta (~44–66 bits según longitud) | | **Frase memorable** | Tú → iniciales | Baja (sesgada) | | **Gestor** | CSPRNG → almacenado | Alta (no memorizas) | ## Qué viene En el repositorio hay una [discusión de diseño abierta](https://github.com/escribanoruben/tale/issues/1) sobre añadir dígitos de control derivados al final de la contraseña, un checksum para detectar errores al recuperar la clave desde las frases (bloque mal ordenado, palabra mal recordada). El código, las specs OpenSpec y las listas de palabras están en [github.com/escribanoruben/tale](https://github.com/escribanoruben/tale). Si pruebas `tale` y encuentras un bloque sin frase válida en tu idioma, abre un issue, las contribuciones al diccionario son bienvenidas. ## Cierre Aleatorio y memorable no son opuestos si el orden es el correcto: primero la entropía, después la historia. El cómic de xkcd lo intuyó; Diceware lo demostró con dados. Tales Vault lo lleva a un flujo reproducible, auditable y local, con un comando que puedes compilar tú mismo. Para el resto de tus contraseñas, sigue usando el gestor. Para las pocas que necesitas llevar en la cabeza, que al menos las letras no las elija tu intuición. ---------------------------------------- ### [Contraseñas: por qué la longitud gana a la complejidad](https://escribano.dev/es/blog/010-password-entropy-length/) > La entropía de una contraseña depende de la aleatoriedad del proceso, no del alfabeto. Las reglas NIST (2003) entrenaron patrones predecibles y el cómic de xkcd solo funciona con dados: cuatro palabras elegidas por ti pierden gran parte de sus 44 bits por sesgos de frecuencia, letra inicial y semántica. La longitud con letras aleatorias gana; los crackers explotan lo que los humanos eligen. Soy fan de [xkcd](https://xkcd.com/), y una de sus viñetas más citadas es [la del panel de contraseñas (#936)](https://xkcd.com/936/): `Tr0ub4dor&3` frente a `correct horse battery staple`. Cambió cómo millones de personas piensan las claves. El mensaje parece claro: olvida la contraseña "compleja", usa cuatro palabras comunes y ganas en seguridad y en memoria. El detalle que casi nadie lee es el adjetivo: **random**. Cuatro palabras elegidas por ti no son cuatro palabras aleatorias. Me interesa el marco: por qué las reglas de complejidad heredadas fallan, por qué el cómic solo funciona bajo supuestos concretos, y qué implica eso cuando el idioma sesga las letras iniciales de las palabras que memorizas. ## El cómic tiene razón… con dados El panel inferior de xkcd asigna 11 bits por palabra a `correct horse battery staple`. Eso asume selección uniforme de un diccionario de unas 2.048 palabras comunes ($2^{11}$). Cuatro palabras independientes y aleatorias suman ~44 bits, unos 550 años a 1.000 intentos por segundo en el escenario del cómic. Ese modelo es **Diceware**: tirar dados, mirar una lista, repetir. Implementaciones como [diceware.dmuth.org](https://diceware.dmuth.org/) lo llevan al navegador con `crypto.getRandomValues()` (la passphrase se genera en local y no sale de tu máquina). La passphrase es memorable *después* de generarse al azar, no *porque* tú la inventaste. Si realmente eliges cuatro palabras al azar de una lista grande, el cómic es correcto. El problema es que casi nadie hace eso. ## El cómic falla… sin dados Cuando tú eliges las palabras para recordarlas, dejas de tener 11 bits por palabra. Tienes algo mucho peor: | Sesgo | Qué pasa | |-------|----------| | **Frecuencia léxica** | Eliges `casa`, `perro`, `amor` antes que `yunque` o `yegua` | | **Letra inicial** | En español abundan palabras que empiezan por `a`, `c`, `p`, `s`; escasean las de `q`, `j`, `w` | | **Semántica** | Frases que "cuentan una historia" reducen el espacio de búsqueda | | **Personal** | Nombres, fechas, canciones, mascotas, el atacante también los prueba primero | La fórmula simplificada $H = L \cdot \log_{2}(N)$ solo vale cuando cada símbolo tiene probabilidad uniforme $1/N$. Si tú eliges las palabras, $N$ efectivo por posición es mucho menor, y no es el mismo para cada letra. Imagina derivar una contraseña de las iniciales de veinte palabras que memorizaste: - Con alfabeto uniforme de 26 letras: $20 \cdot \log_{2}(26) \approx 94$ bits nominales. - Si tus palabras empiezan sobre todo por `a`, `c`, `m`, `p`, `s`, la entropía real por posición se acerca más a $\log_{2}(5\text{–}8)$ que a $\log_{2}(26)$. Un atacante con diccionario enriquecido no prueba todas las combinaciones con la misma prioridad. Prueba primero lo que los humanos eligen. ## Entropía en cinco minutos La entropía de Shannon mide cuánta incertidumbre hay en una elección:
H = −∑i pi log2 pi
Si cada símbolo es equiprobable entre $N$ opciones, se simplifica a $H = \log_{2}(N)$. Para una secuencia de longitud $L$ con símbolos independientes:
Htotal = L · log2 N
| Contraseña | Cálculo | Entropía aprox. | |------------|---------|-----------------| | 20 minúsculas aleatorias | $20 \cdot \log_{2}(26)$ | ~94 bits | | 16 minúsculas aleatorias | $16 \cdot \log_{2}(26)$ | ~75 bits | | 12 chars "complejos" (~94 símbolos) | $12 \cdot \log_{2}(94)$ | ~79 bits | | 4 palabras "al azar" (Diceware real) | $4 \times 11$ | ~44 bits | | 4 palabras que tú eliges | distribución humana | imposible de cuantificar sin modelo; a menudo < 30 bits | | 8 chars cumpliendo mínimo (`Verano2024!`) | patrón humano | ~30–40 bits | La longitud gana al alfabeto "rico" cuando la aleatoriedad es real. Pero sin aleatoriedad, ni la longitud ni el alfabeto salvan la situación. ## Dos caras del mismo error | Enfoque | Síntoma | Ilusión | |---------|---------|---------| | **Complejidad NIST** | `Verano2024!`, `Password1!` | "Cumple mayúscula, número y símbolo" | | **xkcd mal aplicado** | `casa perro amor luna` | "Cuatro palabras, como el cómic" | | **Leetspeak** | `P@ssw0rd`, `Tr0ub4dor` | "Los símbolos confunden al atacante" | En los tres casos la entropía nominal parece razonable. La efectiva es baja porque el proceso de generación es humano, predecible y explotable con reglas de sustitución. ## De dónde salieron las reglas Las políticas de "al menos una mayúscula, un número y un símbolo" tienen origen documentado en [NIST SP 800-63](https://pages.nist.gov/800-63-3/) (2003), asociadas a Bill Burr. La idea era aumentar el espacio de búsqueda. En la práctica, entrenó a generar patrones reconocibles. En 2017, Burr reconoció públicamente que esas directrices habían empeorado la seguridad efectiva. La actualización [SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) recomienda longitud, listas de contraseñas filtradas, gestores y 2FA, no rotación periódica obligatoria ni complejidad ritual. El cierre de xkcd resume el mismo diagnóstico desde otro ángulo: veinte años enseñando contraseñas difíciles de recordar y fáciles de adivinar. ## El coste de teclear complejidad Más allá de la entropía, hay usabilidad. En un teclado QWERTY español, `Shift` y `Alt Gr` para símbolos aumentan errores de entrada, reintentos y la tentación de reutilizar la misma contraseña "difícil" en todos los sitios. Cada mayúscula es un golpe de teclado adicional. En móvil, peor: hay que alternar capas o pulsar prolongado para pasar de minúsculas a mayúsculas, y una contraseña que mezcla ambas se vuelve lenta e incómoda de teclear con el pulgar. Si la clave es algo que introduces a menudo (arranque del portátil, desbloqueo del gestor) ese coste se nota cada día. También importa la transmisión oral. Dictar `Tr0ub4dor&3` obliga a aclarar "ese es mayúscula", "cero, no letra o", "arroba, no a". Una secuencia de solo minúsculas se lee de corrido: "jota, ene, erre, a…" sin ambigüedad. Útil si alguien de confianza te ayuda a recuperar una copia de seguridad o si la apuntas en voz alta mientras la memorizas. Una secuencia larga de minúsculas aleatorias (generada por un gestor) es más rápida de teclear, menos propensa a errores y más clara al dictar que una contraseña corta con reglas de complejidad. ## IAG y cracking moderno Los ataques actuales no prueban solo `password` y `123456`. Usan reglas de sustitución (`a→@`, `e→3`, `s→$`), diccionarios enriquecidos con nombres propios y modelos que aprenden patrones humanos (PassGAN y similares). Las contraseñas "disfrazadas" y las frases "creativas" son el primer candidato, no el último. La IA no rompe la criptografía; explota que los humanos no son fuentes de aleatoriedad. ## Matizaciones obligatorias - Diceware y passphrases aleatorias reales sí funcionan. El problema no son las palabras; es la elección no aleatoria. - **Entropía no es todo.** Credential stuffing, phishing, filtraciones y reutilización pueden comprometer una clave fuerte sin adivinarla. - **Límites de los sitios.** Algunos capan longitud o rechazan ciertos símbolos, otro argumento a favor de secuencias largas de letras simples. ## Qué hacer en la práctica 1. **Gestor de contraseñas** para casi todo. Una clave maestra fuerte; el gestor genera el resto. 2. **Generación CSPRNG**: nunca improvisar. 3. **Longitud adecuada**: ~18–20 caracteres aleatorios dan ~75–94 bits con minúsculas; suficiente para la mayoría de contextos. 4. **2FA** donde importe, TOTP, llave hardware, WebAuthn. 5. **Diceware** si quieres palabras y tienes dados (o un generador fiable como [diceware.dmuth.org](https://diceware.dmuth.org/)). En el [marco RPS](/blog/002-rps-principle/), esto es piedra consciente para el día a día: poco ritual, máxima reversibilidad hacia un gestor, coste del fallo acotado si la clave maestra es fuerte. ## Cierre: aleatorio primero Las reglas de complejidad y el cómic de xkcd apuntan en direcciones opuestas, pero comparten un requisito invisible: la aleatoriedad tiene que ser real. Sin ella, las matemáticas del cómic mienten y las reglas de NIST empeoran las cosas. La pregunta que queda es práctica: si necesitas memorizar pocas claves críticas (la maestra del gestor, el arranque del portátil) sin elegir tú las palabras ni las letras, ¿cómo lo haces sin caer en el sesgo que acabamos de describir? En el [siguiente post](/blog/011-tales-vault-system/) presento **[`tale`](https://github.com/escribanoruben/tale)** (Tales Vault System): entropía primero, historia después. ---------------------------------------- ### [El problema del cloud-first: cuando el miedo disfraza de prudencia](https://escribano.dev/es/blog/004-cloud-first-problem/) > El cloud nació para gestionar incertidumbre de demanda, pero se convirtió en default por inercia y miedo a simplificar. Para cargas estables e internas, un VPS de 20-40 €/mes suele bastar; la elasticidad compensa solo con demanda impredecible o picos reales. La pregunta a recuperar: ¿cuánta incertidumbre de demanda tengo? Cuando Amazon lanzó EC2 en 2006, el discurso no era "mete todo en la nube". Era otro: ¿cuánta capacidad necesitas si no lo sabes? El pitch original del cloud (pay-as-you-go, instancias en minutos, escalar arriba y abajo según la demanda) respondía a un problema concreto. Evitar comprar hardware por adelantado. No quedarte corto si un enlace en Digg o Slashdot te multiplica el tráfico de la noche a la mañana. Convertir costes fijos en costes variables cuando la incertidumbre era alta. Ese encaje era real. Para una startup sin usuarios, para un producto que podía no despegar o explotar sin aviso, para un comercio que necesita absorber el pico de Black Friday sin mantener el doble de servidores el resto del año, la elasticidad tenía sentido. El cloud no era un lujo: era gestión del riesgo de demanda. La promesa era real. Con el tiempo se convirtió en default (*cloud-first*) incluso cuando el riesgo de demanda ya no justificaba la apuesta. ## Cloud-first no es cloud cuando toca **Cloud-first** es una política organizacional: "empezamos en el hyperscaler y ya veremos". A veces es deliberada y razonada. Otras veces sustituye al análisis. La distinción importa. Elegir cloud después de evaluar carga, coste, operación y reversibilidad es una decisión técnica. Elegir cloud porque no se contempló otra cosa es inercia con PowerPoint. En este post, cuando digo **hyperscaler**, me refiero a los proveedores cloud de gran escala que concentran la mayor parte del mercado: AWS, Azure y GCP. No critico a esos proveedores en sí, ofrecen servicios potentes y, en muchos contextos, la opción correcta. Critico el uso automático de su plataforma cuando el perfil de carga no lo pide. ## "Nunca han despedido a nadie por contratarnos" Existe una frase atribuida a IBM en los años ochenta: *"Nunca han despedido a nadie por contratarnos"* (*Nobody ever got fired for buying IBM*). Resume una asimetría que sigue vigente: la decisión segura para la carrera no siempre coincide con la decisión adecuada para el problema. La frase nació en otro contexto, pero encaja de sobra en el de los hyperscalers. *"Nadie ha despedido a nadie por contratar AWS"* (o Azure, o GCP) suena a broma en una reunión técnica, pero describe un incentivo real. El coste de elegir cloud de más se reparte: factura difusa, complejidad acumulada, dependencia que tarda meses en hacerse visible. El coste de elegir "demasiado simple" (un VPS, un servidor dedicado, un despliegue con Docker Compose) puede ser personal e inmediato: te tachan de chapucero, de no escalar, de no ser "serio". Así, la elección de tecnología deja de ser técnica y pasa a ser filosófica por miedo. No se elige cloud porque el problema lo exige; se elige porque simplificar da miedo. ## De la incertidumbre al todo-en-cloud Con los años, el relato original se diluyó. El cloud dejó de ser la herramienta para la incertidumbre máxima y se convirtió en el sitio por defecto para casi todo: * Aplicaciones internas con usuarios acotados y crecimiento predecible. * Herramientas de backoffice con carga estable mes a mes. * MVPs que podrían vivir en un solo servidor durante meses. * Sistemas donde el mayor riesgo no es el tráfico, sino el presupuesto y la operación. La inercia tiene varias fuentes. Tutoriales y certificaciones que asumen cloud. Plantillas de arquitectura importadas de big tech. Consultores cuyo modelo de negocio depende de la complejidad. Y un mercado laboral que premia años de experiencia en AWS en el CV aunque el producto tenga cien usuarios. Ninguna de esas fuerzas responde a la pregunta central: ¿cuánta incertidumbre de demanda tengo realmente? ## Cuándo el cloud sigue teniendo sentido Ser justos: hay contextos donde hyperscaler es la respuesta sensata. * **Demanda impredecible**: no sabes si tendrás diez usuarios o diez millones. * **Picos puntuales**: campañas, eventos, Black Friday; pagar capacidad extra solo cuando hace falta. * **Servicios gestionados que no quieres operar**: bases de datos complejas, colas, CDN global, compliance que el proveedor absorbe mejor que tu equipo. * **Equipos que prefieren OpEx y velocidad de provisión** a operar máquinas propias, y el contexto lo permite. En el [post sobre el principio RPS](/blog/002-rps-principle/) cloud encaja en **papel**: pagar trabajo y complejidad inicial a cambio de control del riesgo. Cuando el riesgo es real (el fallo es caro, la escala es impredecible, el sistema vivirá años) ese papel es auténtico. El fallo está en usar papel por miedo cuando el coste real del fallo era bajo y bastaba **piedra**: un VPS, un despliegue simple, suficiente para el contexto. ## IaaS, serverless y "escala a cero" Conviene no mezclar generaciones. El cloud clásico (IaaS: EC2, instancias, clústeres) ofrecía elasticidad con instancias encendidas: escalabas rápido, pero seguías pagando por capacidad reservada aunque la usaras poco. La **escala a cero** (pagar solo cuando hay ejecuciones) llegó más tarde con el modelo **serverless** (funciones, contenedores bajo demanda). Ahí el argumento de "puede que nadie lo use" cobra aún más fuerza: no mantienes infraestructura inactiva. Si tu carga es esporádica o muy irregular, serverless puede ser la herramienta correcta. Si tu carga es estable y predecible, seguir en IaaS cloud por inercia pierde el beneficio original: sigues pagando elasticidad que no necesitas. ## Cuando un VPS es la respuesta ideal Piensa en una aplicación interna: gestión de inventario, portal de RR. HH., herramienta de reporting. 150 usuarios hoy; previsión de 300 en dos años, crecimiento lineal, sin picos estacionales. Tráfico HTTP modesto, una base de datos relacional, un worker ocasional. Un perfil así no necesita absorber un viral de Twitter. No tiene Black Friday. La pregunta "¿y si mañana tengo un millón de peticiones?" es teórica, no operativa. Montar eso en un hyperscaler (contenedores gestionados, balanceador, base de datos administrada, monitorización nativa, varios servicios acoplados) puede costar varios cientos de euros al mes sin incluir el tiempo de quien mantiene la plantilla cloud. El mismo stack en un VPS de 20–40 €/mes con backups, reverse proxy y despliegue reproducible suele bastar. Es capacidad fija para demanda fija. ¿Por qué entonces acaba en AWS? Rara vez por necesidad técnica. Porque "así lo hace todo el mundo", porque el arquitecto anterior lo dejó ahí, porque nadie quiere defender un VPS en una slide, porque *"escalar es gratis en la nube"*, hasta que miras la factura. En el marco RPS, eso es piedra bien entendida: pragmatismo suficiente cuando la reversibilidad es alta, el fallo es acotado y la incertidumbre es baja. Es ajustar la herramienta al riesgo real, no una chapuza. ## Y entonces, ¿qué serían tijeras? Hasta aquí el post puede leerse como un binomio VPS versus cloud. Pero en el [post RPS](/blog/002-rps-principle/) bare metal encaja en **tijeras**: máximo potencial a largo plazo, más control, más complejidad, más riesgo operativo, elegido a propósito cuando el contexto lo justifica. En el eje cloud-first, tijeras entra cuando ni piedra ni papel encajan del todo. No necesitas la elasticidad de un hyperscaler, pero tampoco te basta un VPS suelto: la carga es grande o crítica, quieres independencia del proveedor cloud, y aceptas pagar operación a cambio de una plataforma propia. Casos típicos: * **Hardware propio autogestionado**: servidores en colo, dedicados o un rack en casa cuando la escala y el control lo compensan. * **Orquestación sobre infra propia**: Kubernetes, Nomad u otra plataforma sobre metal o VPS que tú operas, no EKS/GKE/AKS: escalas u homogeneizas servicios sin delegar en el hyperscaler. Kubernetes es una herramienta, no tijeras por definición. K8s en AWS sigue siendo papel, cloud con más YAML. K8s en tres nodos para la app interna de 150 usuarios suele ser falsas tijeras: complejidad de tijeras sin el beneficio. Las tijeras auténticas consisten en construir tu plataforma sabiendo que asumes su operación, no montarla porque "es lo moderno". Tijeras tampoco es el siguiente paso natural tras descubrir que un VPS bastaba: es otra apuesta, la del equipo que ya superó el monolito simple y necesita escalar u orquestar sin anclarse al cloud, o la de quien prioriza soberanía y coste a escala sobre comodidad gestionada. ## Miedo a simplificar Detrás del cloud-first hay un miedo poco dicho: simplificar parece irresponsable. Un `docker-compose.yml` en un VPS suena menos "enterprise" que un diagrama con diecisiete cajas en tres regiones. Pero la complejidad no es profesionalidad; a veces es ruido que imita seguridad. Más piezas móviles, más permisos IAM, más superficie de fallo, y la misma aplicación de 150 usuarios. Simplificar exige defender una decisión ante dirección, ante auditores, ante compañeros acostumbrados a asociar cloud con madurez. Es más cómodo heredar el default. Por eso la frase de IBM sigue funcionando: externaliza la culpa hacia una marca que nadie cuestiona. ## Puentes, anclas e inercia En el marco RPS, los **puentes** son decisiones que mantienen opcionalidad; las **anclas**, vendor lock-in. El cloud-first acelerado suele crear anclas: APIs propietarias, servicios que no existen fuera del proveedor, datos que migrar cuesta meses. Eso no invalida el cloud cuando lo necesitas. Pero cuando elegiste cloud sin necesidad de elasticidad, heredaste anclas sin haber comprado el beneficio. Un VPS con software libre y contenedores estándar suele ser un puente: mañana puedes moverlo, escalarlo verticalmente o, si la demanda cambia de verdad, subir al cloud con criterio. La inercia cloud-first también choca con otro eje de este blog: la [soberanía digital](/blog/005-digital-sovereignty/), la capacidad de decidir dónde viven tus datos, quién opera tu infraestructura y qué modelos procesan tu información. Acumular dependencia de un hyperscaler por default, sin evaluar alternativas, dificulta esa autonomía aunque hoy no te importe. ## Elegir con conciencia No escribo esto para decir que el cloud es malo ni que todo debería vivir en un VPS. Escribo para recuperar la pregunta original que el cloud respondía bien: *¿cuánta incertidumbre de demanda tengo?* Si la respuesta es "muchísima", la elasticidad es prudencia. Si la respuesta es "poca, predecible, interna", seguir en cloud-first es miedo disfrazado de prudencia: la misma lógica de "nadie ha despedido a nadie por contratar AWS", aplicada a un problema que nunca necesitó esa escala. Importa no esconderse detrás del hyperscaler cuando piedra era la decisión honesta, usar papel cuando el riesgo es real, y reservar tijeras para cuando la ambición de plataforma propia esté justificada, no confundida con moda ni con miedo a lo simple. En el [post de presentación](/blog/001-introduction/) prometí hablar de autoalojamiento y de decisiones informadas frente al *cloud-first* imperante. Este post nombra el problema. Los siguientes bajarán a herramientas concretas (DNS, reverse proxy, identidad) con el mismo criterio: no recetas por recetas, sino saber qué estás pagando cuando eliges. ---------------------------------------- ### [La sensación de tocar algo frágil: snowflake, IaC y el principio RPS](https://escribano.dev/es/blog/003-snowflake-iac-rps/) > La infraestructura snowflake (consola de AWS, wizards de Firebase, VPS configurado a mano por SSH) es frágil porque solo existe en una cabeza. El marco RPS distingue falsa piedra (clickops), falso papel (IaC mal adoptada), papel auténtico (IaC con disciplina) y tijeras (IaC ambiciosa a propósito). La prueba: ¿podrías levantarlo de cero mañana? Cada vez que tengo que tocar directamente la consola de AWS, Firebase o cualquier panel de un hyperscaler, me invade la misma sensación: estoy manipulando algo frágil. Algo que dentro de unos meses no recordaré cómo monté. Algo que, aunque documente con capturas de pantalla, podría no poder reproducir si la interfaz cambia. Esa sensación avisa de que estoy creando infraestructura snowflake: un sistema único, irrepetible, que depende de mi memoria y de circunstancias concretas que no volverán a darse. En el [post sobre el principio RPS](/blog/002-rps-principle/) se definen piedra, papel y tijeras como tres formas de tomar decisiones técnicas. Este post aplica ese marco a la infraestructura: snowflake como falsa piedra, IaC mal adoptada como falso papel, IaC suficiente como papel auténtico, y, en un extremo aparte, IaC ambiciosa como tijeras. ## Qué es un snowflake En ops, un snowflake (copo de nieve) es un servidor, un entorno o una configuración tan particular que no puede reproducirse de forma fiable. No hay dos iguales. Cada uno es único, como un copo de nieve. Suele asociarse a la consola de AWS: años de *clickops*, recursos creados a mano, reglas de seguridad que "alguien añadió en su día" y nadie se atreve a tocar. Pero el problema es más amplio. ## No es solo la consola de AWS Cualquier hyperscaler con interfaz gráfica puede generar snowflakes: * **AWS Console**: el caso más famoso. * **Firebase / Google Cloud Console**: wizards que dejan una estructura difícil de replicar paso a paso. * **Azure Portal**, **Oracle Cloud** y el resto, mismo patrón: clics sin fuente de verdad versionada. Si montaste un proyecto en Firebase usando la interfaz gráfica y mañana tienes que levantarlo de cero, ¿podrías reproducir exactamente la misma estructura? ¿Con los mismos permisos, las mismas reglas, los mismos servicios auxiliares? La duda ya es síntoma. ## Tampoco hace falta interfaz gráfica El snowflake no requiere wizards ni paneles web. Basta con SSH a un VPS e ir instalando cosas a golpe de `apt` o `dnf`, ajustando firewalls, editando ficheros de configuración, reiniciando servicios… hasta que el sistema funciona y nadie sabe ya qué pieza sostiene qué. Me ha pasado en máquinas virtuales: tantos retoques manuales relacionados con el firewall, con permisos o con servicios auxiliares, que ante un problema acabo tirando de backup en lugar de intentar reproducir el sistema. Porque no confío en poder levantarlo de nuevo a mano, ni yo ni nadie del equipo. En esos momentos he lamentado no haber usado herramientas de IaC desde el principio. ## Falsa piedra En el principio RPS, piedra es pragmatismo consciente: resolver rápido, asumiendo que el coste de equivocarse un poco es bajo y que la decisión es reversible. La falsa piedra parece piedra, pero no lo es. Es hacer las cosas de una forma concreta porque *"así se ha hecho siempre"* o *"así lo hace todo el mundo"*. Es el exceso de confianza de pensar que somos más listos de lo que realmente somos: que recordaremos perfectamente ese setup en la consola de AWS, que las capturas de pantalla bastarán, que *"ya lo documentaré"*. La falsa piedra acumula deuda técnica operativa: no la deuda del código mal diseñado, sino la deuda de un sistema que solo existe en una versión concreta, en una cabeza concreta, en un momento concreto. Confundimos velocidad aparente con pragmatismo real. La piedra auténtica asume limitaciones; la falsa piedra las ignora. ## Un modo de operar, no un tipo de hosting En el marco RPS, VPS, cloud y bare metal pueden encajar en piedra, papel y tijeras. Pero el snowflake no añade un cuarto tipo de infraestructura: es una forma de gestionarla: * Un VPS puede ser piedra limpia (script reproducible, imagen base estándar) o un snowflake (años sin reiniciar, configuración artesanal sin documentar). * Un cloud puede ser papel (IaC, políticas, auditoría) o un snowflake (consola, clics, excepciones "temporales" que nunca se eliminan). El fallo está en cómo lo configuras, no en elegir VPS o cloud: sin fuente de verdad, sin idempotencia, sin asumir que los desastres suceden. ## Dónde importa y dónde no No todo snowflake es un crimen. Hay contextos donde tiene sentido: * **Prototipos desechables** con vida corta y presupuesto de destrucción. * **Homelabs** pensados para experimentar y romper cosas, ahí la fragilidad puede ser parte del aprendizaje. Pero hay contextos donde duele: * Cuando hay dinero en juego: costes de downtime, SLA, clientes. * Cuando el sistema no se puede permitir depender de una persona: conocimiento tribal, vacaciones, rotación de equipo. * Cuando un desastre exige levantar todo desde cero y no hay un manual para dummies ni automatización que lo haga sin exigirte el 100% de tu atención. La pregunta útil es si puedo permitírmelo en este contexto, no un sí o no al snowflake. ## IaC en el principio RPS La IaC (*Infrastructure as Code*) describe infraestructura en ficheros versionados (Terraform, OpenTofu, Pulumi, Ansible) en lugar de depender de clics o de memoria. Es el camino natural para salir del snowflake. Pero en RPS no es automáticamente "la respuesta correcta". ### Falso papel: IaC mal adoptada La trampa simétrica a la falsa piedra es el falso papel: parece ingeniería seria, pero no lo es. Adoptas Terraform u OpenTofu porque *"es lo profesional"*, pero sin estado remoto, sin flujo de revisión, sin entender el modelo declarativo. El `apply` lo hace una persona en local; los módulos los copió alguien hace meses y nadie los entiende. Crees que has dejado atrás el snowflake. En realidad has cambiado un snowflake manual por un snowflake con ficheros `.tf` encima: otra fragilidad, con más complejidad de por medio. ### Papel: IaC suficiente Cuando IaC se hace con disciplina (versionado, revisión, estado compartido, entornos reproducibles) se convierte en papel auténtico: pagas trabajo inicial a cambio de estabilidad, previsibilidad y control del riesgo. Tienes un manual para dummies, o mejor aún, un pipeline que levanta el entorno sin que tengas que recordar cada paso. Aceptas tus limitaciones humanas: no vas a acordarte de todo dentro de seis meses, y no pasa nada, porque la fuente de verdad está en el repositorio. ### Tijeras: IaC ambiciosa Hay un tercer escenario, distinto del falso papel y del papel suficiente: IaC ambiciosa a propósito. Multi-entorno, módulos compartidos, policy-as-code, plataformas internas, bare metal autogestionado. Es tijeras en el sentido RPS: máximo potencial a largo plazo, más complejidad, más riesgo, pero elegido conscientemente cuando el contexto (escala, equipo, compliance) lo justifica. No es el siguiente paso natural tras escapar del snowflake; es una apuesta distinta. ## Herramientas (mención, no tutorial) No voy a entrar en detalle ahora (habrá posts para eso) pero sí quiero nombrar el ecosistema: * **[OpenTofu](https://opentofu.org/)**: fork de código abierto de Terraform, tras el cambio de licencia de HashiCorp. Mi recomendación como base declarativa. * **Terraform**: el estándar de facto; conviene conocerlo, pero hoy miraría primero OpenTofu para proyectos nuevos. * **[Pulumi](https://www.pulumi.com/)**: permite describir infraestructura con lenguajes imperativos (TypeScript, Python, Go…). Interesante si vienes de scripts manuales y te cuesta el HCL declarativo. * **[Ansible](https://www.ansible.com/)**: orientado a configuración de sistemas ya existentes; complementa bien a OpenTofu/Pulumi (provisión + configuración). Todas son puentes en el sentido RPS: reducen el coste de recuperarte después. Ninguna es bala de plata; todas exigen disciplina (versionado, revisión, pruebas). ## Elegir con conciencia No escribo esto para demonizar la consola ni para decir que todo debe ser IaC desde el día uno. Escribo para nombrar la sensación (esa fragilidad al tocar algo que no controlas del todo) y para situarla en un marco que ayude a decidir. A veces la falsa piedra basta: un MVP, un entorno personal, una prueba que mañana tirarás. A veces no: producción, datos de clientes, sistemas que deben sobrevivir a tu memoria. Importa saber cuándo lo estás creando, qué sacrificas y si el contexto lo permite. Y aceptar que, cuando no lo permita, necesitas un manual para dummies, o la automatización que lo haga innecesario. ---------------------------------------- ### [Piedra, papel y tijeras: un marco para decisiones técnicas](https://escribano.dev/es/blog/002-rps-principle/) > Toda decisión técnica cae en uno de tres arquetipos: piedra (pragmatismo suficiente), papel (control del riesgo pagando precio inicial) o tijeras (máximo potencial asumiendo riesgo). Los problemas llegan al mezclar estrategias sin saberlo. Los puentes (software libre, estándares abiertos) mantienen opcionalidad; las anclas (vendor lock-in) la bloquean. La mayoría de las discusiones técnicas no fallan por falta de conocimiento. Fallan porque las partes implicadas están jugando a estrategias distintas sin darse cuenta. Uno defiende “lo simple que funciona”. Otro exige “algo sólido y mantenible”. Un tercero quiere “hacerlo bien de una vez”. La discusión se vuelve moral, dogmática o personal. Aparecen palabras como *best practices*, *overengineering*, *chapuza*, *futuro*, *profesional*. Y nadie se entiende. El problema es estratégico, no de falta de conocimiento técnico. Hace falta un marco mental para pensar antes de decidir. ## No existe la estrategia óptima universal En ingeniería (software, sistemas, tooling, arquitectura) no existe una solución que gane siempre. Existen **trade-offs**: riesgo, coste, recompensa, reversibilidad, complejidad. Lo que sí existe es una tendencia constante a discutir como si hubiera una única decisión correcta y todo lo demás fueran errores o incompetencia. Para nombrar esas estrategias antes de discutir herramientas, propongo un marco mental que llamo **principio RPS** (Rock–Paper–Scissors): clasifica decisiones técnicas o estratégicas según qué priorizan y qué sacrifican. No te dice qué elegir. Te obliga a saber qué estás eligiendo. ## El principio RPS El principio RPS parte de una idea simple: toda decisión relevante cae (explícita o implícitamente) en uno de tres arquetipos estratégicos. Para referirse a estos enfoques de forma compacta, en este marco se les llama piedra, papel y tijeras (Rock, Paper, Scissors). Como en el juego, cada uno brilla en ciertos contextos y resulta caro en otros. Las etiquetas piedra, papel y tijeras son solo nombres memorables; no heredan las reglas del juego original ni implican relaciones fijas entre ellas. Podrían haber sido Terran, Protoss y Zerg o cualquier otra triada. A continuación, cada una de las tres estrategias de RPS: ### Piedra (Rock): Pragmatismo suficiente Piedra es la estrategia de *“esto funciona y seguimos adelante”*. * Minimiza el tiempo de decisión. * Reduce el coste cognitivo. * Resuelve el problema inmediato sin aspirar a optimizarlo todo. Poco elegante, poco brillante, y funciona: prioriza avance inmediato sobre refinamiento. Es la estrategia correcta cuando: * El coste de pensar más es mayor que el coste de equivocarse un poco. * El problema no es central. * La reversibilidad es alta. * Ideal para prototipos y MVPs. Piedra suele ser criticada por no ser “limpia” o “correcta”. Muchas veces, esa crítica es puro ruido. ### Papel (Paper): Control de riesgo pagando un precio Papel prioriza estabilidad, previsibilidad y control del riesgo. * Más trabajo inicial. * Más restricciones. * Menos sorpresas a largo plazo. Es la estrategia de quien dice: *“prefiero pagar ahora para no pagar después”*. Funciona bien cuando: * El fallo es caro. * El sistema va a vivir mucho tiempo. * El contexto es hostil al cambio. Papel suele ser acusado de lento o burocrático. A veces lo es. A veces es simplemente realista. ### Tijeras (Scissors): Potencial a largo plazo pagando riesgo Tijeras busca el máximo potencial a largo plazo. * Más flexibilidad. * Más rendimiento. * Más complejidad. * Más riesgo. Es la estrategia del *“si vamos a hacerlo, hagámoslo bien”*. Puede ser la mejor jugada… o un ejercicio de overengineering inútil si el contexto no lo justifica. Tijeras falla cuando se usa por ego técnico o por alergia a lo simple. ## Un ejemplo rápido Imagina que necesitas persistir datos para un MVP que validará si hay mercado. * **Piedra**: SQLite o un JSON en disco. Rápido, reversible, suficiente mientras no sepas si el producto despega. * **Papel**: PostgreSQL gestionado con backups y migraciones desde el día uno. Tiene sentido si ya tienes usuarios de pago o datos que no puedes perder. * **Tijeras**: Diseñar sharding, réplicas y un modelo de datos pensado para millones de filas. Solo si el problema central *ya* es la escala. La pregunta útil es qué estrategia se está usando y si el contexto lo justifica, no cuál es más profesional. ## De dónde vienen los problemas La mayoría de los problemas no vienen de elegir mal, sino de no saber qué estrategia se está usando. Algunos patrones clásicos: * Usar tijeras cuando hacía falta piedra (lo perfecto es enemigo de lo bueno). * Usar papel por miedo, cuando el coste real del fallo era bajo. (Ponerse la venda antes que la herida, actuar antes de que exista el problema) * Criticar una piedra por no ser elegante, cuando era la decisión correcta para ese momento. Basta el caso de [La fábula del columpio](https://en.wikipedia.org/wiki/Tree_swing_cartoon): sirve como metáfora de las situaciones donde un cliente necesita una solución sencilla y se hace todo tipo de ingeniería mal aplicada, cuando lo que realmente necesitaba es una solución simple y rápida aunque poco elegante, suficiente para ese contexto. Cuando no se habla de estrategia, se discute de forma religiosa. ## Puentes y Anclas El principio RPS no está completo sin dos conceptos transversales. **Puentes** Decisiones que reducen el coste de cambiar de estrategia más adelante. Ejemplos típicos: software libre, posibilidad de self-hosting opcional, estándares abiertos, formatos interoperables, contenedores. No te obligan a elegir bien ahora. Te permiten corregir después. Por ejemplo: si eliges [Supabase](https://supabase.com/) para tener pronto un backend para tu aplicación y en algún momento se te va de precio, tienes la opción de autoalojar una instancia propia en tu VPS, con el esfuerzo que conlleva, en lugar de usar la versión [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service). Esto es gracias a que Supabase tiene dos puentes: es software libre y se puede usar tanto en versión PaaS como en modo self-hosting. Si se elige Firebase en lugar de Supabase, es más difícil cambiar de estrategia, porque Firebase **ancla** a su plataforma y hace costoso pasar de un backend delegado en terceros (Papel) a autoalojarlo en un VPS (Piedra). **Anclas** Decisiones que encarecen o bloquean ese cambio. Vendor lock-in, software cerrado, protocolos propietarios, dependencias rígidas. Pueden ser convenientes a corto plazo. Pero convierten decisiones reversibles en apuestas. La diferencia entre una decisión inteligente y una trampa suele estar aquí. ## Para qué sirve todo esto El principio RPS no pretende crear una nueva ortodoxia. Hace justo lo contrario. Sirve para: * Pensar en términos de trade-offs, no de dogmas. * Aceptar que toda estrategia gana algo y pierde algo. * Separar discusiones técnicas reales de discusiones ideológicas disfrazadas. Lo que importa es elegir conscientemente y mantener opcionalidad cuando importa, no coronar siempre “la mejor” estrategia. ## Qué viene después En otros posts de este blog el marco se aplica a decisiones concretas (como atajo mental, no como regla fija): * Distribuciones Linux (Debian podría representar piedra, RHEL papel, y Arch tijeras). * IDEs (VSCode, IntelliJ IDEA, y LazyVim/AstroNvim/NvChad) * Infraestructura (VPS piedra, Cloud papel, Bare metal tijeras), ya en [snowflake e IaC](/blog/003-snowflake-iac-rps/), [cloud-first](/blog/004-cloud-first-problem/) y [acceso al homelab](/blog/006-homelab-vpn-access/). * Lenguajes de programación (Python/TypeScript/Ruby/PHP, Java/C#, y Golang/Rust/Elixir) * Decisiones que surgen a diario, aunque no se les ponga ese nombre. No para decirte qué usar. Sino para dejar claro qué estás pagando cuando eliges. Este post desarrolla el criterio planteado en la [presentación](/blog/001-introduction/). ---------------------------------------- ### [Presentación](https://escribano.dev/es/blog/001-introduction/) > Primer post de escribano.dev: qué va a contener el blog (programación, self-hosting, seguridad y productividad) y con qué criterio. Entender la tecnología en profundidad para ganar autonomía, reducir deuda técnica y evitar el vendor lock-in, documentando en público con errores y correcciones incluidos. Hola, soy Ru, y este es el primer post de un blog sobre **desarrollo de software** y **soberanía digital**. Aquí compartiré, por un lado, herramientas y técnicas relacionadas con la **programación** y el **self-hosting**, y por otro, reflexiones personales sobre seguridad informática, soberanía digital y productividad personal basada en la eliminación de distracciones y la claridad mental. Quiero dejar claro desde el principio que no escribo porque sea un experto. Escribo como forma de documentación para mí mismo y para ayudar a otros que se enfrenten a retos similares a los míos. También escribo para dejar por escrito mis reflexiones e invitar al debate sobre temas que considero de vital importancia hoy en día, como el autoalojamiento y la soberanía digital frente a la filosofía de *cloud-first* que parece haberse impuesto en los últimos años. En este blog no encontrarás recetas rápidas ni snippets sin criterio, ni nos conformaremos con el _“Si funciona, no lo toques”_. Aquí buscamos entender la tecnología en profundidad para obtener **autonomía**, reducir la **deuda técnica** y evitar el **vendor lock-in**. Todo lo que escriba está pensado para que aprendas a mirar más allá del funcionamiento superficial y puedas tomar decisiones informadas sobre las herramientas y sistemas que usas. Este blog será un espacio de aprendizaje honesto: habrá errores, correcciones y descubrimientos en público. No prometo respuestas fáciles, pero sí claridad, rigor y una guía basada en experiencia práctica. Lo que leas aquí no son solo recetas; es una invitación a pensar, probar y comprender cada pieza del sistema que usamos día a día. ## Blog completo (en) ### [Five open-weight LLM providers as Knights of the Zodiac](https://escribano.dev/en/blog/021-llm-caballeros-zodiaco/) > In the schoolyard, nobody agreed on which knight was the best, and open models are no different. A cheat sheet with the five Knights of the Zodiac protagonists to help you find your way. Once you decide to step away from frontier models and start digging into the open-weight catalogue, you find yourself in a proper jungle. Dozens of models, all publishing weights, none leading across the board. The question "which is the best open model?" has no clean answer. The same thing happened to me with Linux distributions. When I got started in 2003, I tried several and even FreeBSD in search of the "perfect" one. Ubuntu did not exist yet (it arrived in 2004, and it is what we would call today the distro for beginners). Before that there was Debian, Red Hat, Mandrake, Slackware, Gentoo, SUSE: each with its community and its holy wars. Asking which one was best always ended in *flamewars*, because the answer depended on what you wanted it for. And there is an even older feeling this reminds me of. TVE started airing the show in 1991, and in the schoolyard everyone had their favourite knight; mine was the Dragon. Nobody seriously asked which of the protagonists was "the best": the answer depended on what you were looking for in a character. With open weights it is the same for me: the answer is not universal, it is situational. And since the series helped me remember who was who on the API, here is the mapping. It is a cheat sheet, not a benchmark; the real map is still the usage you give it. ## The mapping | Knight | Provider | Trait to remember | |---|---|---| | Seiya (Pegasus) | Qwen | Versatile in every size; good at everything, standout at nothing | | Shiryu (Dragon) | GLM | From text-only to truly multimodal | | Hyoga (Cygnus) | Kimi | Cold and precise in code; expensive to keep running | | Ikki (Phoenix) | DeepSeek | Disruptive arrivals: reasoning, cache and prices | | Shun (Andromeda) | MiniMax | The apparent weak one who sometimes surprises | ## Seiya (Pegasus Knight) → Qwen Seiya is the protagonist of the series, but he does not stand out at anything in particular: he is in every arc without being the specialist of any. Qwen works in a similar way. The family covers a huge range of sizes, from models meant to run on modest machines to data-centre ones, with general, code and multimodal checkpoints. If your machine cannot handle the big models, the reasonable bet is Qwen3.8-27B: truly small, downloadable, Apache 2.0 licensed and enough to try things. It is rarely the best on any single axis, but it works for almost everything. A practical warning: with so many versions, the name does not always tell you what you are running. The published open weights are those of **Qwen3.8-Flash-Next** (125B total parameters, 6B active), while the production model the API serves as `qwen3.8-flash` ships with a one-million-token context by default and built-in tools. Alias and weights are not exactly the same model: read the real ID before trusting the name. ## Shiryu (Dragon Knight) → GLM In the Sanctuary arc, Shiryu blinds himself so he cannot look at the Medusa Shield of Algol of Perseus, which petrified whoever looked at it. His sight comes back when he awakens the seventh sense in that same arc, he loses it again against Krishna of Chrysaor in the Poseidon arc, and he does not get it back for good until the Hades arc, when he awakens the eighth sense. That first scene, the voluntary sacrifice, is the one that marked me and explains why he was my favourite. GLM went through a similar cycle. The earlier versions of the GLM-5 series were not multimodal, and **GLM-5.3-Flash** (late August 2026) arrived as the first natively multimodal model of the series (text and image), with MIT weights on Hugging Face, 320B total parameters, 18B active and one million tokens of context. Two caveats: it is not a small model, and the big GLM-5.3 (the *flagship*) is not multimodal. The recovery of "sight", in any case, is the trait that sets it apart. ## Hyoga (Cygnus Knight) → Kimi Hyoga, Camus's disciple, fights with ice: cold and precise. Kimi gives me a similar feeling: reliable at code, but expensive to keep at full throttle. Cursor based its Composer 2 and 2.5 on Kimi K2.5 (in Composer 2.5, 85% of the compute is Cursor's own training). **Kimi K2.7 Code** (June 12, 2026) is much cheaper and I have used it for infrastructure tasks. **Kimi K3** (API on July 16, 2026, weights on July 27, under its own license that is not MIT) is expensive and not an everyday tool: I save it for when I have quota to spare right before my OpenCode Go usage limit resets, and I use it to run code reviews looking for security vulnerabilities. ## Ikki (Phoenix Knight) → DeepSeek Ikki has a habit of showing up at exactly the right moment and turning the fight around. DeepSeek does the same with its models: it was among the first to put reasoning on the map for open weights, and it has kept moving the board with price and cache changes since. On September 10, 2026 it shipped **V4.1 Flash**, with MIT weights, and the old aliases (`deepseek-v4-flash`) started pointing to the new version. Its cache in off-peak hours (the lower-demand windows, at a reduced rate; at peak it costs double) sits at $0.003 per million tokens, far below the competition. In an agentic loop, where context comes back again and again, that can decide the bill more than the input price. ## Shun (Andromeda Knight) → MiniMax Shun looks like the weakest because he is a pacifist: he avoids fighting whenever he can and uses his chain more to defend than to attack, but when things get serious, he delivers. In my experience, MiniMax is one of the providers that gets mentioned the least, but it can be very useful in specific contexts; my impression is that above all in refactors with lots of context. The data at least does not contradict it: **MiniMax M3** (June 1, 2026) ships a one-million-token window (with a guaranteed minimum of 512K on the API). It is not a small model: 428B total parameters and about 23B active. It is natively multimodal, uses a sparse attention architecture ([MSA](https://arxiv.org/abs/2606.13392)) and comes with its own license, not MIT. And it is cheap on top of that. For refactors or log analysis with lots of context, MiniMax is the Andromeda Chain: the one that shows up out of nowhere and holds everything together. ## Closing This is a cheat sheet, not a benchmark. If you want numbers, the [quick guide](/en/blog/018-open-weight-models-api/) and the post on [cache and runtimes](/en/blog/019-open-models-cache-agents/) are there. All I wanted here was a map that fits in your head. *Note: Saint Seiya names and characters are the property of Toei Animation and Masami Kurumada. This post is an original analogy; it does not use images or protected material.* ---------------------------------------- ### [Go dominates my homelab: a self-hosted app catalog](https://escribano.dev/en/blog/020-homelab-go-stack-catalog/) > A catalog with language, image size, and RAM at rest for the 24 apps in the homelab: 13 in Go, 11 of the 15 infrastructure ones. The self-contained binary and low memory footprint explain why Go dominates the layer you administer daily, while heavy media services (Jellyfin, Immich) stay in other languages. The homelab has three machines. kvothe (Ubuntu, Docker) runs the media and the utilities; kilvin (Fedora, Podman and quadlets) carries the infrastructure and identity; elodin (QNAP) stores data and backups. Put the containers from the first two in a table and sort them by size, and a pattern shows up: the lightweight infrastructure services are written in Go, and the heavy media ones in other languages. This post is that catalog: the language, image size and RAM of every piece running today. It is not exhaustive and does not try to be; those three columns show the pattern on their own. The [homelab series](/blog/006-homelab-vpn-access/) already covered access, DNS, [proxy](/blog/008-caddy-reverse-proxy-pihole/) and [identity](/blog/009-homelab-identity/); here I pull the whole stack into one picture once those layers are solved. ## Go in the infrastructure Go has become one of the dominant languages in modern infrastructure tooling. Several of the projects that defined the last decade of operations are written in Go: | Category | Projects | |----------|----------| | Containers and orchestration | Docker, Podman, containerd, Kubernetes, k3s, Helm, Kustomize | | Networking and proxy | Caddy, Traefik, CoreDNS | | Observability | Prometheus, Grafana, InfluxDB, Telegraf, Loki | | Databases and storage | etcd, CockroachDB, SeaweedFS, rclone, restic | | Infrastructure as code | Terraform, OpenTofu, Pulumi, Terragrunt, Packer | | Developer tools | Hugo, Dagger, gh (GitHub CLI) | | Self-hosted apps | Syncthing, Zitadel, Forgejo, Memos, FileBrowser, PhotoPrism, Vikunja, Navidrome | One note on InfluxDB: version 3 is rewritten in Rust; 1.x and 2.x are still Go. Containers, orchestration, observability and infrastructure-as-code share a profile: software that runs all the time, in many places, with little maintenance and a binary you deploy without thinking. That is where Go fits. The counterexample exists too: nginx, Redis and PostgreSQL are written in C and hold their ground with a maturity Go has not displaced. Go has won mostly in the operations layer; in classic databases and web servers, C still rules. ## The data: Go by majority and by layer My homelab is a small version of that pattern. Of the twenty-four applications in the catalog, thirteen are written in Go: just over half. In the infrastructure layer (proxy, DNS, identity, git, metrics, dashboards) it is eleven of fifteen. In media and documents the pattern flips: of the nine applications in that group, only two (Memos and FileBrowser) are Go. I count as an application every service with a purpose of its own; databases, development and CI containers, and work-in-progress projects stay out. That filter favors Go, and I will say it plainly: the three databases in the catalog (MariaDB, Firebird and Postgres) are C or C++, so excluding them raises the percentage. It is a method decision; counting them would make the split less favorable. The trend holds either way: the closer to daily maintenance, the more Go; the closer to serving gigabytes of photos and films, the less. ## The catalog Images are what `docker images` and `podman images` report (layer size, one architecture); RAM is `docker stats` and `podman stats` at rest, rounded. A few services (Caddy, Memos, FileBrowser, Prometheus, Grafana, Vaultwarden and Whisper) run on both machines; for those I give the range. I use rclone on demand for backups; since it is not a resident service, it stays out of the catalog. ### Infrastructure and platform | App | Language | Image | RAM | What for | |-----|----------|-------|-----|----------| | node-exporter | Go | 26 MB | 17 MiB | Host metrics | | DuckDNS | shell | 35 MB | 5 MiB | Dynamic DNS | | oauth2-proxy | Go | 36 MB | 5 MiB | Forward auth | | Syncthing | Go | 43 MB | 26 MiB | File sync | | Caddy | Go | 59–63 MB | 12–14 MiB | Reverse proxy and TLS | | Authelia | Go | 78 MB | 17 MiB | Forward auth and 2FA | | Tailscale | Go | 112 MB | 43 MiB | Mesh and access | | Forgejo | Go | 178 MB | 203 MiB | Self-hosted git | | Zitadel | Go | 179 MB | 81 MiB | Identity (OIDC IdP) | | Portainer | Go | 184 MB | 19 MiB | Container management | | Homepage | TypeScript | 266 MB | 115 MiB | Dashboard | | Prometheus | Go | 296–423 MB | 90–93 MiB | Metrics | | Glances | Python | 502 MB | 90 MiB | System monitor | | wg-easy | Node | 535 MB | 18 MiB | WireGuard with a web panel | | Grafana | Go | 555 MB–1.07 GB | 109–156 MiB | Dashboards | ### Applications, media and documents | App | Language | Image | RAM | What for | |-----|----------|-------|-----|----------| | Memos | Go | 66–69 MB | 12 MiB | Quick notes | | aMule | C++ | 126 MB | 129 MiB | eD2k/Kad P2P | | FileBrowser | Go | 192–219 MB | 24–35 MiB | Files from the browser | | Vaultwarden | Rust | 243–249 MB | 10–14 MiB | Password manager | | BookStack | PHP | 401 MB | 12 MiB | Documentation and wiki | | Whisper | Python | 606–614 MB | 13–315 MiB | Transcription (STT) | | Piper | C++ | 877 MB | 650 MiB | Voice synthesis (TTS) | | Jellyfin | C# | 1.56 GB | 1.0 GiB | Media server | | Immich | TypeScript (+ Python) | 3.5 GB | 385 MiB | Photos | The databases behind those applications (MariaDB, Firebird and Postgres) do not count as applications; they are supporting infrastructure, and none of them are Go. ## Why Go fits the homelab The explanation starts with the binary. Go usually compiles to a self-contained binary: the Go runtime ships inside the executable, so there is no external runtime to install, unlike Python, Java or .NET. Without CGO you can produce static binaries, though not every Go binary is static or free of system dependencies. The model reduces the dependencies that can drift out of sync between the machine and the container. In a homelab that becomes images of tens of megabytes: Caddy 63 MB, node-exporter 26 MB, Syncthing 43 MB, Authelia 78 MB. The ones over a gigabyte in my catalog are all media (Jellyfin 1.56 GB, Immich 3.5 GB). RAM at rest follows the same logic. Caddy settles at 12–14 MiB, Memos at 12 MiB, Authelia at 17 MiB and Vaultwarden (Rust) at 14 MiB. Whisper jumps from 13 MiB at rest to over 300 MiB while transcribing, Piper (TTS) sits at 650 MiB, the Immich server starts at 385 MiB and Jellyfin hovers around a gigabyte. On a low-power mini PC, that difference shows up in how many services fit without swapping and in the power bill. I have it down to the number: kvothe has 16 GB of RAM, and around 12 GB stay free in normal conditions, with Jellyfin and a large chunk of the catalog running. That is the headroom for adding more services without worrying about RAM, as long as I aim for lightweight ones whenever I can. The third piece is maintenance. Fewer moving parts means fewer things that break when you update. Whether that turns into drama-free updates owes more to containers than to Go: isolating the service is what lets me update and roll back when needed. Still, when the container depends on a single binary (as happens with Go and Rust), the image is far smaller and updates run faster. ## Three caveats to the pattern Go does not explain everything. There are three caveats worth keeping in mind. First: Go and lightweight do not always go together. Grafana (Go) weighs more than half a gigabyte, Prometheus (Go) between 296 and 423 MB, and Forgejo (Go) settles at 203 MiB of RAM despite being a single binary. Observability tools bundle the frontend and the plugins, and a git forge carries far more than the runtime. Go shrinks the backend; whatever you bundle on top still weighs what it weighs. Second: compactness is not exclusive to Go. Vaultwarden, written in Rust, produces a 243 MB image and starts in 14 MiB, in line with the Go services on the lightweight end. Rust shares the reason (self-contained binary, small images, and a sensible consumption profile in services like this one), so Go's place on this axis is not permanent. Third: media. Jellyfin (C#) dominates the heavy end and has no Go equivalent as complete for video. With Immich (TypeScript and Python) the argument is weaker: PhotoPrism exists, also in Go, with an approach and a workflow that differ from what I am after. I picked Immich because it is multi-user, although in practice it is only me; somewhere down the list is trying PhotoPrism or even Ente (whose server is Go too), at some point. The underlying criterion is operational friction: when a piece demands more RAM and more attention than its alternative, it shows in the table; when it does not, it stays. ## Closing In the [RPS framework](/blog/002-rps-principle/), Go takes the rock slot: one piece that works, with little to break. For personal infrastructure, where maintenance is what decides, rock wins. The catalog repeats the pattern. Eleven of the fifteen infrastructure services are written in Go. The lightest ones (node-exporter, Caddy, Authelia, Syncthing, Tailscale) stay at tens of megabytes of image and a few tens of MiB of RAM at rest; others such as Forgejo, Zitadel, Prometheus or Grafana sit well above that. The gigabytes and the high RAM concentrate in media: Jellyfin (1.56 GB, 1.0 GiB) and Immich (3.5 GB, 385 MiB). Language is one of the few decisions you cannot change by putting a reverse proxy in front, so it is worth checking before you deploy. In the layer I administer by hand, Go wins by majority; in the media layer, it depends on what each ecosystem has available. This catalog is also a snapshot of what [digital sovereignty](/blog/005-digital-sovereignty/) means in practice: fifteen services on my own hardware, each with its maintenance cost in plain sight and a clear exit if something stops fitting. ---------------------------------------- ### [S3 is no longer (just) Amazon: alternatives, prices, and the cost of getting data back](https://escribano.dev/en/blog/018-s3-compatible-object-storage/) > The S3 API has been frozen since 2006 and became the common language of storage. MinIO, the self-hosted reference, was archived in 2026; Ceph RGW (maturity), SeaweedFS (the broadest S3 subset), and Garage take over. On the bill, the trap is retrieval: a TB from Deep Archive costs over $100 to bring back, while R2 and B2 charge nothing for egress. Open the docs of any tool that touches storage: rclone, restic, Kopia, Duplicati, Cyberduck, PhotoPrism, Forgejo. The same expression shows up on the configuration page every time: *S3-compatible*. The same goes for the website of any object storage provider that has appeared in the last decade. One API became the industry's default language, and how it got there says a lot about how standards are won. The trivia question is usually asked the other way around: what was AWS's first service? The instinctive answer is S3, and it is wrong. Amazon SQS opened its beta in late 2004, a year and a half earlier. S3 launched on March 14, 2006, was the first to reach general availability, and EC2 arrived later that same year. S3 was the second service to exist and the one that lent its name to all the rest. ## An API frozen in time The original announcement was one paragraph long: *storage for the Internet*. The system started with roughly one petabyte of capacity spread across about 400 storage nodes, objects capped at 5 GB, and a price of 15 cents per gigabyte per month. The web console did not exist until 2010: for the first years, using S3 meant writing code or installing a third-party tool. Twenty years later, the API version listed in the documentation is still `2006-03-01`. In its anniversary post, AWS highlights this: the code you wrote against S3 in 2006 works today, unchanged. The service grew by addition (objects up to 50 TB, storage classes, more than 500 trillion objects) while the basic vocabulary stayed still. That stillness is what made everything else possible. ## Protocol and product share a name When someone says S3, they may be naming two different things. The product: the AWS platform, with its exclusive features (lifecycle to cold tiers, replication, event notifications, Object Lock, tight IAM integration). The protocol: the HTTP vocabulary any client can speak (buckets, objects, PUT/GET/DELETE, multipart uploads, SigV4 signing). The *S3-compatible* label on those websites means, in practice, speaking the protocol with more or less product on top. That is the detail worth checking before choosing: clients that use the core vocabulary work against nearly any backend, while clients that depend on product features only work against AWS and the few providers that replicate them. Some examples of that periphery, the part that falls off the edge: * Backblaze B2 reduces ACLs to two values (private or public) at the bucket level, with no object tagging and no website configuration. * Hetzner keeps the most conservative subset: no replication, no website, encryption only with SSE-C. * Cloudflare R2 leaves out Lambda-style notifications and compensates with its own extensions (public buckets, Workers bindings). A client like rclone (or restic, which only needs PUT, GET, and LIST with its own format on top) barely touches the core. That is why it works everywhere. A script leaning on replication or event notifications is already playing on AWS's court, endpoint notwithstanding. ## MinIO: the reference stopped For a decade, if you wanted to speak S3 without a cloud, there was one answer: MinIO. A single Go binary exposing the full API on top of your own disks. The reference for testing, development, and homelabs, and the proof that the protocol could run without AWS in the middle. That reference aged overnight. The project moved to AGPLv3 in 2021, with an explicit warning about what that meant for anyone offering the service over a network with modified code. In December 2025 it entered maintenance mode: no new features, no pull requests, critical security evaluated case by case. In April 2026 the repository was archived and set to read-only. The community edition is now distributed as source code only (via `go install`), the official Docker images were retired, and the company points to AIStor, its commercial product in Free and Enterprise editions. A fork maintained by Chainguard exists, also in maintenance mode, and SeaweedFS's own README recommends RustFS (a Rust rewrite under the Apache 2.0 license with a byte-compatible on-disk format) for anyone who needs to migrate a MinIO installation as is. The lesson weighs more than the project. The community treated MinIO as the cornerstone of the S3-compatible ecosystem, and the cornerstone turned out to be an owned product whose company, when the moment came, reserved the commercial road for itself. The API survived. The binaries, well. ## The succession: Ceph, SeaweedFS, and Garage The gap is now covered by projects with different profiles and years of production behind them. With one warning first: *S3-compatible* is a spectrum, not a binary seal. None of the three implements the full API, and each one publishes its own. Garage keeps a public compatibility page, endpoint by endpoint, where everything unimplemented answers with an honest `501 Not Implemented`. The veteran is Ceph RGW, Ceph's object gateway: a decade and a half of production on OpenStack and similar platforms, backed by the Ceph Foundation, Red Hat, and IBM, with Object Lock, encryption, IAM, and multisite. Its documentation speaks of a broad subset of the Amazon API. The price of that maturity is operations: a RADOS cluster with its OSDs demands a maintenance scale far beyond a Raspberry Pi. SeaweedFS sits in the middle. Go, Apache 2.0, 34,000 stars, weekly release cadence, and the broadest S3 subset of the group: the gateway implements 73 bucket and object operations, IAM, STS, versioning, Object Lock with retention and legal hold, lifecycle, tagging, and CORS, with a compatibility suite in CI. Its honest note is in the README itself: MinIO tracked the AWS API closely, and SeaweedFS keeps working toward that level. The architecture asks for more pieces than a single binary (master, volume, filer, and gateway), although `weed mini` starts everything with one command for small setups. Garage bets on the opposite. Rust, AGPL, a lightweight binary with no external dependencies, designed for small geo-distributed clusters: three nodes in three different homes replicating data over the Internet. Deuxfleurs has run it in production since its first release in 2020. Its S3 subset is the most conservative of the three: no versioning (in progress), no Object Lock, partial lifecycle, encryption only with SSE-C. Plenty for restic, rclone, or serving static websites; a dealbreaker for a platform that depends on versioning. | Project | License | Strength | Caveat | |---------|---------|----------|--------| | Ceph RGW | LGPL2.1 | Maturity and multisite at scale | Demanding operations | | SeaweedFS | Apache 2.0 | The broadest S3 subset | More pieces to maintain | | Garage | AGPL | Lightweight and geo-distributed | No versioning or Object Lock | | RustFS | Apache 2.0 | Direct drop-in from MinIO | 1.0 GA (September 2026) | Two recent options orbit nearby: RustFS, which reached its 1.0 GA on September 16, 2026 after more than a year of public iteration, and VersityGW, a stateless gateway in Go (Apache 2.0) that exposes S3 on top of a POSIX filesystem, Azure, or another S3 backend, with conformance testing against the official AWS SDKs. ## The bills: cheap to store, expensive to recover B2 and R2 compete with AWS on another axis: price, and specifically the line of the bill that appears when you ask for your data back. AWS publishes storage by class, and that is where the first trap lives. The Standard class charges nothing for retrieval (the $0.023 per GB-month, about $23 per TB, is the cost of keeping). The classes that make storage cheaper place a toll at the exit door: | AWS class | Storage ($/GB-month) | Retrieval ($/GB) | Minimum stay | |-----------|----------------------|------------------|--------------| | S3 Standard | 0.023 | free | none | | Standard-IA | 0.0125 | 0.01 | 30 days | | One Zone-IA | 0.01 | 0.01 | 30 days | | Glacier Instant Retrieval | 0.004 | 0.03 | 90 days | | Glacier Flexible | 0.0036 | 0.01 (bulk: free) | 90 days | | Glacier Deep Archive | 0.00099 | 0.02 (bulk: 0.0025) | 180 days | The fine print comes along: minimum storage duration billed if you leave early, a 128 KB minimum billable object size in the IA classes (a billion 4 KB files get billed as if they weighed 128 GB), request fees on top, and a restored Glacier archive billed as a temporary Standard copy while you download it. On top of that comes traffic to the Internet: $0.09 per GB on AWS (the first 100 GB per month are free), free on B2 up to a limit of three times the average stored volume per month (then $0.01 per GB), and unconditionally free on R2. The example that makes the difference visible: restoring a TB stored in Deep Archive via the standard route costs about $20 in retrieval, the temporary Standard copy (under a dollar a day), the requests, and about $85 more in egress if the final destination is your home. More than a hundred dollars to bring back what costs about a dollar a month to store. The same TB from B2 falls inside the free 3x rule (about $10 above it) and from R2 pays no egress; $10 only if it sat in the Infrequent Access class, the only one of the three that keeps a retrieval toll, announced and contained. A direct comparison (per TB and month, published rates): | | AWS S3 (Standard) | B2 | R2 (Standard) | |---|-------------------|----|---------------| | Storage | ≈ $23.5 | $6.95 | ≈ $15 | | Egress to Internet | $0.09/GB | free up to 3x, then $0.01/GB | free | | Cold tiers with a toll | $0.01 to 0.03/GB | no cold tiers | IA only: $0.01/GB | | Requests | $0.005/1k writes, $0.0004/1k reads | free (class D at $0.004/10k after 2,500/day) | $4.50/M writes, $0.36/M reads | And the European flat bundle sits outside the comparison by structure: Hetzner Object Storage sells a base quota with storage and traffic included, data in Falkenstein, Nuremberg, and Helsinki, in exchange for the most conservative S3 subset on the market (no replication, no website, SSE-C only), aimed at backups and file servers. The real catalog is longer (Wasabi, DigitalOcean Spaces, Ceph RGW for those who want to run their own), but these cover the range of models: cheap archive, zero egress, the European bundle, and self-hosted. ## The same binary, another endpoint The practical promise of the standard fits in one line: ```bash aws s3 ls s3://my-bucket --endpoint-url https://.r2.cloudflarestorage.com ``` The same CLI you use against AWS works against R2, B2, or Hetzner by adding one flag. With rclone the change is similar: configuring an `s3` remote only swaps endpoint, region, and keys; the rest of the pipeline (scripts, encryption, cron) stays intact. The effort of learning one tool pays off across all backends at once, which is exactly what a standard promises. ## Rock, paper, scissors The [RPS framework](/blog/002-rps-principle/) orders the options by the level of complexity you are willing to maintain: | | Approach | |--|----------| | **Rock** | One bucket on B2 or R2, restic or rclone, keys in the password manager. A remote copy you can actually restore. Or Garage on your own node if the data can live at home. | | **Paper** | Two providers with the same tool, scripts parametrized by endpoint, and restores rehearsed now and then. Or SeaweedFS on three nodes to move storage to your own hardware without losing vocabulary. | | **Scissors** | Ceph RGW with multisite, lifecycle to cold tiers, a CDN in front of the bucket, and full lifecycle automation. | The bridge/anchor axis (which [I already explored with cloud-first](/blog/004-cloud-first-problem/)) fits here cleanly. Speaking S3 is the bridge: switching providers comes down to changing endpoint and keys, in either direction. The anchor shows up in three places. Coupling yourself to product features only AWS has (fine-grained replication, Object Lock, IAM) ties you to the vendor. Chaining the architecture to the hyperscaler's egress bill ties you to the platform, and it is precisely the anchor R2 turned into a selling point. And the retrieval toll of cold tiers is the silent anchor in the bill: it ties you to the provider's terms at the worst possible moment, the day you need the data. [Digital sovereignty](/blog/005-digital-sovereignty/) has a practical version here: data on Garage or Hetzner carries different weight than data on an AWS bucket, with the same vocabulary in between. ## The common language The story has a pleasant irony. Amazon turned its API into the de facto standard and, for years, that worked as the gateway to a huge platform. MinIO built its position on the free part of that standard and, when it turned toward the commercial product, left the gap that Garage, SeaweedFS, and Ceph now fill. The protocol outlived both: by freezing the vocabulary and growing around it, it became free to copy, and every provider speaks it with its own accent. Data can live in three different homes (Garage), in a serious cluster (SeaweedFS, Ceph), in a cheap archive (B2), or behind a network that does not charge for egress (R2), and the tools remain the same. The choice breaks down into two questions: which protocol your tools speak (S3, almost certainly) and how much it will cost to bring the data back the day you need it. The first decides which backends you can work with. The second decides how much it costs you to change your mind. *Note: while this post was being revised, RustFS reached its 1.0 GA (September 16, 2026). Thanks to Lorenzo from [atareao.es](https://atareao.es/) for the heads-up.* ---------------------------------------- ### [Open-weight models over API: a quick guide](https://escribano.dev/en/blog/018-open-weight-models-api/) > A guide to seven open-weight models consumed over API and when to pick each: Qwen3.8-Flash-Next for cheap high-volume tool use, DeepSeek V4.1 Flash for cache and agentic loops, GLM-5.3-Flash multimodal, MiniMax M3 and Kimi K3 for million-token context, Kimi K2.7 Code for long-horizon coding, and Qwen3.8-Max for specs. All with published weights: switching providers reduces lock-in. Seven open-weight models consumed over API: what each one does well and when to pick it. I verified prices on September 20, 2026; they expire fast (one already did while I was writing), so the table has no figures. All seven have published weights: you can download them and, with the right infrastructure, run them yourself or move to another inference provider. Switching providers is not plug-and-play (URL, model ID, and parameters usually change), but it reduces an important dependency: if prices rise or an API goes away tomorrow, the weights remain available. It does not remove every dependency, but it changes the relationship quite a bit. It is the cognition layer I wrote about in the [post on digital sovereignty](/blog/005-digital-sovereignty/). ## The table | Model | When to use it | |---|---| | Qwen3.8-Flash-Next | High volume with tools and multimodal input; the best price-performance on the list. This is the checkpoint served by the `qwen3.8-flash` API | | DeepSeek V4.1 Flash | Tool-heavy, high-iteration agents (OpenClaw, Hermes, Moltis): among the best agentic scores on the list and by far the cheapest cache, which in those loops can dominate the bill. API ID: `deepseek-flash` (legacy aliases `deepseek-v4-flash` redirect). MIT license | | GLM-5.3-Flash | Genuinely multimodal (image, video and files) under MIT | | MiniMax M3 | Large refactors at low cost with tests or review behind: up to a million-token window (input plus output; MiniMax counts it that way) via its [MSA](https://github.com/MiniMax-AI/MSA) sparse attention architecture (long context stays usable), native multimodality, and adaptive reasoning that switches on only when needed | | Kimi K2.7 Code | Long-horizon coding and systems engineering: completes tasks end to end with few reasoning tokens. Expensive, for when the task justifies it | | Qwen3.8-Max | Writing the spec and breaking down the work (the cheap models do the implementing); also scores very well on Code Arena: WebDev. The API snapshot runs a step ahead of the downloadable weights (`Qwen/Qwen3.8-2.4T-A95B`) | | Kimi K3 | Deep research and long-horizon tasks: cross-checks hundreds of sources with a million-token window (90.4 on BrowseComp in a 1M-context evaluation with no compaction, comparable to top-tier closed models). Always reasons, adjustable across three levels; expensive, for when the task justifies it | ## In practice I've been using Cursor for several months and I'm happy with Composer 2.5, especially the speed in fast mode. Under the hood it's Kimi K2.5, and it has worked well for me both for programming and for homelab infrastructure (Docker, Podman, Quadlet and the like). That's why I'm now trying Kimi K2.7 Code in [OpenCode](https://opencode.ai/): I want to see if the Kimi family keeps that level outside the Cursor ecosystem. I recently subscribed to OpenCode Go and plan to run Cursor, OpenCode and [OpenRouter](https://openrouter.ai/) in parallel for a while on side projects. OpenRouter, pay-as-you-go, is my plan B. At work I stick with closed models; in my own time I calibrate open ones in case they need to go to production someday, especially if inference prices keep rising (Anthropic is the clearest case). Open models can get more expensive too, but vendor lock-in is lower, and that's why this guide focuses on them. The split I have in mind, still being tested: three workhorses for agents like OpenClaw and Moltis (I haven't tried Hermes yet): DeepSeek V4.1 Flash for cost and cache, GLM-5.3-Flash when the flow is multimodal or goes through Z.AI, and MiMo-V2.6-Flash when I want a million-token window in the same price tier. Day to day I still default to DeepSeek; cache and runtime detail belongs in the follow-up to this guide. Qwen3.8-Max for writing specs in [OpenSpec](https://github.com/Fission-AI/OpenSpec). I also want to validate GLM-5.3-Flash on frontend work: Z.AI pushes it hard on visual coding and UI reproduction. Kimi K2.7 Code is for backend (especially Go) and systems engineering: Quadlets, wiring up Tailscale or Zitadel, and similar tasks. I save Kimi K3 for important code reviews: it always reasons and the price shows. With Qwen3.8-Max I learned the hard way: in a few hours I burned half my monthly OpenCode Go quota, largely because I let the context grow without watching it. Watching context matters for OpenSpec too, even though the work there is more bounded. ## What doesn't expire Prices change. GLM-5.3-Flash cost half as much two weeks ago and the promotion ended on September 9 without notice; DeepSeek shipped V4.1 Flash on September 10 with new rates. Anyone reading this in December will find different numbers. ---------------------------------------- ### [The Bitter Lesson: human knowledge as a ceiling](https://escribano.dev/en/blog/017-bitter-lesson/) > Sutton's Bitter Lesson (2019): in the long run, general methods that scale compute (search and learning) beat embedding human domain knowledge. AlphaGo used human games as a ramp; AlphaGo Zero discarded them and within 40 days beat previous versions 89-11. Human knowledge acts as a prior: it speeds up the start but caps the curve at a human-shaped optimum. I first heard it on Inteligencia Artificial Semanal ([Spotify](https://open.spotify.com/show/48stHRcIdLoDi8WPK8JJWq), [YouTube](https://www.youtube.com/channel/UC4ewDq9cCPd9eqA88M6yvEQ), [iVoox](https://www.ivoox.com/podcast-inteligencia-artificial-semanal_sq_f12364979_1.html)). I did not go looking for the paper. The host often jokes that he is a *Seventh-day Suttonist*: aligned with Sutton, with *The Bitter Lesson*, and with reinforcement learning. The podcast kept returning to the essay the way you name a tool you already take for granted, and in some episode the sentence clicked: human knowledge, the thing that feels like the most valuable ingredient we can put into a system, can be both the shortcut and the ceiling. In March 2019, [Richard Sutton](http://www.incompleteideas.net/IncIdeas/BitterLesson.html) compressed seventy years of research into a few pages. The title, *The Bitter Lesson*, warns that the conclusion is unwelcome. The claim is easy to state and hard to accept: in the long run, general methods that leverage computation (search and learning) win. Human knowledge of the domain helps at first, satisfies the person who put it there, and later flattens the curve. Sometimes it gets in the way. That flattening is what I care about. Not the slogan "more GPUs". The shape of the curve: climb faster with what humans already know, then approach, asymptotically, a ceiling that has a human shape. ## What the essay says Sutton starts from a historical observation, not a lab prediction. Most AI research has been done as if the computation available to the agent were constant. If the compute budget will not grow, leveraging human knowledge is almost the only lever. Over a slightly longer horizon than a typical project, the cost per unit of computation falls and vastly more calculation appears. Then the lever that matters is a different one. The two strategies do not have to fight. In practice they do. Time spent formalizing how you think you think is time not spent on methods that scale when ten times more compute shows up tomorrow. There are psychological commitments: you have invested in an approach and you want it to win. And the human-knowledge approach tends to complicate methods in ways that make them worse at absorbing more calculation. Sutton boils the lesson down to four observations: 1. Researchers have tried, again and again, to build knowledge into their agents. 2. This always helps in the short term, and it is personally satisfying. 3. In the long run it plateaus and even inhibits further progress. 4. Breakthrough progress arrives by the opposite route: scaling computation through search and learning. The later success tastes bitter because it wins against an approach the field preferred: the human-centric one, the one that "understands" the domain the way we do. The sentence the essay leaves behind, and the one the podcast left running in my head, is this: we want AI agents that can discover like we can, not ones that contain what we have already discovered. ## Why it is bitter The bitterness is not technical. It is professional, and a little bit about identity. If your craft is putting into the system how chess, go, speech, or vision works (the openings, the phonemes, the generalized cylinders, the features that "make sense"), the system is an extension of your understanding. When a cruder method, with more search or more data, overtakes you, the loss is not just a paper. It is an idea of what counts as intelligence. Sutton tells it without varnish in chess. In 1997, Deep Blue beat Kasparov with massive, deep search. A large part of the community that had worked on understanding the special structure of the game were not good losers. They said brute force had won this time, that it was not a general strategy, and that anyway it was not how people played chess. They wanted methods based on human input to win. That "we wanted ours to win" is the bitter part. The field is slow to digest that progress arrived by the path that least resembled us. ## The examples in the essay Sutton does not invent a pattern. He points to it in four places where the field had already paid for the same cycle. | Domain | Human shortcut | What ended up scaling | |--------|----------------|------------------------| | Chess | Understanding the structure of the game, less search | Deep search at scale (Deep Blue, 1997) | | Go | Avoiding search with knowledge and the board's peculiarities | Search plus learning by self-play, twenty years later | | Speech | Words, phonemes, the vocal tract (DARPA competition, 1970s) | Statistical methods (HMMs) and, later, deep learning | | Vision | Edges, generalized cylinders, SIFT features | Networks that use little more than convolution and invariances | Speech recognition is the cleanest cycle. One side carried human knowledge of language and the vocal apparatus. The other spent more computation on statistical models. The statistical side won. Over decades, natural language processing drifted in that direction. Deep learning was the next step of the same movement: still less embedded knowledge, still more calculation and more data. Vision did the same thing with a different vocabulary. For years people designed primitives that imitated how we thought we saw. Today those primitives are off the critical path. What remains is a minimal inductive bias (convolution, some invariance); training finds the rest. Go, in the essay, is chess with a twenty-year delay. The field spent enormous effort on not searching, on using what humans already knew about the game. Once search and self-play were applied at scale, that effort became irrelevant or worse. Sutton does not walk through the AlphaGo versions. The versions are what make the asymptote concrete. ## The curve: shortcut and ceiling Human knowledge is not noise. It is a **prior**: the belief you start with, before you see new data. In statistics you update that belief with what you observe. In AI the mechanism is the same, more informal: what the system already assumes conditions what it can learn next. A good prior puts you in a decent place sooner. If you feed the network professional games, it does not start from random moves: it already "believes", in some sense, that human openings and good shape are the reasonable place to be. A prior that describes how humans play, speak, or see also pulls you toward that place when the system could have gone further. The shape, in crude form: | | Embedded human knowledge | Search + learning | |--|--------------------------|-------------------| | Start | You climb fast: you imitate the experts | You start near random play | | Asymptote | A ceiling with a human shape (styles, features, biases) | Set by compute and the problem space | | Side effect | More complex methods, worse at absorbing more calculation | Cost, and less control over *what* it found | "A ceiling with a human shape" does not mean "a professional's Elo". AlphaGo, the one that did use human games, was already superhuman: it beat Lee Sedol. The ceiling is not "you cannot surpass people". It is the basin of attraction of what people already explored and of how they described the problem. The system learns to be a very good human, or a very good human with a little extra search. Leaving that basin is hard. That is the asymptotic part. The shortcut gets you earlier to a local optimum that looks like us. The method that starts with less (rules, search, learning) takes longer to look intelligent and can overshoot that optimum. ## AlphaGo and AlphaGo Zero In 2016, [AlphaGo](https://www.nature.com/articles/nature16961) became the first program to beat a world champion at go. The recipe mixed the two strategies. Neural networks trained first with supervised learning on amateur and professional games, then with reinforcement learning; and, at playing time, a tree search (MCTS) guided by those networks. Human knowledge was the ramp: thousands of games that already existed, a style, a repertoire of openings and of "good shape". It worked. Fan Hui, Lee Sedol, later Ke Jie. The shortcut delivered. In a few years go went from "too large for search" to a problem solved above the human ceiling. In 2017, DeepMind published [AlphaGo Zero](https://deepmind.google/blog/alphago-zero-starting-from-scratch/): same game, different ramp. No human games. No domain features beyond the stones on the board. No separate policy and value networks. A single network, a simpler search, and a loop in which the program is its own teacher. It starts near random play. It plays against itself. It updates the network to predict its own moves and the winner. It searches again. It repeats. The *Nature* paper is titled, without disguise, [Mastering the game of Go without human knowledge](https://www.nature.com/articles/nature24270). The team's own numbers: * After three days of self-play, AlphaGo Zero won 100-0 against the published version that had beaten Lee Sedol (AlphaGo Lee). * After 21 days it reached the level of AlphaGo Master, the one that had swept the best humans. * After 40 days it surpassed every previous version. In a 100-game match, 40-day Zero beat Master 89-11. Zero did not only go higher. In three days of self-play it had left behind the AlphaGo trained on people, and then it kept climbing. The paper uses the word that matters here: better *asymptotic performance* than training on expert data. Demis Hassabis put it later: Zero was no longer constrained by the limits of human knowledge. You could see it on the board. Zero rediscovered known joseki and then skipped them. It found moves that looked ugly to a professional and that, once you had the result, were better. Human go is a library of centuries. It turned out to be an incomplete library. Training against that library puts you inside it. Training against yourself lets you leave. One caveat is worth not hiding. Zero did not start from metaphysical nothing. It started from the rules of go, which are human knowledge, and from a search algorithm that is human knowledge too. "Without human knowledge" in the paper means no games, no style advice, and no domain features. People still wrote the environment. They did not write the contents of the agent's mind. That caveat does not weaken the lesson. It sharpens it. What Sutton wants left out is the catalog of what we already found (the good moves, the shapes, the phonemes, the edges). What he wants left in are the meta-methods with which an agent can find that catalog, and a larger one, when more compute arrives. ## Reinforcement learning Sutton is not talking about just any kind of learning. The kind that scales alongside search is, in practice, **reinforcement learning**: the system tries, gets a signal for how it went (won, lost, got it right), and adjusts what it will do next time. Nobody dictates the correct move. Sooner or later they tell it whether the result was worth it. Imitating a human is a different thing. Supervised learning starts from labels we already brought: this position is played this way because a professional did it. It is a prior in the shape of a demonstration. Reinforcement starts from a reward. It can discover a line no professional played, if that line wins. AlphaGo used both in sequence: first imitate games, then reinforcement. Zero skipped the imitation. That is why the paper can talk about better asymptotic performance: the human prior was not there to cap how it plays. In 2026 the same gesture shows up under other names. Pretraining a model on internet text is imitation at scale (a huge human prior). Then a reinforcement phase often follows: the model generates, the result is evaluated (a problem with a checkable answer, a judge, a preference signal), and it updates. The bitter lesson does not ask for a specific algorithm. It asks us not to confuse the library of human examples with the ability to improve from what happens. ## Emergence On Inteligencia Artificial Semanal you hear the verb **emerge** a lot. As you add computational power (more parameters, more data, more self-play, more search at inference) capabilities appear that nobody wrote as a rule. Sometimes you expected them: play go better. Sometimes you did not: a move that looks ugly; a skill that was not the training objective. Sutton describes it without using the word. If you put into the agent how you think you think, the ceiling is that theory. If you put in methods that leverage more compute, the system can find complexity you had not named. The new behavior is not added by hand. It shows up. Zero rediscovering joseki and then skipping them is emergence on a measurable board. In large models the pattern looks similar: as compute grows, the system does something the version ten times smaller did not do, or does it in a way nobody anticipated. There is a debate about whether the jump is sudden or a smooth curve we were measuring badly. The useful point does not change. The surprise is the consequence of not having built the catalog in. Emerge is the name the podcast gives to Sutton's bet: we want agents that discover, and discovery, seen from outside, looks as if it came out of nowhere. ## Two methods that scale Sutton names two families that remain useful when available calculation becomes enormous: **search** and **learning**. Search spends compute at decision time: more depth, more simulations, more candidates. The learning he has in mind is experience turned into better decisions, the reinforcement loop, more than copying labels. Both turn "tomorrow there will be more calculation" into "tomorrow the same method will be better", without you having to rewrite the theory of the domain by hand. And if the method really scales, what shows up afterwards is what the podcast calls emerging. Embedded knowledge does not scale that way. A well-designed go feature from 2014 does not become ten times better because the cluster is ten times larger. Often it becomes a hindrance: the system has to negotiate with it instead of spending the extra budget on searching or learning. The essay's second conclusion is drier. The actual contents of minds are irredeemably complex. We should stop looking for simple ways to think about that content (space, objects, multiple agents, symmetries) in order to build them into the agent as if they were the essence. That is the world, arbitrary and bottomless. What we should build are methods that can find and approximate that complexity. The search for good approximations should be done by our methods, not by us. ## Why it matters now In 2019 the essay read as a recap: chess, speech, vision, go. In 2026 it reads as the working assumption of the labs. The cost per unit of computation has kept falling, and the computation a training run or an inference pass can swallow has kept rising. Imitating text at scale, then reinforcement, and spending more calculation per answer are, under other names, Sutton's two families. Every time a model "reasons" longer and improves, you are looking at search. Every time a long training run displaces a system full of rules, you are looking at learning. And every time, as parameters or compute go up, a capability appears that nobody put in the design, the podcast would say it has emerged. The cycle in the essay has not closed. It is still tempting to build how we think we think: hand-crafted cognitive architectures, curricula that imitate school, features that "make sense", expert recipes injected as if they were the system's destination rather than its first step. It helps. It is satisfying. It publishes well. And it draws an asymptote. Go left it measured. AlphaGo, with human games, got earlier to a place that was already superhuman. AlphaGo Zero, without those games, took a while to look like someone and then overshot the place people (and the system trained on people) had reached. Human knowledge was an accelerator and a magnet. The magnet is what puts the ceiling on. Sutton is not asking us to discard what we know. He is asking us not to confuse it with the content the agent should carry inside. The rules, the evaluation, the judgment of whether something matters: that can still be ours. The catalog of solutions, the "this is how you play", the "this is how you see", the "this is how you speak", is a local optimum that feels like arrival. The bitter part is about identity: we like being the source. The essay's bet is a different one. Put into the system the capacity to discover, not the library of what has already been discovered. The podcast left that said in passing, between the *Seventh-day Suttonist* joke and the verb emerge. Zero's board, 100-0, left it written in the score. ---------------------------------------- ### [Ctrl+J: the newline shortcut agent TUIs don't tell you about](https://escribano.dev/en/blog/016-ctrl-j-terminal-newline/) > Ctrl+J is the LF byte (ASCII 10), which is why it's the only universal shortcut for inserting a newline in agent TUIs like kiro-cli and Cursor Agent. Enter submits because it's CR (Ctrl+M), and Shift+Enter depends on the emulator reporting the modifier. In the VS Code integrated terminal (and forks), Ctrl+J gets caught by the workbench (togglePanel): fixes via commandsToSkipShell or sendSequence. You want a multiline prompt in `kiro-cli` or the `agent` TUI: a list of requirements, a walkthrough of a diff, context that doesn't fit in one sentence. You hit Enter and it submits halfway. **Ctrl+J** inserts the line in almost any terminal, including tmux. Until you press it inside VS Code's integrated terminal (or a fork like Cursor or Kiro IDE) and the panel disappears. This post is short on purpose. It complements the [IAG tooling map](/blog/015-spec-driven-development-iag/) with a keyboard detail those CLIs barely document. ## The trick Enter submits. Ctrl+J inserts the newline. | Tool | Newline (always) | Others, if the terminal reports them | |------|------------------|--------------------------------------| | OpenCode | Shift+Enter / Ctrl+Enter (the app intercepts them) | (none) | | Kiro CLI | Ctrl+J | Shift+Enter, Alt+Enter; `/editor` opens `$EDITOR` | | Cursor Agent (`agent`) | Ctrl+J; `\` then Enter | Shift+Enter; Option+Enter after `/setup-terminal` | Ctrl+J isn't a shortcut Kiro invented. It's the control character terminals have been emitting for decades. **There is no other `Ctrl+letter` that does the same thing**: J is the tenth letter of the Latin alphabet, and Ctrl+J is the only ASCII 10 (LF) byte. Ctrl+M is CR (the Enter that submits). Ctrl+I is Tab. There is no “Ctrl+K but for newline”. What *does* exist are **other combinations the TUI treats as “new line”** without emitting LF: | Combo | What goes down the wire | Works out of the box in | |-------|-------------------------|-------------------------| | Ctrl+J | LF (`\n`, ASCII 10) | Any real terminal; in VS Code the workbench keeps it | | `\` + Enter | backslash then CR; the TUI turns it into a newline | Cursor Agent, including tmux and the VS Code terminal | | Shift+Enter | a modified-key sequence (not LF) | iTerm2, Ghostty, Kitty, Warp, Zed | | Alt+Enter / Option+Enter | usually ESC+CR or another sequence | Terminal.app, Ghostty; VS Code only after setup | Shift+Enter and Alt+Enter **are not Ctrl+J under another name**. They depend on the emulator reporting the modifier. In gnome-terminal, in many IDEs, and inside tmux, Shift+Enter arrives as a plain Enter and submits the prompt. That's why Cursor and Kiro docs call Ctrl+J (and, in Agent, `\`+Enter) the universal option. Kiro documents `/settings terminal` to auto-configure Shift+Enter. Cursor documents `/setup-terminal`. Both end up writing an emulator keymap (in VS Code, a `sendSequence`) so that combo stops being a disguised Enter. ## Why it works `Ctrl+letter` isn't OS magic. In the teletype / VT100 model, **Ctrl plus a letter produces the byte `letter & 0x1F`**: the matching control character. | Shortcut | Character | ASCII | Equivalent | |----------|-----------|-------|------------| | Ctrl+J | LF | 10 | `\n` | | Ctrl+M | CR | 13 | Enter | | Ctrl+I | HT | 9 | Tab | | Ctrl+[ | ESC | 27 | Escape | | Ctrl+D | EOT | 4 | EOF / close input | | Ctrl+L | FF | 12 | Clear screen (often) | | Ctrl+H | BS | 8 | Backspace (sometimes) | Enter is **CR** (Ctrl+M). Ctrl+J is **LF**. Kiro and Agent, in the TUI, treat LF as newline: if the editor eats Ctrl+J first, the input never sees it. OpenCode intercepts Shift+Enter at the application layer and doesn't depend on that byte. There are three layers, and any of them can keep the event: ``` Keyboard → terminal (bytes) → TUI (interpretation) ``` In a real emulator (WezTerm, Alacritty, iTerm2, GNOME Terminal) the three layers cooperate and Ctrl+J is LF. In an IDE's integrated terminal the middle layer is the workbench: it decides whether the shortcut is its own or belongs to the process inside. Linux and macOS behave the same in a real terminal. On Windows, Windows Terminal plus WSL, Git Bash, or SSH do too. PowerShell, cmd, and any GUI text field **do not** follow these rules: Ctrl+J is not LF there. ## The clash in VS Code (and forks) In Visual Studio Code, **Ctrl+J** (Cmd+J on macOS) is bound to `workbench.action.togglePanel`: show or hide the bottom panel. Cursor, Kiro IDE, and the rest of the forks inherit that keymap. If you run `kiro-cli` or `agent` **inside** that terminal, Ctrl+J never reaches the TUI. It reaches the workbench. The panel collapses. On macOS the panel shortcut is usually **Cmd+J**, so **Ctrl+J may already work** in the integrated terminal. The ugly collision is Linux and Windows, where Ctrl+J *is* the panel toggle. There is no second factory `Ctrl+…` that inserts LF and still uses Ctrl+J for the panel. In **Cursor Agent** you don't need one: type `\` and press Enter. I've checked it in Cursor's integrated terminal: it inserts the line, doesn't hide the panel, and doesn't touch the keymap. (Agent's docs also list it as working in tmux and over SSH.) In **Kiro**, without remapping, you're left with `/editor` (opens `$EDITOR`) or pasting text that's already multiline. If you want Shift+Enter in that terminal, it isn't free: you have to configure the IDE (`/setup-terminal` in Agent, `/settings terminal` in Kiro), which ends in a `sendSequence`. The rest of this section is that: give Ctrl+J's LF back to the TUI, or send byte 10 with another key. ### Let Ctrl+J reach the TUI VS Code intercepts a list of commands when the terminal has focus (`terminal.integrated.commandsToSkipShell`). `workbench.action.togglePanel` is on that list. Remove it, and Ctrl+J is LF again for the process: ```json { "terminal.integrated.commandsToSkipShell": [ "-workbench.action.togglePanel" ] } ``` The panel still hides with ``Ctrl+` `` (the usual terminal shortcut). You only give up Ctrl+J while the cursor is in the terminal. ### Send LF with another key If you don't want to touch the toggle, send the byte yourself. `\u000a` is LF (the same 10 as Ctrl+J). **Don't use `\u000d`**: that's CR, the Enter that submits the prompt. ```json { "key": "ctrl+enter", "command": "workbench.action.terminal.sendSequence", "args": { "text": "\u000a" }, "when": "terminalFocus" } ``` Pick a combo the IDE isn't already using. Ctrl+Enter is sometimes taken; Alt+J or Ctrl+Shift+J are usually free. Same recipe in Cursor and Kiro IDE: the command has the same name. You can also rebind Ctrl+J **only when the terminal is focused** to that `sendSequence`. Then Ctrl+J in the editor still folds the panel, and in the terminal it inserts a line. It's still a keymap change; it's the most precise one if you live in the IDE. ### Don't fight the IDE If `\`+Enter isn't enough or you're on Kiro: open the TUI in WezTerm, Alacritty, or whichever emulator you already use. There Ctrl+J is LF and there's no panel to hide. Pasting multiline text from the clipboard also works in almost every TUI. The agent TUI and Cursor's sidebar chat are not the same surface. This clash only shows up when the agent runs **in the integrated terminal**. ## Closing Enter submits because it's CR. Ctrl+J inserts a line because it's LF: there is no equivalent `Ctrl+letter`. Shift+Enter and Alt+Enter are TUI shortcuts, not terminal ones, and they fail where the emulator doesn't report the modifier. In Agent, `\`+Enter fills that gap without fighting VS Code. Everywhere else, either you give LF back to the TUI, or you write the prompt where the editor doesn't steal the keyboard. ---------------------------------------- ### [SDD in the IAG era: Kiro, OpenSpec, and how much structure you need](https://escribano.dev/en/blog/015-spec-driven-development-iag/) > When the agent writes the code, the problem is governing what it does. Vibe-coding is degenerate rock; SDD (Spec-Driven Development) is paper: versioned specs in the repo as a contract with the agent. A four-level maturity map and a tool comparison: AGENTS.md as minimal constitution, OpenSpec with deltas for brownfield, Kiro with EARS traceability, and the trade-offs of each. When the agent writes the code, the problem stops being "which tool do I use?" and becomes "how do I govern what the agent does?" Cursor, Claude Code, Kiro, [OpenCode](https://opencode.ai/), and dozens of CLIs solve the surface: IDE or terminal, model A or model B. But if the workflow is open a chat, describe the feature in prose, and accept the diff, you're in **vibe-coding**: plausible code that drifts from intent, requirements buried in a history you'll close tomorrow, and no artifact that survives the session context. The thread is the frame: why versioned specs matter in the IAG era, how approaches compare, and where each fits in the [RPS principle](/blog/002-rps-principle/). ## The vibe-coding problem Vibe-coding works until it doesn't. The agent generates code that compiles, passes the tests you asked for, and matches the repo style. Three weeks later you don't remember why it chose that abstraction, the original requirement lived in message 47 of the chat, and the next agent (or you, with fresh memory) reinterprets intent from scratch. In the RPS framework, vibe-coding is degenerate rock: it has the shape of rock (zero ceremony, maximum apparent speed) but none of its virtues. Conscious rock assumes high reversibility and low cost of failure. Vibe-coding on a module with sensitive data, on code that will live for years, or in a regulated domain is improvisation dressed as pragmatism, not conscious rock. The symptoms are predictable: * **Drift**: code drifts from what you asked without anyone noticing until review, or production. * **Lost context**: closing the chat deletes the spec. * **Plausible, not correct**: the agent optimizes for "looking right," not for invariants that were never written down. * **Silent hallucinations**: invented APIs, dependencies that don't exist, configurations that "should" work. None of these are the model's fault. They're symptoms of missing explicit agreement between human and agent before implementation. ## What SDD is **Spec-Driven Development (SDD)** puts the specification as a versioned artifact in the repository (not in chat history) and treats it as a contract between humans and agent. The spec says what the system must do, under what constraints, and what it must not do. The agent implements; humans review against the spec, not just the diff. In a bank or a company with regulated data, SDD is authentic paper in RPS: you pay upfront structure (specs, reviews, gates) for risk control. The real risk is data leaking to external APIs, hallucinations in production, or decisions nobody can audit six months later, not that the code looks ugly. It also isn't "always write a twenty-page PRD." It's choosing how much structure you need given team, domain, and cost of failure. ### Maturity levels | Level | Name | Idea | |-------|------|------| | 0 | Vibe-coding | Prompt in chat; requirements in lost history | | 1 | Spec-first ad hoc | Loose PRD, Markdown notes, Cursor Plan Mode, OpenCode in Plan mode | | 2 | Spec-anchored | Spec Kit, Kiro, OpenSpec, specs in repo, agent follows them | | 3 | Spec-as-source | Specs as living source of truth (deltas, archive, requirement→code traceability) | Most teams don't need level 3 from day one. But confusing level 0 with conscious rock is the usual mistake. ## Minimum constitution: `AGENTS.md` and project rules Before formal frameworks there's a minimal step: tell the agent how to behave in this repo. [`AGENTS.md`](https://agents.md/) is an emerging convention, Markdown at the project root with conventions, limits, and context any agent should read before touching code. It's portable across Cursor, Claude Code, Copilot, OpenCode, and most tools that respect project instructions. In Cursor, the practical equivalent is [project rules](https://docs.cursor.com/context/rules) (`.cursor/rules/`): files with conventions for this blog, for example, or style constraints. They don't replace a feature spec, but they stop every session from starting at zero. In RPS that's minimum rock: almost zero ceremony, maximum effect per line written. No formal workflow, deltas, or traceability. But it's a bridge: tomorrow you change IDE or agent and the project constitution travels with you. This repository doesn't have `AGENTS.md` yet; it uses Cursor rules. Adopting `AGENTS.md` would be the natural next step, same intent, more portability. ## OpenCode: the agent that starts at minimum rock It's worth separating SDD frameworks (OpenSpec, Kiro) from agents that do the work. [OpenCode](https://opencode.ai/) is an open-source agent (MIT licensed) built for the terminal, with desktop and IDE extensions. It doesn't replace a spec framework, but it belongs in this post because it bakes in several minimum-governance habits without extra ceremony. **`/init`** analyzes the project and generates an `AGENTS.md` at the root. It's the most direct bridge between "repo constitution" and tool: you don't write the file from scratch; you review it, adjust it, and version it. Same intent as Cursor rules, with a portable artifact other agents can read too. **Build and Plan modes** (toggled with Tab in the TUI) separate analysis from execution. In Plan, the agent is read-only: it explores code, proposes approaches, doesn't touch files. In Build, it writes and runs commands. That's not full SDD (no deltas or requirement→code traceability) but it's honest level 1: think before mutating. If you close the session without exporting the plan, you're back to vibe-coding with extra steps. **Provider-agnostic**: seventy-plus models via API (Anthropic, OpenAI, Google, GitHub Copilot) or local inference with Ollama. In RPS that's rock with a strong bridge: you pay little operational friction and choose who processes the code. The anchor depends on which model provider you connect, not on the agent itself. OpenCode doesn't store your code or context on its servers, relevant when the perimeter matters, though that doesn't replace criteria about what you send to the model. This connects to [digital sovereignty](/blog/005-digital-sovereignty/): the agent can be a bridge; a closed cloud model is still your decision. OpenSpec, Kiro, or Spec Kit define what the system should do. OpenCode (like Cursor or Claude Code) implements it. Governance comes from repo artifacts (`AGENTS.md`, specs, tests), not from the agent's name. ## OpenSpec: paper with a strong bridge [OpenSpec](https://github.com/Fission-AI/OpenSpec) is a tool-agnostic SDD framework aimed at brownfield and incremental change. Specs live in `openspec/specs/`; proposed changes in `openspec/changes/` with explicit deltas: ADDED, MODIFIED, REMOVED. The core idea: before implementing, the agent (or you) proposes a structured change. You review the delta spec, approve it, then implement. Afterward you archive the change and the base spec updates. Why it fits RPS as paper: * You pay structure for alignment and fewer surprises. * Artifacts are Markdown in your repo, not in a proprietary SaaS. * Works with Cursor, Claude Code, Copilot, or OpenCode without framework vendor lock-in. Strong bridge: the specs are yours; the agent is interchangeable. Low anchor: you depend on discipline to follow the workflow, not on a specific vendor. OpenSpec shines when the project already exists, changes are frequent, and you need the agent to not reinvent requirements every session. Commands like `/opsx:propose` or `/opsx:apply` speed the cycle; official docs cover the detail, here the criterion matters, not the syntax. ## Kiro SDD: paper with traceability, ecosystem anchor [Kiro](https://kiro.dev/) takes SDD further: structured workflow requirements → design → tasks, [EARS](https://alistairmavin.com/ears/) notation for requirements, specs in `.kiro/specs/`, and hooks that validate before and after the agent runs tools. Where OpenSpec is agnostic and light, Kiro bets on requirement→design→task→code traceability and deep integration in its own IDE and CLI. For regulated teams or projects where "who asked for this and why?" isn't rhetorical, that traceability is real paper, not theater. The RPS cost: * Anchor in the Kiro/AWS/Bedrock ecosystem: specs may be yours, but the full workflow assumes their tools. * Paper/scissors by context: paper if the domain demands audit; scissors if you adopt hooks, multi-agent flows, and full methodology for a one-person homelab. Kiro doesn't compete with OpenSpec on "which is better." They compete on how much institutional structure you need and how much anchor you accept in return. ## Kiro vs OpenSpec | Dimension | Kiro SDD | OpenSpec | |-----------|----------|----------| | Integration | Own IDE + CLI | Agnostic (Cursor, Claude Code, OpenCode, Copilot…) | | Format | EARS, `.kiro/specs/` | Deltas in `openspec/specs/` and `openspec/changes/` | | Strong at | Requirement→code traceability, hooks, regulated teams | Brownfield, incremental change, no vendor lock-in | | Anchor | Kiro/AWS/model ecosystem | Low, specs in your repo, any agent | | RPS | Paper/scissors | Paper with strong bridge | ## Other names on the map **GitHub Spec Kit**: GitHub's SDD reference with more ceremony than OpenSpec; useful for greenfield teams already living in GitHub. Paper with bridge (Markdown in repo), somewhat more anchor in the GitHub ecosystem. **Cursor Plan Mode**: light SDD without an extra framework: plan before executing, the agent follows the plan in the same session. Rock/paper depending on discipline: if you export the plan to a versioned file, it approaches level 1; if it dies when you close the chat, it's vibe-coding with extra steps. **OpenCode**: covered above: OSS agent, not an SDD framework. Stands out for `/init` → `AGENTS.md`, read-only Plan mode, and model agnosticism. Operational rock with bridges; deep tool comparison (IDE vs CLI, vs Cursor) belongs in the agent landscape post. **BMAD, Tessl, Antigravity**: heavier multi-agent or commercial methodologies. Scissors in RPS: maximum methodological potential, overkill risk if you're one person and the domain doesn't ask for it. Honest mention without in-depth review: I haven't used them in production. ## SDD in the RPS framework Back to the triad: | Approach | RPS | When it makes sense | |----------|-----|---------------------| | **Vibe-coding** | Degenerate rock | Disposable prototypes, one-hour spikes, real reversibility | | **`AGENTS.md` / rules** | Minimum rock | Project constitution; doesn't replace per-feature spec | | **OpenCode (Plan + `/init`)** | Rock / level 1 | Analyze before writing; generates `AGENTS.md`; interchangeable agent | | **OpenSpec / Spec Kit** | Paper | Brownfield, small team, frequent agent-driven changes | | **Kiro SDD** | Paper/scissors | Regulation, traceability, hooks before destructive commands | | **BMAD and similar** | Scissors | Large teams, explicit methodology, complex domain | The useful question is which strategy I'm using and whether the context justifies it, not which framework has the most stars. A bank team processing PII with cloud models and no SDD isn't on rock; it's on degenerate rock with legal consequences. A solo developer writing full Spec Kit for a 40-line script isn't on paper; it's on false paper. ### Bridges and anchors in SDD | Decision | Type | Why | |----------|------|-----| | Specs in Markdown in the repo | Bridge | Any agent, any IDE, diff in PR | | ADDED/MODIFIED/REMOVED deltas (OpenSpec) | Bridge | Auditable incremental changes | | PreToolUse/PostToolUse hooks (Kiro) | Bridge | Validation before execution; fewer surprises | | OpenCode `/init` → versioned `AGENTS.md` | Bridge | Portable constitution; any agent can read it | | OpenCode Plan mode (read-only) | Bridge | Analysis without mutation; doesn't replace feature spec | | Spec in Confluence + code in Git | Anchor | Agent doesn't see the spec; drift guaranteed | | Workflow that only works in one IDE | Anchor | Changing tools means redoing the process | | Vibe-coding with real data in the prompt | Anchor | Context leaves the perimeter; irreversible | This connects to [digital sovereignty](/blog/005-digital-sovereignty/): you can have impeccable specs and still leak data if you send repo contents to a closed model without criteria. SDD governs intent; cognitive sovereignty governs the perimeter. ## When you don't need SDD SDD is paper, and paper has cost: it isn't mandatory. Stay on vibe-coding (conscious rock, not degenerate) when: * The artifact is thrown away in hours or days. * Nobody else will maintain the code. * Failure is cheap and reversible. * You're exploring, not building. Move to `AGENTS.md` or rules when: * The project will live weeks or months. * You return with different agents. * There are conventions the agent breaks every session. Move to OpenSpec or equivalent when: * Code already exists and changes accumulate. * You need the agent to propose deltas before implementing. * Several people (or several sessions) must share the same intent. Move to Kiro or heavy methodology when: * Audit or regulatory requirements exist. * Agent commands can be destructive. * The cost of a production hallucination is unacceptable. ## What SDD doesn't solve SDD doesn't replace human review. It doesn't guarantee the agent follows the spec, only that you have something to measure breach against. It doesn't remove tests, CI, or acceptance criteria. And it doesn't pick the model for you: an excellent spec with an agent that can't see it is decoration. The leverage is knowing when the spec is worth the effort, and when it's noise. ## What comes next This post names governance: vibe-coding as degenerate rock, SDD as paper, and the spectrum between `AGENTS.md` and full frameworks. Other IAG series posts will get concrete on the tool landscape (Cursor, Kiro, Claude Code, OpenCode, IDE vs CLI) and homelab operations (compose → quadlet, SSO, diagnosis with a terminal agent), always with the same criterion: not recipes for their own sake, but knowing what you're paying for when you choose. You pay structure for alignment when the agent writes the code and you need intent to outlive the chat. ---------------------------------------- ### [Sovereign identity in the homelab: Zitadel and the RPS spectrum](https://escribano.dev/en/blog/009-homelab-identity/) > Application identity in the homelab, framed with RPS: basic auth as minimal stone, Authelia and forward auth for few users, Keycloak and Authentik as paper, and Zitadel as scissors (a cloud-native Go IdP, light and multi-tenant). Key difference: forward auth asks the proxy's doorman, an IdP speaks OIDC with you. Plus the spectrum of managed anchors, from social login to hyperscaler IAM. In the [reverse proxy post](/blog/008-caddy-reverse-proxy-pihole/) the stack already had access, names, TLS, and HTTP routing. But `https://grafana.lan` without login is an open door on the LAN (and sometimes beyond it if something failed at the perimeter). The fourth layer answers "who are you?": application identity, OIDC, forward auth. The VPN from [post 006](/blog/006-homelab-vpn-access/) answers "are you on my network?"; DNS from [007](/blog/007-sslip-io-homelab-dns/), "what's it called?". Mix them up and you stand up an IdP when the problem was reaching the NAS, or you expose Grafana to the internet when a tunnel would have been enough. I start from the [RPS principle](/blog/002-rps-principle/) and the bridge/anchor axis applied to homelab identity. The real case is Zitadel (just getting started, not pretending years in production) and the thread continues from [digital sovereignty](/blog/005-digital-sovereignty/) and [cloud-first](/blog/004-cloud-first-problem/). ## The question after the proxy Once you have a name, a certificate, and routing, the temptation is to lock down every panel *now*. Basic auth in Caddy, an Authelia container, or a full IdP with organizations, projects, and policies. All three close *a* login, and each one assumes a different problem. Authelia and OAuth2-Proxy do **forward auth**: the proxy asks a service whether the request may pass. A doorman at the edge, no central directory of applications. Keycloak, Authentik, and Zitadel are an **IdP**: apps speak OIDC or SAML to you. That difference matters more than the product name. In a personal homelab the order is still the one from this series: 1. **Network access**: [Tailscale, WireGuard, or OpenVPN](/blog/006-homelab-vpn-access/) 2. **Name resolution**: [sslip.io](/blog/007-sslip-io-homelab-dns/), MagicDNS, or Pi-hole 3. **TLS termination and HTTP routing**: [Caddy](/blog/008-caddy-reverse-proxy-pihole/) 4. **Application identity** ← here Jumping to 4 without the rest is scissors too early: platform identity for a service that doesn't even have a stable name yet. ## Two dimensions | Dimension | Question | |-----------|----------| | RPS | How much do you invest in identity architecture? | | Bridge / anchor | How much optionality do you keep to migrate tomorrow? | The same decision can be strategic stone and a strong anchor. Social login as the only factor is the usual example: zero ops today, the identity factor isn't yours tomorrow. ## RPS spectrum: self-hosted tools ### Stone: protect services, now Basic auth in Caddy or nginx (`htpasswd`) is the minimum stone: a file, a realm, and the browser asks for a username and password. Fine for a panel only you use. It doesn't scale to several apps with SSO, 2FA is poor or missing, and the day you share the homelab with someone else you start to hate the password list. **Authelia** (and OAuth2-Proxy) step up without making you an IdP administrator. Forward auth and 2FA in front of the proxy. The honest case is a personal homelab, few users, and services that don't really speak OIDC (or that you don't need to speak it). If Grafana, the NAS, and Home Assistant only need "don't let just anyone in on the LAN", Authelia is usually enough. ### Paper: serious IdP, controlled risk **Keycloak** is corporate paper: Java, heavy, predictable, the industry reference. You pick it when failure is expensive (several users, apps that already expect OIDC, someone else depends on login) and you accept running a server that isn't light. **Authentik** fills the same strategic slot with a different balance: full IdP, more modern DX, heterogeneous stack (Python and auxiliary services). More comfortable to live with in a homelab or a small team than Keycloak; it's still paper. You stand up a directory, flows, and you own it when it falls over. Same RPS strategy. Different price in RAM, learning curve, and how much it feels like an enterprise product. ### Scissors: identity as a platform **Zitadel** is born as a cloud-native IdP: event sourcing, multi-tenant by design, written in Go. In a homelab that shows up in the footprint (lighter than Keycloak, more compact than Authentik) and in the mental model: organizations, projects, applications, policies. Grafana becomes one more client. It makes sense when identity *is* the core: several real OIDC apps, SSO you want to own, or a project (a PWA, your own SaaS) that will live for years with those tokens. The risk is the usual scissors risk: overengineering if you're one person and three services behind forward auth. ### False paper Standing up Keycloak or Authentik when Authelia would have done. Standing up Zitadel when the problem was an `htpasswd`. It looks like paper (there's an IdP, a panel, dense docs) but you pay complexity the context doesn't ask for. Same pattern as [cloud-first](/blog/004-cloud-first-problem/) in miniature: the "serious" tool as the default. ## Managed anchors: by depth Not everyone self-hosts the IdP. The managed spectrum is a different scale; better not to mix it with the one above: ``` Shallower ────────────────────────────────────► Deeper Social login BaaS auth Cloud IdP Hyperscaler IAM (Google/GitHub) (Firebase, (Cognito, Auth0, (IAM + cloud stack) Supabase) Clerk) ``` | Layer | Typical RPS | Reversibility | |-------|-------------|---------------| | Social login only | Tactical stone | Anchor in the social provider | | Firebase / Supabase Auth | Stone (MVP) | Medium anchor depending on coupling | | Cognito, Auth0, Clerk | Paper (auth without ops) | Strong anchor | | Hyperscaler IAM | Enterprise paper | Maximum anchor | Supabase can be a bridge if you stay on Auth and standard Postgres; an anchor if the whole product lives there. Firebase anchors earlier. Cognito and Auth0 take ops off your plate and sell you an expensive exit. ## Social login "Sign in with Google" and nothing else is pragmatic stone: zero ops, the users already exist. It's also an anchor: you don't control the identity factor. If Google shuts the API, changes consent, or blocks your project, login dies and there's no directory to fall back to. The nuance I care about is different. Your own IdP (Zitadel, Keycloak, Authentik) with Google as an *identity broker*. Apps speak OIDC to you; you federate with Google. A partial bridge: tomorrow you drop Google and keep local users, or add another IdP, without rewriting every client. The anchor stays on the broker, not on each application. ## My experience with Zitadel I'll say it without posturing: just getting started. First impressions. I stood up Zitadel behind Tailscale, with MagicDNS for names and access. The perimeter is still the tailnet from [post 006](/blog/006-homelab-vpn-access/); the IdP isn't open to the internet. The double anchor: * **Tailscale** coordinates the network: operational bridge, anchor in the control plane unless you move to Headscale. * **Zitadel** holds users and issues OIDC on an instance I run: a bridge if you stay on open standards. Conscious anchor. The alternative I avoid is opening everything to the internet and handing login to Cognito "because that's the professional choice". The setup is conceptually dense. Organizations, projects, applications, redirect URIs, who the issuer is. An `htpasswd` doesn't prepare you for that map. IAG agents in the CLI cut the cost of the first configuration (the compose file, the Caddyfile, the silly Postgres error) and leave the mental model untouched. The agent leaves you a YAML that starts; understanding why an app doesn't get the claim you expected is still on you. In [SDD](/blog/015-spec-driven-development-iag/) I talk about that same gap between artifact and judgment. The operational case, a real agent against the homelab, is another post. What I noticed right away: Go shows. Zitadel takes less than Keycloak (Java) and less than Authentik (Python plus companions). On a mini PC already running Pi-hole and Caddy, you feel it. I would start from the [Zitadel docs with Caddy](https://zitadel.com/docs/self-hosting/manage/reverseproxy/caddy) and the stack you already have. Domains follow the same progression as in [post 008](/blog/008-caddy-reverse-proxy-pihole/): sslip.io with a self-signed cert to try things, DynDNS if you need it, a real domain and Let's Encrypt when the name is worth keeping. Pi-hole and ACME renewals are already covered there; the IdP doesn't invent a different network physics. ## When to use what Basic auth or Authelia is enough when: * It's you, or a household, and the services don't speak OIDC. * The problem is keeping strangers out, not federating five applications. * You don't yet know whether the homelab stays. Keycloak or Authentik when: * You already have several OIDC apps and shared login is the real problem. * You want an IdP many people have operated before, at the cost of weight (Keycloak) or stack (Authentik). Zitadel when: * Identity is part of the system you're building, not a padlock in front of Grafana. * You care about footprint and you'll live with a platform model rather than a doorman. * You can live with first impressions: the tool is young *in your stack*, not in the market. Stay on social login or a BaaS when the product is still an MVP and the cost of running an IdP outweighs an anchor you've already named. ## Honest limitations * An IdP doesn't replace the perimeter. Zitadel without a VPN (or with 443 open to the internet) is another surface, not more sovereignty. * Misconfigured OIDC is worse than `htpasswd`. A loose redirect URI or a misunderstood public client opens more than it closes. * Scissors feels great the first weekend. Three months later, if you only have three services, Authelia would have left you more time for the hobby. * I'm writing from the first setup, not from years of running this in earnest. ## What comes next With this layer, the stack I started in [access](/blog/006-homelab-vpn-access/) is named: network, names, TLS, identity. Each answers a different question. Mix them and you use Caddy to authenticate users or Tailscale to replace OIDC. The next thread leaves the rack: how you govern what the agent writes ([SDD](/blog/015-spec-driven-development-iag/)) and, later, how that same agent helps you operate the homelab without replacing judgment. Zitadel, Keycloak, or Authelia only make sense in your context. The useful question is which strategy you're playing and which anchors you accept. ---------------------------------------- ### [Reverse proxy in the homelab: Caddy, TLS, and when Pi-hole gets in the way](https://escribano.dev/en/blog/008-caddy-reverse-proxy-pihole/) > Caddy terminates TLS and routes HTTP by hostname in the homelab with minimal configuration. Mixing it with Pi-hole surfaces the real friction: split DNS to resolve differently from LAN and internet, rebind protection blocking sslip.io private IPs, and ACME renewals failing when the proxy queries the internal resolver. Includes a sample Caddyfile and dig-based diagnosis. In the [sslip.io post](/blog/007-sslip-io-homelab-dns/) the naming layer was solved in minimal form: hostnames that point at an IP without buying a domain or running BIND. But a name doesn't help much if every service still listens on its own port (`:3000`, `:8123`, `:9000`) and the browser still screams about self-signed certificates. The next layer is TLS termination and HTTP routing: a reverse proxy that listens on 443, presents a valid certificate, and distributes traffic by hostname. Caddy, nginx, and Traefik all do that; this post focuses on Caddy because it fits the personal homelab profile (automatic TLS, readable config, modest footprint) and because that's where the real friction shows up when you mix it with a local resolver like Pi-hole. The thread is the frame: what problem the proxy solves, what breaks when you add internal DNS, and how it fits the [RPS principle](/blog/002-rps-principle/). ## The stack so far Remember the order we've been building: 1. **Network access**: [Tailscale, WireGuard, or OpenVPN](/blog/006-homelab-vpn-access/) 2. **Name resolution**: [sslip.io as a bridge](/blog/007-sslip-io-homelab-dns/), MagicDNS, or Pi-hole 3. **TLS termination and HTTP routing** ← here 4. **Application identity**: OIDC, forward auth; [Zitadel and the RPS spectrum](/blog/009-homelab-identity/) The proxy doesn't replace VPN or DNS. It assumes you already know how to reach the network and resolve the name. Its job is making `https://grafana.192-168-1-47.sslip.io` hit the right container with a certificate the browser accepts. ## What a reverse proxy does (and doesn't) A reverse proxy listens on standard ports (80 and 443) and forwards traffic to internal services by rules: hostname, path, headers. In the homelab that means: | Without proxy | With proxy | |---------------|------------| | `192.168.1.47:3000` | `https://grafana.lan` | | Certificate per service (or none) | One certificate at the edge | | Remember ports | Remember names | What it doesn't do: authenticate application users (that's layer 4) or resolve DNS names (that's layer 2). Confusing layers leads to mounting OAuth in Caddy when you only needed routing, or exposing services without a perimeter when you still don't have VPN. ## Caddy in the RPS framework | Solution | RPS | What you pay | What you gain | |----------|-----|--------------|---------------| | Direct port access | Minimal stone | Exposed surface, no unified TLS | Zero proxy | | **Caddy** | Operational stone | Learning its DSL; coupling to the Caddy ecosystem | Automatic TLS, declarative config, low maintenance | | **nginx** | Stone / paper | More manual config; Certbot on the side | Maximum control, universal reference | | **Traefik** | Operational stone | Docker labels, routers mental model | Auto-discovery in containerized stacks | | **Cloudflare Tunnel** | Operational stone | Anchor in Cloudflare | No open ports; TLS managed outside | Caddy is operational stone in the good sense: TLS + routing with little ceremony. The cost is a soft anchor in the ecosystem (migrating to nginx isn't impossible, but you rewrite configs) and the temptation to use Caddy-specific features (`forward_auth`, modules) without evaluating reversibility. For a one-person homelab that already chose operational simplicity at the VPN layer, Caddy is the honest continuation. ## TLS: HTTP-01, sslip.io, and the private LAN In the previous post we saw that Let's Encrypt with [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) works with `203-0-113-7.sslip.io` if the public IP is reachable on port 80. Caddy automates it: you declare the hostname, Caddy obtains and renews the certificate. With private IPs (`192.168.x.x`) things change. Let's Encrypt can't reach your LAN from the internet. Real options: * Self-signed certificate: fast, the browser complains. * Internal CA: more work, viable on a controlled network. * DNS-01 with wildcard: scissors; sslip.io documents it, not the default path. * Being inside the VPN/tailnet with TLS managed by Tailscale, already covered in the [access post](/blog/006-homelab-vpn-access/). Caddy automates the dance when conditions are met; network physics is still there. ### Subdomains with sslip.io A common pattern: `grafana.192-168-1-47.sslip.io` points at the same IP as `192-168-1-47.sslip.io`; Caddy routes by hostname to the right backend. That gives you several services behind one proxy without Pi-hole or your own domain, as long as the IP doesn't change and you accept the name's aesthetics. ### Minimal `Caddyfile` example One block per hostname shows the pattern: automatic TLS, backend on the LAN. ```caddyfile { # Optional: email for Let's Encrypt notices email admin@example.com } grafana.192-168-1-47.sslip.io { reverse_proxy 192.168.1.47:3000 } homeassistant.192-168-1-47.sslip.io { reverse_proxy 192.168.1.48:8123 } ``` Caddy listens on 80 and 443, obtains certificates for each hostname, and forwards to the internal port. If the proxy host is `192.168.1.47` and backends live on other machines, change the `reverse_proxy` IPs: the sslip.io hostname still points at the proxy IP, not the backend's. With a private IP and no public reachability on 80, add `tls internal` inside the block to force Caddy's self-signed certificate (fine on the LAN, useless for visitors from the internet). ## Pi-hole enters the picture sslip.io resolves names without your own infrastructure. Pi-hole (with Unbound behind it) is the jump to sovereign DNS on the LAN: arbitrary records (`grafana.homelab`, `nas.lan`), ad blocking, full control of the resolver. The problem: mixing sslip.io with Pi-hole without a plan is false stone. Two resolvers, contradictory rules, and headaches that only show up when you deploy the proxy. ### Split DNS Split DNS means the same DNS question gets different answers depending on who asks: | Query | From the internet | From the LAN (Pi-hole) | |-------|-------------------|------------------------| | `app.mydomain.com` | Router's public IP | Proxy's internal IP (`192.168.1.47`) | Without split DNS, a device at home may resolve your domain to the public IP, leave via the router, attempt hairpin NAT, and fail, or take a slow, unpredictable path. With split DNS, the LAN resolves straight to the internal IP. Caddy needs client and proxy to agree on which hostname each service uses. If Pi-hole returns one thing and the public resolver another, the proxy issues certificates for a name that doesn't resolve the same way from inside. ### Rebind protection Pi-hole includes protection against [DNS rebinding](https://en.wikipedia.org/wiki/DNS_rebinding): it blocks answers that point to private IPs for public domains. It's a legitimate defense. But sslip.io returns private IPs by design (`192-168-1-47.sslip.io` → `192.168.1.47`). Pi-hole may block those answers. Symptom: the name resolves outside home and fails inside, or the reverse. Fix: whitelist trusted domains (`sslip.io`, `nip.io`) or disable rebind protection for those cases, conscious of the security trade-off. ### ACME renewals with Pi-hole When Caddy renews a certificate with HTTP-01, Let's Encrypt queries public DNS and connects to your public IP on port 80. If Pi-hole intercepts all DNS queries (including from the proxy host itself) and returns the internal IP for your domain, renewal can fail: LE tries to reach `192.168.x.x` from the internet. Patterns that work: * **Local DNS records** in Pi-hole for the public domain pointing at the internal IP for LAN clients only, plus a firewall rule letting the proxy answer on 80 from outside. * **Conditional DNS**: the proxy uses a different upstream resolver than clients for ACME queries. * **DNS-01** instead of HTTP-01 (more complex, avoids the hairpin problem). There's no single recipe; it depends on whether you have a public IP, CG-NAT, or tailnet-only access. Before blaming Caddy, diagnose with `dig` (@Pi-hole vs @8.8.8.8). ## Boot order and dependencies Another silent friction: Caddy starts and wants to renew certificates; Pi-hole isn't ready yet; the proxy resolves wrong and enters a retry loop. Or the reverse: Pi-hole depends on a container Caddy hasn't routed yet. Practical rules: * Pi-hole and the proxy on stable hosts (fixed IP or DHCP reservation). * `depends_on` in Docker doesn't guarantee the service is *ready*; use healthchecks if the stack is fragile. * Document which service must start first after a long power outage, don't rely on memory. ## RPS framework: bridges and anchors | Decision | Type | Why | |----------|------|-----| | Caddy with versioned `Caddyfile` | Bridge | Migrating to nginx is work, not redoing topology | | Let's Encrypt certificates in Caddy | Bridge | Standard; another ACME client can replace it | | Pi-hole as sole LAN resolver | Stone / anchor | Control and ops friction in exchange for DNS sovereignty | | Mixing sslip.io + Pi-hole + real domain without a map | False stone | Three sources of truth, debugging hell | | Cloudflare Tunnel as sole edge | Anchor | TLS and routing delegated; hard to return to pure self-hosted | Connects to [digital sovereignty](/blog/005-digital-sovereignty/): the proxy may live on your mini PC and certificates on Let's Encrypt, but if Pi-hole filters a renewal wrong, you lose TLS and the whole trust chain suffers. ## When to use what Caddy + sslip.io is enough when: * You're testing the stack before buying a domain. * You have a public IP and few services. * You mostly access over the tailnet with Tailscale TLS for critical stuff. Add Pi-hole (or Unbound) when: * You need dozens of arbitrary local names (`*.homelab`). * You want ad blocking across the LAN. * You have your own domain and need serious split DNS. Move to own domain + Caddy when: * The service is public or semi-professional. * sslip.io already feels fragile or ugly to share. * You need name stability independent of IP. ## Honest limitations * Caddy doesn't fix CG-NAT. Without a reachable public IP, HTTP-01 from the internet won't work; you need tailnet, tunnel, or DNS-01. * A misconfigured proxy is worse than none. Centralized attack surface; one `Caddyfile` mistake exposes everything. * Pi-hole + proxy + sslip.io + real domain can coexist, but needs an explicit map of which resolver answers what for whom. * Let's Encrypt has rate limits. Don't loop renewals against production; use staging or wait (five failed renewals per week and you start having problems). ## What comes next With name, certificate, and HTTP routing settled, the next question is who can log in: Grafana, your PWA, the admin panel. That's the identity layer (OIDC, forward auth, Zitadel vs Authelia vs "basic auth and done") in [post 009](/blog/009-homelab-identity/). The proxy terminates TLS; DNS names things; VPN gets you on the network. Without a map of which resolver answers what, the three layers step on each other. Each in its place, conscious anchors. ---------------------------------------- ### [Names without buying a domain: sslip.io in the homelab](https://escribano.dev/en/blog/007-sslip-io-homelab-dns/) > sslip.io and nip.io embed the IP in the hostname: 192-168-0-42.sslip.io resolves to 192.168.0.42, with nothing to register. Use it to name homelab services without a domain and, with a public IP, get Let's Encrypt TLS via HTTP-01. Operational stone with a soft anchor: you depend on someone else's public DNS. In the [homelab access post](/blog/006-homelab-vpn-access/) the first layer was reaching the network: Tailscale, WireGuard, or OpenVPN. But getting on the LAN doesn't answer the next question: how do you call a service without memorizing IPs? `192.168.1.47:3000` works until it doesn't. The container IP changes, you add a second service, or you want a valid TLS certificate and suddenly you need names. Editing `/etc/hosts` on every device is stone at its worst: fast for a test, unsustainable for a whole stack. This is the middle layer many homelabs skip: pragmatic DNS with [sslip.io](https://sslip.io/) (and its conceptual predecessor, [nip.io](https://nip.io/)), names that resolve to the IP written into the hostname. ## What sslip.io is [sslip.io](https://sslip.io/) is a public DNS service with a simple rule: if the hostname contains an IP address encoded with dashes, the answer is that IP. | Hostname | Resolves to | |----------|-------------| | `192-168-0-42.sslip.io` | `192.168.0.42` | | `app.192-168-0-42.sslip.io` | `192.168.0.42` | | `203-0-113-7.sslip.io` | `203.0.113.7` (example public IP) | You register nothing. You configure no DNS zone. The name is the record. nip.io follows the same principle; today both services are unified under the project maintained by [cunnie](https://github.com/cunnie/sslip.io). For local development, demos, or a homelab that doesn't have its own domain yet, that removes a huge friction: any machine with outbound DNS can resolve the name without touching your router or running BIND. ## Why it matters in the homelab stack Remember the order from the VPN post: 1. Network access 2. **Name resolution** ← here 3. TLS termination and reverse proxy 4. Application identity sslip.io covers step 2 in minimal form. It doesn't replace full internal DNS (you don't get arbitrary `A` records like `grafana.lan`) but it does solve *"I want a hostname pointing at this specific IP"* without ceremony. ### What Tailscale already gives you (and doesn't) If you use Tailscale, **MagicDNS** resolves names inside the tailnet (`minipc.tail12345.ts.net`) without sslip.io. That's operational stone with an anchor in Tailscale coordination; I covered that in the previous post. sslip.io fits when: * You want names inside the LAN without depending on the tailnet, other devices, guest VLANs, CI on the same network. * You need a hostname Let's Encrypt recognizes via HTTP-01 on a public IP (or a service reachable from outside). * You're testing a reverse proxy or self-hosted PaaS (Coolify, CapRover…) before buying a domain. MagicDNS and sslip.io don't compete; they solve different layers. You can use both. ## TLS: why many people end up here A valid TLS certificate requires the certificate authority to verify you control the name. With a normal domain, Caddy or Certbot handle that in minutes. Without a domain, the usual dead end was a self-signed certificate, and the browser screaming. With sslip.io and a public IP reachable on port 80, the [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) challenge works: Caddy (or nginx, or Traefik) serves on `203-0-113-7.sslip.io`, Let's Encrypt checks the token, and issues the certificate. That's the use case sslip.io's own documentation highlights. With private IPs (`192.168.x.x`) the picture changes: Let's Encrypt can't reach your LAN from the internet. sslip.io still resolves names on the local network, but the certificate will need to be self-signed, from an internal CA, or obtained another way, for example, from inside the VPN or using a wildcard with DNS-01 and your own authoritative server, which is real scissors territory and deserves its own post. I won't detail the `*.external-ip.sslip.io` wildcard dance with DNS-01; the [project docs](https://github.com/cunnie/sslip.io/blob/master/docs/wildcard.md) cover it. Enough to know it exists and isn't the default path. ## In the RPS framework | Approach | RPS | What you pay | What you gain | |----------|-----|--------------|---------------| | Manual `/etc/hosts` | Minimum rock | Edit every machine | Zero infra | | **sslip.io / nip.io** | Operational rock | Dependence on someone else's public DNS | Instant names, TLS possible with public IP | | **Pi-hole / local Unbound** | Rock / paper | Operate and maintain the resolver | Local names (`*.homelab`), ad blocking, control | | **Own domain + DNS** | Paper | Money, zone management, renewals | Brand, portability, real sovereignty | | **MagicDNS (Tailscale)** | Operational rock | Anchor in coordination | Zero config inside the tailnet | sslip.io is operational rock in the good sense: it solves today's naming problem without requiring a domain or your own DNS server. The cost is a soft anchor: you depend on `sslip.io` keeping resolution working and on your homelab not needing DNS records the scheme doesn't support. A bridge to something that works while you decide whether a domain or Pi-hole is worth it, not DNS sovereignty. ### Bridges and anchors | Decision | Type | Why | |----------|------|-----| | Hostname `service.IP.sslip.io` in versioned configs | Bridge | Tomorrow you switch to your own domain; the pattern is explicit | | sslip.io DNS authority run by a third party | Anchor | If the service goes away or changes, your names stop resolving | | [Self-hosted sslip.io DNS server](https://github.com/cunnie/sslip.io) | Bridge | Same logic, your infrastructure | | Mixing sslip.io with Pi-hole without criteria | False rock | Two resolvers, split DNS, headaches, that's the next post | This connects to [digital sovereignty](/blog/005-digital-sovereignty/): the data layer can live on your NAS while the name resolver is a public service you don't even own. Conscious is fine; invisible is a trap. ## When to use sslip.io It makes sense when: * You're building the homelab and don't want to buy a domain yet. * You need to test HTTPS with Let's Encrypt on a public IP without DynDNS. * A service requires a hostname instead of an IP (OAuth, cookies, WebSockets) and it's temporary. * You want to document or share a demo without touching anyone's DNS. Move on when: * The service is public and professional, it deserves its own domain. * You need dozens of arbitrary local names, Pi-hole or Unbound. * An external resolver is unacceptable by policy, internal DNS or self-hosted domain. * You already have Tailscale and only access from the tailnet, MagicDNS may be enough. ## Honest limitations * Not generic DNS. You don't create `db.internal` pointing elsewhere; the name must embed the IP (except advanced wildcard tricks). * External dependence. Not Cloudflare, but not yours either. * Privacy. Every lookup goes out to sslip.io's resolver; on an isolated LAN that may matter. * Aesthetics and stability. `grafana.192-168-1-47.sslip.io` isn't a product name; if the IP changes, the hostname changes. ## What comes next sslip.io names the minimal resolution layer. The natural next step in this stack is TLS termination and HTTP routing (reverse proxy with Caddy or similar) and that's where real friction appears when you mix a local resolver (Pi-hole) with the proxy: split DNS, rebind protection, boot order. That's another post. Application identity (OIDC, forward auth) comes after services have names and certificates. sslip.io is operational rock with a conscious anchor: a bridge that lets you move forward without pretending you already have DNS sovereignty when you only have an elegant trick with dashes. ---------------------------------------- ### [Homelab access: Tailscale, WireGuard, and OpenVPN](https://escribano.dev/en/blog/006-homelab-vpn-access/) > Remote access is the homelab's first layer, before DNS, proxy, or identity. OpenVPN is legacy paper, manual WireGuard is stone with full control, Tailscale is operational stone with a conscious anchor (their coordination plane), and Headscale is scissors with a bridge. With CG-NAT in the way, keeping the hobby alive matters more than architectural purity. In the posts on [cloud-first](/blog/004-cloud-first-problem/) and [digital sovereignty](/blog/005-digital-sovereignty/) I talked about data, infrastructure, and cognition. But before DNS, reverse proxy, or identity there's a more basic question: how do you reach your network from outside? A homelab without controlled remote access is a collection of LAN services that only exist when you're home. A VPN (or its modern equivalent) is the first practical layer of network sovereignty: you decide who gets in, through which tunnel, and under whose coordination. I'm not writing this to say you should expose services to the internet with port forwarding and hope for the best. I'm writing to place OpenVPN, WireGuard, and Tailscale in the [RPS framework](/blog/002-rps-principle/) and the bridge/anchor axis, with the same question as always: *what are you paying for when you choose?* ## The question before login It's tempting to start a homelab with identity, standing up an IdP, federating Google, protecting Grafana with OIDC. But identity answers *"who are you?"*; VPN answers *"are you on my network?"* They're different layers. Confusing them leads to deploying Zitadel or Keycloak when the real problem was reaching the NAS from your phone without opening ports. Or exposing every service on 443 to the internet when a private tunnel would have been enough. In a personal homelab, the sensible order is usually: 1. **Network access** (this post) 2. **Name resolution** (internal DNS; a later post) 3. **TLS termination and HTTP routing** (reverse proxy) 4. **Application identity** (IdP, forward auth; see the [Zitadel issue](https://github.com/escribanoruben/escribano.dev/issues/30)) Skipping step 1 and publishing services directly is the worst kind of stone: fast today, attack surface tomorrow. ## Two dimensions again As with identity or cloud, it's worth separating: | Dimension | Question | |-----------|----------| | RPS | How much network infrastructure do you build and operate yourself? | | Bridge / anchor | Can you change solutions without redoing your whole topology? | A VPN can be operational stone (low friction to configure) and a strong anchor: if the coordination plane isn't yours. Tailscale is the usual example. ## OpenVPN: historical paper For years OpenVPN was the default answer to self-hosted remote access. X.509 certificates, tun/tap topologies, `.ovpn` files, renewals, sometimes a heavy server and configs nobody wants to touch six months later. In the RPS frame it's legacy paper: you pay upfront complexity and ongoing maintenance for a mature protocol, widely supported and understood by corporate firewalls. It isn't elegant. But for a long time it was realistic when failure (not being able to reach the backup server) was expensive. It still shows up from inertia: old tutorials, routers with built-in clients, companies that standardized on it a decade ago. Keeping it out of habit when WireGuard or a modern mesh covers your case is the same pattern as cloud-first: paying complexity the problem no longer asks for. Self-hosted OpenVPN on your VPS or at home is usually a bridge if you control certificates and configuration: you can migrate clients tomorrow. But operational cost is high for a one-person homelab. ## WireGuard: modern stone at the protocol level WireGuard changed the baseline. Minimal design, modern cryptography, Linux kernel implementation, small codebase (written in C, with user-space tools in Go) and excellent performance on modest hardware. In RPS it's stone in the good sense: it does one thing well with little noise. It doesn't give you MagicDNS, an admin panel, or device identity; it gives you point-to-point or hub-and-spoke tunnels with static keys or rotation you design yourself. Homelab advantages: * Minimal footprint; runs on a Raspberry Pi or in a container without drama. * Explicit configuration: you know which peer is which machine. * **Strong bridge**: open protocol, multiple implementations, no vendor behind the tunnel itself. Cost: * You manage internal IPs, DNS (if you want names instead of IPs), high availability, and onboarding for each new device. * Without a coordination layer, scaling to ten devices is tedious; at thirty, it's a mini-project. Pure WireGuard is the honest choice when you want maximum control and minimum dependence, and accept friction on every new client. ## Tailscale: operational stone, anchor in coordination Tailscale builds a mesh network on top of WireGuard. Install the client, authenticate the device with your identity (Google, GitHub, Microsoft, etc.), and suddenly your machines see each other (with MagicDNS, optional ACLs, subnet routers, exit nodes) without opening ports on the router. ### The big advantage: no router port forwarding The operational advantage that matters most in a real homelab isn't technical, it's human: you don't need to forward ports on your router. For years homelab tutorials assumed you knew how to open the router UI, create a NAT rule, point 443 or 1194 at your server's IP, and hope your public IP wouldn't change. Today that workflow is unfamiliar to many people, especially newcomers with a Raspberry Pi or mini PC who want to see if the hobby sticks before investing months. But the problem goes beyond lack of practice. Port forwarding alone isn't enough anymore. A huge share of home connections (fiber and mobile, in Spain and much of Europe) sits behind CG-NAT (*Carrier-Grade NAT*): your router doesn't have its own public IPv4; it shares an address with other ISP customers. From the outside there's no reliable way to reach your homelab even with a perfect port-forward rule. The classic fix is calling your ISP and asking to be removed from CG-NAT, or to get a public IPv4. Sometimes it works; sometimes it means changing plans, waiting, arguing with tier-one support who've never heard of CG-NAT, or finding out your provider simply doesn't offer it. That's disproportionate friction for someone who just wants to reach their NAS from the train. Tailscale (and mesh solutions in general that negotiate exit via relay or hole punching) sidesteps that dead end. It doesn't replace the sovereignty of having a public IP; it removes the prerequisite of having one for the homelab to be useful. ### Hobby continuity vs. architectural purity I personally prefer accepting Tailscale's coordination anchor over risking the homelab's continuity, especially for newcomers. A homelab you can't reach from anywhere loses half its value: it stops being your personal cloud and becomes a box that only exists when you're in the living room. The abandonment curve is predictable: frustration, months without touching it, and that Raspberry Pi or mini PC ends up in a drawer, healthy hardware, dead project. That doesn't invalidate manual WireGuard or Headscale when you have the context and patience. But demanding port forwarding or CG-NAT escape before the hobby proves its worth is asking for architectural purity at the cost of a real chance of failure. In the RPS frame, Tailscale is operational stone with a conscious anchor; abandoning the homelab because you can't get in is stone misunderstood: you saved dependence and lost the whole system. ### MagicDNS, HTTPS, and a shorter stack The second big advantage shows up when you start naming and serving things, not just connecting. MagicDNS resolves your machine names inside the tailnet (`minipc.tail12345.ts.net`, for example) without running DynDNS, DuckDNS, or a cron job that updates your public IP every time the router reboots. For access between devices on your private mesh, that problem goes away. To a large extent, so does the Certbot / Let's Encrypt / manual renewal ritual. Tailscale can issue valid TLS certificates for your tailnet hostnames (with Let's Encrypt underneath, but managed by them) so your services answer on HTTPS without you operating the PKI. Combined with Tailscale Serve (serving HTTP/HTTPS from the node itself), friction drops another notch. That unlocks cases that in a classic homelab felt like overkill to set up just to experiment. For example, deploying a PWA on a home server and installing it in the browser on your phone or laptop without fighting self-signed certificates. Browsers require a secure origin to treat a site as installable: you need HTTPS and a recognizable domain. A LAN IP (`192.168.1.50`) won't do, plain HTTP won't do, and the "your connection is not private" warning isn't something most users know how to bypass. With a `*.ts.net` name and managed TLS, that requirement is met inside the tailnet with little friction. Tailscale lends you the domain and certificate; you don't own them. That's operational convenience, another face of the anchor. The day you want your own domain on the public internet, a reverse proxy with Caddy and your own Let's Encrypt still makes sense; I'll cover that in a later post. But for services that are only yours, reachable from your devices, the MagicDNS + tailnet HTTPS combo skips an entire stack that for years was the homelab initiation ritual. For a personal homelab it's operational stone: the "I want in from my phone" problem is solved in minutes. No PKI. No hand-editing `wg0.conf` for every laptop. But the coordination plane (who issues keys, who maintains the network map) lives on Tailscale Inc.'s servers, unless you use a plan that lets you self-host the control plane (not the typical free homelab case). That's conscious anchoring: * You depend on their coordination service and their policies. * MagicDNS and `*.ts.net` names are convenient (and they avoid DynDNS and, on the tailnet, much of the Certbot dance) but they aren't your sovereign DNS; they're a shortcut that works while you accept the model. * Initial device authentication usually goes through a social or external SSO provider, another delegation layer worth naming. I'm not saying this to demonize Tailscale. I use it in my own homelab as the access layer before standing up application identity with Zitadel. It's the right call when the cost of not having remote access outweighs the cost of delegating coordination. But it's an anchor: worth knowing. ## Headscale: scissors with bridge Headscale is an open reimplementation of Tailscale's coordination server. Same idea (mesh over WireGuard) but you operate the control plane. In RPS it's scissors: maximum sovereignty potential within the mesh model (without depending on Tailscale Inc.'s coordination servers) in exchange for more complexity and operational risk. You update the server, debug why a node won't register, own backups and availability. It's building your own coordination platform while keeping the protocol and clients you already know. In reversibility it's still a bridge: the tunnel remains standard WireGuard; you recover independence from the SaaS without abandoning the mesh mental model. But the price in time and attention is scissors-level, not paper. It makes sense when Tailscale's anchor stops being acceptable (by policy, jurisdiction, or principle) and you've already validated that the mesh delivers value. Jumping to Headscale on day one, without stable remote access, is usually scissors misapplied: platform complexity before the hobby has proved what it's for. ## Comparison at a glance | Solution | Typical RPS | Reversibility | Homelab profile | |----------|-------------|---------------|-----------------| | **OpenVPN** | Legacy paper | Bridge if self-hosted | Compatibility, inertia, heavy maintenance | | **WireGuard** (manual) | Stone | Strong bridge | Full control, more friction per device | | **Tailscale** | Operational stone | Anchor in coordination | Fast access, MagicDNS, low ops | | **Headscale** | Scissors | Bridge | Mesh with self-hosted control plane; more ops, more sovereignty | ## Bridges, anchors, and public exposure Opening router ports (443 for Plex, 8123 for Home Assistant, whatever) is direct exposure, not a VPN. And in many homes it isn't even viable without escaping CG-NAT first. Sometimes combined with Cloudflare Tunnel or other relays; the pattern is the same: your service is reachable from the internet without the visitor going through your private network. That can be legitimate stone for one specific service. But as the default strategy for the whole homelab it reproduces cloud-first in miniature: convenience now, attack surface and dependence on third parties (DNS, CDN, certificates) later. A VPN or private mesh doesn't replace reverse proxy or IdP. It gives you perimeter: first you enter the network, then you resolve names, then you authenticate to applications. ## When to move from stone to paper (or scissors) Stay on manual WireGuard or Tailscale when: * You're one person or one household, few devices, no compliance requirements. * The problem is getting home, not segmenting a company. Move to Headscale (scissors) when: * Tailscale's coordination anchor is no longer acceptable and you want the mesh without the SaaS. * You already have working remote access and the next step is control-plane sovereignty, not learning VPN from scratch. * You accept operating, updating, and debugging the coordination server yourself. Reserve other scissors (serious hybrid topologies, multiple sites, routing between VLANs, policy-driven exit nodes, integration with corporate identity) for when the homelab stops being a hobby and becomes infrastructure others depend on. ## What comes next This post names the access layer. Next posts will get into the rest of the stack I promised in [cloud-first](/blog/004-cloud-first-problem/): DNS, reverse proxy, and identity. Access with Tailscale (or WireGuard) doesn't answer *"what is this service called?"* or *"who can log into Grafana?"*; it only answers *"am I on the network where Grafana lives?"* The question is who coordinates your network, what anchors you accept, and when the stone of a managed mesh is more honest than the scissors of an IdP you don't need yet. ---------------------------------------- ### [What is digital sovereignty](https://escribano.dev/en/blog/005-digital-sovereignty/) > Digital sovereignty is three layers: data (where it lives and whether you can export it), infrastructure (who runs your software), and cognition (which AI model processes your information). AI adds a spectrum of choices: closed API, open-weight API, or local inference. What matters is the cost of leaving each technology, not just the cost of entering. # What is digital sovereignty As I previewed in the [cloud-first post](/blog/004-cloud-first-problem/), for me digital sovereignty means being able to decide over data, infrastructure, and AI models, without a vendor, a past decision, or market inertia making those choices for us. I'm not trying to write an anti-cloud manifesto. What interests me is something much more practical: understanding which parts of our stack we actually control, which we've delegated, and above all whether that delegation is reversible. The debate on digital sovereignty often focuses on data jurisdiction, regulation, or technological dependence between countries. But here I want to talk about an aspect closer to the day-to-day of those of us who develop software: the ability to revisit a decision when circumstances change. I like to think of digital sovereignty as three complementary layers. Two of them (data and infrastructure) we've known well forever: * **Data.** Where it's stored, who can access it, and whether you can export or delete it without depending on a third party. * **Infrastructure.** Who runs the software, with what tools, and how much it would cost to migrate it elsewhere. But now, with the rise of generative AI, there's a third layer: * **Cognition.** Which AI models process your information and to what extent you can switch models, providers, or even run them yourself. You can have your own VPS, run free software, and manage your own services. But if every document, every conversation, and every snippet of code ends up processed exclusively by a closed model, your sovereignty remains partial. In the earlier posts ([introduction](/blog/001-introduction/) and [cloud-first](/blog/004-cloud-first-problem/)), free software, open formats, and self-hosting form the foundation. But self-hosting a service doesn't guarantee sovereignty if, for example, your service requires a third-party API you don't control and that would be costly to replace if it stopped being available in your country tomorrow or changed its pricing. As I noted when discussing the [RPS principle](/blog/002-rps-principle/), decisions that preserve the ability to change are **bridges**; those that make it hard to abandon a technology are **anchors**. Free software and self-hosting usually build bridges in infrastructure. Open-weight models can do exactly the same in the cognitive layer. ## Cognitive sovereignty The arrival of AI adds a new question: who actually runs the model that processes your information? Closed models like GPT, Claude, or Gemini offer very strong capabilities, but the user depends entirely on the provider: you can't run those same models on your own or move them to an independent operator. Open-weight models work differently. If you work with families like Qwen, Gemma, DeepSeek, or Kimi, you can use the original developer's API, turn to different inference providers, or (if your hardware allows) run exactly the same model on your own infrastructure. The model stays; who runs it changes. Choosing an open-weight model keeps more options open for the future: today you can consume an external API, and tomorrow decide to move that same workload to your own server without having to rebuild your entire integration. A brief aside: some open-weight model licenses include commercial restrictions, but they usually target use at scale (a SaaS, a company with hundreds of users) rather than personal use. Even so, it's worth checking each license: not all follow that pattern. Cognitive sovereignty doesn't require setting up a multi-GPU server from day one. There's a reasonable spectrum of decisions: 1. A closed model API. 2. An open-weight model API. 3. Local inference (open-weight model). Option one anchors you to a specific model you don't control. Option two keeps the same model but lets you switch providers or bring it in-house. Option three offers maximum control, in exchange for more operational cost. When we evaluate a technology, we usually focus on the cost of entry: how long it will take to get running, how much it costs per month, or what the learning curve looks like. However, we rarely think about the cost of leaving it. Migrating a database, replacing an email provider, or swapping out an AI model can take weeks or months of work. The higher that cost, the less real capacity we'll have to decide in the future. In any case, we can always opt for hybrid solutions, for example, using a local model for most requests and reserving frontier models for one-off cases where extra power is needed. ## My experience with local inference My experience with local inference is limited, but it has helped me calibrate expectations. With **Gemma 4** (4B) and **SmolLM3** (3B) on CPU via Ollama, small models are usable, though slow; a GPU completely changes the experience. You don't need powerful hardware to get started: without a GPU you can already experiment and build personal tools; with a mid-range one, many open models are enough for development, writing, or automation. For more demanding tasks (especially agentic flows) I've chosen a midpoint on the spectrum: **open-weight models consumed via API**, specifically **`glm-4.7-flash`** and **`deepseek-v4-flash`** through Ollama Cloud. These are models I could, in principle, run on my own hardware (something like a Mac Mini) if I had the kit. I don't today, so I use the API: I'm still handing inference over to a third party. The bridge isn't sovereignty in current use, but the exit door: if tomorrow I have access to a machine with enough unified memory (or a GPU with 24 GB of VRAM), I can download the same weights and migrate inference with little to no change to the integration. That's the difference between a bridge and an anchor. ## When a closed model still makes sense Digital sovereignty doesn't mean rejecting closed models. They're still the best option when they offer capabilities that don't yet exist in the open-weight ecosystem, when the cost of being wrong is high, or when it's a one-off use. The problem isn't using them, but letting them become the default without evaluating whether a more open alternative would be enough. The same criticism that can be made of *cloud-first* can also be made of *LLM-first*: delegating out of habit rather than out of necessity. It's not about self-hosting everything or avoiding every commercial service, but about preserving the ability to choose. Free software, self-hosting, and open-weight models lower the cost of switching; closed models and big cloud will still solve many problems better than home infrastructure. The question isn't forbidding yourself from using them, but preventing initial convenience from turning into permanent dependence. If you enjoyed the post (or disagree on something), feel free to comment. ---------------------------------------- ### [Tales Vault System: random first, memorable second](https://escribano.dev/en/blog/011-tales-vault-system/) > Tales Vault System (tale) inverts the passphrase flow: random letters first from a CSPRNG (20 characters, ~85 bits), then mnemonic phrases derived with a fixed template (noun, adjective, verb, noun) per block of 4 letters. Lowercase only for easier typing and dictation, en and es dictionaries embedded in an offline Go binary. You recover the password by concatenating the initials of each phrase. In the [previous post](/blog/010-password-entropy-length/) we saw that entropy only matters when randomness is real. A password manager solves 99% of cases. But for the master password, laptop boot, or the few passwords you need to memorize without a sticky note, the question remains: how do you generate entropy without falling into the bias of picking words that "sound good"? **[Tales Vault System](https://github.com/escribanoruben/tale)** (the [`tale`](https://github.com/escribanoruben/tale) repository) inverts the usual flow. Password first (CSPRNG). Mnemonic story second, derived with fixed rules. You do not choose the letters; in interactive mode you only pick among valid word candidates if you want to customize the phrase. ## The problem a manager does not solve A manager stores a hundred passwords. But someone has to remember the key that unlocks the manager. And sometimes you must type a password in an environment where pasting from the clipboard is not an option. Common solutions fail for the same reason: | Approach | Flow | Problem | |----------|------|---------| | **"My favorite phrase"** | Phrase → password | Human patterns, low entropy | | **Leetspeak** | Word + substitutions | Modern cracking anticipates it | | **xkcd without dice** | Four words you pick | Skewed distribution by initial letter | | **Diceware** | Random words → phrase | Works; see [diceware.dmuth.org](https://diceware.dmuth.org/) | Diceware is solid (and [diceware.dmuth.org](https://diceware.dmuth.org/) is an excellent implementation that even links the xkcd strip in its FAQ). Tales Vault targets a different balance: a short random letter sequence (more bits per typed character) with grammatical phrases to recover it. ## How it works ``` [CSPRNG] → password (20 chars, language alphabet) ↓ blocks of 4: jnra | ngtb | ... ↓ noun + adjective + verb + noun (per block) ↓ password + phrases + recovery rules ``` Real example from `tale generate --language en`: ``` Password: pwtiscdobsonvdpnudwt Mnemonic phrases: [pwti] people whole take information [scdo] state certain did order [bson] book such obtained new [vdpn] value different put number [udwt] use due was time Entropy: ~85.0 bits ``` Each word contributes its normalized initial (`árbol` → `a` in Spanish profiles; `ñ` excluded from the Spanish alphabet). Concatenate initials per block and you recover the password. ## Design principles | Principle | Detail | |-----------|--------| | **Randomness first** | The key comes from a cryptographic generator; the phrase is derived, never the reverse | | **Restricted alphabet** | Top 20 most frequent word-initial letters per language, maximizes dictionary coverage | | **Fixed template** | `noun → adjective → verb → noun` per 4-letter block | | **No letter picking** | The user does not invent initials; the TUI only allows choosing among valid words | | **Lowercase only** | No `Shift` or layer switching; faster on mobile and clearer when dictating | | **Local-first** | Word lists embedded in the binary (`go:embed`); no network at runtime | | **Single binary** | ~3 MB with curated EN + ES dictionaries (~13k words) | The restricted alphabet slightly reduces per-character entropy versus 26 lowercase letters, but length compensates: 20 characters over ~20 letters yield ~85 bits, enough for a master password or a few critical memorized passwords. In Spanish, the uneven distribution of words by initial letter (which we described as human bias in the previous post) is handled explicitly here: the alphabet is computed from the corpus, and `tale stats` shows real coverage per letter (`a`: 360 words, `q`: 48). ## Lowercase only Tales Vault generates passwords in lowercase on purpose. It targets the scenarios where typing a memorized key hurts most. In the [previous post](/blog/010-password-entropy-length/) we already covered the cost of `Shift`, symbols, and uppercase on a physical keyboard. Here the decision is product-level: - **One fewer keystroke per character.** Each uppercase letter needs `Shift` or a layer change. Over twenty characters, avoiding uppercase removes up to twenty extra presses and the risk of hitting the wrong key. - **Mobile.** On a touch screen, switching between upper- and lowercase slows entry a lot. An all-lowercase key stays on the default layer with no mode hopping. - **Dictation.** If you say the password out loud (backup with someone you trust, rehearsal while memorizing) you do not need to clarify "that's uppercase" or spell with ambiguity. `jnrangtbtljnlbiqnhqm` is read letter by letter, always the same. Entropy is not wasted: the alphabet has ~20 letters and the default length is 20 characters (~85 bits). More length, one case, a deliberate trade-off versus mixing uppercase, digits, and symbols that nobody actually types well in practice. ## The `tale` command The repository ships a Go binary named `tale`. Minimal install: ```bash git clone https://github.com/escribanoruben/tale cd tale make build # or: go build -o tale ./cmd/tale ``` ### Automatic generation ```bash ./tale generate --language es ./tale generate --language en --length 16 ``` Full random mode: password, phrases, estimated entropy, and recovery rules on stdout. No secrets stored to disk by default. ### Interactive mode (TUI) ```bash ./tale tui --language es ./tale tui --language en --password abcdefghijklmnopqrst ``` Terminal UI ([Bubble Tea](https://github.com/charmbracelet/bubbletea)) with a roller-style picker: for each letter in a block, you choose among words whose initial matches. Useful if you want a more personal phrase without touching letter entropy, those are already fixed. ### Dictionary statistics ```bash ./tale stats ``` Shows alphabet, words per letter, and part of speech. A transparency tool: verify language coverage before trusting the generator. ### Languages | Language | Code | Alphabet (20 letters) | |----------|------|-------------------------| | English | `en` | `abcdefghilmnoprstuvw` | | Spanish | `es` | `abcdefghijlmnopqrstv` | Custom languages under `~/.tale/` are also supported (`tale language new`, `tale language init`). Built-ins load embedded lists; custom ones require filling the corresponding TSV files. ## Local-first and [digital sovereignty](/blog/005-digital-sovereignty/) Word lists travel inside the binary. No API calls, no "generate your password in the cloud." In contexts where sending secrets to third parties is unacceptable (or when you simply want to audit which words the system can propose) that matters. In the [RPS framework](/blog/002-rps-principle/), Tales Vault is pragmatic stone: one binary, zero deployment ceremony, maximum transparency over the dictionary. It does not pretend to be an identity platform or a full manager. ## Honest limits - Does not replace a password manager. It is meant for a few passwords you memorize yourself. - **Slightly lower entropy** than 20 pure lowercase letters due to the trimmed alphabet, documented in the output. - **Dictionary coverage.** Blocks without valid candidates are regenerated; in Spanish some letters have fewer words than others. - **TUI mode and bias.** If you always pick the "prettiest" word among candidates, you introduce bias in memorability, not in the letters, but the phrase is no longer the one automatic scoring would have chosen. - **Project license:** TBD in the repository; word list sources have their own licenses (see `internal/worddata/README.md`). ## Diceware and [diceware.dmuth.org](https://diceware.dmuth.org/) Before talking about `tale`, Diceware done right deserves its place. [diceware.dmuth.org](https://diceware.dmuth.org/), by Douglas Muth, is the web reference most people know: virtual dice rolls, [EFF](https://www.eff.org/dice) word list, passphrase generated in the browser without sending anything to a server. Four random words ≈ 44 bits; six ≈ 66. The words *are* the password: you remember them and type them as-is. Cases where Diceware shines (the site lists them): smart TVs, awkward keyboards, someone else's machine without a manager installed. Typing `correct horse battery staple` is slower than twenty letters in a row, but each word is a recognizable mental chunk. Tales Vault System does not compete with that; it solves a different profile: | | [diceware.dmuth.org](https://diceware.dmuth.org/) | Tales Vault (`tale`) | |---|---------------------------------------------------|----------------------| | **What you memorize** | The random words | Mnemonic phrases; the key is the initials | | **What you type** | Full words with spaces (or without) | Lowercase only, no `Shift` or layer switching | | **Flow** | Random words → passphrase | Random letters (CSPRNG) → derived phrase | | **Languages** | EFF list in English | Embedded English and Spanish; alphabet per language | | **Runtime** | Web (local JS) or source on GitHub | Offline Go binary (`go:embed`) | | **Typical entropy** | ~44 bits (4 words) / ~66 (6) | ~85 bits (20 letters, ~20-letter alphabet) | If you want a whole-word passphrase and Diceware's rhythm suits you, use Diceware (for real). If you need more entropy per keystroke, lowercase only, recovery via templated phrases, and native Spanish support, that is what `tale` is for. ## How it compares | System | Who chooses | Entropy | |--------|-------------|---------| | **Tales Vault** | CSPRNG → derived phrase | High (~85 bits, uniform letters) | | **Diceware** ([dmuth.org](https://diceware.dmuth.org/)) | Dice/PRNG → words | High (~44–66 bits by length) | | **Memorable phrase** | You → initials | Low (skewed) | | **Manager** | CSPRNG → stored | High (you do not memorize) | ## What's next The repository has an [open design discussion](https://github.com/escribanoruben/tale/issues/1) on adding derived control digits at the end of the password, a checksum to detect errors when recovering the key from phrases (wrong block order, misremembered word). Code, OpenSpec specs, and word lists live at [github.com/escribanoruben/tale](https://github.com/escribanoruben/tale). If you try `tale` and hit a block with no valid phrase in your language, open an issue, dictionary contributions are welcome. ## Closing Random and memorable are not opposites if the order is right: entropy first, story second. The xkcd comic intuited it; Diceware proved it with dice. Tales Vault brings it to a reproducible, auditable, local flow, with a command you can build yourself. For everything else, keep using your manager. For the few passwords you need in your head, at least let the letters be chosen by something other than your intuition. ---------------------------------------- ### [Passwords: why length beats complexity](https://escribano.dev/en/blog/010-password-entropy-length/) > Password entropy depends on the randomness of the process, not the alphabet. The NIST complexity rules (2003) trained predictable patterns, and the xkcd comic only works with dice: four words you pick yourself lose much of their nominal 44 bits to frequency, initial-letter, and semantic biases. Length with random letters wins, and modern crackers exploit exactly what humans choose. I'm a fan of [xkcd](https://xkcd.com/), and one of its most quoted strips is [the password panel (#936)](https://xkcd.com/936/): `Tr0ub4dor&3` versus `correct horse battery staple`. It changed how millions of people think about passwords. The message seems clear: forget the "complex" password, use four common words, and win on both security and memory. The detail almost nobody reads is the adjective: **random**. Four words you pick are not four random words. The frame I care about: why inherited complexity rules fail, why the comic only works under specific assumptions, and what that means when language biases the initial letters of the words you memorize. ## The comic is right… with dice The bottom panel of xkcd assigns 11 bits per word to `correct horse battery staple`. That assumes uniform selection from a dictionary of about 2,048 common words ($2^{11}$). Four independent random words add up to ~44 bits, about 550 years at 1,000 guesses per second in the comic's scenario. That model is **Diceware**: roll dice, look up a list, repeat. Implementations like [diceware.dmuth.org](https://diceware.dmuth.org/) bring it to the browser with `crypto.getRandomValues()` (the passphrase is generated locally and never leaves your machine). The passphrase is memorable *after* being generated at random, not *because* you invented it. If you truly pick four words at random from a large list, the comic is correct. The problem is almost nobody does that. ## The comic fails… without dice When you choose the words to remember them, you no longer have 11 bits per word. You have something much worse: | Bias | What happens | |------|----------------| | **Lexical frequency** | You pick `house`, `dog`, `love` before `yurt` or `yak` | | **Initial letter** | English has far more words starting with `c`, `p`, `s` than `x`, `z`, `q` | | **Semantics** | Phrases that "tell a story" shrink the search space | | **Personal** | Names, dates, songs, pets, attackers try those first too | The simplified formula $H = L \cdot \log_{2}(N)$ only holds when each symbol has uniform probability $1/N$. If you choose the words, the effective $N$ per position is much smaller, and not the same for every letter. Imagine deriving a password from the initials of twenty words you memorized: - With a uniform 26-letter alphabet: $20 \cdot \log_{2}(26) \approx 94$ nominal bits. - If your words mostly start with `c`, `h`, `p`, `s`, `t`, real entropy per position is closer to $\log_{2}(5\text{–}8)$ than $\log_{2}(26)$. An attacker with an enriched dictionary does not try all combinations with equal priority. They try what humans choose first. ## Entropy in five minutes Shannon entropy measures uncertainty in a choice:
H = −∑i pi log2 pi
If each symbol is equally likely among $N$ options, this simplifies to $H = \log_{2}(N)$. For a sequence of length $L$ with independent symbols:
Htotal = L · log2 N
| Password | Calculation | Approx. entropy | |----------|-------------|-----------------| | 20 random lowercase letters | $20 \cdot \log_{2}(26)$ | ~94 bits | | 16 random lowercase letters | $16 \cdot \log_{2}(26)$ | ~75 bits | | 12 "complex" chars (~94 symbols) | $12 \cdot \log_{2}(94)$ | ~79 bits | | 4 truly random words (Diceware) | $4 \times 11$ | ~44 bits | | 4 words you choose | human distribution | hard to quantify; often < 30 bits | | 8 chars meeting minimum rules (`Summer2024!`) | human pattern | ~30–40 bits | Length beats a "rich" alphabet when randomness is real. But without randomness, neither length nor alphabet saves you. ## Two sides of the same mistake | Approach | Symptom | Illusion | |----------|---------|----------| | **NIST complexity** | `Summer2024!`, `Password1!` | "Has uppercase, number, and symbol" | | **xkcd misapplied** | `house dog love moon` | "Four words, like the comic" | | **Leetspeak** | `P@ssw0rd`, `Tr0ub4dor` | "Symbols confuse the attacker" | In all three cases nominal entropy looks reasonable. Effective entropy is low because generation is human, predictable, and exploitable with substitution rules. ## Where the rules came from Policies requiring "at least one uppercase, one number, and one symbol" trace to [NIST SP 800-63](https://pages.nist.gov/800-63-3/) (2003), associated with Bill Burr. The intent was to enlarge the search space. In practice, it trained people to produce recognizable patterns. In 2017, Burr publicly acknowledged those guidelines had worsened effective security. The updated [SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) recommends length, breached-password lists, password managers, and 2FA, not mandatory periodic rotation or ritual complexity. xkcd's closing line makes the same diagnosis from another angle: twenty years teaching passwords that are hard for humans to remember and easy for computers to guess. ## The cost of typing complexity Beyond entropy, there is usability. On a QWERTY keyboard, `Shift` and symbol keys increase typos, retries, and the temptation to reuse the same "hard" password everywhere. Each uppercase letter is an extra keystroke. On mobile it is worse: you switch layers or long-press to move between cases, and a password that mixes both becomes slow and awkward to type with your thumb. If you enter that key often (laptop boot, vault unlock) you pay that cost every day. Oral transmission matters too. Dictating `Tr0ub4dor&3` forces you to clarify "that's uppercase", "zero, not letter O", "at sign, not a". An all-lowercase string reads straight through: "jay, en, ar, a…" without ambiguity. Useful when someone you trust helps recover a backup or when you rehearse the password out loud while memorizing it. A long random lowercase string (generated by a manager) is faster to type, less error-prone, and clearer to dictate than a short password with complexity rules. ## Modern cracking and AI-assisted attacks Current attacks do not only try `password` and `123456`. They use substitution rules (`a→@`, `e→3`, `s→$`), enriched dictionaries with proper names, and models that learn human patterns (PassGAN and similar). "Disguised" passwords and "creative" phrases are the first candidates, not the last. AI does not break cryptography; it exploits that humans are not randomness sources. ## Required caveats - Real Diceware and truly random passphrases work. The problem is not words; it is non-random choice. - **Entropy is not everything.** Credential stuffing, phishing, leaks, and reuse can compromise a strong password without guessing it. - **Site limits.** Some cap length or reject certain symbols, another argument for long simple letter sequences. ## What to do in practice 1. **Password manager** for almost everything. One strong master password; the manager generates the rest. 2. **CSPRNG generation**: never improvise. 3. **Adequate length**: ~18–20 random characters give ~75–94 bits with lowercase; enough for most contexts. 4. **2FA** where it matters, TOTP, hardware keys, WebAuthn. 5. **Diceware** if you want words and have dice (or a reliable generator like [diceware.dmuth.org](https://diceware.dmuth.org/)). In the [RPS framework](/blog/002-rps-principle/), this is conscious stone for day-to-day use: little ritual, maximum reversibility toward a manager, bounded cost of failure if the master password is strong. ## Closing: random first Complexity rules and the xkcd comic point in opposite directions, but they share an invisible requirement: randomness must be real. Without it, the comic's math lies and NIST's rules make things worse. The practical question that remains: if you need to memorize a few critical passwords (the manager's master key, laptop boot) without choosing the words or letters yourself, how do you do it without falling into the bias we just described? In the [next post](/blog/011-tales-vault-system/) I introduce **[`tale`](https://github.com/escribanoruben/tale)** (Tales Vault System): entropy first, story second. ---------------------------------------- ### [The cloud-first problem: when fear masquerades as prudence](https://escribano.dev/en/blog/004-cloud-first-problem/) > Cloud was built to manage demand uncertainty, but became the default through inertia and fear of simplifying. For stable internal workloads, a €20-40/month VPS is usually enough; elasticity only pays off with unpredictable demand or real spikes. The question to recover: how much demand uncertainty do I actually have? When Amazon launched EC2 in 2006, the pitch wasn't "put everything in the cloud." It was something else: how much capacity do you need when you don't know? The original cloud story (pay-as-you-go, instances in minutes, scale up and down with demand) addressed a concrete problem. Avoid buying hardware upfront. Don't get caught short if a Digg or Slashdot link multiplies your traffic overnight. Turn fixed costs into variable ones when uncertainty was high. That fit was real. For a startup with no users yet, for a product that might never take off or might explode without warning, for a retailer that needs to absorb Black Friday without running double the servers the rest of the year, elasticity made sense. Cloud wasn't a luxury: it was demand-risk management. The promise was real. Over time it became the default (*cloud-first*) even when demand risk no longer justified the bet. ## Cloud-first is not cloud when it fits **Cloud-first** is an organizational policy: "we start on the hyperscaler and figure it out later." Sometimes that's deliberate and reasonable. Other times it replaces analysis altogether. The distinction matters. Choosing cloud after evaluating load, cost, operations, and reversibility is a technical decision. Choosing cloud because nothing else was considered is inertia with slides. In this post, when I say **hyperscaler**, I mean the large-scale cloud providers that dominate the market: AWS, Azure, and GCP. I'm not criticizing those providers as such, they offer powerful services and, in many contexts, the right choice. I'm criticizing the automatic use of their platform when the load profile doesn't call for it. ## "Nobody ever got fired for buying IBM" There's a phrase attributed to IBM in the 1980s: *"Nobody ever got fired for buying IBM."* It captures an asymmetry that still holds: the career-safe decision doesn't always match the problem-appropriate one. The phrase comes from another era, but it maps cleanly onto hyperscalers. *"Nobody ever got fired for buying AWS"* (or Azure, or GCP) sounds like a joke in a technical meeting, but it describes a real incentive. The cost of choosing too much cloud is diffuse: a vague bill, accumulated complexity, dependency that takes months to become visible. The cost of choosing "too simple" (a VPS, a dedicated server, a Docker Compose deployment) can be personal and immediate: you're labeled a hack, non-scalable, not "serious." Technology choice stops being technical and becomes philosophy driven by fear. You don't pick cloud because the problem demands it; you pick it because simplifying is scary. ## From uncertainty to everything-in-cloud Over time, the original narrative thinned out. Cloud ceased to be the tool for maximum uncertainty and became the default home for almost everything: * Internal apps with bounded users and predictable growth. * Back-office tools with stable month-to-month load. * MVPs that could live on a single server for months. * Systems where the main risk isn't traffic, but budget and operations. Inertia has several sources. Tutorials and certifications that assume cloud. Architecture templates imported from big tech. Consultants whose business model depends on complexity. And a job market that rewards years of AWS on a CV even when the product has a hundred users. None of those forces answer the central question: how much demand uncertainty do I actually have? ## When cloud still makes sense To be fair: there are contexts where hyperscaler is the sensible answer. * **Unpredictable demand**: you don't know whether you'll have ten users or ten million. * **Occasional spikes**: campaigns, events, Black Friday; pay for extra capacity only when needed. * **Managed services you don't want to operate**: complex databases, queues, global CDN, compliance the provider absorbs better than your team. * **Teams that prefer OpEx and fast provisioning** over running their own machines, when the context allows it. In the [post on the RPS principle](/blog/002-rps-principle/) cloud fits **paper**: pay upfront work and complexity for risk control. When the risk is real (failure is expensive, scale is unpredictable, the system will live for years) that paper is authentic. The failure is using paper out of fear when the real cost of failure was low and **rock** would have been enough: a VPS, a simple deployment, sufficient for the context. ## IaaS, serverless, and "scale to zero" Worth not mixing generations. Classic cloud (IaaS: EC2, instances, clusters) offered elasticity with running instances: you scaled fast, but still paid for reserved capacity even when you used little of it. **Scale to zero** (pay only when there are executions) came later with **serverless** (functions, on-demand containers). There the "maybe nobody will use it" argument gets even stronger: you don't keep idle infrastructure. If your load is sporadic or highly irregular, serverless may be the right tool. If your load is stable and predictable, staying on IaaS cloud by inertia loses the original benefit: you keep paying for elasticity you don't need. ## When a VPS is the ideal answer Consider an internal app: inventory management, HR portal, reporting tool. 150 users today; forecast of 300 in two years, linear growth, no seasonal spikes. Modest HTTP traffic, one relational database, an occasional worker. A profile like that doesn't need to absorb a Twitter viral moment. It has no Black Friday. The question "what if I get a million requests tomorrow?" is theoretical, not operational. Running that on a hyperscaler (managed containers, load balancer, managed database, native monitoring, several coupled services) can cost several hundred euros a month before counting the time spent maintaining the cloud template. The same stack on a €20–40/month VPS with backups, reverse proxy, and reproducible deployment is usually enough. Fixed capacity for fixed demand. Why does it end up on AWS anyway? Rarely for technical need. Because "everyone does it," because the previous architect left it there, because nobody wants to defend a VPS on a slide, because *"scaling is free in the cloud"*, until you read the bill. In the RPS framework, that's rock done right: sufficient pragmatism when reversibility is high, failure is bounded, and uncertainty is low. Matching the tool to real risk, not a hack. ## So what would scissors be? So far this post can read like a VPS-versus-cloud binary. But in the [RPS post](/blog/002-rps-principle/) bare metal fits **scissors**: maximum long-term potential, more control, more complexity, more operational risk, chosen on purpose when the context justifies it. On the cloud-first axis, scissors enters when neither rock nor paper quite fit. You don't need hyperscaler elasticity, but a lone VPS isn't enough either: load is large or critical, you want independence from the cloud provider, and you accept operational cost in exchange for your own platform. Typical cases: * **Self-managed owned hardware**: colo servers, dedicated machines, or a home rack when scale and control make it worth it. * **Orchestration on your own infra**: Kubernetes, Nomad, or another platform on metal or VPS you operate, not EKS/GKE/AKS: scale or homogenize services without delegating to the hyperscaler. Kubernetes is a tool, not scissors by definition. K8s on AWS is still paper, cloud with more YAML. K8s on three nodes for a 150-user internal app is usually false scissors: scissors complexity without the benefit. Authentic scissors mean building your platform knowing you own its operations, not deploying it because "it's modern." Scissors aren't the natural next step after discovering a VPS was enough either: it's a different bet, the team that outgrew a simple monolith and needs to scale or orchestrate without anchoring to cloud, or whoever prioritizes sovereignty and cost at scale over managed convenience. ## Fear of simplifying Behind cloud-first there's an unspoken fear: simplifying looks irresponsible. A `docker-compose.yml` on a VPS sounds less "enterprise" than a diagram with seventeen boxes across three regions. But complexity isn't professionalism; sometimes it's noise mimicking safety. More moving parts, more IAM permissions, more failure surface, and the same 150-user app. Simplifying means defending a decision to management, auditors, colleagues used to associating cloud with maturity. It's easier to inherit the default. That's why the IBM line still works: it outsources blame to a brand nobody questions. ## Bridges, anchors, and inertia In the RPS framework, **bridges** are decisions that preserve optionality; **anchors**, vendor lock-in. Accelerated cloud-first often creates anchors: proprietary APIs, services that don't exist outside the provider, data that takes months to migrate. That doesn't invalidate cloud when you need it. But when you chose cloud without needing elasticity, you inherited anchors without buying the benefit. A VPS with free software and standard containers is usually a bridge: tomorrow you can move it, scale vertically, or (if demand truly changes) move to cloud with criteria. Cloud-first inertia also clashes with another theme of this blog: [digital sovereignty](/blog/005-digital-sovereignty/), the ability to decide where your data lives, who operates your infrastructure, and which models process your information. Accumulating hyperscaler dependency by default, without evaluating alternatives, makes that autonomy harder even when you don't care today. ## Choosing consciously I'm not writing this to say cloud is bad or everything should live on a VPS. I'm writing to recover the original question cloud answered well: *how much demand uncertainty do I have?* If the answer is "a lot," elasticity is prudence. If the answer is "little, predictable, internal," staying cloud-first is fear masquerading as prudence: the same logic as "nobody ever got fired for buying AWS," applied to a problem that never needed that scale. What matters is not hiding behind the hyperscaler when rock was the honest decision, using paper when risk is real, and reserving scissors for when the ambition of your own platform is justified, not confused with fashion or fear of the simple option. In the [introduction post](/blog/001-introduction/) I promised to discuss self-hosting and informed decisions against the prevailing *cloud-first* mindset. This post names the problem. Next ones will get concrete (DNS, reverse proxy, identity) with the same criterion: not recipes for their own sake, but knowing what you're paying for when you choose. ---------------------------------------- ### [The feeling of touching something fragile: snowflake, IaC and the RPS principle](https://escribano.dev/en/blog/003-snowflake-iac-rps/) > Snowflake infrastructure (the AWS console, Firebase wizards, a VPS configured by hand over SSH) is fragile because it exists only in one person's head. The RPS framework separates false rock (clickops), false paper (poorly adopted IaC), genuine paper (IaC with discipline), and scissors (ambitious IaC on purpose). The test: could you rebuild it from scratch tomorrow? Every time I have to touch the AWS console, Firebase, or any hyperscaler dashboard directly, the same feeling hits me: I'm handling something fragile. Something I won't remember how I set up in a few months. Something that, even if I document it with screenshots, I might not be able to reproduce if the interface changes. That feeling is a signal that I'm creating snowflake infrastructure: a unique, irreproducible system that depends on my memory and on specific circumstances that won't repeat. In the [post on the RPS principle](/blog/002-rps-principle/) rock, paper, and scissors are defined as three ways to make technical decisions. This post applies that framework to infrastructure: snowflake as false rock, poorly adopted IaC as false paper, sufficient IaC as genuine paper, and, at a separate extreme, ambitious IaC as scissors. ## What is a snowflake In ops, a snowflake is a server, environment, or configuration so particular that it can't be reliably reproduced. No two are alike. Each one is unique, like a snowflake. It's often associated with the AWS console: years of *clickops*, manually created resources, security rules that "someone added back in the day" and nobody dares touch. But the problem is broader. ## It's not just the AWS console Any hyperscaler with a graphical interface can produce snowflakes: * **AWS Console**: the most famous case. * **Firebase / Google Cloud Console**: wizards that leave behind a structure that's hard to replicate step by step. * **Azure Portal**, **Oracle Cloud**, and the rest, same pattern: clicks without a versioned source of truth. If you set up a project in Firebase using the graphical interface and tomorrow you have to rebuild it from scratch, could you reproduce exactly the same structure? The same permissions, rules, auxiliary services? The doubt itself is a symptom. ## You don't need a graphical interface either Snowflakes don't require wizards or web panels. SSH into a VPS and install things with `apt` or `dnf`, tweak firewalls, edit config files, restart services… until the system works and nobody knows which piece holds up what. It's happened to me on virtual machines: so many manual tweaks to firewalls, permissions, or auxiliary services that when something breaks I end up reaching for a backup instead of trying to reproduce the system. Because I don't trust that I (or anyone on the team) could rebuild it by hand. In those moments I've wished I'd used IaC tools from the start. ## False rock In the RPS principle, rock is conscious pragmatism: solve quickly, assuming the cost of being slightly wrong is low and the decision is reversible. False rock looks like rock, but isn't. It's doing things a certain way because *"that's how it's always been done"* or *"that's what everyone does."* It's the excess confidence of thinking we're smarter than we really are: that we'll perfectly remember that AWS console setup, that screenshots will be enough, that *"I'll document it later."* False rock accumulates operational technical debt: not the debt of poorly designed code, but the debt of a system that only exists in one specific version, in one specific head, at one specific moment. We confuse apparent speed with real pragmatism. Genuine rock accepts limitations; false rock ignores them. ## A mode of operation, not a type of hosting In the RPS framework, VPS, cloud, and bare metal can fit rock, paper, and scissors. But snowflake isn't a fourth type of infrastructure: it's a way of managing it: * A VPS can be clean rock (reproducible script, standard base image) or a snowflake (years without rebooting, undocumented artisanal configuration). * Cloud can be paper (IaC, policies, auditing) or a snowflake (console, clicks, "temporary" exceptions that never get removed). The failure is how you configure it, not choosing VPS or cloud: without a source of truth, without idempotency, without assuming disasters will happen. ## Where it matters and where it doesn't Not every snowflake is a crime. There are contexts where it makes sense: * **Disposable prototypes** with a short life and a budget for destruction. * **Homelabs** meant for experimenting and breaking things, there, fragility can be part of the learning. But there are contexts where it hurts: * When there's money at stake: downtime costs, SLAs, customers. * When the system can't afford to depend on one person: tribal knowledge, vacations, team turnover. * When a disaster requires rebuilding everything from scratch and there's no manual for dummies or automation that does it without demanding 100% of your attention. The useful question is whether I can afford it in this context, not a yes or no on snowflakes. ## IaC in the RPS principle IaC (*Infrastructure as Code*) describes infrastructure in versioned files (Terraform, OpenTofu, Pulumi, Ansible) instead of depending on clicks or memory. It's the natural path out of snowflake territory. But in RPS it isn't automatically "the right answer." ### False paper: poorly adopted IaC The symmetric trap to false rock is false paper: it looks like serious engineering, but isn't. You adopt Terraform or OpenTofu because *"it's the professional thing to do,"* but without remote state, without a review workflow, without understanding the declarative model. One person runs `apply` locally; someone copied the modules months ago and nobody understands them. You think you've left snowflakes behind. In reality you've swapped a manual snowflake for a snowflake with `.tf` files on top: another fragility, with extra complexity in the bargain. ### Paper: sufficient IaC When IaC is done with discipline (versioning, review, shared state, reproducible environments) it becomes genuine paper: you pay upfront work in exchange for stability, predictability, and risk control. You have a manual for dummies, or better yet, a pipeline that brings up the environment without you having to remember every step. You accept your human limitations: you won't remember everything in six months, and that's fine, because the source of truth lives in the repository. ### Scissors: ambitious IaC There's a third scenario, distinct from false paper and sufficient paper: ambitious IaC by design. Multi-environment, shared modules, policy-as-code, internal platforms, self-managed bare metal. That's scissors in the RPS sense: maximum long-term potential, more complexity, more risk, but chosen consciously when the context (scale, team, compliance) justifies it. It's not the natural next step after escaping snowflakes; it's a different bet. ## Tools (a mention, not a tutorial) I won't go into detail now (there will be posts for that) but I do want to name the ecosystem: * **[OpenTofu](https://opentofu.org/)**: open source fork of Terraform, after HashiCorp's license change. My recommendation as a declarative base. * **Terraform**: the de facto standard; worth knowing, but today I'd look at OpenTofu first for new projects. * **[Pulumi](https://www.pulumi.com/)**: lets you describe infrastructure with imperative languages (TypeScript, Python, Go…). Interesting if you come from manual scripts and struggle with declarative HCL. * **[Ansible](https://www.ansible.com/)**: focused on configuring existing systems; complements OpenTofu/Pulumi well (provisioning + configuration). All are bridges in the RPS sense: they reduce the cost of recovering later. None is a silver bullet; all require discipline (versioning, review, testing). ## Choosing consciously I'm not writing this to demonize the console or to say everything must be IaC from day one. I'm writing to name the feeling (that fragility when touching something you don't fully control) and to place it in a framework that helps you decide. Sometimes false rock is enough: an MVP, a personal environment, a test you'll throw away tomorrow. Sometimes it isn't: production, customer data, systems that must outlive your memory. What matters is knowing when you're creating one, what you're sacrificing, and whether the context allows it. And accepting that, when it doesn't, you need a manual for dummies, or the automation that makes one unnecessary. ---------------------------------------- ### [Rock, paper, scissors: a framework for technical decisions](https://escribano.dev/en/blog/002-rps-principle/) > Every technical decision falls into one of three archetypes: rock (sufficient pragmatism), paper (risk control paid with upfront work), or scissors (maximum potential at the cost of risk). Problems arrive when strategies get mixed without realizing it. Bridges (free software, open standards) preserve optionality; anchors (vendor lock-in) block it. Most technical discussions don't fail because of a lack of knowledge. They fail because the people involved are playing different strategies without realizing it. One defends "the simple thing that works." Another demands "something solid and maintainable." A third wants "to do it right the first time." The discussion turns moral, dogmatic, or personal. Words like *best practices*, *overengineering*, *hack*, *future*, and *professional* start flying around. And nobody understands each other. The problem is strategic, not a lack of technical knowledge. You need a mental framework to think before you decide. ## There is no universally optimal strategy In engineering (software, systems, tooling, architecture) there is no solution that always wins. There are **trade-offs**: risk, cost, reward, reversibility, complexity. What does exist is a constant tendency to argue as if there were a single correct decision and everything else were mistakes or incompetence. To name those strategies before discussing tools, I propose a mental framework I call the **RPS principle** (Rock–Paper–Scissors): it classifies technical or strategic decisions according to what they prioritize and what they sacrifice. It doesn't tell you what to choose. It forces you to know what you are choosing. ## The RPS principle The RPS principle starts from a simple idea: every relevant decision falls (explicitly or implicitly) into one of three strategic archetypes. To refer to these approaches compactly, the framework names them rock, paper, and scissors. As in the game, each shines in certain contexts and proves costly in others. The labels rock, paper, and scissors are just memorable names; they don't inherit the rules of the original game or imply fixed relationships between them. They could have been Terran, Protoss, and Zerg, or any other triad. Below are the three RPS strategies: ### Rock: Sufficient pragmatism Rock is the strategy of *"this works and we move on"*. * Minimizes decision time. * Reduces cognitive cost. * Solves the immediate problem without trying to optimize everything. Not elegant, not brilliant, and it works: it prioritizes immediate progress over refinement. It's the right strategy when: * The cost of thinking more is greater than the cost of being slightly wrong. * The problem isn't central. * Reversibility is high. * Ideal for prototypes and MVPs. Rock is often criticized for not being "clean" or "correct." Often, that criticism is pure noise. ### Paper: Risk control at a price Paper prioritizes stability, predictability, and risk control. * More upfront work. * More constraints. * Fewer surprises in the long run. It's the strategy of someone who says: *"I'd rather pay now than pay later"*. It works well when: * Failure is expensive. * The system will live a long time. * The context is hostile to change. Paper is often accused of being slow or bureaucratic. Sometimes it is. Sometimes it's simply realistic. ### Scissors: Long-term potential at the cost of risk Scissors seeks maximum long-term potential. * More flexibility. * More performance. * More complexity. * More risk. It's the strategy of *"if we're going to do it, let's do it right"*. It can be the best move… or a useless exercise in overengineering if the context doesn't justify it. Scissors fails when used out of technical ego or an allergy to simplicity. ## A quick example Imagine you need to persist data for an MVP that will validate whether there's a market. * **Rock**: SQLite or a JSON file on disk. Fast, reversible, enough while you don't know if the product will take off. * **Paper**: Managed PostgreSQL with backups and migrations from day one. Makes sense if you already have paying users or data you can't afford to lose. * **Scissors**: Designing sharding, replicas, and a data model built for millions of rows. Only if scale is already the core problem. The useful question is which strategy is being used and whether the context justifies it, not which one is more professional. ## Where problems come from Most problems don't come from choosing badly, but from not knowing which strategy is being used. Some classic patterns: * Using scissors when rock was needed (the perfect is the enemy of the good). * Using paper out of fear, when the real cost of failure was low. (Putting on the bandage before the wound, acting before the problem exists.) * Criticizing a rock for not being elegant, when it was the right decision for that moment. Think of [The Tree Swing cartoon](https://en.wikipedia.org/wiki/Tree_swing_cartoon): it serves as a metaphor for situations where a client needs a simple solution and all kinds of misapplied engineering gets built, when what they really needed was a simple, quick solution (even if inelegant) sufficient for that context. When strategy isn't discussed, the debate turns religious. ## Bridges and Anchors The RPS principle isn't complete without two cross-cutting concepts. **Bridges** Decisions that reduce the cost of changing strategy later. Typical examples: free software, optional self-hosting, open standards, interoperable formats, containers. They don't force you to choose well now. They let you correct course later. For example: if you choose [Supabase](https://supabase.com/) to quickly get a backend for your application and at some point the price gets out of hand, you have the option to self-host your own instance on your VPS, with the effort that entails, instead of using the [PaaS](https://en.wikipedia.org/wiki/Platform_as_a_service) version. That's thanks to Supabase having two bridges: it's free software and can be used both as PaaS and in self-hosted mode. If you choose Firebase instead of Supabase, it's harder to change strategy, because Firebase **anchors** you to its platform and makes it costly to move from a third-party backend (Paper) to self-hosting it on a VPS (Rock). **Anchors** Decisions that make that change more expensive or block it entirely. Vendor lock-in, closed software, proprietary protocols, rigid dependencies. They can be convenient in the short term. But they turn reversible decisions into bets. The difference between a smart decision and a trap is usually here. ## What this is for The RPS principle doesn't aim to create a new orthodoxy. It does the opposite. It helps you: * Think in terms of trade-offs, not dogmas. * Accept that every strategy wins something and loses something. * Separate real technical discussions from ideological debates in disguise. What matters is choosing consciously and preserving optionality when it matters, not always crowning "the best" strategy. ## What comes next Other posts on this blog apply the framework to concrete decisions (as a mental shortcut, not a fixed rule): * Linux distributions (Debian could represent rock, RHEL paper, and Arch scissors). * IDEs (VSCode, IntelliJ IDEA, and LazyVim/AstroNvim/NvChad) * Infrastructure (VPS rock, Cloud paper, Bare metal scissors), already covered in [snowflake and IaC](/blog/003-snowflake-iac-rps/), [cloud-first](/blog/004-cloud-first-problem/), and [homelab access](/blog/006-homelab-vpn-access/). * Programming languages (Python/TypeScript/Ruby/PHP, Java/C#, and Golang/Rust/Elixir) * Decisions that come up every day, even when they aren't named that way. Not to tell you what to use. But to make clear what you're paying when you choose. This post develops the criterion set out in the [introduction](/blog/001-introduction/). ---------------------------------------- ### [Introduction](https://escribano.dev/en/blog/001-introduction/) > First post on escribano.dev: what the blog will cover (programming, self-hosting, security, and productivity) and the criterion behind it. Understanding technology in depth to gain autonomy, reduce technical debt, and avoid vendor lock-in, documenting in public with mistakes and corrections included. Hi, I'm Ru, and this is the first post on a blog about **software development** and **digital sovereignty**. Here I'll share, on one hand, tools and techniques related to **programming** and **self-hosting**, and on the other, personal reflections on information security, digital sovereignty, and personal productivity based on eliminating distractions and mental clarity. I want to make clear from the start that I don't write because I'm an expert. I write as a form of documentation for myself and to help others facing challenges similar to mine. I also write to put my reflections on paper and invite debate on topics I consider vitally important today, such as self-hosting and digital sovereignty in contrast to the *cloud-first* philosophy that seems to have taken hold in recent years. On this blog you won't find quick recipes or thoughtless snippets, and we won't settle for _"if it works, don't touch it"_. Here we seek to understand technology in depth to gain **autonomy**, reduce **technical debt**, and avoid **vendor lock-in**. Everything I write is meant to help you look beyond superficial functionality and make informed decisions about the tools and systems you use. This blog will be an honest learning space: there will be mistakes, corrections, and discoveries in public. I don't promise easy answers, but I do promise clarity, rigor, and guidance based on practical experience. What you read here isn't just recipes; it's an invitation to think, experiment, and understand every piece of the system we use day to day. ## Sobre el autor - [Acerca de mí](https://escribano.dev/es/about/): Programador desde 1996, formación en informática de gestión, aficionado al self-hosting y desarrollo de software de facturación. - [About me](https://escribano.dev/en/about/): Programming since 1996, background in management informatics, self-hosting hobbyist, and billing software developer. - [Contacto](https://escribano.dev/es/contact/): Escríbeme si quieres comentar algo del blog, proponer un tema o simplemente saludar. - [Contact](https://escribano.dev/en/contact/): Get in touch if you want to comment on the blog, suggest a topic, or just say hello. ## Feeds - [RSS (es)](https://escribano.dev/rss.xml): Feed del blog en español - [RSS (en)](https://escribano.dev/en/rss.xml): Blog feed in English