FreeBSD 14.5
ZFS FreeBSD Copia de seguridad del servidor tormentaZFS FreeBSD Copia de seguridad del cliente solaris
Sistema: solaris y tormenta (FreeBSD 14.5)
Método: Backup manual a disco USB (Modelo B — USB guardado fuera)
Fecha: 2026-10-03
1. Introducción y filosofía
Este documento describe el sistema de backup manual a disco USB implementado en solaris y tormenta. Está diseñado para:
- Ejecutarse manualmente cuando se conecta el disco USB.
- Mantener los USB guardados fuera de las máquinas para protección ante ransomware y desastres locales.
- Ser recuperable tanto para archivos individuales como para el sistema completo.
- Ransomware que cifraría también el USB conectado.
- Robo o incendio que afectaría a máquina y backup a la vez.
- Errores humanos que borrarían el backup por accidente.
Estado del sistema
| Elemento | solaris | tormenta |
|---|---|---|
| Pool origen | zroot | zroot |
| Pool destino (USB) | zbackup | zbk |
| Dataset destino | zbackup/recovery/solaris | zbk/tormenta |
| Etiqueta USB | ZFSBACKUP | backup1tb |
| Tamaño del pool origen | ~220 GB | ~312 GB |
| Script | /root/bin/zfs-backup.sh | /root/bin/zfs-backup.sh |
| Log | /var/log/zfs-backup.log | /var/log/zfs-backup.log |
| Retención origen | 7 snapshots | 7 snapshots |
| Retención destino | 30 snapshots | 30 snapshots |
2. El script zfs-backup.sh
Script único que funciona en ambas máquinas, detectando automáticamente el hostname. Versión 2.4.
Ubicación
/root/bin/zfs-backup.sh
Contenido completo
#!/bin/sh
# zfs-backup.sh - Backup incremental de zroot a pool USB
# Versión: 2.4 (snapshot por dataset + export en error)
# Uso: sh /root/bin/zfs-backup.sh
set -e
# ========== CONFIGURACIÓN SEGÚN MÁQUINA ==========
HOSTNAME=$(hostname -s)
case "$HOSTNAME" in
solaris)
SRC_POOL="zroot"
DST_POOL="zbackup"
DST_DATASET="zbackup/recovery/solaris"
USB_DEV="/dev/gpt/ZFSBACKUP"
RETENTION_SRC=7
RETENTION_DST=30
;;
tormenta)
SRC_POOL="zroot"
DST_POOL="zbk"
DST_DATASET="zbk/tormenta"
USB_DEV="/dev/gpt/backup1tb"
RETENTION_SRC=7
RETENTION_DST=30
;;
*)
echo "Hostname no reconocido: $HOSTNAME" >&2
exit 1
;;
esac
SNAP_PREFIX="snap"
SNAP_TODAY="${SNAP_PREFIX}-$(date +%Y%m%d)"
LOG="/var/log/zfs-backup.log"
LOCKFILE="/var/run/zfs-backup.lock"
# ========== VARIABLES DE ESTADO ==========
IMPORTED_BY_US=0
# ========== FUNCIONES ==========
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"
}
die() {
log "ERROR: $*"
if [ "${IMPORTED_BY_US:-0}" -eq 1 ]; then
log "Exportando pool $DST_POOL antes de salir..."
zpool export "$DST_POOL" >> "$LOG" 2>&1 || log "Aviso: no se pudo exportar"
fi
rm -f "$LOCKFILE"
exit 1
}
# ========== BLOQUEO ==========
if [ -f "$LOCKFILE" ]; then
PID=$(cat "$LOCKFILE" 2>/dev/null)
if kill -0 "$PID" 2>/dev/null; then
echo "Ya hay una ejecución en curso (PID $PID)"
exit 0
fi
rm -f "$LOCKFILE"
fi
echo $$ > "$LOCKFILE"
log "=========================================="
log "Iniciando backup en $HOSTNAME"
log "=========================================="
# ========== VERIFICAR POOL USB ==========
if ! zpool list "$DST_POOL" >/dev/null 2>&1; then
log "Pool $DST_POOL no está importado. Intentando importar..."
if [ ! -e "$USB_DEV" ]; then
die "Disco USB $USB_DEV no encontrado. ¿Está conectado?"
fi
if ! zpool import "$DST_POOL" 2>>"$LOG"; then
die "No se pudo importar el pool $DST_POOL"
fi
log "Pool $DST_POOL importado correctamente"
IMPORTED_BY_US=1
else
log "Pool $DST_POOL ya estaba importado"
IMPORTED_BY_US=0
fi
# ========== CREAR SNAPSHOT DE HOY (dataset por dataset) ==========
log "Creando snapshot $SNAP_TODAY en todos los datasets (los que falten)..."
CREATED=0
SKIPPED=0
FAILED=0
for ds in $(zfs list -H -o name -r "$SRC_POOL" 2>/dev/null); do
if zfs list -t snapshot "$ds@$SNAP_TODAY" >/dev/null 2>&1; then
SKIPPED=$((SKIPPED + 1))
else
if zfs snapshot "$ds@$SNAP_TODAY" >> "$LOG" 2>&1; then
CREATED=$((CREATED + 1))
else
log " → error al crear $ds@$SNAP_TODAY"
FAILED=$((FAILED + 1))
fi
fi
done
log "Snapshots: creados=$CREATED, ya existían=$SKIPPED, fallos=$FAILED"
if [ "$FAILED" -gt 0 ]; then
log "AVISO: hubo $FAILED fallos al crear snapshots (se continúa)"
fi
# ========== DETECTAR ÚLTIMA SNAPSHOT EN DESTINO ==========
LAST_DST_SNAP=$(zfs list -t snapshot -H -o name -r "$DST_DATASET" 2>/dev/null \
| grep '@snap-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$' \
| sort -r \
| head -1 \
| sed 's/.*@//')
if [ -z "$LAST_DST_SNAP" ]; then
log "No hay snapshots previas en destino. Envío COMPLETO."
zfs send -Rwcv "$SRC_POOL@$SNAP_TODAY" \
| zfs receive -Fsuv -x mountpoint "$DST_DATASET" >> "$LOG" 2>&1 \
|| die "Error en el envío completo"
elif [ "$LAST_DST_SNAP" = "$SNAP_TODAY" ]; then
log "Última snapshot en destino: $LAST_DST_SNAP"
log "La snapshot $SNAP_TODAY ya está replicada. Nada que enviar."
elif ! zfs list -t snapshot "$SRC_POOL@$LAST_DST_SNAP" >/dev/null 2>&1; then
log "AVISO: $LAST_DST_SNAP no existe en origen. Envío COMPLETO."
zfs send -Rwcv "$SRC_POOL@$SNAP_TODAY" \
| zfs receive -Fsuv -x mountpoint "$DST_DATASET" >> "$LOG" 2>&1 \
|| die "Error en el envío completo"
else
log "Enviando incremental desde $LAST_DST_SNAP hasta $SNAP_TODAY"
zfs send -Rwcv -i "$SRC_POOL@$LAST_DST_SNAP" "$SRC_POOL@$SNAP_TODAY" \
| zfs receive -Fsuv -x mountpoint "$DST_DATASET" >> "$LOG" 2>&1 \
|| die "Error en el envío incremental"
fi
log "Envío completado correctamente"
# ========== FUNCIÓN DE RETENCIÓN ==========
apply_retention() {
local root="$1"
local label="$2"
local ret="$3"
log "Aplicando retención de $ret snapshots en $label..."
zfs list -t snapshot -H -o name -r "$root" 2>/dev/null \
| grep '@snap-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$' \
| sort -t@ -k1,1 -k2,2r \
| awk -F@ -v r="$ret" '
{
ds = $1
if (ds != prev_ds) { count = 0; prev_ds = ds }
count++
if (count > r) print $0
}' \
| while read snap; do
log "Borrando $snap"
zfs destroy "$snap" >> "$LOG" 2>&1 || log " → error al borrar"
done
log "Retención en $label aplicada"
}
# ========== RETENCIÓN EN DESTINO (30 snapshots) ==========
apply_retention "$DST_DATASET" "destino" "$RETENTION_DST"
# ========== RETENCIÓN EN ORIGEN (7 snapshots) ==========
apply_retention "$SRC_POOL" "origen" "$RETENTION_SRC"
# ========== EXPORTAR POOL ==========
if [ "$IMPORTED_BY_US" -eq 1 ]; then
log "Exportando pool $DST_POOL"
zpool export "$DST_POOL" >> "$LOG" 2>&1 || log "Aviso: no se pudo exportar"
log "Ya puedes desconectar el USB de forma segura"
fi
log "Backup completado con éxito"
log "=========================================="
rm -f "$LOCKFILE"
Características clave
| Elemento | Descripción |
|---|---|
| Detección automática | Usa el hostname para elegir el pool y dataset correctos |
| Snapshot dataset por dataset | Evita el fallo atómico de zfs snapshot -r |
| Detección de sincronización | Si ya está todo replicado, no envía nada |
| Incremental automático | Detecta la última snapshot en destino y envía solo diferencias |
| Fallback a full send | Si no hay base común, hace envío completo automáticamente |
| Retención independiente | 7 snapshots en origen, 30 en destino |
| Export automático | Si el script importó el USB, lo exporta al terminar |
| Logging completo | Todo se registra en /var/log/zfs-backup.log |
| Lockfile | Evita ejecuciones simultáneas |
3. Ejecución manual paso a paso
Paso 1 — Conectar el USB
Conectar el disco USB (ZFSBACKUP en solaris, backup1tb en tormenta) a la máquina correspondiente. Esperar unos segundos a que el sistema lo detecte.
ls -la /dev/gpt/ZFSBACKUP # En solaris ls -la /dev/gpt/backup1tb # En tormenta
Debe aparecer el device node. Si no aparece, probar con otro puerto USB o revisar dmesg:
dmesg | tail -20
Paso 2 — Ejecutar el script
sh /root/bin/zfs-backup.sh
Paso 3 — Verificar el resultado
tail -30 /var/log/zfs-backup.log
Debe verse al final:
[XX:XX:XX] Backup completado con éxito [XX:XX:XX] ==========================================
Paso 4 — Desconectar el USB
Si el script importó el pool, lo habrá exportado automáticamente. Se verá en el log:
[XX:XX:XX] Ya se puede desconectar el USB de forma segura
Verificar que el pool está exportado:
zpool list
El pool de backup (zbackup o zbk) no debe aparecer en la lista. Si aparece, exportarlo manualmente antes de desconectar:
zpool export zbackup # En solaris zpool export zbk # En tormenta sync
Paso 5 — Guardar el USB en lugar seguro
Una vez exportado y sincronizado, se puede retirar el cable USB y guardarlo donde corresponda (fuera de casa, caja fuerte, casa de un familiar, etc.).
4. Interpretación del log
Ejemplo de ejecución exitosa (con cambios)
[2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Iniciando backup en solaris [2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Pool zbackup no está importado. Intentando importar... [2026-10-03 18:30:02] Pool zbackup importado correctamente [2026-10-03 18:30:02] Creando snapshot snap-20261003 en todos los datasets (los que falten)... [2026-10-03 18:30:02] Snapshots: creados=18, ya existían=0, fallos=0 [2026-10-03 18:30:03] Última snapshot en destino: snap-20261002 [2026-10-03 18:30:03] Enviando incremental desde snap-20261002 hasta snap-20261003 [2026-10-03 18:32:15] Envío completado correctamente [2026-10-03 18:32:15] Aplicando retención de 30 snapshots en destino... [2026-10-03 18:32:15] Retención en destino aplicada [2026-10-03 18:32:15] Aplicando retención de 7 snapshots en origen... [2026-10-03 18:32:15] Retención en origen aplicada [2026-10-03 18:32:15] Exportando pool zbackup [2026-10-03 18:32:16] Ya puedes desconectar el USB de forma segura [2026-10-03 18:32:16] Backup completado con éxito [2026-10-03 18:32:16] ==========================================
Ejemplo de ejecución sin cambios
[2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Iniciando backup en solaris [2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Pool zbackup ya estaba importado [2026-10-03 18:30:00] Creando snapshot snap-20261003 en todos los datasets (los que falten)... [2026-10-03 18:30:00] Snapshots: creados=18, ya existían=0, fallos=0 [2026-10-03 18:30:00] Última snapshot en destino: snap-20261003 [2026-10-03 18:30:00] La snapshot snap-20261003 ya está replicada. Nada que enviar. [2026-10-03 18:30:00] Envío completado correctamente [2026-10-03 18:30:00] Aplicando retención de 30 snapshots en destino... [2026-10-03 18:30:00] Retención en destino aplicada [2026-10-03 18:30:00] Aplicando retención de 7 snapshots en origen... [2026-10-03 18:30:00] Retención en origen aplicada [2026-10-03 18:30:00] Backup completado con éxito [2026-10-03 18:30:00] ==========================================
Ejemplo de error (USB no conectado)
[2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Iniciando backup en solaris [2026-10-03 18:30:00] ========================================== [2026-10-03 18:30:00] Pool zbackup no está importado. Intentando importar... [2026-10-03 18:30:00] ERROR: Disco USB /dev/gpt/ZFSBACKUP no encontrado. ¿Está conectado?
5. Política de retención
El script aplica automáticamente dos políticas distintas:
| Ubicación | Snapshots conservados | Razón |
|---|---|---|
Origen (zroot) | 7 | Liberar espacio en el NVMe. Solo se necesitan los últimos días para incrementales. |
| Destino (USB) | 30 | Histórico más largo. El USB tiene más espacio y es el "archivo". |
Ajustar la retención
Si se quieren cambiar los valores, editar el script:
ee /root/bin/zfs-backup.sh
Buscar las líneas:
RETENTION_SRC=7 RETENTION_DST=30
Y modifícalas a los valores se preferencia. Por ejemplo, para guardar más histórico en el USB:
RETENTION_SRC=7 RETENTION_DST=60
zpool list).
6. Recuperación de archivos individuales
Escenario: se ha borrado un archivo por error o se quiere recuperar una versión anterior de algo.
Paso 1 — Conectar el USB y localizar el snapshot
# Conectar el USB de backup zpool import zbackup # En solaris zpool import zbk # En tormenta # Ver snapshots disponibles zfs list -t snapshot -r zbackup/recovery/solaris | grep 'snap-2026'
Paso 2 — Montar el snapshot en un punto temporal
mkdir -p /mnt/snap mount -t zfs zbackup/recovery/solaris/home/carlos@snap-20261002 /mnt/snap
Ahora /mnt/snap contiene el contenido de /home/carlos tal como estaba en esa fecha.
Paso 3 — Buscar y copiar el archivo
# Ver qué hay ls -la /mnt/snap/ # Copiar el archivo deseado cp /mnt/snap/documento.txt /home/carlos/documento_recuperado.txt
Paso 4 — Desmontar y exportar
umount /mnt/snap rmdir /mnt/snap zpool export zbackup # En solaris zpool export zbk # En tormenta sync
7. Recuperación completa del sistema
Escenario: el NVMe del sistema ha fallado o necesitas reinstalar FreeBSD desde cero.
Paso 1 — Arrancar desde USB de FreeBSD
Insertar el USB de instalación, arrancar la máquina desde él, y elegir "Shell" en lugar de instalar.
Paso 2 — Identificar discos
geom disk list camcontrol devlist
Paso 3 — Preparar el disco destino
gpart destroy -F nda0 gpart create -s gpt nda0 gpart add -t efi -a 1m -s 260m -l EFI nda0 gpart add -t freebsd-boot -a 1m -s 512k -l BOOT nda0 gpart add -t freebsd-swap -a 1m -s 16g -l SWAP nda0 gpart add -t freebsd-zfs -a 1m -l ZFS nda0 newfs_msdos -F 32 -c 1 /dev/nda0p1
Paso 4 — Crear el pool zroot
zpool create -o ashift=12 \
-O compression=lz4 \
-O atime=off \
-O mountpoint=none \
-R /mnt \
zroot /dev/gpt/ZFS
Paso 5 — Importar el USB de backup
zpool import zbackup # En solaris zpool import zbk # En tormenta
Paso 6 — Restaurar el sistema
zfs send -Rwcv zbackup/recovery/solaris@snap-20261003 \
| zfs receive -Fduv zroot
Paso 7 — Configurar el bootfs
zpool set bootfs=zroot/ROOT/default zroot zpool get bootfs zroot
Paso 8 — Copiar el cargador UEFI
mkdir -p /mnt2 mount_msdosfs /dev/nda0p1 /mnt2 mkdir -p /mnt2/efi/boot cp /mnt/zroot/ROOT/default/boot/loader.efi /mnt2/efi/boot/bootx64.efi umount /mnt2 rmdir /mnt2
Paso 9 — Ajustar propiedades de montaje
zfs set mountpoint=/ zroot/ROOT/default zfs set mountpoint=/home zroot/home zfs set mountpoint=/usr zroot/usr zfs set mountpoint=/var zroot/var zfs set mountpoint=/tmp zroot/tmp zpool set altroot= zroot zpool set cachefile=/boot/zfs/zpool.cache zroot
Paso 10 — Exportar y reiniciar
zpool export zbackup zpool export zbk reboot
Paso 11 — Verificar
freebsd-version zpool status zroot zfs list -r zroot | head -20
8. Recuperación del dataset cifrado
El dataset zroot/nfsv4/secifrado en tormenta está cifrado con ZFS native encryption.
- Gestor de contraseñas (Bitwarden, KeePass, etc.).
- Papel en caja fuerte.
- USB de emergencia separado del USB de backup.
Ver el contenido de la clave
base64 /etc/zfs/secifrado.key
Restaurar la clave
echo 'TU_CLAVE_EN_BASE64' | base64 -d > /etc/zfs/secifrado.key chmod 400 /etc/zfs/secifrado.key chown root:wheel /etc/zfs/secifrado.key ls -la /etc/zfs/secifrado.key
Montar el dataset cifrado
zfs mount zroot/nfsv4/secifrado ls -la /nfsv4/secifrado/
9. Checklist y rutinas
Rutina semanal (domingo)
- [ ] Conectar USB
ZFSBACKUPa solaris - [ ] Ejecutar
sh /root/bin/zfs-backup.sh - [ ] Verificar log:
tail -20 /var/log/zfs-backup.log - [ ] Confirmar "Backup completado con éxito"
- [ ] Desconectar USB y guardarlo en lugar seguro
- [ ] Repetir con USB
backup1tben tormenta
Rutina mensual
- [ ] Verificar snapshots recientes en los USB
- [ ] Comprobar espacio disponible:
zpool list - [ ] Revisar el log en busca de errores:
grep "ERROR" /var/log/zfs-backup.log
Rutina semestral
- [ ] Prueba de restauración de un archivo aleatorio.
- [ ] Verificar integridad del pool USB:
zpool scrub zbackup - [ ] Comprobar que las claves de cifrado siguen accesibles.
Apéndice A — Comandos útiles
Ver estado de los pools
zpool list zpool status zpool status -v
Ver snapshots
# Todos los snapshots del pool
zfs list -t snapshot -r zroot
# Solo los snapshots diarios (formato snap-YYYYMMDD)
zfs list -t snapshot -r zroot | grep 'snap-2026'
# Contar snapshots por dataset
zfs list -t snapshot -r zroot | awk -F@ '{print $1}' | sort | uniq -c
Comprobar integridad
zpool scrub zroot zpool scrub zbackup zpool status
Ver procesos de backup en curso
ps aux | grep -E "zfs (send|receive)" | grep -v grep tmux ls
Reconectar a una sesión tmux
tmux attach -t backup_usb
Apéndice B — Solución de problemas
Error: "Pool zbackup no está importado"
ls -la /dev/gpt/ZFSBACKUP zpool import zbackup sh /root/bin/zfs-backup.sh
Error: "cannot create snapshot ... dataset already exists"
Causa: alguno de los datasets ya tiene la snapshot de hoy. La versión 2.4 del script ya maneja esto dataset por dataset.
Error: "cannot send ... encrypted dataset"
Causa: falta el flag -w en zfs send.
Solución: usar -Rwcv en lugar de -Rcv.
El USB no se puede exportar (pool is busy)
lsof | grep zbackup fuser -m /zbackup zfs unmount -a zpool export -f zbackup
Backup incompleto o corrupto
zpool scrub zbackup zpool status zbackup
Apéndice C — Glosario
| Término | Significado |
|---|---|
| Pool | Conjunto de discos gestionados por ZFS como una unidad |
| Dataset | Sistema de archivos dentro de un pool |
| Snapshot | Imagen inmutable de un dataset en un momento concreto |
| Full send | Envío completo de un dataset a otro pool |
| Incremental send | Envío solo de los cambios desde una snapshot base |
| Retención | Número de snapshots que se conservan antes de borrar las más antiguas |
| ashift | Tamaño de sector usado por ZFS (12 = 4096 bytes) |
| -w (raw) | Envío de bloques cifrados sin descifrar |
| BE | Boot Environment — entorno de arranque de FreeBSD |
| tmux | Multiplexor de terminal (permite sesiones persistentes)
|