Plan2026/09-decision-almacenamiento.md

28 KiB
Raw Blame History

Decision: Capa de almacenamiento y datos (punto 6)

Contexto

Infraestructura de backup distribuida geográficamente:

  • PVE (producción): Francia, datacenters OVH
  • PBS1 (backup tránsito): Alemania, OVH
  • PBS2 (backup archivo): Polonia, OVH (nuevo, por contratar)

ZFS se usa en PBS para crear datastores por cliente con quotas/reservations (modelo multiplicador 6x). En PVE se usa ZFS estándar para almacenamiento de VMs.

2 subcapas:

  • 6.1 Gestión de ZFS a escala
  • 6.2 Backup del backup (estrategia de retención y protección)

6.1 Gestión de ZFS a escala

Uso actual de ZFS

ZFS se usa como herramienta de gestión de almacenamiento:

  • En PBS: un dataset por cliente con quota (size×6) y reservation (size×5)
  • En PVE: almacenamiento estándar de VMs
  • Propiedades optimizadas: recordsize=1M, compression=lz4, atime=off

Herramientas evaluadas

Herramienta Qué hace Estado
Sanoid/Syncoid Snapshots automáticos + replicación ZFS Descartado: PBS gestiona snapshots y replicación nativamente
zrepl Replicación ZFS continua Descartado: PBS sync jobs cubren esto
zfswatcher Monitoring dedicado ZFS con alertas Descartado: node_exporter ya exporta métricas ZFS

DECISION 6.1 (confirmada)

Solo monitorización. No se necesitan herramientas adicionales para ZFS. PBS y PVE gestionan sus snapshots y replicación con sus mecanismos nativos.

Monitorización ya cubierta por el stack existente:

Métrica Fuente Alerta
Pool health (degraded, faulted) node_exporter → VictoriaMetrics ZFSPoolDegraded (critical, 1min)
Pool space > 85% node_exporter → VictoriaMetrics ZFSPoolSpaceHigh (warning, 30min)
Scrub errors node_exporter → VictoriaMetrics ZFSScrubErrors (warning)
IO latency node_exporter → VictoriaMetrics Dashboard Grafana
ARC hit rate node_exporter → VictoriaMetrics Dashboard Grafana

Gestión ZFS via Ansible:

  • Playbook optimizar_zfs.sh convertido a role Ansible
  • Aplicar recordsize=1M, compression=lz4, atime=off a nuevos servidores
  • Crear/modificar datasets de clientes (role pbs_customer)

6.2 Estrategia de backup: modelo de 3 capas geográficas

El problema

  • Los PVE hacen backup en PBS, pero si PBS se pierde, se pierden todos los backups
  • Regla 3-2-1: 3 copias, 2 medios, 1 offsite
  • OVH Estrasburgo ardió en 2021. Un solo DC no es suficiente.
  • Si ransomware entra en PVE y PBS1, debe haber una copia inaccesible

DECISION 6.2 (confirmada)

Arquitectura de 3 capas geográficas

Francia (OVH)              Alemania (OVH)             Polonia (OVH, nuevo)
┌──────────────┐          ┌──────────────────┐        ┌──────────────────────┐
│  PVE         │          │  PBS1 (tránsito)  │        │  PBS2 (archivo)       │
│  (producción) │──backup─►│                   │◄─pull──│                       │
│              │   WG     │  Retención corta: │   WG   │  Retención larga:     │
│  Permisos:   │          │  keep-last: 5     │        │  keep-daily: 7        │
│  - Backup    │          │  keep-daily: 3    │        │  keep-weekly: 4       │
│  - Reader    │          │                   │        │  keep-monthly: 5      │
│  - NO delete │          │  ~8 snapshots/VM  │        │  ~16 snapshots/VM     │
│              │          │                   │        │                       │
│              │          │  Acceso:          │        │  Acceso:              │
│              │          │  - PVE escribe    │        │  - MUY restrictivo    │
│              │          │  - PBS2 lee (sync)│        │  - Solo PBS2 inicia   │
│              │          │  - Técnicos leen  │        │    conexiones         │
│              │          │                   │        │  - Invisible al resto │
└──────────────┘          └──────────────────┘        │  - Verify semanal     │
                                                       └──────────────────────┘

Principio 1: PVE no puede borrar

El usuario PVE en PBS1 tiene permisos mínimos:

Rol en PBS1 para PVE:
  - DatastoreBackup (puede crear backups)
  - DatastoreReader (puede restaurar)

  NO tiene:
  - DatastoreAdmin (puede borrar, podar)
  - DatastorePowerUser (puede podar)

Si ransomware compromete PVE, puede crear backups (inofensivo) y restaurar, pero no puede borrar los existentes.

Principio 2: PBS2 PULL de PBS1 (no push)

PBS2 es quien inicia la conexión hacia PBS1:

PBS2 tiene sync jobs:
  - remote: PBS1 (Alemania)
  - remote-store: cada datastore
  - credenciales: usuario read-only en PBS1
  - schedule: diario (o cada 12h)

PBS1 NO tiene:
  - Credenciales de PBS2
  - Conectividad hacia PBS2
  - Conocimiento de que PBS2 existe

Si PBS1 es comprometido, el atacante no puede alcanzar PBS2 porque PBS1 no tiene credenciales ni ruta de red hacia PBS2.

Principio 3: PBS2 es invisible

Firewall PBS2 (Polonia):
  INPUT:
    - ACCEPT SSH desde IP bastion (solo emergencia)
    - ACCEPT WG UDP desde peer de gestión
    - DROP todo lo demás

  OUTPUT:
    - ACCEPT hacia PBS1:8007 (sync, por WG)
    - ACCEPT DNS, NTP
    - DROP todo lo demás

  → PBS2 no tiene puertos abiertos a internet
  → PBS2 no es alcanzable desde PVE ni desde PBS1
  → Solo PBS2 decide cuándo hablar con PBS1

Principio 4: PBS2 verifica integridad

PBS2 ejecuta verify jobs sobre los backups sincronizados:

PBS2 verify jobs:
  - schedule: semanal
  - outdated-after: 14 días
  - ignore-verified: true

  Si verify falla → alerta Telegram + ticket Zammad (crítico)

Valida que lo que llegó de PBS1 no está corrupto. Si un backup corrupto se sincroniza, lo sabéis antes de necesitarlo.

Conectividad entre DCs

Ruta Transporte Cifrado
PVE (Francia) → PBS1 (Alemania) WireGuard sobre vRack cross-DC o internet WG cifrado
PBS2 (Polonia) → PBS1 (Alemania) WireGuard sobre vRack cross-DC o internet WG cifrado

OVH vRack puede extenderse entre DCs y países (sin coste extra si ya está contratado). Si no, WireGuard sobre internet funciona bien: el tráfico de sync es eficiente (PBS deduplica, solo transfiere chunks nuevos).

Sync inicial (primera vez): puede ser grande. Syncs diarios: solo incrementales, poco tráfico.

Retención detallada

# PBS1 (Alemania) - TRÁNSITO
# Objetivo: buffer rápido, pocas copias, rota frecuente
prune-schedule: daily
keep-last: 5        # últimos 5 backups independiente de fecha
keep-daily: 3       # 3 días adicionales
# Total estimado: ~8 snapshots por VM
# Espacio: moderado (pocos días de retención)

# PBS2 (Polonia) - ARCHIVO
# Objetivo: retención profunda, recuperación ante desastre
prune-schedule: daily
keep-daily: 7       # última semana completa
keep-weekly: 4      # último mes en semanas
keep-monthly: 5     # últimos 5 meses
# Total estimado: ~16 snapshots por VM
# Espacio: mayor (meses de retención, pero PBS deduplica)

PBS1 y PBS2 aplican su política de retención independientemente. PBS1 puede borrar snapshots viejos sin afectar a PBS2 (PBS2 ya tiene su copia).

Escenarios de desastre y procedimientos de recuperación

Desastre Impacto Recuperación
Fallo HW en PVE (Francia) VMs caídas Restaurar desde PBS1 Alemania (minutos/horas)
Fallo HW en PBS1 (Alemania) Backups de tránsito perdidos PBS2 Polonia tiene todo con retención de meses
Ransomware en PVE VMs cifradas PVE no puede borrar PBS1. Restaurar desde PBS1.
Ransomware en PVE + PBS1 VMs + backups tránsito PBS2 inaccesible desde ambos. Restaurar desde PBS2.
Ransomware en PVE + PBS1 + PBS2 Catastrófico Extremadamente improbable: PBS2 no es alcanzable desde los otros
DC Francia cae (tipo OVH Estrasburgo 2021) PVE caído PBS1 (Alemania) + PBS2 (Polonia) intactos
DC Francia + Alemania caen PVE + PBS1 caídos PBS2 (Polonia) intacto
Error humano borra datastore en PBS1 Backups cliente borrados PBS2 tiene copias con retención de meses
Fallo HW en PBS2 (Polonia) Copias históricas perdidas Solo quedan copias de tránsito en PBS1 (retención corta). Opción PBS3 para clientes críticos.

Procedimiento si PBS1 se pierde

  1. Contratar/levantar un PBS1 nuevo (Alemania)
  2. Crear datasets ZFS, datastores y usuarios (playbook Ansible pbs_customer)
  3. PVE empieza a hacer backups al PBS1 nuevo → se rellena con copias frescas
  4. Configurar WG peer en PBS1 nuevo para que PBS2 pueda conectar
  5. PBS2 empieza a sincronizar (pull) del PBS1 nuevo cuando hay datos

Hueco: ~1 día sin capa de tránsito. Las VMs siguen funcionando. PBS2 conserva las copias históricas intactas. El riesgo real es perder una VM durante ese hueco (sin backup disponible en PBS1), pero PBS2 tiene la última sincronización previa al fallo.

Procedimiento si PBS2 se pierde

  1. Se pierden las copias históricas (retención larga: semanas/meses)
  2. PBS1 sigue funcionando con retención corta (3d/5 copias)
  3. PVE sigue haciendo backups normalmente a PBS1
  4. Contratar/levantar un PBS2 nuevo (Polonia) y configurar sync desde PBS1

Impacto: durante el período sin PBS2, solo hay 1 capa de backup (PBS1, retención corta). Si PBS1 también falla durante ese periodo → datos perdidos.

Opción PBS3 para clientes críticos (decisión de producto)

Clientes que requieran máxima protección pueden contratar una capa adicional:

PBS3 (opcional por cliente):
  - Ubicación: otro país/proveedor (ej. Hetzner Finlandia, Scaleway París)
  - Modelo: PULL de PBS2 (misma lógica)
  - Retención: igual o más larga que PBS2
  - Coste: repercutido al cliente

Esto es una decisión de producto/SLA, no técnica:

  • El producto base incluye 2 capas (PBS1 tránsito + PBS2 archivo)
  • El producto premium añade PBS3 como 3ª capa
  • Documentar claramente en la definición de producto los riesgos de cada nivel
  • El cliente decide su nivel de protección según su criticidad y presupuesto

Monitorización del modelo de backup

Alerta Condición Severidad
PBSBackupOld48h VM sin backup en 48h en PBS1 Critical
PBSSyncFailed Sync PBS2←PBS1 falla Critical
PBSSyncOld48h PBS2 no sincroniza en 48h Critical
PBSVerifyErrors Verify en PBS2 encuentra errores Critical
PBS1DatastoreSpace90 Datastore PBS1 > 90% quota Warning
PBS2DatastoreSpace90 Datastore PBS2 > 90% Warning

6.3 Backup de configuraciones PVE/PBS a git

El problema

Si un PVE o PBS se pierde, los datos de VMs están en PBS (backup). Pero la configuración del propio nodo (qué VMs había, cómo estaban configuradas, qué datastores, qué usuarios, qué red) está solo en el nodo. ICSManager sabe qué clientes hay, pero no tiene el detalle de cada .conf de VM ni cada regla de firewall.

Reconstruir un nodo sin tener su config es posible pero lento: hay que averiguar qué había, preguntar, deducir. Tenerla guardada en git permite saber exactamente qué había y reconstruir rápido.

DECISION 6.3 (confirmada)

Copia diaria de directorios de configuración a un repo en Forgejo.

Son ficheros de texto, poca información (KB), y git muestra exactamente qué cambió y cuándo.

Qué se copia

Servidor Ruta Contenido
PVE /etc/pve/qemu-server/ Configs de VMs (.conf: disco, RAM, red, opciones)
PVE /etc/pve/lxc/ Configs de contenedores
PVE /etc/pve/storage.cfg Definiciones de almacenamiento
PVE /etc/pve/user.cfg Usuarios y permisos
PVE /etc/pve/datacenter.cfg Configuración del datacenter
PVE /etc/pve/firewall/ Reglas de firewall por nodo y VM
PVE /etc/pve/corosync.conf Configuración del cluster
PVE /etc/network/interfaces Configuración de red (vmbr, vRack, VLANs)
PBS /etc/proxmox-backup/datastore.cfg Definiciones de datastores
PBS /etc/proxmox-backup/user.cfg Usuarios PBS
PBS /etc/proxmox-backup/acl.cfg Permisos por datastore
PBS /etc/proxmox-backup/remote.cfg Remotos configurados
PBS /etc/proxmox-backup/sync.cfg Sync jobs
PBS /etc/proxmox-backup/verify.cfg Verify jobs
Ambos /etc/wireguard/ Configs WG + claves
Ambos ZFS properties (exportadas) Quotas, reservations, recordsize por dataset

Implementación

Playbook Ansible ejecutado diariamente desde Semaphore:

# playbook: backup-configs.yml
# Semaphore: diario, 06:00
- hosts: all_pve:all_pbs
  tasks:
    - name: Exportar ZFS properties
      shell: "zfs get -Hp quota,reservation,recordsize,compression -r pool9 2>/dev/null || true"
      register: zfs_props
      changed_when: false

    - name: Guardar ZFS properties en fichero temporal
      copy:
        content: "{{ zfs_props.stdout }}"
        dest: /tmp/zfs-properties.txt
      changed_when: false

    - name: Recoger configs PVE
      fetch:
        src: "{{ item }}"
        dest: "backup-configs/{{ inventory_hostname }}/"
        flat: no
      loop:
        - /etc/pve/storage.cfg
        - /etc/pve/user.cfg
        - /etc/pve/datacenter.cfg
        - /etc/pve/corosync.conf
        - /etc/network/interfaces
      ignore_errors: true
      when: "'pve' in group_names"

    - name: Recoger configs de VMs PVE
      shell: "ls /etc/pve/qemu-server/*.conf /etc/pve/lxc/*.conf /etc/pve/firewall/*.fw 2>/dev/null || true"
      register: vm_configs
      changed_when: false
      when: "'pve' in group_names"

    - name: Fetch configs de VMs
      fetch:
        src: "{{ item }}"
        dest: "backup-configs/{{ inventory_hostname }}/"
        flat: no
      loop: "{{ vm_configs.stdout_lines | default([]) }}"
      when: "'pve' in group_names"

    - name: Recoger configs PBS
      fetch:
        src: "{{ item }}"
        dest: "backup-configs/{{ inventory_hostname }}/"
        flat: no
      loop:
        - /etc/proxmox-backup/datastore.cfg
        - /etc/proxmox-backup/user.cfg
        - /etc/proxmox-backup/acl.cfg
        - /etc/proxmox-backup/remote.cfg
        - /etc/proxmox-backup/sync.cfg
        - /etc/proxmox-backup/verify.cfg
      ignore_errors: true
      when: "'pbs' in group_names"

    - name: Fetch WG configs
      fetch:
        src: "{{ item }}"
        dest: "backup-configs/{{ inventory_hostname }}/"
        flat: no
      loop:
        - /etc/wireguard/
      ignore_errors: true

    - name: Fetch ZFS properties
      fetch:
        src: /tmp/zfs-properties.txt
        dest: "backup-configs/{{ inventory_hostname }}/"
        flat: no

- hosts: localhost
  tasks:
    - name: Commit y push al repo Forgejo
      shell: |
        cd backup-configs/
        git add -A
        git diff --cached --quiet || git commit -m "Config backup {{ ansible_date_time.date }}"
        git push        

Resultado en Forgejo

infra/backup-configs/
├── pve-node1/
│   ├── etc/pve/qemu-server/
│   │   ├── 100.conf    ← VM cliente ACME
│   │   ├── 101.conf    ← VM cliente BETA
│   │   └── ...
│   ├── etc/pve/storage.cfg
│   ├── etc/pve/user.cfg
│   ├── etc/network/interfaces
│   └── tmp/zfs-properties.txt
├── pve-node2/
│   └── ...
├── pbs-alemania/
│   ├── etc/proxmox-backup/datastore.cfg
│   ├── etc/proxmox-backup/sync.cfg
│   └── tmp/zfs-properties.txt
└── pbs-polonia/
    └── ...

Cada commit muestra el diff: "se añadió VM 105, se cambió la RAM de la 100 de 4GB a 8GB, se creó datastore nuevo".

Valor operativo

Escenario Sin backup de configs Con backup en git
PVE se pierde "¿Qué VMs había? ¿Con qué config?" git show pve-node1/etc/pve/qemu-server/ → lista exacta
Alguien cambia config de VM Nadie se entera Diff en el commit diario
Técnico nuevo necesita ver infra SSH a cada nodo Lee el repo en Forgejo
Auditoría: "¿cuándo se cambió esto?" No hay registro git log --follow fichero.conf
Reconstruir nodo Horas/días adivinando Minutos leyendo configs exactas

Complemento con Ansible (evolución)

Fase 1 (día 1): backup de configs a git = foto de lo que hay Fase 2 (mes 1-2): Ansible gestiona configs = declaración de lo que debe haber

Ambos conviven: Ansible aplica la config deseada, el backup a git verifica que lo real coincide con lo declarado. Si alguien cambia algo manualmente, el diff del backup lo delata.


6.4 Réplica de VMs de gestión a oficina (contingencia fuera de OVH)

El problema

Toda la infraestructura de gestión (monitoring, seguridad, SSOT, SSO) vive en un único servidor en OVH. Si OVH pierde ese servidor, nos quedamos ciegos: sin métricas, sin logs, sin alertas, sin Netbox, sin Authentik. Los datos de clientes están protegidos por 3 capas geográficas (6.2), pero la infra que nos da visibilidad no.

DECISION 6.4 (confirmada)

Replicación ZFS incremental de las VMs críticas de gestión al servidor de oficina (Xeon 96GB existente).

Coste: cero (hardware que ya existe). Las VMs están apagadas en oficina, solo se encienden en contingencia.

Qué se replica

VM Replicar Razón
Monitoring Sin ella: sin Grafana, sin métricas, sin logs, ceguera total
Wazuh Sin ella: sin detección de seguridad, sin FIM
Servicios Netbox (SSOT), Authentik (SSO), Semaphore, Outline
Bastion No Sin estado, se recrea en minutos con Ansible
WG Hub A/B No Sin estado, configs en git (6.3), se recrean

Arquitectura

OVH (servidor gestión)                    Oficina (Xeon 96GB, existente)
┌──────────────────────┐                 ┌──────────────────────┐
│  PVE gestión         │                 │  PVE contingencia     │
│  pool: local-zfs     │  ZFS send/recv  │  pool: local-zfs     │
│                      │  incremental    │                      │
│  VM Monitoring ──────│──── WG ────────►│  VM Monitoring (rpl) │
│  VM Wazuh ───────────│──── WG ────────►│  VM Wazuh (rpl)      │
│  VM Servicios ───────│──── WG ────────►│  VM Servicios (rpl)  │
│                      │                 │                      │
│                      │  Cada 4h        │  VMs apagadas        │
│                      │  ~1-5 GB/sync   │  Solo encienden en   │
│                      │  cifrado por WG │  contingencia        │
└──────────────────────┘                 └──────────────────────┘

Implementación

Playbook Ansible ejecutado desde Semaphore cada 4 horas:

# playbook: replicate-to-office.yml
# Semaphore: cada 4h
- hosts: pve_gestion
  vars:
    office_host: "oficina-pve.mgmt.vpn6.com.es"  # por WG
    vms_to_replicate:
      - vm-monitoring
      - vm-wazuh
      - vm-servicios
  tasks:
    - name: Crear snapshot incremental
      shell: "zfs snapshot local-zfs/{{ item }}@replicate-$(date +%Y%m%d-%H%M)"
      loop: "{{ vms_to_replicate }}"

    - name: Enviar incremental a oficina
      shell: |
        LATEST=$(zfs list -t snapshot -o name -s creation -r local-zfs/{{ item }} | grep replicate | tail -1)
        PREV=$(zfs list -t snapshot -o name -s creation -r local-zfs/{{ item }} | grep replicate | tail -2 | head -1)
        if [ "$PREV" != "$LATEST" ]; then
          zfs send -i "$PREV" "$LATEST" | ssh {{ office_host }} zfs recv -F tank/{{ item }}
        else
          zfs send "$LATEST" | ssh {{ office_host }} zfs recv -F tank/{{ item }}
        fi        
      loop: "{{ vms_to_replicate }}"

    - name: Rotar snapshots antiguos (mantener últimos 12 = 48h)
      shell: |
        zfs list -t snapshot -o name -s creation -r local-zfs/{{ item }} | grep replicate | head -n -12 | xargs -r -n1 zfs destroy        
      loop: "{{ vms_to_replicate }}"

Ancho de banda

Parámetro Valor
Tamaño total VMs a replicar ~400-700 GB
Sync inicial (primera vez) Grande: programar de noche o seed con disco físico
Sync incremental cada 4h ~1-5 GB (solo bloques cambiados)
Ancho de banda sostenido ~5-10 Mbps (factible con fibra de oficina)
Transporte WireGuard cifrado (red de gestión)

Plan de contingencia

Paso Acción Tiempo
1 Detectar que el servidor OVH está caído Minutos (Uptime Kuma externo, ping, o aviso OVH)
2 Encender VMs réplica en PVE oficina 2 minutos
3 Actualizar DNS/WG para apuntar a oficina 5 minutos
4 Monitoring + Wazuh + Servicios operativos ~10 minutos total

No es HA automático (no hace falta). Es un plan B manual que se activa en emergencia. Runbook documentado en Outline.

Qué NO se replica aquí

  • VMs de clientes (producción): protegidas por 3 capas geográficas PBS (6.2)
  • Configs de PVE/PBS: protegidas por backup a git (6.3)
  • WG Hub / Bastion: sin estado, se recrean con Ansible

Esta capa protege específicamente la infraestructura de gestión para no quedarse ciego si OVH tiene un fallo mayor.

Monitorización

Alerta Condición Severidad
ReplicaOfficeOld8h Última réplica a oficina > 8h Warning
ReplicaOfficeFailed Playbook de réplica falla Critical

Resumen Capa 6 completa (DECISIONES FINALES)

┌─────────────────────────────────────────────────────┐
│           CAPA 6: ALMACENAMIENTO Y DATOS             │
├─────────────────────────────────────────────────────┤
│                                                      │
│  6.1 ZFS (confirmada)                                │
│  ┌───────────────────────────────────┐               │
│  │  Solo monitorización              │               │
│  │  - node_exporter → métricas ZFS  │               │
│  │  - Alertas: degraded, space, scrub│               │
│  │  - Ansible: optimización nuevos   │               │
│  │  Sanoid/Syncoid/zrepl: DESCARTADO │               │
│  │  (PBS gestiona nativamente)       │               │
│  └───────────────────────────────────┘               │
│                                                      │
│  6.2 BACKUP 3 CAPAS (confirmada)                     │
│  ┌───────────────────────────────────┐               │
│  │  PVE (Francia)                    │               │
│  │  → backup a PBS1 (Alemania)       │               │
│  │  → permisos: solo Backup+Reader   │               │
│  │  → NO puede borrar               │               │
│  │                                    │               │
│  │  PBS1 (Alemania) - TRÁNSITO       │               │
│  │  → retención corta (3d/5 copias) │               │
│  │  → PVE escribe, PBS2 lee         │               │
│  │                                    │               │
│  │  PBS2 (Polonia) - ARCHIVO         │               │
│  │  → retención larga (7d/4w/5m)    │               │
│  │  → PULL de PBS1 (no push)        │               │
│  │  → Invisible: no alcanzable      │               │
│  │  → Verify semanal                │               │
│  │                                    │               │
│  │  Conectividad: WG sobre vRack    │               │
│  │  cross-DC o internet             │               │
│  │                                    │               │
│  │  3 países, 3 jurisdicciones      │               │
│  │  Francia → Alemania → Polonia    │               │
│  │                                    │               │
│  │  Recuperación PBS1 perdido:       │               │
│  │  → Nuevo PBS1 + PVE replica       │               │
│  │  → PBS2 pull cuando hay datos     │               │
│  │  → Hueco ~1 día                   │               │
│  │                                    │               │
│  │  Recuperación PBS2 perdido:       │               │
│  │  → Copias históricas perdidas    │               │
│  │  → PBS1 sigue con retención corta│               │
│  │  → Opción PBS3 para clientes     │               │
│  │    críticos (decisión de producto)│               │
│  └───────────────────────────────────┘               │
│                                                      │
│  6.3 CONFIGS A GIT (confirmada)                      │
│  ┌───────────────────────────────────┐               │
│  │  Backup diario de /etc/pve/ y    │               │
│  │  /etc/proxmox-backup/ a Forgejo  │               │
│  │  - Configs VMs, storage, users   │               │
│  │  - Red, firewall, WG, ZFS props  │               │
│  │  - Playbook Ansible + Semaphore  │               │
│  │  - Cada commit = diff de cambios │               │
│  │  - Coste: 0 (texto, KB, Forgejo  │               │
│  │    ya existe)                     │               │
│  └───────────────────────────────────┘               │
│                                                      │
│  6.4 RÉPLICA OFICINA (confirmada)                    │
│  ┌───────────────────────────────────┐               │
│  │  ZFS send/recv incremental cada  │               │
│  │  4h a servidor oficina (Xeon 96GB)│              │
│  │  - VMs: Monitoring, Wazuh,       │               │
│  │    Servicios                      │               │
│  │  - VMs apagadas, solo emergencia │               │
│  │  - Transporte: WG cifrado        │               │
│  │  - Contingencia: ~10 min levant. │               │
│  │  - Coste: 0 (HW ya existe)      │               │
│  │  - Protege: infra de gestión     │               │
│  │    (no datos de clientes)         │               │
│  └───────────────────────────────────┘               │
│                                                      │
└─────────────────────────────────────────────────────┘

Recursos Capa 6:
- PBS1: ya existente (Alemania)
- PBS2: servidor nuevo por contratar (Polonia)
  - Dimensionado según volumen de datos × retención
  - Solo necesita disco (CPU/RAM mínimos, PBS es eficiente)
- PBS3 (opcional): por cliente premium, otro proveedor/país
- ZFS monitoring: ya cubierto por node_exporter (0 extra)
- Réplica oficina: servidor Xeon 96GB existente (0 coste)