Ingeniería defensiva: sistemas que resisten el desastre
Patrones de ingeniería defensiva para evitar desastres en producción: molly guards, modos dry-run, soft deletes y por qué los diálogos de confirmación fallan.

Un ingeniero junior de una fintech de tamaño mediano ejecutó un script de migración de base de datos un viernes por la tarde. El script se suponía que limpiaba registros huérfanos en un entorno de staging. En cambio, se conectó a la base de datos de producción y borró 4,2 millones de registros de transacciones de clientes. ¿El backup? De hace tres días. La empresa pasó las siguientes 72 horas en plena respuesta a incidentes, reconciliando a mano los registros a partir de los logs del procesador de pagos. El coste total superó los 400.000 dólares entre horas de ingeniería, compensaciones a clientes e informes regulatorios.
El script no tenía ninguna comprobación de entorno. Ni flag de dry-run. Ni un prompt de confirmación que mostrara a qué base de datos estaba conectado. La cadena de conexión venía de una variable de entorno que, por casualidad, apuntaba a producción en el portátil del ingeniero desde una sesión de depuración dos semanas antes. Cada uno de esos fallos era evitable.
Por qué los diálogos del tipo «¿Estás seguro?» no funcionan
El mecanismo de seguridad más común en software es el diálogo de confirmación. También es el más inútil. Los estudios sobre la fatiga de confirmación muestran que, tras encontrarse con ellos unas cuantas veces, los usuarios aceptan los prompts de «¿Estás seguro?» casi siempre sin leerlos. El aviso se vuelve invisible: un clic más en el camino hacia algo que ya habías decidido hacer.
El problema no es que las confirmaciones sean una mala idea. Es que las confirmaciones genéricas no aportan ninguna información. «¿Seguro que quieres continuar?» no te dice qué vas a hacer. Compáralo con: «Estás a punto de borrar 4.217.893 filas de la tabla transactions en prod-us-east-1. Escribe el nombre de la base de datos para confirmar.». La segunda versión te obliga a leer de verdad lo que está pasando. Esa es la diferencia entre un reductor de velocidad y un molly guard.
Qué es un molly guard y por qué importa
El término «molly guard» viene de una cubierta física de plástico colocada sobre el Big Red Button de los mainframes para evitar apagados accidentales. Se dice que recibe su nombre de la hija pequeña de un programador, Molly, que no paraba de pulsarlo. En software, un molly guard es cualquier mecanismo que hace que las acciones destructivas sean difíciles de ejecutar por accidente, pero siguen siendo posibles cuando son intencionadas.
El paquete de Linux molly-guard hace exactamente esto. Intercepta los comandos shutdown, reboot y halt en sesiones SSH y te pide que escribas el hostname de la máquina que vas a apagar. No puedes pasar por ahí en piloto automático: tienes que demostrar que sabes en qué máquina estás.
$ sudo reboot
Good grief! You're on a remote SSH session to prod-web-03.
Please type the hostname to confirm: prod-web-03
# Compare with the useless version:
$ sudo reboot
Are you sure? (y/n): y
# *types y without reading, muscle memory*
El principio es simple: la confirmación debe exigir información que demuestre que el operador entiende la acción. Escribir «y» no demuestra nada. Escribir el hostname, el nombre de la tabla o el número de registros afectados sí demuestra que leíste la advertencia.
Modos dry-run: mira antes de disparar
Toda operación destructiva debería tener un modo dry-run. No «debería» como buena práctica, sino como «algún día te arrepentirás de no tenerlo». Un dry run ejecuta toda la lógica de la operación, registra exactamente lo que pasaría y luego se detiene. Sin efectos secundarios. Visibilidad total.
El patrón es fácil de implementar. Aquí tienes un script de migración con dry-run integrado:
#!/bin/bash
set -euo pipefail
DRY_RUN=${DRY_RUN:-true} # Default to dry-run!
DB_HOST=$(get_db_host)
DB_NAME=$(get_db_name)
echo "Target: $DB_HOST / $DB_NAME"
echo "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'LIVE')"
ROWS=$(psql -h "$DB_HOST" -d "$DB_NAME" -t -c \
"SELECT count(*) FROM orphaned_records WHERE created_at < now() - interval '90 days'")
echo "Records to delete: $ROWS"
if [ "$DRY_RUN" = true ]; then
echo "Dry run complete. Set DRY_RUN=false to execute."
exit 0
fi
# Require explicit confirmation for live runs
read -p "Type '$DB_NAME' to confirm deletion: " CONFIRM
if [ "$CONFIRM" != "$DB_NAME" ]; then
echo "Aborted."
exit 1
fi
psql -h "$DB_HOST" -d "$DB_NAME" -c \
"DELETE FROM orphaned_records WHERE created_at < now() - interval '90 days'"
echo "Deleted $ROWS records."
Fíjate en tres cosas: el valor por defecto es dry-run (tienes que optar por la destrucción, no salirte de ella), muestra la base de datos objetivo y el número de filas antes de hacer nada, y, incluso en modo real, exige escribir el nombre de la base de datos. Tres capas de defensa. Cualquiera de ellas habría evitado el incidente que describí al principio.
Redes de seguridad para migraciones de base de datos
Las migraciones de base de datos son una de las operaciones de mayor riesgo en cualquier pipeline de despliegue. Suelen ser irreversibles, se ejecutan sobre estado compartido y una mala puede tumbar toda tu aplicación. Y, aun así, la mayoría de equipos las tratan como un paso más del deploy.
Esta es una jerarquía de mecanismos de seguridad, de lo básico a lo blindado:
- Comprobaciones de entorno: el script de migración verifica que apunta al entorno previsto antes de ejecutarse. Suena obvio. Te sorprendería la frecuencia con la que falta.
- Snapshots previos a la migración: crea automáticamente un snapshot de la base de datos antes de cualquier migración. Si falla o causa problemas, puedes restaurar en minutos, no en horas.
- Guardas por número de filas: si una migración afectará a más de N filas, exige confirmación explícita. Una migración que toca millones de filas sin previo aviso casi siempre es un bug.
- Timeouts de sentencia: configura timeouts agresivos en las queries de migración. Una migración que tarda 45 minutos está bloqueando tablas y degradando el rendimiento. Falla rápido.
- Solo compatible hacia atrás: exige que cada migración sea compatible con el código desplegado actualmente. Eso significa nada de renombrar columnas, nada de añadir NOT NULL sin valor por defecto, nada de eliminar columnas que todavía se referencian.
# Rails example using strong_migrations gem
class AddPhoneToUsers < ActiveRecord::Migration[7.1]
def change
# This will raise an error with a safe alternative:
# add_column :users, :phone, :string, null: false
#
# Instead, do it in two steps:
add_column :users, :phone, :string
# Then in a separate migration after backfill:
# change_column_null :users, :phone, false
end
end
Herramientas como strong_migrations en Rails, squawk para PostgreSQL y skeema para MySQL detectan patrones peligrosos antes de que lleguen a producción. Si no usas algo parecido, estás confiando en que la code review pille problemas sutiles de migración, y los revisores se les escapan cosas.
Soft deletes y ventanas de deshacer
Los hard deletes son un compromiso permanente tomado en un momento de certeza. El problema es que esa certeza suele estar equivocada. Los soft deletes —marcar registros como borrados sin eliminarlos realmente— te dan una ventana para recuperarte de los errores.
-- Hard delete: gone forever
DELETE FROM projects WHERE id = 4872;
-- Soft delete: recoverable
UPDATE projects
SET deleted_at = now(),
deleted_by = 'user:priya'
WHERE id = 4872;
-- Recovery is trivial
UPDATE projects
SET deleted_at = NULL,
deleted_by = NULL
WHERE id = 4872;
El compromiso es real: los soft deletes añaden complejidad a las queries (necesitas WHERE deleted_at IS NULL en todas partes), aumentan el almacenamiento y pueden generar confusión sobre el estado real de los datos. Pero para cualquier dato de cara al usuario en el que un borrado accidental sea posible, merece la pena. GitHub no elimina tu repositorio hasta pasados 90 días. Slack conserva los mensajes borrados por cumplimiento normativo. La papelera de Gmail se vacía a los 30 días. No son accidentes: son decisiones de ingeniería deliberadas.
El mismo principio se aplica a la infraestructura. En lugar de terminar una instancia EC2, párala primero. En lugar de eliminar una base de datos, renómbrala a mydb_deleted_20260315 y pon un recordatorio en el calendario para borrarla de verdad dentro de dos semanas. El coste de mantener una instancia parada o una base renombrada durante unos días es insignificante comparado con el de restaurar desde backup.
Seguridad en el despliegue: canarios, circuit breakers y rollbacks
Los despliegues son otra categoría de acción destructiva que la mayoría de equipos no tratan con suficiente cautela. Un mal deploy puede tumbar producción tan eficazmente como una base de datos borrada, y ocurre con mucha más frecuencia.
La configuración mínima viable de seguridad en despliegues incluye:
- Despliegues canary: envía entre el 1 y el 5 % del tráfico a la nueva versión. Si los errores se disparan, haz rollback automático antes de que crezca el radio de impacto.
- Disparadores de rollback automático: define umbrales de tasa de error, de latencia y de fallos en health checks que activen el rollback automáticamente. No dependas de que un humano lo note a las 2 de la madrugada.
- Congelación de despliegues durante incidentes: si hay un incidente activo, bloquea todos los despliegues. Lo último que necesitas mientras apagas fuegos es que alguien despliegue cambios no relacionados.
- Rollback en un clic: volver atrás debería ser más fácil que avanzar. Si tu proceso de rollback implica entrar por SSH en servidores y ejecutar comandos a mano, no tienes un proceso de rollback.
# Kubernetes progressive delivery with Argo Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 5m }
- analysis:
templates:
- templateName: error-rate-check
- setWeight: 25
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
# Auto-rollback if error rate exceeds 1%
abortScaleDownDelaySeconds: 30
El patrón aquí es el compromiso progresivo. No pasas del 0 al 100 % de golpe. Das pequeños pasos, verificas cada uno y mantienes la capacidad de retroceder en cada etapa. Es más lento que desplegar en todas las instancias a la vez sin pensar, pero la primera vez que detecte un mal deploy antes de llegar a todos tus usuarios, agradecerás cada minuto extra.
Prevención arquitectónica: hacer imposible lo incorrecto
Los mejores mecanismos de seguridad no te piden que tengas cuidado. Hacen que sea estructuralmente imposible hacer lo incorrecto. Esa es la diferencia entre una barandilla y una señal de advertencia.
- Infraestructura inmutable: si no puedes entrar por SSH a los servidores de producción, no puedes ejecutar comandos en ellos por accidente. Si los despliegues son siempre instancias nuevas desde una imagen conocida, no puede haber deriva de configuración.
- Acceso de mínimo privilegio: los ingenieros no deberían tener credenciales de la base de datos de producción en sus portátiles. Punto. Usa herramientas de acceso just-in-time que concedan credenciales temporales con trazas de auditoría.
- Credenciales separadas por entorno: si staging y producción usan almacenes de credenciales distintos, literalmente no puedes conectarte a producción por accidente con las herramientas de staging.
- Protección contra borrado: AWS te permite activar la protección contra terminación en instancias EC2 y la protección contra borrado en bases RDS. Actívalas en todo lo que importe. Es un cambio de configuración de cinco segundos que evita por completo toda una clase de errores catastróficos.
Si alguien puede destruir producción por accidente con un solo comando, el problema no es la persona: es el sistema que permitió que un solo comando destruyera producción.
He visto equipos responder a incidentes de producción añadiendo más documentación, más checklists y más formación. Ayudan, pero todas esas medidas dependen de que los humanos sean perfectos. Los humanos no lo son. La mejor respuesta es cambiar el sistema para que el error no pueda ocurrir en primer lugar o, si ocurre, el radio de impacto esté contenido y la recuperación sea rápida.
Construir una cultura de ingeniería que priorice la seguridad
Las herramientas y la arquitectura importan, pero la cultura decide si de verdad se implementan. Los equipos que ven los mecanismos de seguridad como sobrecarga o burocracia los saltarán bajo presión de plazos, y la presión de plazos es permanente.
El patrón más efectivo que he visto es tratar los mecanismos de seguridad como un requisito de ingeniería de primera clase, no como algo deseable. Cada operación destructiva pasa por una revisión de diseño que pregunta específicamente: ¿qué pasa si esto se ejecuta contra el objetivo equivocado? ¿Qué pasa si se ejecuta dos veces? ¿Qué pasa si se ejecuta con datos desactualizados? ¿Cómo lo deshacemos?
Los post-mortems sin culpables ya son lo mínimo exigible. Pero la práctica menos común que creo que importa más es el pre-mortem. Antes de lanzar un cambio arriesgado, reúne al equipo y pregunta: «Asumamos que esto salió terriblemente mal. ¿Qué pasó?». La gente es sorprendentemente buena identificando modos de fallo cuando lo planteas como imaginación y no como predicción. Los fallos que identifican se convierten en los mecanismos de seguridad que construyes.
¿Aquel ingeniero junior que borró la base de datos de producción? Sigue en la empresa. Ahora es uno de los mayores defensores de la ingeniería defensiva del equipo. El incidente no fue culpa suya: fue un fallo del sistema. Y el sistema es mucho más difícil de romper ahora.


