Los grupos de ransomware aprendieron hace años que cifrar servidores no basta: si la víctima restaura desde el backup, nadie paga. Por eso lo primero que hace un ataque serio después de conseguir acceso de administrador es buscar los backups y destruirlos. La variante que describimos en el ransomware que ya ataca Proxmox directamente quita la protección de los backups y los borra antes de cifrar un solo disco.
La lección es incómoda. Un backup que la máquina comprometida puede borrar no es un backup, es una segunda copia que desaparece con la primera. Y un backup que nadie ha restaurado tampoco lo es: es una esperanza.
Este post enseña a construir el otro tipo, con comandos que ejecutamos en un laboratorio: un servidor de backups append-only en el que producción puede escribir pero nunca borrar, una copia externa bajo Object Lock que nadie puede destruir antes de que termine su retención, y una prueba de restauración mensual que demuestra que los datos vuelven y te avisa cuando no vuelven.
Por qué fallan los backups frente al ransomware
Cuando un ataque de ransomware se lleva los backups, casi siempre es por una de tres razones:
- Las mismas credenciales. El job de backup del servidor tiene una clave que puede borrar lo que escribió. El atacante que controla el servidor controla esa clave.
- Los mismos administradores. El servidor de backups unido al mismo directorio, gestionado desde el mismo jump host, con las mismas cuentas de administrador. Si se compromete el dominio, la consola de backups cae con él.
- Restauraciones sin probar. El backup salía en verde cada noche y, el día que importaba, el dump estaba vacío, la contraseña de cifrado estaba en el servidor cifrado o la restauración tardó cuatro días.
La solución para las dos primeras es una regla: producción puede añadir backups, nunca eliminarlos. La solución para la tercera es una prueba que se ejecute aunque nadie se acuerde.
La regla: producción añade, nunca elimina
La clásica regla 3-2-1 (tres copias, en dos tipos de soporte, una fuera de las instalaciones) sigue siendo válida, pero por sí sola no dice nada sobre quién puede borrar las copias. Añade dos condiciones:
- Al menos una copia es inmutable: nada a lo que llegue producción puede borrarla ni sobrescribirla antes de que termine un periodo de retención.
- Las restauraciones se prueban, con el resultado registrado y con alertas, no se dan por supuestas.
A continuación, dos formas de conseguir la copia inmutable, ambas con herramientas open source. Usa la primera en tu propia infraestructura, la segunda para la copia externa, o las dos.
Opción 1: un servidor de backups append-only
restic es una herramienta de backup open source, y su complemento rest-server puede funcionar en modo append-only: los clientes pueden crear snapshots y leerlos, pero cualquier petición para borrar o sobrescribir un fichero se rechaza. La limpieza de snapshots antiguos solo se hace en el servidor, por alguien que ha iniciado sesión allí.
En el servidor de backups, un repositorio por host, con TLS:
rest-server --path /srv/restic --listen :8000 --append-only --private-repos \
--tls --tls-cert /etc/rest-server/cert.pem --tls-key /etc/rest-server/key.pem
# one user per host; --private-repos limits each user to its own repository
htpasswd -B -c /srv/restic/.htpasswd db01En cada host de producción, el job de backup solo necesita la URL del repositorio y su contraseña:
export RESTIC_REPOSITORY=rest:https://db01:USER_PASSWORD@backup.internal:8000/db01/
export RESTIC_PASSWORD_FILE=/root/.restic-repo-password
pg_dump -Fc -f /var/backups/postgres/app.dump app
restic backup --host db01 /var/backups/postgres /etcProbamos lo que puede hacer un atacante con las credenciales de backup de un host. Hacer backup funciona. Intentar eliminar snapshots falla, y los snapshots se quedan:
$ restic forget --host web01 --keep-last 1 --prune
remove 1 snapshots:
...
Remove(<snapshot/55f0cb7a99>) failed: unexpected HTTP response (403): 403 Forbidden
unable to remove snapshot/55f0cb7a99... from the repositoryLa retención se aplica en el propio servidor de backups, sobre la ruta local, con una política basada en tiempo:
restic -r /srv/restic/db01 forget --host db01 --keep-within 30d --pruneLa política basada en tiempo no es un detalle. La documentación de restic advierte de que un atacante que puede añadir snapshots puede añadir algunos vacíos con fechas un poco más recientes que las reales, de modo que una política por número, como "conserva los tres últimos", borre por ti todos los snapshots legítimos. --keep-within 30d conserva todo lo de los 30 días anteriores al snapshot más reciente, así que los reales sobreviven. Ejecútala mientras el último backup bueno tenga menos de 30 días, revisa la lista de snapshots antes de limpiar y mantén la alerta de frescura del backup que verás más abajo.
Dos puntos más. La contraseña del repositorio protege la confidencialidad de los datos; guarda una copia en un sitio que no sea ni el host de producción ni el servidor de backups, porque perderla es perder todos los backups. Y el servidor de backups no debe compartir dominio de administración, claves SSH ni ruta de red de gestión con producción.
Opción 2: Object Lock para la copia externa
El almacenamiento de objetos compatible con S3, en las grandes nubes o autoalojado, admite Object Lock: una vez escrita, una versión de un objeto no se puede borrar ni sobrescribir hasta su fecha de retención. En modo compliance, nadie puede acortar esa retención, ni siquiera el administrador de la cuenta. En modo governance, pueden hacerlo los usuarios con un permiso especial. Para backups contra ransomware, usa el modo compliance y elige una retención cuyo almacenamiento estés dispuesto a pagar.
Activa Object Lock al crear el bucket: algunos proveedores pueden añadirlo a un bucket con versionado ya existente, no todos. Requiere versionado, y crear el bucket con el bloqueo activa el versionado. Con el cliente de línea de comandos estándar aws, que funciona con cualquier endpoint compatible con S3:
aws s3api create-bucket --bucket backups --object-lock-enabled-for-bucket \
--endpoint-url https://s3.example.internal
aws s3api put-object-lock-configuration --bucket backups --endpoint-url https://s3.example.internal \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'Después, restic escribe en él como en cualquier otro repositorio (RESTIC_REPOSITORY=s3:https://s3.example.internal/backups/db01).
Esta es la parte que la mayoría de las guías se salta, y la comprobamos en el laboratorio. Un atacante con la clave de backup todavía puede ejecutar restic forget --prune, o borrar el prefijo entero, y las dos cosas parecen funcionar. Después, restic dice que ni siquiera hay repositorio. Lo que pasó en realidad es que cada borrado añadió un delete marker, que oculta el objeto, mientras las versiones bloqueadas seguían exactamente donde estaban. Un intento directo de borrar una versión bloqueada fue rechazado.
Así que los datos están a salvo, pero tienes que saber cómo recuperarlos. Elimina los delete markers bajo el prefijo del repositorio, con credenciales que el atacante nunca tuvo:
EP=https://s3.example.internal
aws s3api list-object-versions --bucket backups --prefix db01/ --endpoint-url "$EP" \
--query 'DeleteMarkers[?IsLatest].[Key,VersionId]' --output text |
while read -r key ver; do
aws s3api delete-object --bucket backups --key "$key" --version-id "$ver" --endpoint-url "$EP" >/dev/null
done
restic unlock --remove-all # stale locks come back with the markers
restic snapshots
restic check --read-dataEn nuestra prueba volvieron todos los snapshots, incluido el que el "atacante" había eliminado, y check --read-data no encontró errores. Ejecuta unlock --remove-all solo cuando estés seguro de que no hay ningún backup en curso.
Dos notas prácticas: da a la clave del job de backup solo los permisos que restic necesita en ese bucket, y añade una regla de ciclo de vida que expire las versiones no actuales después del periodo de retención, o los datos borrados seguirán sumando coste de almacenamiento.
¿No tienes claro si tus backups sobrevivirían a un atacante con permisos de administrador? Solicita una evaluación gratuita de backups y preparación ante ransomware: rastreamos qué credenciales pueden borrar cada copia y probamos contigo una restauración.
Mantén las claves fuera de su alcance
La inmutabilidad falla en silencio cuando las credenciales que la controlan están al lado de producción:
- Identidades separadas. El servidor de backups y la cuenta de almacenamiento de objetos se administran con cuentas que no existen en el directorio de producción, con MFA.
- Ningún camino desde producción. Los hosts de producción llegan al puerto de la API del servidor de backups y a nada más. Sin SSH de producción al servidor de backups, sin jump host compartido.
- La contraseña del repositorio, las credenciales del administrador del bucket y los pasos de recuperación también se guardan offline, por ejemplo impresos en una caja fuerte, para que la recuperación no dependa de sistemas que podrían estar cifrados.
- Los administradores del hipervisor y de los backups son personas distintas, o al menos cuentas distintas. Quien controla el hipervisor puede llegar a los discos de todas las máquinas virtuales y, a menudo, también a la configuración de backups.
La prueba de restauración que nadie hace
Un job de backup que termina con 0 demuestra que el job se ejecutó, no que los datos vuelvan. La única prueba es restaurar, así que automatízalo. Este script restaura el último snapshot de un host, carga el dump de la base de datos en un contenedor PostgreSQL desechable, comprueba que una tabla clave tiene filas y publica el resultado para Prometheus mediante el textfile collector de node_exporter. Ejecútalo cada mes desde una máquina sin permisos para borrar nada:
#!/usr/bin/env bash
# Restore drill: restore the latest snapshot of one host, prove the database dump in it
# loads and holds data, and publish the result for Prometheus (node_exporter textfile
# collector). Run it from a machine whose credentials cannot delete from the repository.
# RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE must be set; docker must be available.
# usage: restore-drill.sh HOST DUMP_PATH TABLE
# e.g. restore-drill.sh db01 /var/backups/postgres/app.dump orders
set -uo pipefail
HOST="$1" DUMP="$2" TABLE="$3"
METRICS="${METRICS_DIR:-/var/lib/node_exporter/textfile_collector}"
WORK=$(mktemp -d)
PG="drill-pg-$$"
trap 'docker rm -f "$PG" >/dev/null 2>&1; rm -rf "$WORK"' EXIT
drill() {
restic restore latest --host "$HOST" --include "$DUMP" --target "$WORK" --verify || return 1
[ -s "$WORK$DUMP" ] || { echo "no dump at $DUMP in the latest snapshot" >&2; return 1; }
docker run -d --name "$PG" -e POSTGRES_PASSWORD=drill -v "$WORK$DUMP:/drill.dump:ro" postgres:16-alpine >/dev/null || return 1
for _ in $(seq 1 60); do docker exec "$PG" pg_isready -q -U postgres && break; sleep 1; done
docker exec "$PG" pg_restore -U postgres -d postgres --no-owner --exit-on-error /drill.dump || return 1
rows=$(docker exec "$PG" psql -U postgres -tAc "SELECT count(*) FROM $TABLE") || return 1
[ "$rows" -gt 0 ] || { echo "table $TABLE is empty in the restored dump" >&2; return 1; }
echo "restored $rows rows from $TABLE"
}
start=$(date +%s)
if drill; then ok=1; else ok=0; fi
end=$(date +%s)
publish() { # file, content: write then rename, so the collector never reads half a file
printf '%s\n' "$2" > "$METRICS/$1.tmp" && mv "$METRICS/$1.tmp" "$METRICS/$1"
}
publish "restore_drill_$HOST.prom" "backup_restore_drill_success{host=\"$HOST\"} $ok
backup_restore_drill_duration_seconds{host=\"$HOST\"} $((end - start))"
# Only a success moves this timestamp, so a run of failures shows up as an overdue drill.
[ "$ok" = 1 ] && publish "restore_drill_last_success_$HOST.prom" \
"backup_restore_drill_last_success_timestamp_seconds{host=\"$HOST\"} $end"
exit $((1 - ok))Lo probamos en los dos sentidos. Con un dump bueno restauró 500 filas y publicó backup_restore_drill_success 1. Con un dump cuya tabla estaba vacía, el tipo de backup que sale en verde durante meses, falló, publicó 0 y dejó intacta la marca de tiempo del último éxito. La métrica de duración es tu tiempo de restauración medido para ese sistema; compárala con el tiempo de recuperación que prometiste al negocio.
Adapta la comprobación a lo que importa: un recuento de filas de la tabla que más dolería perder, una lista de checksums para recursos compartidos de ficheros o arrancar la aplicación con los datos restaurados.
Alertas sobre lo que importa
Después de cada backup, publica cuándo se hizo el snapshot más reciente:
ts=$(restic snapshots --host "$HOST" --latest 1 --json | jq -r '.[-1].time')
printf 'backup_last_snapshot_timestamp_seconds{host="%s"} %s\n' "$HOST" "$(date -d "$ts" +%s)" \
> "$METRICS/backup_$HOST.prom.tmp" && mv "$METRICS/backup_$HOST.prom.tmp" "$METRICS/backup_$HOST.prom"Con eso, tres alertas de Prometheus cubren los fallos que importan: ningún backup nuevo, una prueba fallida y una prueba que lleva demasiado tiempo sin salir bien:
groups:
- name: backups
rules:
- alert: BackupStale
expr: time() - backup_last_snapshot_timestamp_seconds > 26 * 3600
labels:
severity: critical
annotations:
summary: "No new backup snapshot for {{ $labels.host }} in 26 hours"
- alert: RestoreDrillFailed
expr: backup_restore_drill_success == 0
labels:
severity: warning
annotations:
summary: "The last restore drill for {{ $labels.host }} failed"
- alert: RestoreDrillOverdue
expr: time() - backup_restore_drill_last_success_timestamp_seconds > 35 * 86400
labels:
severity: warning
annotations:
summary: "No successful restore drill for {{ $labels.host }} in 35 days"Validamos las reglas con promtool check rules y las probamos con promtool test rules: la alerta de backup antiguo salta tras 26 horas sin snapshot y no antes, y la de prueba atrasada salta el día 35 después de la última prueba con éxito.
Una rutina mensual
Ponlo en el calendario, con un nombre al lado:
- 1Comprueba que la prueba de restauración pasó en todos los sistemas críticos y lee las duraciones de restauración.
- 2Ejecuta
restic check --read-data-subset 10%en cada repositorio: cada mes se relee y verifica una décima parte aleatoria de los datos guardados. - 3Lee la lista de snapshots de cada repositorio antes de que se ejecute la limpieza en el servidor. Los snapshots que no esperabas son la primera señal de que alguien tiene las credenciales de backup.
- 4Una vez al trimestre, restaura un sistema completo en una máquina limpia, solo con la copia externa y las instrucciones offline. Es la única prueba del procedimiento que de verdad usarás.
Nuestra práctica de backup y recuperación ante desastres lo configura y lo opera, y nuestro equipo de ciberseguridad comprueba si un atacante con acceso de administrador aún podría llegar a tus backups.
Evaluación gratuita de backups y preparación ante ransomware
Si no tienes claro que tus backups sobrevivirían a alguien con acceso de administrador, lo comprobamos contigo:
- Caminos de borrado: cada credencial, host y cuenta de administrador que puede borrar o sobrescribir cada copia de backup.
- Inmutabilidad: si al menos una copia es append-only o está bajo Object Lock en modo compliance, y durante cuánto tiempo.
- Prueba de restauración: una restauración real de un sistema crítico, cronometrada, con el resultado comparado con el tiempo de recuperación que has prometido.
Recibes una lista por escrito de las carencias y del orden en que cerrarlas, sin ningún compromiso.
Solicita tu evaluación gratuita, o descubre cómo funciona nuestra práctica de backup y recuperación ante desastres.