Páginas

sábado, 3 de octubre de 2026

Backup Manual y Recuperación

FreeBSD 14.5

ZFS FreeBSD Copia de seguridad del servidor tormenta
ZFS 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.
Filosofía del Modelo B: los USB no están conectados permanentemente. Se conectan solo cuando se va a hacer el backup, y después se guardan en otro lugar. Esto protege contra:
  • 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

Elementosolaristormenta
Pool origenzrootzroot
Pool destino (USB)zbackupzbk
Dataset destinozbackup/recovery/solariszbk/tormenta
Etiqueta USBZFSBACKUPbackup1tb
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 origen7 snapshots7 snapshots
Retención destino30 snapshots30 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

ElementoDescripción
Detección automáticaUsa el hostname para elegir el pool y dataset correctos
Snapshot dataset por datasetEvita el fallo atómico de zfs snapshot -r
Detección de sincronizaciónSi ya está todo replicado, no envía nada
Incremental automáticoDetecta la última snapshot en destino y envía solo diferencias
Fallback a full sendSi no hay base común, hace envío completo automáticamente
Retención independiente7 snapshots en origen, 30 en destino
Export automáticoSi el script importó el USB, lo exporta al terminar
Logging completoTodo se registra en /var/log/zfs-backup.log
LockfileEvita ejecuciones simultáneas

3. Ejecución manual paso a paso

Cuándo ejecutar: semanalmente (recomendado domingo por la tarde), o siempre que haya cambios importantes que se quieran respaldar.

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
Importante: NO ejecutar el script si no se está seguro de que el USB está conectado. Si no lo está, el script fallará limpiamente sin tocar nada.

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.).

¡Backup completado! El proceso completo tarda típicamente entre 1 y 15 minutos dependiendo de cuántos datos hayan cambiado desde el último backup.

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?
Este error es normal si se ejecuta el script sin el USB conectado. No corrompe nada. Simplemente conectar el USB y volver a ejecutar.

5. Política de retención

El script aplica automáticamente dos políticas distintas:

UbicaciónSnapshots conservadosRazón
Origen (zroot)7Liberar espacio en el NVMe. Solo se necesitan los últimos días para incrementales.
Destino (USB)30Histó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
Espacio: cada snapshot diaria ocupa entre 50 MB y varios GB según lo que cambie. Con 30 snapshots en destino, asegurarse de que el USB tiene espacio suficiente (consultar con 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
Ventaja sobre el método .gz: con el backup ZFS solo montas el snapshot y accedes a los archivos directamente. No hay que descomprimir 300 GB para sacar un documento.

7. Recuperación completa del sistema

Escenario: el NVMe del sistema ha fallado o necesitas reinstalar FreeBSD desde cero.

ATENCIÓN: Este procedimiento borra todo el contenido del disco actual. Asegurarse de tener el backup antes de empezar.

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.

Guardar la clave en al menos 3 sitios distintos:
  • 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/
Recomendación: una vez al año, haz una prueba real de recuperación del dataset cifrado en una máquina de prueba.

9. Checklist y rutinas

Rutina semanal (domingo)

  • [ ] Conectar USB ZFSBACKUP a 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 backup1tb en 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érminoSignificado
PoolConjunto de discos gestionados por ZFS como una unidad
DatasetSistema de archivos dentro de un pool
SnapshotImagen inmutable de un dataset en un momento concreto
Full sendEnvío completo de un dataset a otro pool
Incremental sendEnvío solo de los cambios desde una snapshot base
RetenciónNúmero de snapshots que se conservan antes de borrar las más antiguas
ashiftTamaño de sector usado por ZFS (12 = 4096 bytes)
-w (raw)Envío de bloques cifrados sin descifrar
BEBoot Environment — entorno de arranque de FreeBSD
tmux

Multiplexor de terminal (permite sesiones persistentes)

 

FreeBSD es genial!.

No hay comentarios:

Publicar un comentario