Saltar al contenido
Todo Automatizado
Ciberseguridad Industrial

Automatizar Backup SCADA: Herramientas y Scripts

Cómo automatizar el backup de sistemas SCADA con scripts y herramientas. Rsync, Veeam, snapshots y planes de recuperación para entornos industriales.

TodoAutomatizado 20 min de lectura

Pregunta rápida: si mañana un ransomware cifra tu servidor SCADA, ¿cuántas horas tarda tu planta en volver a producir? Si la respuesta es “no lo sé” o incluye la palabra “días”, tienes un problema serio.

Automatizar el backup de sistemas SCADA no es un proyecto “para cuando haya tiempo”. Es la diferencia entre recuperarte en horas o en semanas. Aquí tienes las herramientas, los scripts y los procedimientos que funcionan — todo probado en entornos reales, desde los programas de PLC hasta los históricos de proceso.

Para planes de recuperación completos, la guía sobre Backup SCADA y Recuperación ante Desastres cubre la estrategia global.

Qué respaldar (y lo que todo el mundo olvida)

“Hacer backup del SCADA” no es copiar una carpeta. Un sistema SCADA tiene componentes distribuidos por toda la planta, cada uno con sus propios mecanismos de respaldo:

Inventario completo de componentes

ComponenteUbicación típicaTipo de datoFrecuencia recomendada
Proyecto SCADA (pantallas, scripts, configuración)Servidor SCADAArchivos de proyectoTras cada cambio + semanal
Base de datos de configuraciónServidor SCADASQL/propietarioDiaria
Históricos de proceso (Historian)Servidor HistorianBase de datos temporalDiaria incremental + semanal completa
Programas de PLCPLCs en campoBinario/proyectoTras cada cambio + mensual
Pantallas HMI (paneles locales)Paneles HMIArchivos de proyectoTras cada cambio
Configuración de red OTSwitches, firewalls, routersConfig text/binarioSemanal + tras cada cambio
Certificados y clavesServidor SCADA, firewallArchivosTras cada renovación
Recetas y parámetros de producciónSCADA/MESBase de datosDiaria
Documentación del sistemaRepositorio/NASDocumentosContinua (versionado)

Lo que nadie respalda (y genera los peores dolores de cabeza)

Estos tres componentes causan los mayores problemas en una recuperación porque casi nunca se incluyen:

  1. Configuración de switches industriales: un switch Scalance o Stratix mal configurado después de un reemplazo puede dejar sin comunicación a toda una línea.
  2. Licencias de software: las licencias de WinCC, FactoryTalk o Ignition pueden estar vinculadas a hardware. Sin el archivo de licencia o la clave de activación, el software no arranca.
  3. Parámetros de variadores de frecuencia: un variador Sinamics o PowerFlex con 200 parámetros ajustados en puesta en marcha puede tardar días en reconfigurarse sin backup.

Arquitectura: dónde poner el servidor de backup

No puedes tratar la red OT como una red IT más. La arquitectura de backup debe respetar la segmentación de red de IEC 62443, detallada en la guía de ciberseguridad industrial OT.

Zona DMZ industrial

El servidor de backup debe ubicarse en la DMZ industrial, la zona intermedia entre la red OT (nivel 2-3 del modelo Purdue) y la red corporativa IT:

┌───────────────────────────────────────────────────────┐
│  RED CORPORATIVA (IT)                                  │
│  - Correo, ERP, ofimática                             │
└──────────────────────┬────────────────────────────────┘
                       │ Firewall IT/OT
┌──────────────────────┴────────────────────────────────┐
│  DMZ INDUSTRIAL                                        │
│  ┌──────────────────┐  ┌───────────────────────────┐  │
│  │ Servidor Backup  │  │ Historian (réplica)       │  │
│  │ - rsync receiver │  │ - Datos para IT           │  │
│  │ - Veeam proxy    │  │                           │  │
│  └──────────────────┘  └───────────────────────────┘  │
└──────────────────────┬────────────────────────────────┘
                       │ Firewall OT (reglas estrictas)
┌──────────────────────┴────────────────────────────────┐
│  RED OT (SCADA / Control)                              │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│  │ Servidor │ │ Historian│ │ HMI      │ │ PLCs     │ │
│  │ SCADA    │ │          │ │ Servers  │ │          │ │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────┘

Reglas de firewall

Mínimas y unidireccionales — siempre:

OrigenDestinoPuertoProtocoloDirección
Servidor SCADAServidor Backup (DMZ)22SSH/SCPOT → DMZ
Servidor HistorianServidor Backup (DMZ)22SSH/SCPOT → DMZ
Servidor Backup (DMZ)NAS externo / cloud443HTTPS (S3)DMZ → IT

Esto es fundamental: la red OT inicia la conexión hacia la DMZ, nunca al revés. Si tu servidor de backup puede conectarse a los PLCs, tienes un problema de seguridad independientemente de para qué lo uses.

rsync: el caballo de batalla

Rsync es la herramienta más versátil para backup en entornos Linux (y también funciona en Windows con Cygwin o WSL). Solo transfiere los cambios, lo que reduce drásticamente el tiempo y el ancho de banda. Para el 80 % de los casos, rsync con un buen script es todo lo que necesitas.

Script de backup SCADA con rsync

#!/bin/bash
# backup_scada.sh — Backup automatizado de servidor SCADA
# Ejecutar desde el SERVIDOR SCADA (OT) hacia el servidor de backup (DMZ)

set -euo pipefail

# === CONFIGURACIÓN ===
BACKUP_SERVER="backup@10.10.50.10"     # Servidor en DMZ industrial
BACKUP_BASE="/backup/scada"
SSH_KEY="/root/.ssh/id_backup_scada"    # Clave SSH dedicada (sin passphrase)
LOG_FILE="/var/log/scada_backup.log"
RETENTION_DAYS=90                       # Retención de backups diarios
DATE=$(date +%Y-%m-%d_%H%M)
HOSTNAME=$(hostname)

# Directorios a respaldar (ajustar según SCADA)
SCADA_DIRS=(
    "/opt/ignition/data"                # Ignition: proyectos, config, módulos
    "/opt/ignition/backups"             # Ignition: backups automáticos del gateway
    "/var/lib/postgresql"               # Base de datos PostgreSQL (historian)
    "/etc/scada"                        # Configuraciones personalizadas
    "/opt/hmi-projects"                 # Proyectos de paneles HMI
)

# === FUNCIONES ===
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

check_connectivity() {
    if ! ssh -i "$SSH_KEY" -o ConnectTimeout=10 "$BACKUP_SERVER" "echo ok" &>/dev/null; then
        log "ERROR: No se puede conectar al servidor de backup $BACKUP_SERVER"
        exit 1
    fi
}

backup_directory() {
    local src="$1"
    local dest="$BACKUP_BASE/$HOSTNAME/$(basename "$src")"

    if [ ! -d "$src" ]; then
        log "AVISO: Directorio $src no existe, saltando"
        return 0
    fi

    log "Respaldando $src$BACKUP_SERVER:$dest"

    rsync -azh --delete \
        --backup --backup-dir="$BACKUP_BASE/$HOSTNAME/histórico/$DATE/$(basename "$src")" \
        --exclude='*.tmp' \
        --exclude='*.lock' \
        --exclude='core.*' \
        --timeout=300 \
        -e "ssh -i $SSH_KEY -o StrictHostKeyChecking=yes" \
        "$src/" \
        "$BACKUP_SERVER:$dest/" 2>&1 | tee -a "$LOG_FILE"

    if [ ${PIPESTATUS[0]} -eq 0 ]; then
        log "OK: $src respaldado correctamente"
    else
        log "ERROR: Fallo en backup de $src (código ${PIPESTATUS[0]})"
        return 1
    fi
}

backup_postgresql() {
    local dump_file="/tmp/scada_db_${DATE}.sql.gz"
    log "Exportando base de datos PostgreSQL..."

    pg_dumpall -U postgres | gzip > "$dump_file"

    rsync -azh \
        -e "ssh -i $SSH_KEY" \
        "$dump_file" \
        "$BACKUP_SERVER:$BACKUP_BASE/$HOSTNAME/database/"

    rm -f "$dump_file"
    log "OK: Base de datos PostgreSQL respaldada"
}

cleanup_old_backups() {
    log "Limpiando backups antiguos (> $RETENTION_DAYS días)..."
    ssh -i "$SSH_KEY" "$BACKUP_SERVER" \
        "find $BACKUP_BASE/$HOSTNAME/histórico -type d -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>/dev/null || true"
    ssh -i "$SSH_KEY" "$BACKUP_SERVER" \
        "find $BACKUP_BASE/$HOSTNAME/database -type f -mtime +$RETENTION_DAYS -delete 2>/dev/null || true"
    log "OK: Limpieza completada"
}

verify_backup() {
    log "Verificando integridad del backup..."
    local remote_size
    remote_size=$(ssh -i "$SSH_KEY" "$BACKUP_SERVER" \
        "du -sh $BACKUP_BASE/$HOSTNAME/ 2>/dev/null | cut -f1")
    log "Tamaño total del backup en servidor: $remote_size"

    # Verificar que los archivos críticos existen
    local critical_files=(
        "$BACKUP_BASE/$HOSTNAME/data/gateway.xml"
        "$BACKUP_BASE/$HOSTNAME/database/scada_db_${DATE}.sql.gz"
    )

    for f in "${critical_files[@]}"; do
        if ssh -i "$SSH_KEY" "$BACKUP_SERVER" "test -f $f"; then
            log "OK: Archivo crítico verificado: $(basename "$f")"
        else
            log "AVISO: Archivo crítico no encontrado: $f"
        fi
    done
}

# === EJECUCIÓN ===
log "=========================================="
log "Inicio backup SCADA - $HOSTNAME"
log "=========================================="

check_connectivity

ERRORES=0
for dir in "${SCADA_DIRS[@]}"; do
    backup_directory "$dir" || ((ERRORES++))
done

backup_postgresql || ((ERRORES++))
cleanup_old_backups
verify_backup

if [ $ERRORES -eq 0 ]; then
    log "RESULTADO: Backup completado sin errores"
else
    log "RESULTADO: Backup completado con $ERRORES errores — REVISAR"
    # Enviar alerta (ajustar según sistema de notificación)
    # mail -s "ALERTA: Backup SCADA con errores" ot-admin@empresa.com < "$LOG_FILE"
fi

log "=========================================="

Programación con cron

# /etc/cron.d/scada_backup
# Backup diario a las 02:00 (hora de menor actividad)
0 2 * * * root /opt/scripts/backup_scada.sh >> /var/log/scada_backup_cron.log 2>&1

# Backup adicional antes de ventana de mantenimiento (domingos 22:00)
0 22 * * 0 root /opt/scripts/backup_scada.sh >> /var/log/scada_backup_cron.log 2>&1

Backup de PLCs: lo más crítico y lo más descuidado

Un PLC sin programa es un ladrillo de 5.000 €. Reprogramar un S7-1500 desde cero puede llevar semanas. Restaurar desde backup, minutos. La diferencia es tener o no tener una copia.

Siemens S7 (TIA Portal)

TIA Portal almacena los proyectos en formato propietario (carpetas con archivos .ap1X). El backup puede hacerse de dos formas:

Opción A: Backup de archivos de proyecto

#!/bin/bash
# Backup de proyectos TIA Portal desde el PC de ingeniería
# Los proyectos suelen estar en C:\Users\<usuario>\Documents\Automation

TIA_PROJECTS="/mnt/engineering-pc/Users/Shared/TIA_Projects"
BACKUP_DEST="backup@10.10.50.10:/backup/plc/siemens"

rsync -azh --delete \
    --include='*/' \
    --include='*.ap17' \
    --include='*.ap18' \
    --include='*.ap19' \
    --include='*.zap17' \
    --include='*.zap18' \
    --include='*.zap19' \
    --exclude='*' \
    -e "ssh -i /root/.ssh/id_backup" \
    "$TIA_PROJECTS/" \
    "$BACKUP_DEST/$(date +%Y-%m-%d)/"

Opción B: Upload desde el PLC (online)

Para extraer el programa directamente del PLC se puede usar la librería python-snap7:

"""
Backup online de PLC Siemens S7-1500 usando snap7.
Extrae los bloques de programa del PLC y los guarda en archivos.
"""
import snap7
import os
from datetime import datetime

def backup_plc_s7(plc_ip: str, rack: int, slot: int, output_dir: str):
    """
    Conecta al PLC y descarga todos los bloques de programa.
    """
    client = snap7.client.Client()
    client.connect(plc_ip, rack, slot)

    if not client.get_connected():
        raise ConnectionError(f"No se pudo conectar al PLC {plc_ip}")

    fecha = datetime.now().strftime("%Y-%m-%d_%H%M")
    backup_dir = os.path.join(output_dir, f"{plc_ip}_{fecha}")
    os.makedirs(backup_dir, exist_ok=True)

    # Listar y descargar bloques OB, FC, FB, DB
    block_types = {
        snap7.types.Block.OB: "OB",
        snap7.types.Block.FC: "FC",
        snap7.types.Block.FB: "FB",
        snap7.types.Block.DB: "DB",
    }

    total_bloques = 0
    for block_type, prefix in block_types.items():
        block_list = client.list_blocks_of_type(block_type, 1024)
        for block_num in block_list:
            try:
                data = client.full_upload(block_type, block_num)
                filename = f"{prefix}{block_num}.blk"
                filepath = os.path.join(backup_dir, filename)
                with open(filepath, "wb") as f:
                    f.write(data)
                total_bloques += 1
            except Exception as e:
                print(f"Error descargando {prefix}{block_num}: {e}")

    client.disconnect()
    print(f"Backup completado: {total_bloques} bloques guardados en {backup_dir}")
    return backup_dir

# Ejemplo: backup de 3 PLCs de una línea
plcs = [
    {"ip": "10.10.1.10", "rack": 0, "slot": 1, "nombre": "PLC_Linea1"},
    {"ip": "10.10.1.11", "rack": 0, "slot": 1, "nombre": "PLC_Linea2"},
    {"ip": "10.10.1.12", "rack": 0, "slot": 1, "nombre": "PLC_Robot"},
]

for plc in plcs:
    try:
        backup_plc_s7(plc["ip"], plc["rack"], plc["slot"], "/backup/plc/online")
        print(f"OK: {plc['nombre']} ({plc['ip']})")
    except Exception as e:
        print(f"ERROR: {plc['nombre']} ({plc['ip']}): {e}")

Allen-Bradley (Rockwell)

Para PLCs CompactLogix y ControlLogix, el backup se puede automatizar con la herramienta de línea de comandos logix-cli o mediante scripts que interactúan con RSLogix/Studio 5000:

#!/bin/bash
# Backup de proyectos Rockwell desde el PC de ingeniería
# Los archivos .ACD son los proyectos de Studio 5000

ROCKWELL_PROJECTS="/mnt/engineering-pc/Rockwell/Projects"
BACKUP_DEST="backup@10.10.50.10:/backup/plc/rockwell"

# Copiar todos los archivos .ACD y .RSS
find "$ROCKWELL_PROJECTS" \( -name "*.ACD" -o -name "*.RSS" -o -name "*.L5K" \) \
    -newer /tmp/last_plc_backup 2>/dev/null | while read -r file; do
    rsync -azh -e "ssh -i /root/.ssh/id_backup" \
        "$file" "$BACKUP_DEST/$(date +%Y-%m-%d)/"
    echo "Respaldado: $(basename "$file")"
done

touch /tmp/last_plc_backup

Octoplant y MDT AutoSave: cuándo merece la pena pagar

Si tienes decenas o cientos de dispositivos programables, las herramientas especializadas justifican cada euro:

  • Octoplant (antes Versiondog): gestión de versiones de proyectos de automatización. Soporta TIA Portal, Studio 5000, Unity Pro, DCS y muchos más. Detecta cambios automáticamente y mantiene un historial completo de versiones.
  • MDT AutoSave: similar a Octoplant, con fuerte presencia en el mercado norteamericano. Integración con Rockwell, Siemens, Schneider y GE.

Lo que justifica el coste: la comparación inteligente de programas. No solo te dicen “algo cambió” — te muestran exactamente qué rung se modificó, qué DB se añadió. Rsync no puede hacer eso.

Backup de red OT: switches y firewalls

Un switch industrial con VLANs, QoS, IGMP snooping y port mirroring configurados puede tardar días en reconstruirse desde cero. Hemos visto plantas paradas esperando a que alguien recuerde cómo estaba configurado un Scalance.

Script para switches Cisco / Cisco IE

"""
Backup de configuración de switches de red OT.
Compatible con Cisco IE (Industrial Ethernet) y switches gestionados genéricos.
Usa Netmiko para conexión SSH.
"""
from netmiko import ConnectHandler
from datetime import datetime
import os
import json

# Inventario de dispositivos de red OT
DISPOSITIVOS = [
    {
        "device_type": "cisco_ios",
        "host": "10.10.0.1",
        "username": "admin",
        "password": "REDACTED",  # Usar vault en producción
        "nombre": "SW-OT-Core"
    },
    {
        "device_type": "cisco_ios",
        "host": "10.10.0.2",
        "username": "admin",
        "password": "REDACTED",
        "nombre": "SW-OT-Linea1"
    },
    {
        "device_type": "cisco_ios",
        "host": "10.10.0.3",
        "username": "admin",
        "password": "REDACTED",
        "nombre": "SW-OT-Linea2"
    },
]

BACKUP_DIR = "/backup/red-ot"
fecha = datetime.now().strftime("%Y-%m-%d_%H%M")

resultados = []

for dispositivo in DISPOSITIVOS:
    nombre = dispositivo.pop("nombre")
    try:
        conn = ConnectHandler(**dispositivo)

        # Obtener running-config
        running = conn.send_command("show running-config")
        # Obtener versión y modelo
        version = conn.send_command("show version")
        # Obtener tabla de VLANs
        vlans = conn.send_command("show vlan brief")

        # Guardar archivos
        device_dir = os.path.join(BACKUP_DIR, nombre, fecha)
        os.makedirs(device_dir, exist_ok=True)

        with open(os.path.join(device_dir, "running-config.txt"), "w") as f:
            f.write(running)
        with open(os.path.join(device_dir, "version.txt"), "w") as f:
            f.write(version)
        with open(os.path.join(device_dir, "vlans.txt"), "w") as f:
            f.write(vlans)

        conn.disconnect()
        resultados.append({"nombre": nombre, "status": "OK"})
        print(f"OK: {nombre} ({dispositivo['host']})")

    except Exception as e:
        resultados.append({"nombre": nombre, "status": "ERROR", "detalle": str(e)})
        print(f"ERROR: {nombre} ({dispositivo['host']}): {e}")

    # Restaurar el campo nombre para siguiente iteración
    dispositivo["nombre"] = nombre

# Guardar resumen
with open(os.path.join(BACKUP_DIR, f"resumen_{fecha}.json"), "w") as f:
    json.dump(resultados, f, indent=2)

Firewalls industriales (Fortinet FortiGate, Palo Alto)

#!/bin/bash
# Backup de firewall FortiGate (muy común en segmentación IT/OT)
# FortiGate permite backup por SSH o API REST

FIREWALL_IP="10.10.0.254"
BACKUP_DIR="/backup/firewall"
DATE=$(date +%Y-%m-%d_%H%M)

# Opción 1: Backup por SSH
ssh admin@$FIREWALL_IP "execute backup config ssh" > \
    "$BACKUP_DIR/fortigate_${DATE}.conf"

# Opción 2: Backup por API REST (requiere API key)
API_KEY="REDACTED"
curl -sk "https://$FIREWALL_IP/api/v2/monitor/system/config/backup?scope=global" \
    -H "Authorization: Bearer $API_KEY" \
    -o "$BACKUP_DIR/fortigate_${DATE}.conf"

echo "Backup firewall completado: fortigate_${DATE}.conf"

Veeam: la solución para entornos virtualizados

Si tus servidores SCADA corren sobre VMware o Hyper-V (cada vez más habitual), Veeam Backup & Replication es la herramienta de referencia:

  • Backup a nivel de VM completa: no es necesario instalar agentes dentro del SCADA
  • Snapshots consistentes: coordina con VMware/Hyper-V para garantizar consistencia
  • Restauración granular: puede restaurar una VM completa, un disco individual o archivos sueltos
  • Réplica para disaster recovery: mantiene una copia actualizada de la VM lista para arrancar en otro host

Configuración para SCADA (cuidado con estas trampas)

  1. No usar agentes dentro de la VM SCADA: algunos SCADA (WinCC, FactoryTalk) son sensibles a procesos adicionales. Usar backup a nivel de hipervisor.
  2. Excluir el procesamiento de aplicaciones (Application-Aware Processing) si causa problemas con el SCADA. Mejor un backup crash-consistent que un backup fallido.
  3. Programar el backup fuera del horario de producción o durante turnos de baja carga.
  4. Verificar con SureBackup: Veeam puede arrancar automáticamente la VM desde el backup en un entorno aislado para verificar que arranca correctamente.

Script PowerShell para Veeam (backup programado)

# Verificar estado del último backup de las VMs SCADA
# Ejecutar desde el servidor Veeam

$scada_vms = @("SCADA-Server-01", "Historian-01", "HMI-Server-01")

foreach ($vm in $scada_vms) {
    $session = Get-VBRBackupSession |
        Where-Object { $_.JobName -match $vm } |
        Sort-Object EndTimeUTC -Descending |
        Select-Object -First 1

    $status = $session.Result
    $end_time = $session.EndTimeUTC
    $size_gb = [math]::Round($session.BackupStats.BackupSize / 1GB, 2)

    Write-Host "$vm : $status | Fin: $end_time | Tamaño: ${size_gb} GB"

    if ($status -ne "Success") {
        # Enviar alerta
        Send-MailMessage -From "veeam@empresa.com" -To "ot-admin@empresa.com" `
            -Subject "ALERTA: Backup fallido de $vm" `
            -Body "El backup de $vm finalizó con estado: $status" `
            -SmtpServer "smtp.empresa.com"
    }
}

Backup del Historian: datos que no puedes perder

Los datos históricos de proceso no son “nice to have”. En sectores como farmacéutico o alimentario, perder la trazabilidad tiene implicaciones regulatorias graves. Y en cualquier sector, perder meses de datos históricos elimina la capacidad de análisis y mejora.

PostgreSQL / TimescaleDB (Ignition, Historian open-source)

#!/bin/bash
# Backup de base de datos Historian (PostgreSQL/TimescaleDB)
# Usa pg_dump con compresión y paralelismo

DB_NAME="scada_historian"
DB_USER="postgres"
BACKUP_DIR="/backup/historian"
DATE=$(date +%Y-%m-%d_%H%M)
JOBS=4  # Paralelismo (ajustar según CPU)

# Backup completo con compresión
pg_dump -U "$DB_USER" -d "$DB_NAME" \
    -Fd -j "$JOBS" \
    -f "$BACKUP_DIR/${DB_NAME}_${DATE}" \
    --verbose 2>&1 | tee "/var/log/historian_backup.log"

# Verificar integridad
pg_restore -U "$DB_USER" -d template1 \
    --list "$BACKUP_DIR/${DB_NAME}_${DATE}" > /dev/null 2>&1

if [ $? -eq 0 ]; then
    echo "OK: Backup verificado correctamente"
else
    echo "ERROR: Backup corrupto — verificar manualmente"
fi

# Comprimir para transferencia
tar czf "$BACKUP_DIR/${DB_NAME}_${DATE}.tar.gz" \
    -C "$BACKUP_DIR" "${DB_NAME}_${DATE}"
rm -rf "$BACKUP_DIR/${DB_NAME}_${DATE}"

echo "Tamaño: $(du -h "$BACKUP_DIR/${DB_NAME}_${DATE}.tar.gz" | cut -f1)"
#!/bin/bash
# Backup de InfluxDB (v2.x)
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_DIR="/backup/influxdb"

influx backup "$BACKUP_DIR/$DATE" \
    --org "scada" \
    --token "$INFLUX_TOKEN"

# Comprimir
tar czf "$BACKUP_DIR/influxdb_${DATE}.tar.gz" \
    -C "$BACKUP_DIR" "$DATE"
rm -rf "$BACKUP_DIR/$DATE"

echo "Backup InfluxDB completado: influxdb_${DATE}.tar.gz"

Verificación: sin esto, no tienes backup

Un backup que no se verifica no es un backup — es una esperanza. Hemos visto empresas descubrir que sus backups estaban corruptos justo en el momento de la crisis. La verificación tiene que ser automática.

Script de verificación integral

#!/bin/bash
# verify_backups.sh — Verificación diaria de todos los backups SCADA
# Ejecutar a las 06:00, después de que todos los backups nocturnos hayan terminado

set -euo pipefail

BACKUP_BASE="/backup"
REPORT_FILE="/tmp/backup_verification_$(date +%Y-%m-%d).txt"
ALERTAS=0

log() { echo "[$(date '+%H:%M:%S')] $1" | tee -a "$REPORT_FILE"; }

log "=== VERIFICACIÓN DE BACKUPS SCADA ==="
log "Fecha: $(date '+%Y-%m-%d %H:%M')"
log ""

# 1. Verificar que existe backup de hoy
verificar_existencia() {
    local nombre="$1"
    local patron="$2"
    local min_size="$3"  # Tamaño mínimo en KB

    local archivo
    archivo=$(find "$BACKUP_BASE" -name "$patron" -newer /tmp/last_verify 2>/dev/null | head -1)

    if [ -z "$archivo" ]; then
        log "FALLO: No se encontró backup reciente de $nombre"
        ((ALERTAS++))
        return
    fi

    local size
    size=$(du -k "$archivo" | cut -f1)
    if [ "$size" -lt "$min_size" ]; then
        log "FALLO: Backup de $nombre demasiado pequeño (${size}KB < ${min_size}KB esperado)"
        ((ALERTAS++))
        return
    fi

    log "OK: $nombre — $(du -h "$archivo" | cut -f1) — $(basename "$archivo")"
}

# Verificar cada componente
verificar_existencia "SCADA (Ignition)" "data" 50000
verificar_existencia "Base de datos" "scada_db_*.sql.gz" 10000
verificar_existencia "Red OT" "running-config.txt" 1
verificar_existencia "Historian" "scada_historian_*.tar.gz" 100000

# 2. Verificar integridad de archivos comprimidos
log ""
log "--- Verificación de integridad ---"
find "$BACKUP_BASE" -name "*.tar.gz" -newer /tmp/last_verify | while read -r archivo; do
    if gzip -t "$archivo" 2>/dev/null; then
        log "OK: Integridad verificada: $(basename "$archivo")"
    else
        log "FALLO: Archivo corrupto: $(basename "$archivo")"
        ((ALERTAS++))
    fi
done

# 3. Verificar espacio en disco
ESPACIO_LIBRE=$(df -BG "$BACKUP_BASE" | tail -1 | awk '{print $4}' | tr -d 'G')
if [ "$ESPACIO_LIBRE" -lt 50 ]; then
    log "AVISO: Espacio libre bajo en servidor de backup: ${ESPACIO_LIBRE}GB"
    ((ALERTAS++))
fi

# Resumen
log ""
log "=== RESUMEN ==="
if [ $ALERTAS -eq 0 ]; then
    log "RESULTADO: Todos los backups verificados correctamente"
else
    log "RESULTADO: $ALERTAS alertas detectadas — REQUIERE ATENCIÓN"
fi

touch /tmp/last_verify

# Enviar alerta si hay problemas
if [ $ALERTAS -gt 0 ]; then
    cat "$REPORT_FILE"
    # Integrar con sistema de notificación (email, Telegram, SNMP trap)
fi

Plan de recuperación: si no lo has probado, no funciona

Tener backups es la mitad del trabajo. La otra mitad es saber restaurar — y haberlo practicado.

Procedimiento de restauración SCADA (Ignition)

# 1. Detener el servicio Ignition
sudo systemctl stop ignition

# 2. Restaurar datos desde backup
rsync -azh backup@10.10.50.10:/backup/scada/$(hostname)/data/ /opt/ignition/data/

# 3. Restaurar base de datos
gunzip -c /tmp/scada_db_latest.sql.gz | psql -U postgres

# 4. Verificar permisos
chown -R ignition:ignition /opt/ignition/data

# 5. Arrancar servicio
sudo systemctl start ignition

# 6. Verificar en el gateway web (https://localhost:8088)
echo "Verificar estado del gateway en https://$(hostname):8088"

Simulacros de recuperación

El cumplimiento de IEC 62443 exige demostrar que la recuperación funciona, no solo que los backups existen. Programa simulacros que cumplan esto:

  • Realizarse al menos una vez al año (idealmente cada 6 meses)
  • Restaurar en un entorno de prueba, nunca en producción
  • Documentar el tiempo de recuperación real (RTO) y compararlo con el objetivo
  • Identificar gaps (¿faltó algún componente? ¿las contraseñas estaban documentadas?)

Métricas clave de recuperación

MétricaDefiniciónObjetivo típico
RPO (Recovery Point Objective)Máxima pérdida de datos tolerable24 h para configuración, 1 h para historian
RTO (Recovery Time Objective)Tiempo máximo de inactividad tolerable4-8 h para SCADA completo
Tiempo real de recuperaciónTiempo medido en simulacroDebe ser < RTO

Estrategia 3-2-1: no te compliques más

La regla de oro que funciona:

  • 3 copias del dato (original + 2 backups)
  • 2 tipos de soporte diferentes (disco local + NAS/cinta/cloud)
  • 1 copia fuera del sitio (en otra ubicación física)

Aplicación al entorno SCADA

Copia 1: Datos originales en el servidor SCADA (red OT)
Copia 2: Backup en servidor de la DMZ industrial (disco NAS)
Copia 3: Copia cifrada en almacenamiento fuera del sitio
          - Opción A: NAS en otra ubicación de la empresa
          - Opción B: Cloud privado (S3 compatible, cifrado AES-256)
          - Opción C: Cinta LTO en armario ignífugo externo

Cifrado obligatorio fuera de la red OT

Cualquier copia que salga de la red OT va cifrada. Sin excepciones. Los datos SCADA contienen información sobre tu proceso productivo que un atacante podría usar para planificar un ataque dirigido.

#!/bin/bash
# Cifrar backup antes de enviar a almacenamiento externo
# Usa GPG con clave simétrica (AES-256)

BACKUP_FILE="$1"
PASSPHRASE_FILE="/root/.backup_passphrase"  # Archivo con passphrase (permisos 600)

# Cifrar
gpg --batch --yes --passphrase-file "$PASSPHRASE_FILE" \
    --symmetric --cipher-algo AES256 \
    --output "${BACKUP_FILE}.gpg" \
    "$BACKUP_FILE"

# Subir a S3 compatible (MinIO, Wasabi, AWS S3)
aws s3 cp "${BACKUP_FILE}.gpg" \
    s3://scada-backup-offsite/$(date +%Y/%m)/ \
    --endpoint-url https://s3.backup-externo.com \
    --storage-class STANDARD_IA

# Limpiar archivo cifrado local
rm -f "${BACKUP_FILE}.gpg"

Monitorización centralizada

Con múltiples sistemas, revisar logs manualmente en cada servidor no escala. Centraliza el estado de todos los backups en un punto.

Dashboard de estado de backups

Una opción sencilla pero efectiva es generar un JSON de estado que el dashboard de planta pueda consultar:

"""
Generador de estado de backups para dashboard.
Ejecutar después de la verificación diaria.
"""
import json
import os
from datetime import datetime, timedelta
from pathlib import Path

BACKUP_BASE = Path("/backup")
STATUS_FILE = "/var/www/html/api/backup_status.json"

def check_backup(nombre: str, path_pattern: str, max_age_hours: int = 26) -> dict:
    """Verifica un backup y retorna su estado."""
    archivos = sorted(BACKUP_BASE.rglob(path_pattern), key=os.path.getmtime, reverse=True)

    if not archivos:
        return {"nombre": nombre, "status": "MISSING", "ultimo": None, "tamaño": None}

    ultimo = archivos[0]
    mtime = datetime.fromtimestamp(ultimo.stat().st_mtime)
    edad = datetime.now() - mtime
    size_mb = ultimo.stat().st_size / (1024 * 1024)

    status = "OK" if edad < timedelta(hours=max_age_hours) else "STALE"

    return {
        "nombre": nombre,
        "status": status,
        "ultimo": mtime.isoformat(),
        "edad_horas": round(edad.total_seconds() / 3600, 1),
        "tamaño_mb": round(size_mb, 1),
        "archivo": str(ultimo.name)
    }

# Verificar todos los backups
estado = {
    "timestamp": datetime.now().isoformat(),
    "backups": [
        check_backup("SCADA Server", "scada/*/data/gateway.xml"),
        check_backup("Base de datos", "scada/*/database/scada_db_*.sql.gz"),
        check_backup("Historian", "historian/scada_historian_*.tar.gz"),
        check_backup("Red OT", "red-ot/*/running-config.txt"),
        check_backup("PLC Siemens", "plc/siemens/**/*.ap*"),
        check_backup("PLC Rockwell", "plc/rockwell/**/*.ACD"),
        check_backup("Firewall", "firewall/fortigate_*.conf"),
    ]
}

# Resumen
ok = sum(1 for b in estado["backups"] if b["status"] == "OK")
total = len(estado["backups"])
estado["resumen"] = {
    "ok": ok,
    "total": total,
    "porcentaje": round(ok / total * 100) if total > 0 else 0
}

with open(STATUS_FILE, "w") as f:
    json.dump(estado, f, indent=2)

print(f"Estado actualizado: {ok}/{total} backups OK")

Los errores que se repiten en cada planta

1. Backup sin verificación

El peor de todos. Empresas que llevan años “haciendo backup” y descubren en el momento de la crisis que los archivos están corruptos. Años de falsa tranquilidad.

2. Backup solo del servidor, no de los PLCs

El servidor SCADA se reinstala en horas. Reprogramar 15 PLCs desde cero lleva semanas. Respalda los PLCs. Versiónalos. No es opcional.

3. Credenciales no documentadas

El backup se restaura, pero nadie recuerda la contraseña del SCADA, la clave API del Historian o el pin del firewall. Documenta todas las credenciales necesarias para la recuperación en un lugar seguro y accesible durante emergencias.

4. Backups accesibles desde la máquina comprometida

Si el ransomware puede llegar a tus backups, los cifrará también. Las copias deben ser inmutables (write-once) o estar en una red que el ransomware no pueda alcanzar. Si tus backups están en un share montado del servidor SCADA, no tienes backup.

5. Nunca probar la restauración

Tener backups sin probar la restauración es como tener un extintor sellado. Cuando lo necesitas, descubres que no funciona.

Empieza hoy

Las herramientas están ahí: rsync para ficheros, Veeam para VMs, scripts para PLCs y red, Octoplant para gestión de versiones. Lo que falta en la mayoría de plantas no es tecnología — es disciplina.

La próxima vez que alguien diga “ya haremos el backup cuando haya tiempo”, pregúntale cuánto cuesta un día entero de parada. Ese número suele acelerar la conversación.

#backup SCADA #recuperación ante desastres #rsync #ciberseguridad industrial #OT #scripts backup #Veeam #snapshots

Sigue leyendo