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 (Actualizado tras prueba de recuperación real el 2026-10-21)
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 de 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 (Probado en real)
Escenario: el NVMe del sistema ha fallado o necesitas reinstalar FreeBSD desde cero. Este procedimiento está validado en una prueba real de Disaster Recovery y tiene en cuenta los problemas típicos de un entorno Live (read-only, altroot, etc.).
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 gpart show
Identifica el disco destino (ej. nda0 o da1) y el USB de backup (da0).
Paso 3 — Preparar el disco destino
gpart destroy -F nda0 gpart create -s gpt nda0 gpart add -t efi -a 1m -s 260m -l efi0 nda0 gpart add -t freebsd-boot -a 1m -s 512k -l boot0 nda0 gpart add -t freebsd-swap -a 1m -s 16g -l swap0 nda0 gpart add -t freebsd-zfs -a 1m -l zfs0 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 \
zroot /dev/gpt/zfs0
Paso 5 — Importar el USB de backup
zpool import zbackup
Verifica el snapshot más reciente que vas a restaurar:
zfs list -t snapshot zbackup/recovery/solaris | tail -5
Paso 6 — Restaurar el sistema
-w (raw) porque tenemos datasets cifrados. NO usamos el flag -d para que la estructura caiga directamente en zroot/ROOT/default y no se anide en zroot/Solaris/ROOT/default.
# Sustituye snap-XXXX por el nombre exacto del snapshot zfs send -Rwcv zbackup/recovery/solaris@snap-XXXX | zfs receive -Fuv zroot
Paso 7 — Configurar el Boot Environment
zpool set bootfs=zroot/ROOT/default zroot zpool get bootfs zroot
Paso 8 — Copiar el cargador UEFI (Solución a "Read-only file system")
El entorno Live no permite crear carpetas en /mnt porque la raíz es de solo lectura. Además, la partición EFI puede estar "sucia" y montarse en Read-only. Usaremos /tmp (que está en RAM) y forzaremos el montaje:
# 1. Crear puntos de montaje temporales en RAM mkdir -p /tmp/myroot /tmp/myefi # 2. Montar el sistema restaurado para acceder al loader.efi mount -t zfs zroot/ROOT/default /tmp/myroot # 3. Reparar la partición EFI por si tiene flags de "sucio" fsck_msdosfs -f /dev/nda0p1 # 4. Montar la EFI explícitamente en modo escritura mount -t msdosfs -o rw /dev/nda0p1 /tmp/myefi # 5. Crear la estructura y copiar el cargador mkdir -p /tmp/myefi/efi/boot cp /tmp/myroot/boot/loader.efi /tmp/myefi/efi/boot/bootx64.efi # 6. Sincronizar y desmontar sync umount /tmp/myefi umount /tmp/myroot
Paso 9 — Ajustar propiedades de montaje (El truco del -u)
No intentes usar zpool set altroot= zroot, ya que dará error "can only be set during pool creation". Tampoco puedes desmontar la raíz. Debemos usar zfs set -u para actualizar las propiedades de montaje sin intentar montar en caliente:
zfs set -u mountpoint=/ zroot/ROOT/default zfs set -u mountpoint=/home zroot/home zfs set -u mountpoint=/usr zroot/usr zfs set -u mountpoint=/var zroot/var zfs set -u mountpoint=/tmp zroot/tmp zfs set -u mountpoint=none zroot zfs set -u mountpoint=none zroot/ROOT
Paso 10 — Regenerar base de datos de usuarios
Para evitar el bucle de login (donde el sistema autentica pero rebota al usuario porque el home no estaba montado), regeneramos las contraseñas:
pwd_mkdb -p /etc/master.passwd
Paso 11 — Exportar y reiniciar
zpool export zbackup zpool export zroot reboot
Al reiniciar, el sistema leerá las propiedades corregidas en el Paso 9, montará todo en su sitio y podrás iniciar sesión con normalidad.
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) |
Apéndice D — Lecciones aprendidas (Disaster Recovery)
Esta sección documenta los problemas reales que surgieron al realizar una prueba de recuperación en un disco de 250 GB desde el Live CD de FreeBSD. Es vital conocerlos para futuras recuperaciones:
- El entorno Live es read-only: No se puede usar
mkdir /mnt/efi. Usar siempre/tmp/para los puntos de montaje temporales. - El comando
zpool set altroot=falla: Esta propiedad solo se puede tocar al importar. Para corregir los puntos de montaje en caliente, usar el flag-uenzfs set -u mountpoint=/.... - Bucle de login: Si el sistema arranca pero te rebota al login (o el comando
suda error "cannot open"), es porque el directorio/home/no se montó bien. Se soluciona regenerando las contraseñas conpwd_mkdb -p /etc/master.passwd. - El snapshot Base es sagrado: El snapshot
snap-14.4-RELEASE-p8-casa.lan-2026-28-sept-2026es el cimiento de todos los incrementales. Si se borra, ZFS forzará un envío completo (Full Send) de 200 GB en el próximo backup.
FreeBSD es genial!.

No hay comentarios:
Publicar un comentario