rsync copia ficheros entre directorios o servidores transfiriendo solo las diferencias, lo que lo hace ideal para copias de seguridad diarias. Combinado con la opción --link-dest, cada copia se comporta como una instantánea completa y navegable, pero los ficheros que no han cambiado no ocupan espacio adicional. En este tutorial configurarás un servidor de backup con Ubuntu 24.04 que cada noche descarga por SSH los datos de un servidor de producción, conserva un número fijo de instantáneas y usa una clave que solo permite leer.

Requisitos previos

Para seguir esta guía necesitas:

  • Servidor de producción (your_server_ip): el que quieres respaldar, con Ubuntu 24.04 LTS.
  • Servidor de backup: otro servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath en una ubicación distinta, con espacio en disco suficiente para una copia completa más los cambios de los días que quieras conservar.
  • Un usuario no root con privilegios sudo en ambos servidores.
  • Acceso SSH del servidor de backup al de producción por el puerto 22 (o el que uses).

Esta guía usa el modelo pull: es el servidor de backup quien se conecta y descarga los datos. Así, si alguien compromete el servidor de producción, no tiene credenciales para borrar las copias.

Paso 1: Comprobar rsync en ambos servidores

Ubuntu 24.04 incluye rsync de serie. Compruébalo en los dos servidores e instálalo si falta:

sudo apt update
sudo apt install rsync
rsync --version | head -n 1
rsync  version 3.2.7  protocol version 31

Antes de automatizar nada, conviene entender las opciones que usarás:

OpciónEfecto
-aModo archivo: recursivo, conserva permisos, propietarios, fechas y enlaces simbólicos
-HConserva los enlaces duros
-AConserva las ACL
-XConserva los atributos extendidos
--numeric-idsGuarda los UID y GID numéricos, sin traducirlos a nombres del servidor de backup
--link-dest=DIRSi un fichero no ha cambiado respecto a DIR, crea un enlace duro en lugar de copiarlo
-nSimulación: muestra qué haría sin copiar nada

Recuerda también la regla de la barra final: rsync -a /var/www dest/ crea dest/www, mientras que rsync -a /var/www/ dest/ copia el contenido de www directamente en dest/.

Paso 2: Crear una clave SSH para el backup

Para conservar propietarios y permisos de todos los ficheros, rsync necesita leer como root en producción y escribir como root en el servidor de backup. En el servidor de backup, genera una clave dedicada, sin frase de paso porque la usará un proceso automático:

sudo ssh-keygen -t ed25519 -f /root/.ssh/rsync_backup -N "" -C "rsync-backup"
sudo cat /root/.ssh/rsync_backup.pub

Copia la línea que empieza por ssh-ed25519.

Paso 3: Restringir la clave a lectura con rrsync

Una clave de root sin restricciones daría al servidor de backup control total sobre producción. El paquete rsync de Ubuntu incluye rrsync, un script que, usado como comando forzado de SSH, solo permite ejecutar rsync, y con -ro solo en modo lectura.

En el servidor de producción, edita las claves autorizadas de root:

sudo nano /root/.ssh/authorized_keys

Añade una línea con este formato, sustituyendo your_backup_server_ip por la IP del servidor de backup y pegando la clave pública al final:

command="/usr/bin/rrsync -ro /",restrict,from="your_backup_server_ip" ssh-ed25519 AAAA...tu_clave... rsync-backup
  • command="/usr/bin/rrsync -ro /": cualquier conexión con esta clave ejecuta rrsync, que solo acepta lecturas de rsync.
  • restrict: desactiva terminal, reenvío de puertos y de agente.
  • from=: solo acepta la clave desde la IP del servidor de backup.

Comprueba cómo trata SSH el acceso de root en producción:

sudo sshd -T | grep -i permitrootlogin

El valor por defecto en Ubuntu, prohibit-password, permite el acceso con clave y sirve. Si el valor es no, cámbialo a forced-commands-only, que solo permite a root entrar con claves que tienen comando forzado, como esta. Edita /etc/ssh/sshd_config (o el fichero de /etc/ssh/sshd_config.d/ donde esté definido):

PermitRootLogin forced-commands-only

Y recarga SSH:

sudo systemctl reload ssh

Paso 4: Probar la conexión y hacer la primera copia manual

Desde el servidor de backup, prueba una simulación. La primera vez SSH te pedirá confirmar la huella del servidor; compárala con la de producción antes de aceptar:

sudo rsync -an -e "ssh -i /root/.ssh/rsync_backup" root@your_server_ip:/etc/hostname /tmp/

Si no hay errores, la clave funciona. Comprueba ahora que esa misma clave no permite abrir una shell:

sudo ssh -i /root/.ssh/rsync_backup root@your_server_ip

La conexión debe cerrarse con un mensaje de error de rrsync en lugar de darte una terminal.

Crea el directorio de las copias y un fichero de exclusiones con lo que no tiene sentido respaldar:

sudo install -d -m 700 /srv/backups/web01
sudo install -d -m 755 /etc/rsync-backup
sudo nano /etc/rsync-backup/web01.exclude
*.tmp
*.swp
/var/www/*/cache/
/var/www/*/node_modules/
/root/.cache/
/home/*/.cache/

Los patrones que empiezan por / se anclan a la raíz de cada ruta transferida. Ajusta la lista a tus aplicaciones.

Paso 5: Crear el script de instantáneas

El script hace una copia en un directorio temporal incomplete, usando la instantánea anterior como referencia para --link-dest. Solo si rsync termina bien, renombra el directorio con la fecha y actualiza el enlace latest. Por último, borra las instantáneas más antiguas que excedan el número a conservar.

sudo nano /usr/local/sbin/rsync-snapshot
#!/usr/bin/env bash
set -euo pipefail

SOURCE_HOST="root@your_server_ip"
SOURCE_PATHS=(/etc /home /root /var/www /var/backups)
BACKUP_ROOT="/srv/backups/web01"
EXCLUDE_FILE="/etc/rsync-backup/web01.exclude"
SSH_KEY="/root/.ssh/rsync_backup"
KEEP=14

stamp="$(date +%Y-%m-%d_%H%M%S)"
work="${BACKUP_ROOT}/incomplete"
latest="${BACKUP_ROOT}/latest"

sources=()
for path in "${SOURCE_PATHS[@]}"; do
    sources+=("${SOURCE_HOST}:${path}")
done

link_dest=()
if [[ -d "${latest}" ]]; then
    link_dest=(--link-dest="${latest}")
fi

mkdir -p "${work}"

rc=0
rsync -aHAX --numeric-ids --delete \
    --exclude-from="${EXCLUDE_FILE}" \
    "${link_dest[@]}" \
    -e "ssh -i ${SSH_KEY} -o BatchMode=yes" \
    "${sources[@]}" "${work}/" || rc=$?

# 24 = algunos ficheros desaparecieron durante la copia (normal en sistemas en uso)
if [[ ${rc} -ne 0 && ${rc} -ne 24 ]]; then
    echo "rsync falló con código ${rc}" >&2
    exit "${rc}"
fi

mv "${work}" "${BACKUP_ROOT}/${stamp}"
ln -sfn "${stamp}" "${latest}"
echo "Instantánea creada: ${BACKUP_ROOT}/${stamp}"

mapfile -t snapshots < <(find "${BACKUP_ROOT}" -mindepth 1 -maxdepth 1 -type d \
    -name '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_[0-9]*' | sort)
excess=$(( ${#snapshots[@]} - KEEP ))
for (( i = 0; i < excess; i++ )); do
    echo "Borrando instantánea antigua: ${snapshots[i]}"
    rm -rf -- "${snapshots[i]}"
done

Algunos detalles del diseño:

  • Si una ejecución falla, el directorio incomplete se queda y la siguiente lo reutiliza, así que solo transfiere lo que faltaba. --delete limpia de él lo que ya no existe en origen.
  • El enlace latest es relativo, de modo que sigue funcionando si montas el disco en otra ruta.
  • Las rutas de SOURCE_PATHS sin barra final se guardan con su nombre dentro de cada instantánea (etc/, home/...). Quita de la lista las que no existan en tu servidor, porque rsync devuelve un error si falta alguna.

Hazlo ejecutable solo por root y lánzalo a mano:

sudo chmod 700 /usr/local/sbin/rsync-snapshot
sudo /usr/local/sbin/rsync-snapshot
Instantánea creada: /srv/backups/web01/2026-09-25_103012

Lanza una segunda ejecución y compara el espacio de ambas. du cuenta cada enlace duro una sola vez, así que la segunda instantánea solo muestra lo que ha cambiado:

sudo /usr/local/sbin/rsync-snapshot
sudo du -sh /srv/backups/web01/2026-*
1.8G	/srv/backups/web01/2026-09-25_103012
4.2M	/srv/backups/web01/2026-09-25_103540

Paso 6: Programar el backup con un temporizador systemd

systemd registra la salida en el journal y no inicia una segunda ejecución mientras la anterior siga en marcha. Crea el servicio en el servidor de backup:

sudo nano /etc/systemd/system/rsync-snapshot.service
[Unit]
Description=Instantánea rsync de web01
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/rsync-snapshot
Nice=10
IOSchedulingClass=idle

Y el temporizador, que lo ejecuta cada noche a las 02:30 con un retraso aleatorio de hasta 15 minutos:

sudo nano /etc/systemd/system/rsync-snapshot.timer
[Unit]
Description=Instantánea rsync diaria de web01

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

[Install]
WantedBy=timers.target

Actívalo y comprueba la próxima ejecución:

sudo systemctl daemon-reload
sudo systemctl enable --now rsync-snapshot.timer
systemctl list-timers rsync-snapshot.timer
NEXT                        LEFT     LAST PASSED UNIT                 ACTIVATES
Fri 2026-09-26 02:38:11 UTC 15h left -    -      rsync-snapshot.timer rsync-snapshot.service

Después de la primera ejecución programada, revisa el resultado en el journal:

sudo journalctl -u rsync-snapshot.service -n 20 --no-pager

Si el backup falla, systemctl status rsync-snapshot.service mostrará el estado failed. Incluye ese estado en tu monitorización o añade OnFailure= al servicio para lanzar una unidad que te notifique.

Paso 7: Verificar y restaurar

Cada instantánea es un árbol de directorios normal, así que puedes navegarla directamente:

sudo ls /srv/backups/web01/latest/
backups  etc  home  root  www

Para comprobar que la copia coincide con producción, lanza una simulación con --checksum, que compara el contenido de cada fichero. Una lista vacía o con pocos ficheros recientes indica que la copia está al día:

sudo rsync -an --checksum --itemize-changes \
    -e "ssh -i /root/.ssh/rsync_backup" \
    root@your_server_ip:/etc/ /srv/backups/web01/latest/etc/

Para restaurar, la clave de backup no sirve porque es de solo lectura, y es intencionado. Copia lo que necesites a producción con tu usuario de administración y muévelo después con sudo. Por ejemplo, para recuperar un fichero de la instantánea de hace unos días, cópialo primero a tu directorio personal en el servidor de backup:

sudo install -o "$USER" -m 600 /srv/backups/web01/2026-09-22_023512/www/html/wp-config.php ~/wp-config.php

Envíalo a producción con tu usuario y borra la copia temporal:

rsync -a ~/wp-config.php your_user@your_server_ip:/tmp/
rm ~/wp-config.php

Y en el servidor de producción:

sudo install -o www-data -g www-data -m 640 /tmp/wp-config.php /var/www/html/wp-config.php

Haz una prueba de restauración completa de vez en cuando en un servidor de pruebas: es la única forma de saber que el backup sirve y cuánto tardas en recuperarte.

Solución de problemas

Permission denied (publickey). La clave pública no está en /root/.ssh/authorized_keys de producción, la IP de from= no coincide con la IP de salida del servidor de backup o PermitRootLogin está en no. Revisa sudo journalctl -u ssh en producción.

Host key verification failed. El servidor de backup aún no conoce la huella de producción y el script usa BatchMode. Ejecuta una vez a mano el comando de prueba del paso 4 y acepta la huella.

Código de salida 23 (some files/attrs were not transferred). Alguna ruta de SOURCE_PATHS no existe o hay ficheros que no se pueden leer. El mensaje anterior del journal indica cuáles.

file has vanished. Un fichero se borró mientras rsync lo copiaba. Es normal en directorios con ficheros temporales y el script lo trata como éxito (código 24). Si ocurre mucho, excluye esos directorios.

El backup satura la red de producción. Limita el ancho de banda añadiendo, por ejemplo, --bwlimit=20000 (en KiB/s) a la orden rsync del script.

El disco de backup se llena. Reduce KEEP, amplía las exclusiones o revisa con sudo du -sh /srv/backups/web01/* qué instantáneas crecen más, lo que señala directorios con muchos cambios diarios.

Conclusión

Has configurado backups diarios con rsync en modelo pull, con una clave de solo lectura limitada por rrsync, instantáneas incrementales con enlaces duros, retención automática y un temporizador systemd. Esta copia en otro servidor cubre uno de los soportes de la regla 3-2-1; como siguientes pasos, añade una copia cifrada en un almacenamiento externo con restic, programa volcados de tus bases de datos antes de la ventana de rsync y configura una alerta cuando el servicio falle.