Danila (Dayfing)
Volver a publicaciones
3079 palabras16 min

Copias de seguridad 3-2-1 con restic: copia externa y restauración

Una copia de seguridad de la que se puede restaurar se ejecuta según un calendario sin intervención, guarda una copia fuera de las instalaciones donde un servidor comprometido no puede borrarla, captura las bases de datos en un estado consistente y se ha restaurado y cronometrado hace poco. Con restic eso significa un repositorio cifrado en almacenamiento compatible con S3 protegido por object lock o por un servidor append-only, volcados de las bases antes de cada ejecución, un timer de systemd con alertas de fallo y un simulacro de restauración periódico. Esta guía monta esa configuración para un servidor Linux, y las mismas piezas sirven para el equipo de un desarrollador.

Defina el RPO y el RTO antes de elegir herramientas

El recovery point objective (RPO) indica cuántos datos puede permitirse perder, medido en tiempo. El recovery time objective (RTO) indica cuánto tiempo puede estar caído el servicio durante una restauración. El responsable de un servicio puede responder a ambas preguntas con palabras sencillas: «podemos perder como máximo un día de pedidos» y «la tienda debe volver a funcionar en menos de cuatro horas».

La pérdida máxima de una copia programada es aproximadamente el intervalo entre ejecuciones más el tiempo que tarda una ejecución en llegar al almacenamiento externo. Una tarea nocturna que empieza a las 02:00 y termina de subir a las 02:40 da un RPO en el peor caso de unas 24 horas y 40 minutos. Si eso es demasiado para una base de datos, haga su copia con más frecuencia o añada el archivado de WAL.

El RTO depende sobre todo del tiempo de transferencia y reconstrucción. Los segundos de transferencia equivalen al volumen de datos en megabits dividido por la velocidad del enlace en Mbit/s. Una restauración de 200 GB son 1 600 000 megabits, así que a 100 Mbit/s tarda 16 000 segundos, unas 4,4 horas, antes incluso de restaurar la base. Si eso ya supera el RTO, un bucket remoto por sí solo no basta, y necesita una copia local o un sistema de reserva.

La regla 3-2-1 y su extensión 3-2-1-1-0

La regla 3-2-1 significa tres copias de los datos, en dos tipos de almacenamiento distintos, con una copia fuera de las instalaciones. En un VPS, el disco de producción es la primera copia, un volcado o repositorio local en almacenamiento separado es la segunda, y un bucket en otro proveedor o en otra cuenta es la tercera. Una segunda partición en el mismo disco muere junto con la primera, así que ponga la copia local en otro dispositivo (la guía de compra de SSD NVMe ayuda a elegirlo) o en otro host.

Los fabricantes de soluciones de copia de seguridad popularizaron la extensión 3-2-1-1-0. El 1 adicional es una copia desconectada o inmutable, para que un atacante con root en el servidor y sus credenciales de nube siga sin poder borrarla. El 0 significa cero errores en las comprobaciones de integridad y en las pruebas de restauración. La mayoría de las configuraciones se salta ese último dígito, y es precisamente el que demuestra si los otros cuatro son reales.

Qué incluir en la copia y qué excluir

Guarde lo que no pueda recrear rápidamente desde una fuente pública: /etc, el código de la aplicación y los archivos subidos en /srv o /var/www, los directorios personales con claves SSH y GPG, los crontabs, el material TLS, los volcados de bases de datos y la lista de paquetes instalados que devuelve apt-mark showmanual. En el equipo de un desarrollador lo valioso es el trabajo sin subir al remoto, los stashes, los dotfiles, las claves y los documentos.

Excluya los datos grandes o reproducibles: cachés, node_modules, entornos virtuales, resultados de compilación, imágenes de contenedores, pesos de modelos y swap. Excluya los directorios de bases de datos en ejecución, como /var/lib/postgresql, porque una copia tomada de un servidor en marcha no es una copia utilizable. En los archivos de exclusión de restic, un patrón que empieza por / coincide con una ruta absoluta, y un nombre simple como node_modules coincide a cualquier profundidad:

/var/lib/postgresql
/var/lib/docker
/var/cache
/var/tmp
/swapfile
/home/*/.cache
node_modules
.venv
__pycache__
*.tmp

--exclude-caches también omite los directorios que contienen un archivo estándar CACHEDIR.TAG, y --one-file-system impide que restic pase a otros sistemas de archivos montados.

Fundamentos de restic: repositorio, cifrado y contraseña

restic guarda snapshots deduplicados en un repositorio en disco local, SFTP, su propio servidor REST o almacenamiento de objetos, y solo sube los fragmentos nuevos. El cifrado no se puede desactivar: según la documentación de diseño de restic, todos los datos se cifran con AES-256 en modo contador y se autentican con Poly1305-AES, y la clave está protegida por una contraseña reforzada con scrypt.

Guarde la configuración en un archivo de entorno que solo pueda leer root, para que los comandos interactivos y las tareas programadas usen los mismos valores. La documentación de repositorios enumera los formatos de URL, por ejemplo s3:https://server:port/bucket para servicios compatibles con S3.

# /etc/restic/restic.env, mode 600
RESTIC_REPOSITORY=s3:https://s3.example.com/web1-backups
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=replace-me
AWS_SECRET_ACCESS_KEY=replace-me
HEARTBEAT_URL=https://monitor.example.net/ping/web1-backup
ALERT_WEBHOOK_URL=https://chat.example.net/hooks/backups
install -d -m 700 /etc/restic
(umask 077; openssl rand -base64 32 > /etc/restic/password)
set -a; . /etc/restic/restic.env; set +a
restic init
restic key add    # segunda contraseña para una copia de recuperación sin conexión
restic backup --one-file-system --exclude-caches \
  --exclude-file=/etc/restic/excludes.txt /etc /root /home /srv /var/www
restic snapshots

La documentación lo dice sin rodeos: perder la contraseña significa perder los datos de forma irrecuperable. Guarde la contraseña, la URL del repositorio y las credenciales del bucket en un gestor de contraseñas y en una copia sin conexión sellada, nunca solo en el servidor que protege. Con un gestor de secretos, RESTIC_PASSWORD_COMMAND puede imprimir la contraseña en lugar de leer un archivo.

Retención con forget y prune, integridad con check

restic forget aplica una política de retención a cada grupo de snapshots (por host y rutas de forma predeterminada), y prune elimina los datos a los que ya no hace referencia ningún snapshot. La opción --prune ejecuta ambos pasos:

restic forget --prune --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
restic check --read-data-subset=10%

Esta política conserva el snapshot más reciente de cada uno de los últimos 14 días que tengan uno, después uno por semana durante 8 semanas, uno por mes durante 12 meses y uno por año durante 3 años. Revise los cambios de política añadiendo --dry-run. La documentación de forget indica que el repositorio queda bloqueado durante prune, así que prográmelo lejos de las copias y añada --retry-lock al comando de copia.

restic check verifica la estructura y los metadatos. --read-data descarga cada pack, lo que cuesta tiempo y tráfico de salida, mientras que --read-data-subset lee una parte: 10% elige un subconjunto aleatorio en cada ejecución, y 3/12 lee una doceava parte fija, como describe la guía de mantenimiento de repositorios. Una comprobación correcta demuestra consistencia, no que pueda recuperar un servicio.

Un repositorio externo que el ransomware no puede borrar

Un atacante con root puede leer /etc/restic/restic.env. Si esas credenciales permiten borrar objetos, también se puede borrar la copia externa. Limite las credenciales a un solo bucket, mantenga las credenciales de administración fuera del servidor y aplique los hábitos de mínimo privilegio de la guía de bastionado de un VPS Linux. Después elija una de dos protecciones.

Servidor REST en modo append-only

El rest-server de restic tiene un modo --append-only que acepta copias nuevas pero impide borrar o modificar las existentes. La documentación de restic recomienda ejecutar forget y prune para estos repositorios desde una máquina separada y bien protegida, y usar --keep-within en lugar de reglas como --keep-weekly, porque un cliente comprometido podría añadir snapshots falsos con marcas de tiempo elegidas que ocupen el lugar de los reales.

Object lock en almacenamiento compatible con S3

S3 Object Lock conserva las versiones de los objetos con un modelo write-once-read-many. Requiere versionado, y en modo compliance nadie, ni siquiera el usuario root de la cuenta, puede borrar una versión bloqueada antes de su fecha de retención. Desde la versión 0.13, restic envía las sumas de comprobación de subida que exigen estos buckets, así que funciona con una retención predeterminada del bucket (añada --endpoint-url para otros proveedores):

aws s3api create-bucket --bucket web1-backups --object-lock-enabled-for-bucket
aws s3api put-object-lock-configuration --bucket web1-backups \
  --object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'

Cuando restic borra un archivo en un bucket así, S3 añade un marcador de borrado y la versión bloqueada permanece. Tras un ataque, copie el bucket tal como estaba, por ejemplo con rclone copy --s3-version-at "2026-09-20" offsite:web1-backups /srv/recovered-repo, y restaure desde esa copia. Los datos eliminados por prune siguen ocupando espacio hasta que termina la retención, así que haga expirar las versiones no actuales con una regla de ciclo de vida que no actúe antes de ese plazo. Pruebe el ciclo completo con su proveedor, porque las implementaciones compatibles con S3 difieren.

Copias consistentes de PostgreSQL

La documentación de PostgreSQL sobre copias a nivel de sistema de archivos es explícita: el servidor debe estar detenido para obtener una copia utilizable de su directorio de datos, y bloquear las conexiones no basta, porque herramientas como tar no toman una instantánea atómica y el servidor mantiene datos en búferes internos. Un snapshot atómico de un volumen con todos los archivos de datos y el WAL sí funciona, porque PostgreSQL reproduce el WAL como tras una caída.

Para la mayoría de los servidores, los volcados lógicos siguen siendo la opción correcta más sencilla. pg_dump genera una exportación consistente mientras la base está en uso y no bloquea a otros usuarios. El formato directory permite volcados y restauraciones en paralelo. Los roles no se incluyen, así que hay que volcar los objetos globales por separado. Este script se ejecuta antes de cada copia y conserva los últimos volcados en disco local:

#!/usr/bin/env bash
# /usr/local/sbin/pg-dump-for-backup
set -euo pipefail
out=/var/backups/postgresql
install -d -m 700 -o postgres -g postgres "$out"
runuser -u postgres -- pg_dumpall --globals-only --file="$out/globals.sql"
runuser -u postgres -- psql -Atc \
  "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate" |
while read -r db; do
  rm -rf "$out/$db.new"
  runuser -u postgres -- pg_dump --format=directory --jobs=2 --file="$out/$db.new" "$db"
  rm -rf "$out/$db"
  mv "$out/$db.new" "$out/$db"
done

Use pg_basebackup cuando la restauración lógica de un clúster grande no cabe en el RTO o cuando necesita recuperación a un punto en el tiempo. Copia el clúster completo en un directorio vacío mediante el protocolo de replicación, necesita un rol con REPLICATION y una entrada en pg_hba.conf, y transmite el WAL necesario para arrancar la copia. pg_verifybackup la comprueba con el manifiesto:

sudo -u postgres pg_basebackup --pgdata=/var/backups/pg-base --wal-method=stream --checkpoint=fast
sudo -u postgres pg_verifybackup /var/backups/pg-base

pg_restore reconstruye los índices a partir de sus definiciones. Con índices vectoriales grandes, como los índices HNSW de la guía de búsqueda híbrida con pgvector, esa reconstrucción puede dominar el tiempo de restauración.

Programación con un servicio y un timer de systemd

Un servicio de tipo oneshot ejecuta primero el volcado y luego la copia. Si el volcado falla, la copia no arranca y la unidad queda en estado fallido. Guarde las unidades en /etc/systemd/system/:

# restic-backup.service
[Unit]
Description=restic backup to the off-site repository
Wants=network-online.target
After=network-online.target postgresql.service
OnFailure=backup-alert@%n.service

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
CacheDirectory=restic
Nice=10
IOSchedulingClass=idle
ExecStartPre=/usr/local/sbin/pg-dump-for-backup
ExecStart=/usr/local/bin/restic backup --retry-lock 30m \
    --one-file-system --exclude-caches \
    --exclude-file=/etc/restic/excludes.txt --tag scheduled \
    /etc /root /home /srv /var/www /var/backups/postgresql
ExecStartPost=-/usr/bin/curl -fsS --max-time 10 --retry 3 ${HEARTBEAT_URL}
# restic-backup.timer
[Unit]
Description=Nightly restic backup

[Timer]
OnCalendar=*-*-* 02:15:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

Actívelo con systemctl enable --now restic-backup.timer y compruébelo con systemctl list-timers. Según systemd.timer, Persistent=true recupera una ejecución perdida mientras la máquina estaba apagada, y RandomizedDelaySec= reparte en el tiempo los hosts que comparten un bucket. Los servicios oneshot no tienen tiempo límite de arranque de forma predeterminada. En un portátil Linux, la misma pareja funciona como unidades de usuario para el directorio personal.

Un restic-maintenance.service semanal con la misma cabecera se encarga de la retención y las comprobaciones:

ExecStart=/usr/local/bin/restic forget --prune --retry-lock 2h \
    --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3
ExecStart=/usr/local/bin/restic check --read-data-subset=10%%

Escriba %% para obtener un signo de porcentaje literal, porque % inicia un especificador en los archivos de unidad.

Monitorización y alertas de fallo

Use dos señales independientes, porque de lo contrario un fallo silencioso se descubre durante el incidente. La primera es OnFailure=. Los códigos de salida de restic son concretos: 1 indica un comando fallido, 3 que no se pudieron leer algunos archivos de origen, 10 que el repositorio no existe, 11 un fallo de bloqueo y 12 una contraseña incorrecta. systemd trata cualquier código distinto de cero como fallo, lo cual es correcto también para 3: el snapshot existe, pero tiene huecos.

# [email protected]
[Unit]
Description=Alert for failed %i

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
ExecStart=/usr/bin/curl -fsS --max-time 10 --retry 3 \
    --data-urlencode "text=%H: %i failed, see journalctl -u %i" \
    ${ALERT_WEBHOOK_URL}

Una sola plantilla sirve para todas las tareas, y systemd le pasa variables como MONITOR_EXIT_STATUS. Pruebe la cadena con systemctl start [email protected].

La segunda señal es un dead man's switch: ExecStartPost= llama a una URL de heartbeat solo tras un éxito, y el sistema de monitorización alerta si no llega ninguna llamada en, por ejemplo, 26 horas. Así se detectan un timer desactivado o un host apagado. Para una visión externa, ejecute cada día restic snapshots --latest 1 --json --no-lock desde otra máquina y alerte si el snapshot más reciente es más antiguo que el RPO.

Haga un simulacro de restauración cronometrado

Un simulacro convierte las suposiciones en cifras. Hágalo después de la configuración inicial, tras cambios importantes y al menos una vez por trimestre, en una VM limpia y no en el host de producción, usando solo lo que tendría un ingeniero en un incidente real: la entrada del gestor de contraseñas y el runbook.

  1. Ponga en marcha un cronómetro cuando empiece el incidente simulado.
  2. Obtenga las credenciales, instale restic y liste los snapshots. La antigüedad del snapshot más reciente es su RPO real.
  3. Restaure los archivos y los volcados y luego la base de datos, cronometrando cada paso.
  4. Arranque una copia de preproducción de la aplicación y complete un flujo real.
  5. Anote las duraciones y los problemas, y actualice el runbook.
set -a; . ./restic.env; set +a
restic snapshots --latest 3
time restic restore latest --target /restore --verify
sudo -u postgres psql -f /restore/var/backups/postgresql/globals.sql
sudo -u postgres createdb app
time sudo -u postgres pg_restore --jobs=4 --dbname=app /restore/var/backups/postgresql/app
sudo -u postgres psql -d app -c "SELECT max(created_at) FROM orders;"

--verify vuelve a leer los archivos restaurados y los compara con el snapshot. La última consulta muestra cuántos datos perdió realmente la restauración. Los errores sobre roles que ya existen son esperables. Practique también la restauración de un solo archivo con --include /etc/nginx/nginx.conf, la petición real más habitual.

Compare el total con el RTO. Las sorpresas típicas son credenciales que faltan, una contraseña que solo existía en el servidor perdido, reconstrucciones de índices lentas y archivos olvidados como claves TLS o un .env fuera de las rutas copiadas.

restic, BorgBackup o snapshots del sistema de archivos

Muchas configuraciones combinan estos enfoques: un snapshot de ZFS, Btrfs o LVM para volver atrás rápido en local, más copias de archivos fuera de las instalaciones. BorgBackup 1.4 es la rama estable actual.

Propiedad restic BorgBackup 1.4 Snapshots del sistema de archivos
Destinos Local, SFTP, servidor REST, almacenamiento compatible con S3 y otras nubes Local, o SSH, mejor con Borg en el servidor El mismo pool; lo externo requiere replicación como zfs send
Cifrado Siempre activo Se elige al crear el repositorio, opcional Depende del almacenamiento
Cliente comprometido rest-server --append-only, object lock en el bucket borg serve --append-only Protegido solo si se replica a un sistema administrado por separado
Consistencia de bases Requiere volcados Requiere volcados Consistente ante caídas si datos y WAL comparten un snapshot atómico
Restauración De un archivo al árbol completo, montaje FUSE De un archivo al árbol completo, montaje FUSE Reversión del volumen entero o copia desde un snapshot montado
Clientes Linux, macOS, Windows, BSD Linux, macOS, BSD; Windows solo mediante WSL o Cygwin, en modo experimental Depende del sistema de archivos

Un snapshot en el mismo pool protege frente a un despliegue fallido, no frente a la pérdida del pool ni a un host comprometido, así que por sí solo no es una copia en el sentido de la regla 3-2-1.

Lista de comprobación

  • El RPO y el RTO están por escrito y acordados con el responsable del servicio.
  • Existen tres copias, una fuera de las instalaciones y una inmutable o append-only.
  • La contraseña, la URL del repositorio y las credenciales están en un gestor de contraseñas y sin conexión.
  • Los directorios de bases en ejecución están excluidos, y los volcados incluidos.
  • PostgreSQL está cubierto por pg_dump con objetos globales o por un pg_basebackup verificado.
  • El timer usa Persistent=true, y el manejador de OnFailure= se ha disparado al menos una vez en una prueba.
  • Un heartbeat alerta si falta un éxito, y una comprobación externa si los snapshots están desfasados.
  • forget --prune y check --read-data-subset se ejecutan cada semana.
  • Las credenciales del servidor no pueden eliminar versiones bloqueadas.
  • Este trimestre se hizo un simulacro de restauración cronometrado, y el runbook refleja sus conclusiones.

Más publicaciones