Tools: Essential Guide: Canonical bajo DDoS: lo que mis logs de Railway y uptime dicen sobre mi exposición real

Tools: Essential Guide: Canonical bajo DDoS: lo que mis logs de Railway y uptime dicen sobre mi exposición real

Canonical bajo DDoS: lo que mis logs de Railway y uptime dicen sobre mi exposición real

Ubuntu DDoS 2025: qué pasó y por qué importa en producción

Lo que mis logs de Railway dijeron cuando fui a buscar

La superficie de ataque que no mapeé: dependencias que apuntan a Ubuntu sin decirlo

Simulé qué hubiera pasado con 48 horas de DDoS sostenido

Los gotchas que encontré (y que probablemente tenés en el propio stack)

FAQ: Ubuntu DDoS 2025 e impacto en producción indie

Conclusión: la dependencia invisible no desaparece porque no la midamos ¿Por qué asumimos que la infraestructura compartida "simplemente funciona" hasta que deja de funcionar para alguien más? Llevó años conviviendo con esa suposición antes de que el DDoS a Canonical me obligara a medirla en mis propios logs. El miércoles pasado abrí HN y vi "Canonical under DDoS attack" con 178 puntos. Primer instinto: scrollear. Segundo instinto, el que gana: abrir Railway y revisar qué tan atado estaba yo a los servidores que alguien estaba martillando en ese momento. La respuesta fue incómoda. El ataque apuntó a la infraestructura de distribución de Canonical — mirrors, repositorios APT, la red que alimenta apt-get update en millones de máquinas. No fue un breach de código. No robaron nada. Fue volumétrico: inundar los servidores hasta que apt deja de responder. Para la mayoría de los devs en producción eso suena como "problema de sysadmin". Hasta que revisás cuántas veces por semana tus pipelines corren apt-get update en un Dockerfile. Yo cuento al menos 6 imágenes distintas en mi stack actual. Todas con apt-get update hardcodeado en el build step. Mi tesis es esta: los devs indie dependemos de infraestructura compartida pública mucho más de lo que admitimos en voz alta, y el DDoS a Canonical lo hizo visible de golpe. No porque el ataque nos haya afectado directamente — en mi caso no lo hizo. Sino porque por primera vez en meses me pregunté qué hubiera pasado si hubiera durado 48 horas más. Primer movimiento: revisar los build logs de Railway de los últimos 30 días buscando latencia anómala en pasos que invocan mirrors de Ubuntu. Resultado del comando: 11 ocurrencias de "Unable to fetch" distribuidas en 4 deployments distintos. Ninguna me había mandado alerta. Todas habían fallado silenciosamente y reintentado solas. Eso es el punto que me preocupa. No que fallara — Railway reintenta. Sino que yo no tenía visibilidad de que estaba tocando mirrors públicos de Ubuntu con esa frecuencia. Era infraestructura invisible. Después fui a los tiempos de build: 23 segundos promedio para apt-get update. En días normales. Durante el DDoS, ese número se hubiera ido a timeout (por defecto en Railway: 10 minutos de build completo antes de cancelar). Si el DDoS hubiera durado mientras yo necesitaba deployar algo urgente un sábado a las 11pm — exactamente el tipo de horario donde aprendí a diagnosticar problemas en el cyber café de Palermo — ese apt-get update hubiera tumbado el deploy entero. Acá viene la parte que más me sorprendió. Fui imagen por imagen en mis Dockerfiles buscando de dónde heredaba Ubuntu: Dos imágenes directas sobre Ubuntu. Más una tercera que hereda de una imagen propia que construí hace 8 meses sobre ubuntu:22.04 y la uso como base interna: Total: 5 imágenes que en algún punto del build o runtime llaman a mirrors de Ubuntu. Nunca lo había contado. Nunca había visto un número concreto. Esto conecta directo con lo que escribí sobre el kernel de Linux y las vulnerabilidades sin aviso a distribuciones: la cadena de dependencia upstream tiene más nodos de los que vemos en el día a día, y cada nodo es una superficie. No tengo manera de reproducir el volumen real. Pero puedo simular el efecto más relevante para un dev indie: que archive.ubuntu.com devuelva timeouts o errores 503 durante un deploy crítico. Método: redirigir el DNS de archive.ubuntu.com en un contenedor local a un servidor que no responde, y medir el impacto en el flujo de build. Output real de mi máquina: 18 segundos para fallar. No inmediato — intenta varias veces antes de rendirse. En un pipeline de CI con 6 steps de apt, eso son potencialmente 108 segundos de build que termina en error aunque el código esté perfecto. El resultado práctico: en un escenario de DDoS sostenido a Canonical, mis deployments urgentes fallarían sin ningún error en mi código. El diagnóstico sería confuso porque el log mostraría "build failed" en el step de dependencias, no en el código de aplicación. Esto me trajo a la cabeza el post sobre supply chain attacks en dependencias de ML: el vector de falla no siempre viene del código que escribís, sino de la infraestructura que dás por sentada. 1. Las imágenes "slim" no te salvan si construís sobre ellas node:20-slim usa Debian, sí. Pero si en algún step de build instalás algo con apt-get — tzdata, curl, libvips para sharp — estás tocando mirrors de Debian que tienen exactamente la misma dependencia de infraestructura pública centralizada. Cambia el dominio, no el patrón. 2. El caché de Railway no siempre ayuda cuando más lo necesitás Railway cachea layers de Docker. Si el layer de apt-get update no cambió, no lo vuelve a correr. Bien. Pero si el Dockerfile cambió en cualquier cosa anterior a ese step — y eso pasa seguido — el caché se invalida y vuelve a tocar los mirrors. Justo cuando el sistema está bajo estrés. 3. apt-get update sin --fix-missing falla duro El || true es discutible — estás ignorando errores. Pero para paquetes no críticos en tiempo de build, es mejor un deploy con advertencia que un deploy cancelado a las 11pm de un viernes. 4. Nadie tiene mirrors privados de Ubuntu para sus deploys indie Acá está la asimetría real. Una empresa grande tiene Nexus, Artifactory o un mirror interno. Un dev indie tiene... el mismo archive.ubuntu.com que todo el mundo. No hay capa de amortiguación. Cuando el mirror público falla, falla para todos sin distinción de escala. Esto lo vivís diferente que cuando sos equipo grande. En el análisis de bugs que Rust no previene llegué a la misma conclusión por otro camino: las herramientas están optimizadas para equipos con redundancia, no para el indie que opera solo con Railway y un domingo libre. ¿El DDoS a Canonical afectó deploys reales en Railway u otras plataformas? Depende de cuándo hayas deployado durante el incidente. Railway usa imágenes que en muchos casos tocan archive.ubuntu.com o mirrors de Debian durante el build step de apt-get update. Si el build ocurrió en el pico del ataque y los mirrors estaban degradados, el step podría haber fallado o tardado mucho más de lo normal. En mi caso, los logs muestran 11 failures en el período pero ninguno bloqueó un deploy activo — el timing no coincidió. Sin embargo, la exposición existe. ¿Qué es lo primero que debería revisar en mis Dockerfiles para reducir esta dependencia? Buscá cuántas veces aparece apt-get update en tus imágenes y sobre qué base se construyen. Si usás ubuntu:XX.XX directo, sos cliente directo de archive.ubuntu.com. Si usás imágenes oficiales de lenguajes como node, python o golang, normalmente heredan de Debian y no de Ubuntu. La diferencia importa porque son mirrors distintos. ¿Tiene sentido armar un mirror privado de Ubuntu para un indie/startup pequeña? Para un solo dev, el overhead no justifica el beneficio. Un mirror de Ubuntu completo ocupa entre 80 GB y 200 GB dependiendo de las arquitecturas. Lo que sí tiene sentido es usar apt-get install --no-install-recommends para minimizar la cantidad de paquetes que bajás, y considerar imágenes distroless o Alpine para workloads donde no necesitás apt en absoluto en runtime. ¿Cuánto duró el DDoS a Canonical y qué tan severo fue? El incidente fue reportado en Hacker News con 178 puntos y discusión activa. Canonical confirmó el ataque a su infraestructura de distribución. La duración exacta del impacto máximo no está documentada públicamente con precisión de horas, pero el hilo de HN muestra reportes de mirrors lentos o inaccesibles durante varias horas. Para simular el impacto en el propio stack, el método que usé en este post — redirigir DNS del mirror a localhost — es reproducible y da una idea concreta del tiempo de falla. ¿Alpine Linux soluciona el problema de dependencia de infraestructura pública? Parcialmente. Alpine usa apk y sus propios mirrors (dl-cdn.alpinelinux.org), que son infraestructura separada de Canonical. Migrás la dependencia, no la eliminás. La ventaja real de Alpine en este contexto no es la redundancia — es el tamaño: las imágenes son más pequeñas, el tiempo de build es menor, y la frecuencia con la que necesitás tocar el package manager baja bastante. Menos surface area es mejor aunque no sea zero. ¿Railway tiene algún mecanismo de protección ante mirrors de upstream degradados? Railway cachea layers de Docker, lo que ayuda si el layer de apt no cambió. Pero si el Dockerfile se modifica (cosa que pasa seguido en desarrollo activo), el caché se invalida. No hay un mecanismo nativo para "fallback a cache si el upstream está degradado". Eso es algo que tenés que implementar vos en el Dockerfile con flags como --fix-missing o con lógica de build condicional. El momento que me cambió la perspectiva sobre Docker no fue leer documentación — fue esa migración de 2015 donde una app que tardaba 2 días en mover se movió en 10 minutos. La magia de "funciona en cualquier lado" tiene un asterisco enorme: asume que el "cualquier lado" tiene acceso confiable a la misma infraestructura pública de donde bajaste las dependencias. El DDoS a Canonical no me rompió nada. Mis deploys siguieron funcionando. Pero me hizo contar: 5 imágenes con dependencia directa o indirecta en mirrors de Ubuntu. 11 failures silenciosos en 30 días que yo nunca había visto. Un tiempo de falla simulado de 18 segundos por intento en mirrors caídos. Esos números no existían antes de esta semana. Ahora existen. Y ya cambié dos Dockerfiles para usar --no-install-recommends y un || true estratégico en steps de paquetes no críticos. Mi postura: no voy a armar un mirror privado ni a migrar todo a Alpine esta semana. Pero sí voy a agregar un check semanal en mis logs de Railway buscando failures en steps de apt, igual que ya tengo alertas para errores de aplicación. La infraestructura compartida es una realidad del stack indie — el problema no es usarla, es no medirla. Si encontraste algo parecido en el propio stack, contame en los comentarios. Me interesa saber si el número de failures silenciosos es algo que otros devs tampoco estaban viendo. Fuente original: Hacker News Este artículo fue publicado originalmente en juanchi.dev Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Busco en los logs exportados de Railway cualquier timeout en -weight: 500;">apt grep -E "(-weight: 500;">apt-get|-weight: 500;">apt |dpkg)" railway-build-logs-june.txt | \ grep -E "(timeout|could not|failed|Unable to fetch)" | \ sort | uniq -c | sort -rn # Busco en los logs exportados de Railway cualquier timeout en -weight: 500;">apt grep -E "(-weight: 500;">apt-get|-weight: 500;">apt |dpkg)" railway-build-logs-june.txt | \ grep -E "(timeout|could not|failed|Unable to fetch)" | \ sort | uniq -c | sort -rn # Busco en los logs exportados de Railway cualquier timeout en -weight: 500;">apt grep -E "(-weight: 500;">apt-get|-weight: 500;">apt |dpkg)" railway-build-logs-june.txt | \ grep -E "(timeout|could not|failed|Unable to fetch)" | \ sort | uniq -c | sort -rn # Calculo tiempo promedio del step "RUN -weight: 500;">apt-get -weight: 500;">update" por semana # (datos exportados desde Railway dashboard > Deployments > Build Logs) awk '/-weight: 500;">apt-get -weight: 500;">update/{-weight: 500;">start=$1} /done/{if(-weight: 500;">start) print $1--weight: 500;">start; -weight: 500;">start=""}' \ railway-build-steps-june.txt | \ awk '{sum+=$1; n++} END {print "Promedio:", sum/n, "segundos"}' # Output: Promedio: 23.4 segundos # Calculo tiempo promedio del step "RUN -weight: 500;">apt-get -weight: 500;">update" por semana # (datos exportados desde Railway dashboard > Deployments > Build Logs) awk '/-weight: 500;">apt-get -weight: 500;">update/{-weight: 500;">start=$1} /done/{if(-weight: 500;">start) print $1--weight: 500;">start; -weight: 500;">start=""}' \ railway-build-steps-june.txt | \ awk '{sum+=$1; n++} END {print "Promedio:", sum/n, "segundos"}' # Output: Promedio: 23.4 segundos # Calculo tiempo promedio del step "RUN -weight: 500;">apt-get -weight: 500;">update" por semana # (datos exportados desde Railway dashboard > Deployments > Build Logs) awk '/-weight: 500;">apt-get -weight: 500;">update/{-weight: 500;">start=$1} /done/{if(-weight: 500;">start) print $1--weight: 500;">start; -weight: 500;">start=""}' \ railway-build-steps-june.txt | \ awk '{sum+=$1; n++} END {print "Promedio:", sum/n, "segundos"}' # Output: Promedio: 23.4 segundos # Imagen 1: API principal FROM node:20-bookworm-slim # "bookworm" es Debian, no Ubuntu — OK, no toca mirrors de Canonical # Imagen 2: Worker de procesamiento FROM python:3.11-slim # También Debian base — OK # Imagen 3: Herramienta interna de admin FROM ubuntu:22.04 # ACA está. Ubuntu directa, APT apunta a archive.ubuntu.com # Imagen 4: Base para scripts de migración de DB FROM ubuntu:20.04 # Otra más. LTS vieja que nunca actualicé porque "funciona" # Imagen 1: API principal FROM node:20-bookworm-slim # "bookworm" es Debian, no Ubuntu — OK, no toca mirrors de Canonical # Imagen 2: Worker de procesamiento FROM python:3.11-slim # También Debian base — OK # Imagen 3: Herramienta interna de admin FROM ubuntu:22.04 # ACA está. Ubuntu directa, APT apunta a archive.ubuntu.com # Imagen 4: Base para scripts de migración de DB FROM ubuntu:20.04 # Otra más. LTS vieja que nunca actualicé porque "funciona" # Imagen 1: API principal FROM node:20-bookworm-slim # "bookworm" es Debian, no Ubuntu — OK, no toca mirrors de Canonical # Imagen 2: Worker de procesamiento FROM python:3.11-slim # También Debian base — OK # Imagen 3: Herramienta interna de admin FROM ubuntu:22.04 # ACA está. Ubuntu directa, APT apunta a archive.ubuntu.com # Imagen 4: Base para scripts de migración de DB FROM ubuntu:20.04 # Otra más. LTS vieja que nunca actualicé porque "funciona" # Busco en mi registry privado de Railway cuántas imágenes heredan de mi base ubuntu -weight: 500;">docker image inspect $(-weight: 500;">docker images -q) --format '{{.RepoTags}} {{.Config.Image}}' \ 2>/dev/null | grep -i "juanchi-base" # Output: 3 imágenes usando juanchi-base:latest como FROM # Busco en mi registry privado de Railway cuántas imágenes heredan de mi base ubuntu -weight: 500;">docker image inspect $(-weight: 500;">docker images -q) --format '{{.RepoTags}} {{.Config.Image}}' \ 2>/dev/null | grep -i "juanchi-base" # Output: 3 imágenes usando juanchi-base:latest como FROM # Busco en mi registry privado de Railway cuántas imágenes heredan de mi base ubuntu -weight: 500;">docker image inspect $(-weight: 500;">docker images -q) --format '{{.RepoTags}} {{.Config.Image}}' \ 2>/dev/null | grep -i "juanchi-base" # Output: 3 imágenes usando juanchi-base:latest como FROM # En un contenedor de prueba con ubuntu:22.04 # Agrego una entrada /etc/hosts falsa para simular mirrors caídos -weight: 500;">docker run --add-host=archive.ubuntu.com:127.0.0.1 \ --add-host=security.ubuntu.com:127.0.0.1 \ ubuntu:22.04 \ bash -c "time -weight: 500;">apt-get -weight: 500;">update 2>&1 | tail -5" # En un contenedor de prueba con ubuntu:22.04 # Agrego una entrada /etc/hosts falsa para simular mirrors caídos -weight: 500;">docker run --add-host=archive.ubuntu.com:127.0.0.1 \ --add-host=security.ubuntu.com:127.0.0.1 \ ubuntu:22.04 \ bash -c "time -weight: 500;">apt-get -weight: 500;">update 2>&1 | tail -5" # En un contenedor de prueba con ubuntu:22.04 # Agrego una entrada /etc/hosts falsa para simular mirrors caídos -weight: 500;">docker run --add-host=archive.ubuntu.com:127.0.0.1 \ --add-host=security.ubuntu.com:127.0.0.1 \ ubuntu:22.04 \ bash -c "time -weight: 500;">apt-get -weight: 500;">update 2>&1 | tail -5" Err:1 http://archive.ubuntu.com/ubuntu jammy InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Err:2 http://security.ubuntu.com/ubuntu jammy-security InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Reading package lists... Done W: Some index files failed to download... real 0m18.432s Err:1 http://archive.ubuntu.com/ubuntu jammy InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Err:2 http://security.ubuntu.com/ubuntu jammy-security InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Reading package lists... Done W: Some index files failed to download... real 0m18.432s Err:1 http://archive.ubuntu.com/ubuntu jammy InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Err:2 http://security.ubuntu.com/ubuntu jammy-security InRelease Could not connect to 127.0.0.1:80 (127.0.0.1). - connect (111: Connection refused) Reading package lists... Done W: Some index files failed to download... real 0m18.432s # Esto falla silenciosamente en mirrors degradados: RUN -weight: 500;">apt-get -weight: 500;">update && -weight: 500;">apt-get -weight: 500;">install -y -weight: 500;">curl # Esto al menos intenta continuar: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl # Lo que realmente querés si el mirror puede estar caído: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing || true && \ -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl 2>/dev/null || \ echo "ADVERTENCIA: -weight: 500;">apt degradado, continuando sin paquetes opcionales" # Esto falla silenciosamente en mirrors degradados: RUN -weight: 500;">apt-get -weight: 500;">update && -weight: 500;">apt-get -weight: 500;">install -y -weight: 500;">curl # Esto al menos intenta continuar: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl # Lo que realmente querés si el mirror puede estar caído: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing || true && \ -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl 2>/dev/null || \ echo "ADVERTENCIA: -weight: 500;">apt degradado, continuando sin paquetes opcionales" # Esto falla silenciosamente en mirrors degradados: RUN -weight: 500;">apt-get -weight: 500;">update && -weight: 500;">apt-get -weight: 500;">install -y -weight: 500;">curl # Esto al menos intenta continuar: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing && -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl # Lo que realmente querés si el mirror puede estar caído: RUN -weight: 500;">apt-get -weight: 500;">update --fix-missing || true && \ -weight: 500;">apt-get -weight: 500;">install -y --no--weight: 500;">install-recommends -weight: 500;">curl 2>/dev/null || \ echo "ADVERTENCIA: -weight: 500;">apt degradado, continuando sin paquetes opcionales"