Copias inmutables anti-ransomware: MinIO como S3 propio y Kopia en Linux y Windows

Almacenamiento de objetos autoalojado con Object Lock en modo Compliance y agentes de copia en dos sistemas operativos

Guía técnica para levantar un almacenamiento compatible con S3 mediante MinIO y Docker Compose, servirlo por HTTPS con un proxy inverso, crear un depósito con bloqueo de objetos que impide el borrado incluso al administrador, y respaldar contra él un Ubuntu Server y un Windows 11 con Kopia, incluyendo copias programadas, un simulacro de ransomware y la restauración completa.

1. Introducción

Una copia de seguridad solo vale lo que vale su capacidad de sobrevivir al incidente que pretende cubrir. El ransomware moderno lo sabe: antes de cifrar los datos busca los respaldos, y si el agente de copia tiene permiso para borrar en el destino, ese permiso es exactamente el que se utiliza para destruirlos. De poco sirve entonces una copia diaria impecable si el atacante puede eliminarla con las mismas credenciales que la crearon.

La respuesta a ese problema se llama inmutabilidad, y en el mundo del almacenamiento de objetos tiene un nombre concreto: Object Lock. Es un mecanismo WORM (Write Once, Read Many): una vez escrito, un objeto no se puede modificar ni borrar hasta que expire el plazo de retención fijado, y esa regla la impone el propio almacenamiento, no la aplicación que escribe. Esta guía monta ese almacenamiento con MinIO, un servidor de objetos compatible con la API de S3 que se despliega con Docker, y respalda contra él dos equipos reales con Kopia, un motor de copias con cifrado extremo a extremo, deduplicación y soporte nativo de Object Lock.

Esta práctica es la materialización de la regla 3-2-1-1-0, la evolución del clásico 3-2-1: tres copias de los datos, en dos soportes distintos, una de ellas fuera del emplazamiento, una inmutable o desconectada —esa es la que se construye aquí— y cero errores en las pruebas de restauración, que es por lo que la guía no termina al hacer la copia sino al recuperarla.
Dos decisiones condicionan el montaje y conviene entenderlas desde el principio. La primera: el bloqueo de objetos solo puede activarse al crear el depósito, nunca después sobre uno que ya contiene datos, así que hay que planificarlo antes de escribir la primera copia. La segunda: el modo de retención elegido aquí es Compliance, el estricto, en el que ni siquiera la cuenta administradora del propio MinIO puede borrar un objeto protegido. Es lo que hace la demostración contundente, y también lo que obliga a dimensionar bien el plazo antes de empezar.

2. Variables de esta guía

Todos los bloques usan los siguientes marcadores en MAYÚSCULAS. Sustitúyelos por los valores reales del entorno donde se aplique esta guía.

MarcadorQué esDónde se obtiene
IP_HOST_DOCKERIP del servidor Docker por la red LAN, que es por donde los equipos a respaldar alcanzan el almacenamientoLa interfaz LAN de ese host (ip a)
IP_HOST_DOCKER_2Segunda IP del servidor, la de la red WAN o de administración, desde la que se accede a la consola con el navegadorLa segunda interfaz de ese host (ip a). Si el servidor solo tiene una IP, este marcador se ignora
MINIO_ROOT_USERUsuario administrador de MinIOSe define en /docker/minio/.env (sección 5)
MINIO_ROOT_PASSWORDContraseña del administrador de MinIO, mínimo 8 caracteresSe define en /docker/minio/.env (sección 5)
KOPIA_ACCESS_KEYClave de acceso dedicada que usarán los agentes de copia, distinta de la del administradorSe crea en la sección 10
KOPIA_SECRET_KEYClave secreta asociada a la anteriorSe crea en la sección 10
KOPIA_REPO_PASSWORDContraseña de cifrado del repositorio Kopia. Sin ella los datos no se recuperan, ni siquiera teniendo acceso completo al almacenamientoLa eliges tú en la sección 11
USUARIO_SERVIDORUsuario con acceso SSH al servidor Docker, para traerse el certificado al equipo WindowsEl que uses habitualmente para administrar ese servidor
ID_DE_LA_COPIAIdentificador de una copia concreta, necesario para restaurarlaLo devuelve kopia snapshot list (sección 14)
KOPIA_REPO_PASSWORD no es una contraseña más. Kopia cifra los datos en el cliente antes de subirlos, de modo que el servidor de almacenamiento nunca ve el contenido en claro; ese es justamente su punto fuerte. La consecuencia es directa: si se pierde esa contraseña, el repositorio es irrecuperable por diseño y no hay procedimiento de rescate. Guárdala fuera de los equipos que estás respaldando, por ejemplo en el gestor de contraseñas del propio laboratorio.
Esta guía publica el servicio en los puertos 8447 (API S3) y 8448 (consola web). Si en el mismo servidor ya tienes otros stacks ocupando puertos, compruébalo antes con ss -tlnp | grep 8447 y elige otros libres si hiciera falta, cambiándolos en las secciones 5, 6, 7, 8, 11 y 16.

3. Arquitectura y flujo

Dos contenedores en el servidor central, y un agente de copia nativo instalado en cada equipo a respaldar. Igual que con los agentes de inventario, Kopia no vive en un contenedor: se instala sobre el sistema operativo de cada máquina, que es como se hace en un despliegue real.

Resumen visual
   [UBUNTU SERVER]                    [WINDOWS 11]
        kopia                             kopia
          |                                  |
          |     copia cifrada por HTTPS      |
          |     (el agente inicia la conexion)
          +------------------+---------------+
                             |
                          red LAN
                             |
                             v
      +--------------------------------------------+
      |            SERVIDOR DOCKER                 |
      |   LAN: IP_HOST_DOCKER                      |
      |   WAN: IP_HOST_DOCKER_2                    |
      |                                            |
      |   [minio-proxy]  :8447 (S3)  :8448 (web)   |
      |          |                                 |
      |          v                                 |
      |   [minio]  :9000 (API S3)  :9001 (consola) |
      |          |                                 |
      |          v                                 |
      |   volumen minio-data                       |
      |   deposito "backups"                       |
      |   Object Lock = COMPLIANCE 30d             |
      +--------------------------------------------+
El sentido de la conexión importa: es el agente quien contacta al servidor para depositar su copia. Ningún equipo respaldado necesita abrir puertos de entrada, y toda la superficie expuesta queda concentrada en un único punto, igual que en el modelo de inventario o de monitorización por checks activos.
El servidor tiene dos interfaces y eso es deliberado: los agentes llegan por la LAN y la administración se hace desde la WAN. Docker publica los puertos en todas las interfaces a la vez sin configuración adicional, pero el certificado solo será válido para las direcciones que se le indiquen al generarlo, así que ambas deben constar en él (sección 6).

4. Requisitos

  • Un host Linux con Docker Engine y el plugin Docker Compose (docker compose, sin guion) ya instalados, y preferiblemente con dos interfaces de red.
  • Un Ubuntu Server y un Windows 11 en la misma red, que serán los equipos respaldados.
  • Salida a Internet desde los tres equipos, para descargar las imágenes de Docker y los paquetes de Kopia.
  • Espacio libre en el servidor para el volumen de copias. Ten presente que con Object Lock nada se borra antes de tiempo, así que el consumo solo crece durante el periodo de retención.
  • Hora sincronizada en los tres equipos. La retención se calcula sobre fechas, y un reloj desviado desajusta la protección.

5. Levantar MinIO con Docker

Misma organización de siempre: cada stack en su carpeta dentro de /docker, y las credenciales en un .env aparte, nunca escritas dentro del docker-compose.yml.

Crear la carpeta del stack
sudo mkdir -p /docker/minio && cd /docker/minio
Crear .env
sudo nano .env
.env
MINIO_ROOT_USER=clockworkadmin
MINIO_ROOT_PASSWORD=minio_root_pwd
Estos valores sirven para un entorno de pruebas; en una instalación real conviene generar la contraseña de forma aleatoria. MinIO exige un mínimo de 8 caracteres y se niega a arrancar si no se cumple. Buena costumbre en cualquier caso: sudo chmod 600 .env para que solo root pueda leer el archivo.

Ahora el archivo del stack, en la misma carpeta:

Crear docker-compose.yml
sudo nano docker-compose.yml
docker-compose.yml
services:
  minio:
    image: minio/minio:latest
    container_name: minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
      TZ: Europe/Madrid
    volumes:
      - minio-data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 15s
    restart: unless-stopped

  proxy:
    image: nginx:alpine
    container_name: minio-proxy
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./proxy-selfsigned.crt:/etc/nginx/certs/proxy-selfsigned.crt:ro
      - ./proxy-selfsigned.key:/etc/nginx/certs/proxy-selfsigned.key:ro
    ports:
      - "8447:8447"
      - "8448:8448"
    depends_on:
      minio:
        condition: service_healthy
    restart: unless-stopped

volumes:
  minio-data:
MinIO separa dos servicios en puertos distintos dentro del contenedor: el 9000 atiende la API de S3, que es por donde hablan los agentes, y el 9001 sirve la consola web. Por eso el proxy publica dos puertos y no uno: son dos servicios diferentes, y mezclarlos en un único puerto es una fuente habitual de comportamientos raros.
Puede que en otras guías veas una variable MINIO_BROWSER_REDIRECT_URL para indicarle a la consola cuál es su dirección pública detrás de un proxy inverso. Aquí no se usa a propósito: MinIO valida esa variable al arrancar y solo admite un nombre de host, nunca una dirección IP. Si se la pasas con una IP, el contenedor no arranca y se queda en un bucle de reinicio con el mensaje FATAL Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable: invalid hostname. Como todo este montaje trabaja por IP, la variable sencillamente se omite: la consola funciona igual a través del proxy.
Si en tu entorno tienes un nombre DNS propio para el servidor —por ejemplo minio.clockwork.lan— sí puedes añadirla con ese nombre, y entonces ese mismo nombre debe constar también en el subjectAltName del certificado, con el prefijo DNS: en lugar de IP: (sección 6).
El volumen minio-data monta /data, que es donde MinIO guarda tanto los objetos como sus metadatos y las versiones. Persistir esa ruta en un volumen con nombre es lo que marca la diferencia entre poder recrear el contenedor sin consecuencias y perder todas las copias al hacerlo.

6. Configurar el proxy inverso con HTTPS propio

Genera un certificado autofirmado con clave EC, válido para las dos direcciones del servidor: la de la LAN por la que llegan los agentes y la de la WAN desde la que administras.

Generar el certificado autofirmado
cd /docker/minio
sudo openssl ecparam -name prime256v1 -genkey -noout -out proxy-selfsigned.key
sudo openssl req -new -x509 -key proxy-selfsigned.key -out proxy-selfsigned.crt -days 825 \
  -subj "/CN=IP_HOST_DOCKER" \
  -addext "subjectAltName=IP:IP_HOST_DOCKER,IP:IP_HOST_DOCKER_2"
Decide esto antes de continuar. Los agentes van a confiar en este certificado tras instalarlo en su almacén del sistema (secciones 11 y 16). Si lo regeneras más adelante, todos los clientes dejarán de conectar hasta actualizarlo uno por uno. Emite ahora el certificado definitivo, con todas las direcciones necesarias en subjectAltName, y no vuelvas a tocarlo.

Ahora la configuración de Nginx, con un bloque por cada servicio:

Crear nginx.conf
sudo nano nginx.conf
nginx.conf
resolver 127.0.0.11 valid=10s;

# ---------- API S3: por aqui hablan los agentes de copia ----------
server {
    listen 8447 ssl;
    server_name IP_HOST_DOCKER;

    ssl_certificate     /etc/nginx/certs/proxy-selfsigned.crt;
    ssl_certificate_key /etc/nginx/certs/proxy-selfsigned.key;

    # Sin limite de tamano: los bloques de una copia superan el limite por defecto
    client_max_body_size 0;
    ignore_invalid_headers off;
    proxy_buffering off;
    proxy_request_buffering off;
    chunked_transfer_encoding off;

    location / {
        set $upstream_minio http://minio:9000;
        proxy_pass $upstream_minio;

        # La firma de S3 incluye la cabecera Host: debe llegar intacta
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_connect_timeout 300;
        proxy_send_timeout 300;
        proxy_read_timeout 300;
    }
}

# ---------- Consola web de administracion ----------
server {
    listen 8448 ssl;
    server_name IP_HOST_DOCKER_2;

    ssl_certificate     /etc/nginx/certs/proxy-selfsigned.crt;
    ssl_certificate_key /etc/nginx/certs/proxy-selfsigned.key;

    location / {
        set $upstream_console http://minio:9001;
        proxy_pass $upstream_console;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # La consola usa WebSockets para el estado en tiempo real
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        chunked_transfer_encoding off;
    }
}
De todo este archivo, la línea más importante es proxy_set_header Host $http_host;. El protocolo S3 firma cada petición con la Signature V4, y esa firma incluye la cabecera Host tal y como la envió el cliente. Si el proxy la reescribe —por ejemplo con el habitual $host, que descarta el puerto—, la firma que calcula el servidor deja de coincidir con la del cliente y toda petición se rechaza con SignatureDoesNotMatch. Es un error desconcertante porque parece un problema de credenciales cuando en realidad las credenciales son correctas.
client_max_body_size 0 desactiva el límite de tamaño de subida. El valor por defecto de Nginx es 1 MB, muy por debajo de los bloques que sube Kopia, así que sin esta línea las copias fallan con 413 Request Entity Too Large en cuanto el primer bloque supera ese umbral. Junto a ella, proxy_request_buffering off evita que Nginx acumule en disco cada bloque antes de reenviarlo.
El resolver 127.0.0.11 (el DNS interno de Docker) junto con set $upstream_minio; proxy_pass $upstream_minio; no es cosmético: sin la variable, Nginx intenta resolver el nombre minio una sola vez al arrancar, y si en ese instante el contenedor todavía no está listo, se niega a arrancar con un error de host not found in upstream. Con la variable, la resolución se hace en cada petición.

7. Arrancar el stack y verificar

Arrancar los contenedores
cd /docker/minio
docker compose up -d
docker compose ps

Comprueba el registro de MinIO, donde debe aparecer que la API y la consola están escuchando:

Log de MinIO
docker logs -f minio

Sal con Ctrl+C y comprueba que el proxy responde por las dos interfaces:

Probar la API S3 desde el propio host
curl -kI https://IP_HOST_DOCKER:8447/minio/health/live
curl -kI https://IP_HOST_DOCKER_2:8447/minio/health/live
Una respuesta HTTP/1.1 200 OK en ambas confirma que el TLS funciona, que Nginx alcanza el contenedor y que el puerto está publicado correctamente en las dos interfaces. Si una responde y la otra no, el problema es de red o de enrutado, no del despliegue.

8. La consola web y el cliente mc

Abre en el navegador, desde la red de administración:

URL de la consola
https://IP_HOST_DOCKER_2:8448
El navegador mostrará un aviso de conexión no privada. Es esperado: el certificado es autofirmado, no lo ha emitido una autoridad pública reconocida. Pulsa "Avanzado" y continúa. Entra con MINIO_ROOT_USER y MINIO_ROOT_PASSWORD.
No esperes encontrar un panel de administración completo. En 2025 MinIO retiró de la edición Community prácticamente todas las funciones administrativas de su consola web —gestión de cuentas y políticas, configuración, ciclo de vida y replicación—, dejando poco más que un explorador de objetos. Lo verás en la propia interfaz: aparece el rótulo Community Edition bajo el logotipo y el menú lateral se reduce a Object Browser. La propia MinIO remite a los usuarios de esta edición al cliente de línea de comandos mc para todo lo demás, y reserva la consola completa para su producto de pago. No es un fallo de tu instalación: es así desde entonces.
Sí queda un botón Create Bucket, y ahí está la trampa: el formulario que abre pide únicamente el nombre. En la consola antigua ese mismo diálogo ofrecía interruptores para versionado, cuota y Object Locking; ahora no. Un depósito creado desde la interfaz web nace por tanto sin bloqueo de objetos, y como el bloqueo solo puede activarse en el momento de la creación, ese depósito no sirve para esta práctica y habría que borrarlo y empezar de nuevo. Por eso el depósito se crea desde mc en la sección siguiente, y no desde el navegador.
Para el resto de la práctica no supone ningún inconveniente: la consola web sigue siendo útil para ver los objetos, sus versiones y sus fechas de retención, que es justo la parte visualmente interesante de la demostración.

Instalar el cliente mc en el servidor Docker

El cliente se instala en el host donde corre Docker, que es la máquina desde la que se administra el almacenamiento y donde ya está el certificado generado en la sección 6. Los equipos que se respaldan no lo necesitan: allí solo va Kopia.

Descargar e instalar mc
cd /tmp
curl -LO https://dl.min.io/client/mc/release/linux-amd64/mc
ls -lh mc
file mc
La opción -L no es opcional. La dirección de descarga responde con una redirección hacia el servidor de distribución real, y curl no sigue redirecciones por defecto: sin esa opción se descarga el cuerpo de la redirección —un fichero de texto de apenas un centenar de bytes— con el nombre mc. Al no ser un ejecutable, el sistema intenta interpretarlo como un guion de consola y devuelve errores desconcertantes del tipo mc: line 32: a: No such file or directory, que no tienen nada que ver con MinIO. Por eso se comprueba el fichero antes de instalarlo.
Antes de continuar, ls -lh debe mostrar un fichero de decenas de megabytes y file debe identificarlo como ELF 64-bit LSB executable. Si ves unos pocos bytes o la palabra HTML, la descarga no ha traído el binario y no tiene sentido seguir.

Con el fichero verificado, ya se puede instalar:

Instalar el binario
chmod +x mc
sudo mv mc /usr/local/bin/
mc --version

Antes de crear el alias, enseña a mc a confiar en el certificado autofirmado copiándolo a su almacén propio. Es más limpio que ir arrastrando la opción --insecure en cada comando:

Confiar en el certificado
mkdir -p ~/.mc/certs/CAs
sudo cp /docker/minio/proxy-selfsigned.crt ~/.mc/certs/CAs/minio-clockwork.crt
sudo chown $USER: ~/.mc/certs/CAs/minio-clockwork.crt
Crear el alias del servidor
mc alias set clockwork https://IP_HOST_DOCKER:8447 MINIO_ROOT_USER MINIO_ROOT_PASSWORD
mc admin info clockwork
Si mc admin info devuelve el estado del servidor, la cadena completa funciona: TLS válido, cabecera Host intacta y credenciales correctas. Este comando es la mejor prueba de humo antes de seguir.

9. Crear el depósito inmutable

Aquí está el corazón de la práctica. El depósito se crea con el bloqueo de objetos activado desde el primer momento, y acto seguido se le fija una retención por defecto en modo Compliance. Se hace desde mc, en el servidor Docker:

Crear el depósito con Object Lock
mc mb --with-lock clockwork/backups
Fijar la retención por defecto
mc retention set --default COMPLIANCE 30d clockwork/backups
Comprobar el bloqueo y el versionado
mc retention info --default clockwork/backups
mc version info clockwork/backups
La retención debe aparecer como COMPLIANCE con validez de 30 días, y el versionado como habilitado. El versionado no se activa a mano: el bloqueo de objetos lo enciende automáticamente y ya no puede desactivarse, porque sin versiones no habría forma de conservar el estado anterior de un objeto sobrescrito.

Governance y Compliance: la diferencia que lo cambia todo

 GovernanceCompliance (el usado aquí)
Quién puede borrar antes de tiempoUn usuario con el permiso s3:BypassGovernanceRetentionNadie, ni el administrador del propio MinIO
Se puede acortar el plazo de un objeto ya escritoSí, con el permiso de excepciónNo. Solo se puede ampliar
Protege frente a credenciales robadasSolo si la cuenta robada no tiene la excepciónSí, en cualquier caso
Para qué sirveEvitar borrados accidentalesCumplimiento normativo y defensa real ante ransomware
Elegir Compliance tiene una contrapartida que hay que asumir con los ojos abiertos: durante esos 30 días no vas a poder borrar nada, ni siquiera lo que hayas subido por error, ni con la cuenta administradora, ni recreando el contenedor. La única salida sería destruir el volumen entero desde el sistema anfitrión. En un laboratorio conviene empezar con un plazo corto —3d en lugar de 30d— hasta tener el montaje rodado, y subirlo después.
Un plazo de retención no es una política de conservación de copias. La retención dice cuánto tiempo un objeto es indestructible; cuántas copias se conservan y durante cuánto lo decide Kopia con sus propias políticas. Ambas cosas deben estar coordinadas, y de eso trata la sección 15.
Conviene tener claro desde ya, porque reaparece en el simulacro de la sección 13, que lo que se acaba de configurar es la retención por defecto del depósito: una plantilla que se aplica a cada objeto en el instante de escribirlo. Modificarla más adelante no afecta a lo ya almacenado, porque cada versión de objeto conserva grabada su propia fecha de vencimiento. Esa fecha individual es la protección de verdad, y en modo Compliance no hay forma de acortarla.

10. Credenciales dedicadas para los agentes

Los agentes no deben usar la cuenta administradora. Se crea un usuario propio y se le concede acceso únicamente al depósito de copias, siguiendo el principio de mínimo privilegio:

Crear el usuario de los agentes
mc admin user add clockwork KOPIA_ACCESS_KEY KOPIA_SECRET_KEY
Crear el fichero de política
sudo nano /docker/minio/kopia-policy.json
kopia-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:ListBucketVersions",
        "s3:GetBucketLocation",
        "s3:GetBucketObjectLockConfiguration",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:PutObject",
        "s3:PutObjectRetention",
        "s3:GetObjectRetention",
        "s3:DeleteObject"
      ],
      "Resource": [
        "arn:aws:s3:::backups",
        "arn:aws:s3:::backups/*"
      ]
    }
  ]
}
Aplicar la política al usuario
mc admin policy create clockwork kopia-backups /docker/minio/kopia-policy.json
mc admin policy attach clockwork kopia-backups --user KOPIA_ACCESS_KEY
Fíjate en lo que no está en la política: no aparece s3:BypassGovernanceRetention ni s3:DeleteBucket. El agente puede escribir sus copias y hacer su mantenimiento, pero no puede saltarse retenciones ni destruir el depósito. En modo Compliance la primera ni siquiera existiría como opción, pero conviene que la política lo diga explícitamente: si algún día bajas a Governance, esa línea es la que marca la diferencia.
s3:DeleteObject sí es necesario, y no contradice la inmutabilidad. Kopia necesita ese permiso para su mantenimiento interno, pero en un depósito con versionado y bloqueo una petición de borrado no destruye nada: se limita a crear un marcador de borrado sobre la versión actual, mientras el dato original sigue guardado e intacto. Esta distinción es la clave de la sección 13 y conviene tenerla presente desde ya.

11. Kopia en Ubuntu Server

Kopia se instala desde su repositorio oficial de paquetes. Registra primero la clave de firma y el origen:

Añadir el repositorio de Kopia
sudo mkdir -p /etc/apt/keyrings
curl -s https://kopia.io/signing-key | sudo gpg --dearmor -o /etc/apt/keyrings/kopia-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kopia-keyring.gpg] http://packages.kopia.io/apt/ stable main" | sudo tee /etc/apt/sources.list.d/kopia.list
sudo apt update
Instalar Kopia
sudo apt install kopia -y
kopia --version

Confiar en el certificado del servidor

Kopia usa el almacén de certificados del sistema. En lugar de copiar el fichero a mano desde el servidor, se puede pedir directamente al propio servicio y añadirlo al almacén:

Descargar el certificado y confiar en él
openssl s_client -connect IP_HOST_DOCKER:8447 -showcerts </dev/null 2>/dev/null \
  | openssl x509 -outform PEM \
  | sudo tee /usr/local/share/ca-certificates/minio-clockwork.crt

sudo update-ca-certificates
La salida debe indicar que se ha añadido un certificado (1 added). A partir de ese momento cualquier herramienta del sistema que use el almacén estándar —Kopia incluido— confía en el servidor sin necesidad de desactivar comprobaciones.
La alternativa rápida es el parámetro --disable-tls-verification en los comandos de Kopia, que desactiva por completo la validación del certificado. Sirve para salir del paso en una prueba, pero deja al agente enviando sus copias a cualquiera que suplante la dirección del servidor, así que no debería quedarse así. Es el mismo criterio que se aplica a cualquier agente que valide un certificado privado.

Crear los datos de prueba

Carpeta y ficheros de prueba
sudo mkdir -p /datos/pruebas
echo "Informe trimestral - Clockwork Computer" | sudo tee /datos/pruebas/informe.txt
echo "Listado de clientes confidencial" | sudo tee /datos/pruebas/clientes.txt
sudo dd if=/dev/urandom of=/datos/pruebas/base-datos.bin bs=1M count=20
ls -lh /datos/pruebas

Crear el repositorio con retención

Este es el comando que enlaza Kopia con el bloqueo de objetos de MinIO. Los parámetros --retention-mode y --retention-period son los que hacen que cada bloque subido nazca ya protegido:

Crear el repositorio Kopia sobre MinIO
sudo kopia repository create s3 \
  --bucket=backups \
  --endpoint=IP_HOST_DOCKER:8447 \
  --access-key=KOPIA_ACCESS_KEY \
  --secret-access-key=KOPIA_SECRET_KEY \
  --region=us-east-1 \
  --retention-mode=COMPLIANCE \
  --retention-period=30d \
  --password=KOPIA_REPO_PASSWORD
El valor de --endpoint se escribe sin https://: solo dirección y puerto. Kopia usa TLS por defecto y añade el esquema por su cuenta; si se incluye, el comando falla con un error de resolución que despista bastante.
Sobre la región, que es la pregunta que todo el mundo hace: us-east-1 no significa que nada viaje a Estados Unidos ni tiene la menor implicación geográfica. Los datos van únicamente al servidor de esta misma red y no salen de ahí. La región es un artefacto del protocolo: la firma Signature V4 de S3 incorpora el nombre de la región al derivar la clave con la que se firma cada petición, de modo que el cliente está obligado a enviar alguna, y us-east-1 es el valor que todos usan por convenio cuando no hay ninguna configurada. Un MinIO sin región definida acepta la que le llegue sin comprobarla, así que en la práctica funciona como una etiqueta.
Se puede personalizar —en un despliegue español encajaría eu-south-2, que es el código de la región de Madrid— definiéndola en el servidor y usando ese mismo valor en el --region de todos los clientes. Pero ambos lados deben coincidir exactamente: si no lo hacen, las peticiones se rechazan con un AuthorizationHeaderMalformed porque la firma se calcula con una clave distinta. Como no aporta nada funcional y añade una forma más de romper el montaje, lo razonable es dejar us-east-1 y explicar por qué.
La salida confirma el cifrado que se va a usar (AES256-GCM-HMAC-SHA256), la política de conservación por defecto y, al final, quién queda como propietario del mantenimiento, que será este mismo equipo. Anota ese dato: es la pieza de la que depende que los bloqueos se sigan renovando, y se explica en la sección 15.

Validar el proveedor de almacenamiento

El propio Kopia sugiere al terminar una comprobación que conviene no saltarse. No se limita a probar que hay conexión: escribe bloques reales y verifica que el almacenamiento se comporta como Kopia necesita —creaciones condicionales, listados coherentes, lecturas parciales y acceso concurrente—, que es justo donde suelen fallar las implementaciones de S3 de terceros:

Validar la compatibilidad del almacenamiento
sudo kopia repository validate-provider
Tarda alrededor de un minuto, porque incluye una prueba de concurrencia de 30 segundos, y debe terminar con un escueto All good. seguido de la limpieza de los datos temporales. Con eso queda demostrado que MinIO detrás del proxy inverso cumple todo lo que Kopia le va a exigir, y cualquier problema posterior ya no será del almacenamiento. Es una comprobación excelente para dejar grabada, porque valida de una sola vez el certificado, la cabecera Host, el límite de subida de Nginx y los tiempos de espera.
Activar la renovación automática de los bloqueos
sudo kopia maintenance set --extend-object-locks true
sudo kopia maintenance set --full-interval=24h
sudo kopia maintenance info
Estos dos ajustes son imprescindibles y se explican en detalle en la sección 15. En resumen: el bloqueo de cada objeto tiene fecha de caducidad, y es el mantenimiento periódico de Kopia el que va renovando la de los bloques que siguen siendo necesarios. Sin --extend-object-locks la protección se evapora al cumplirse el plazo. El intervalo de 24 horas coincide con el valor que Kopia trae de fábrica, pero se fija de forma explícita a propósito: es un parámetro del que depende la protección y conviene que quede declarado, no heredado.
En un servidor conviene además desactivar la comprobación periódica de actualizaciones que Kopia hace contra GitHub, tanto por no generar tráfico saliente innecesario como por no depender de él: basta con definir la variable de entorno KOPIA_CHECK_FOR_UPDATES=false, que en la sección 12 ya queda recogida en el fichero de entorno del servicio.

Primera copia

Crear la copia y listarla
sudo kopia snapshot create /datos/pruebas
sudo kopia snapshot list
La copia aparece con su identificador, fecha, tamaño y número de ficheros. Desde la consola web de MinIO, en backups, verás aparecer las carpetas internas de Kopia (p, q, x, kopia.repository): los datos ya están arriba, cifrados y con su bloqueo puesto.

Conviene rematar comprobando que la protección quedó grabada en el repositorio, y no solo en la línea de comandos que lo creó:

Confirmar la retención del repositorio
sudo kopia repository status
Al final de la salida deben aparecer Blob retention mode: COMPLIANCE y Blob retention period: 720h0m0s. Esas 720 horas son los 30 días expresados en la unidad que usa Kopia internamente, así que si ves ese par de líneas, cada bloque que suba el agente nacerá protegido. Si faltasen, el repositorio se creó sin retención y habría que rehacerlo.
Al repetir la copia sin cambiar nada verás en el listado un mensaje del tipo + 2 identical snapshots until .... No son copias duplicadas ocupando espacio: Kopia detecta que el contenido es idéntico y agrupa esas entradas, guardando los datos una sola vez. Es la deduplicación haciendo su trabajo, y viene bien enseñarlo para que no parezca que cada ejecución multiplica el consumo.

12. Programar las copias en Ubuntu Server

Una copia manual demuestra que el montaje funciona; lo que protege de verdad es la que se ejecuta sola todos los días, sin que nadie se acuerde de lanzarla. En el Ubuntu Server se resuelve con las herramientas nativas del sistema: un servicio y un temporizador de systemd, que sustituyen con ventaja al viejo cron porque dejan registro consultable de cada ejecución y sobreviven a los apagados.

El temporizador de systemd

Primero, el fichero con la contraseña del repositorio, con permisos restringidos para que solo root pueda leerlo:

Fichero de entorno
sudo mkdir -p /etc/kopia
sudo tee /etc/kopia/kopia.env >/dev/null <<'EOF'
KOPIA_PASSWORD=KOPIA_REPO_PASSWORD
KOPIA_CHECK_FOR_UPDATES=false
EOF
sudo chmod 600 /etc/kopia/kopia.env
Crear el servicio
sudo nano /etc/systemd/system/kopia-backup.service
kopia-backup.service
[Unit]
Description=Copia de seguridad con Kopia hacia MinIO
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/etc/kopia/kopia.env
Environment=HOME=/root
ExecStart=/usr/bin/kopia --config-file=/root/.config/kopia/repository.config snapshot create /datos/pruebas
ExecStartPost=/usr/bin/kopia --config-file=/root/.config/kopia/repository.config maintenance run --full
Las dos líneas que evitan el fallo más habitual de este paso son Environment=HOME=/root y el --config-file explícito. El motivo: la configuración de Kopia vive en $HOME/.config/kopia/repository.config, y systemd no define $HOME en los servicios del sistema aunque se ejecuten como root. Sin ellas, la copia funciona a mano con sudo pero el servicio falla con open repository: repository is not connected, que hace pensar en un problema de red o de credenciales cuando en realidad Kopia sencillamente no encuentra su propio fichero de configuración. Definir HOME importa además por la caché, que también cuelga de ahí.
Crear el temporizador
sudo nano /etc/systemd/system/kopia-backup.timer
kopia-backup.timer
[Unit]
Description=Copia diaria con Kopia

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
Activar y comprobar
sudo systemctl daemon-reload
sudo systemctl enable --now kopia-backup.timer
systemctl list-timers kopia-backup.timer
sudo systemctl start kopia-backup.service
journalctl -u kopia-backup.service -n 30 --no-pager
Persistent=true es un detalle que marca la diferencia en equipos que no están encendidos las 24 horas: si el servidor estaba apagado a las 02:00, la copia se lanza en cuanto arranca en lugar de perderse hasta el día siguiente. El último comando ejecuta el servicio a mano para validarlo sin esperar a la hora programada.

13. Simulacro de ransomware

Con las copias en su sitio, toca comprobar que la inmutabilidad no es una casilla marcada en una configuración sino una defensa real. El simulacro tiene tres fases, en orden de agresividad creciente.

Fase 1: el ransomware ataca los datos originales

Cifrar y destruir los ficheros originales (Ubuntu)
sudo bash -c 'for f in /datos/pruebas/*; do \
  openssl enc -aes-256-cbc -pbkdf2 -in "$f" -out "$f.locked" -k "rescate123" && rm -f "$f"; \
done'
ls -l /datos/pruebas
Los ficheros originales han desaparecido y en su lugar quedan versiones cifradas con extensión .locked. Es exactamente el estado en el que un usuario descubre que ha sido víctima de un ataque.

Fase 2: el atacante intenta borrar las copias

El siguiente paso de cualquier ransomware serio es ir a por el respaldo, usando las credenciales que encuentre en el equipo comprometido. Y ahí está la gracia de hacerlo desde el propio equipo atacado, no desde el servidor: es donde estarían realmente esas credenciales, dentro de la configuración del agente de copia. Como mc se instaló en el servidor Docker, hay que ponerlo también aquí:

Instalar mc en el equipo comprometido
cd /tmp
curl -LO https://dl.min.io/client/mc/release/linux-amd64/mc
file mc
chmod +x mc
sudo mv mc /usr/local/bin/
mc --version
No hace falta volver a tocar el certificado: en la sección 11 ya se añadió al almacén del sistema con update-ca-certificates, y mc lo respeta. Y recuerda el -L del curl, por el mismo motivo que en la sección 8.
La dirección del alias debe ser la misma por la que este equipo alcanza el servidor, es decir la que se usó en --endpoint al crear el repositorio, no la de administración. Si el servidor tiene dos interfaces y aquí pones la equivocada, la conexión no llegará —o llegará por una ruta que no toca— y parecerá un fallo de la práctica cuando solo es un error de direccionamiento.
El atacante se conecta con las credenciales robadas
mc alias set atacante https://IP_HOST_DOCKER:8447 KOPIA_ACCESS_KEY KOPIA_SECRET_KEY
mc ls atacante/backups/
Si prefieres simplificar y no instalar nada en el equipo atacado, todos los comandos de esta fase funcionan igual desde el servidor Docker, donde mc ya está. El resultado técnico es idéntico; solo se pierde algo de verosimilitud en el relato, porque el ataque real parte del equipo comprometido y no del servidor de copias.
Intento de borrado convencional
mc rm --recursive --force atacante/backups/
mc ls atacante/backups/
Atención al resultado, porque es contraintuitivo y es el momento más interesante de la práctica: el comando parece haber funcionado y el listado posterior aparece completamente vacío. El atacante celebraría. Pero en un depósito con versionado, una petición de borrado no destruye el dato: solo coloca un marcador de borrado sobre él, que lo oculta del listado normal dejándolo intacto por debajo.
Y no hay que deducirlo: la propia herramienta lo confiesa línea por línea. Cada objeto genera un mensaje Created delete marker ... (versionId=...) en lugar de un borrado. Se reconocen ahí, además, las piezas internas del repositorio de Kopia: kopia.repository con los metadatos, kopia.blobcfg y kopia.maintenance con su configuración, y los bloques de datos e índices con prefijos p, q y xn0_. Todo cifrado: ni los nombres revelan qué se ha copiado.
A partir de este momento el agente del equipo atacado tampoco ve su repositorio: si pruebas un kopia snapshot list fallará, porque para él los objetos han desaparecido. Es justo la sensación que tendría el administrador al descubrir el incidente, y por eso la restauración de la sección 14 empieza retirando estos marcadores.
Comprobar que los datos siguen ahí
mc ls --versions atacante/backups/
Todas las versiones aparecen listadas, con su tamaño y su fecha. No se ha perdido absolutamente nada: solo se ha añadido una capa de marcadores por encima.

Un atacante con conocimientos lo sabría e iría a por las versiones. Aquí es donde entra el bloqueo:

Intento de borrado definitivo de las versiones
mc rm --recursive --force --versions atacante/backups/
La salida de este comando engaña, y conviene leerla despacio antes de dar nada por perdido. Aparecerá una larga lista de líneas Removed ... que parece indicar que el ataque está teniendo éxito, y solo al final un único error. No cunda el pánico: lo que se ha borrado no son los datos.

La prueba está en los identificadores de versión. Si comparas las líneas Removed con los Created delete marker del paso anterior, verás que coinciden uno a uno: lo que el comando ha eliminado son los propios marcadores de borrado, que no llevan retención porque no contienen datos. Y borrar un marcador no destruye nada; al contrario, devuelve la visibilidad al objeto que ocultaba. El resto de líneas corresponde a blobs temporales que dejó la validación del proveedor de la sección 11, que tampoco estaban protegidos.

El desenlace es el único error del final, y es exactamente el que buscábamos: Object, '..." is WORM protected and cannot be overwritten. En cuanto el comando llega a la primera versión real de un dato —fíjate en que su identificador de versión ya no coincide con ningún marcador—, MinIO lo rechaza y la operación se detiene. Los datos son indestructibles y el ataque ha fracasado.
Detalle que conviene anticipar en cámara: mc aborta en el primer objeto protegido en lugar de continuar y devolver un error por cada uno. Por eso se ve una sola línea de error y no un muro de rechazos. Y como efecto colateral curioso, al haber eliminado los marcadores, el depósito vuelve a mostrar su contenido en un listado normal: el atacante, intentando afinar, ha deshecho sin querer su propio borrado.

Fase 3: el atacante consigue las credenciales de administrador

Vamos al peor escenario posible dentro del almacenamiento: que el atacante llegue a la cuenta administradora de MinIO. Se usa el alias clockwork, el de la cuenta root.

Los alias de mc son locales a cada equipo: viven en ~/.mc/config.json. El alias clockwork se creó en la sección 8 en el servidor Docker, así que en el equipo atacado no existe. Si lanzas ahí estos comandos sin más, mc no reconoce clockwork como un alias, lo interpreta como una ruta del sistema de ficheros local y devuelve errores tan desconcertantes como Requested path /tmp/clockwork/backups not found o SetObjectLockConfig is not supported for 'filesystem'. No es un fallo del bloqueo: es que está mirando en el disco local.

Hay dos formas de resolverlo, y ambas valen. La primera es ejecutar esta fase desde el servidor Docker, donde el alias ya existe. La segunda, más coherente con el relato —el atacante ha escalado privilegios y ahora tiene las credenciales del administrador—, es darlo de alta también en el equipo atacado:

Dar de alta el alias de administrador en el equipo atacado
mc alias set clockwork https://IP_HOST_DOCKER:8447 MINIO_ROOT_USER MINIO_ROOT_PASSWORD
mc ls clockwork/backups/

Con el alias resuelto, los intentos con la cuenta administradora:

Intentos con la cuenta administradora
mc rm --recursive --force --versions clockwork/backups/
mc rb --force clockwork/backups
Los dos fallan, y con el mismo motivo: is WORM protected and cannot be overwritten. El modo Compliance no admite excepciones ni siquiera para la cuenta administradora, así que no se pueden borrar las versiones protegidas y tampoco eliminar el depósito mientras las contenga. Esta es exactamente la propiedad que convierte una copia en una copia inmutable.

Lo que el administrador sí puede hacer, y por qué no le sirve de nada

Queda un último intento, el más sutil: si no puede borrar, que rebaje el plazo de retención y espere a que caduque. Este comando sí se ejecuta correctamente:

Rebajar la retención por defecto del depósito
mc retention set --default COMPLIANCE 1d clockwork/backups
Aquí hay una distinción que se confunde constantemente y que conviene explicar con cuidado, porque de ella depende entender qué protege realmente el bloqueo. Lo que acaba de cambiarse es la retención por defecto del depósito, que no es más que la plantilla que se aplica a los objetos en el momento de escribirlos. Es modificable, sí. Pero la protección real no vive ahí: vive en cada versión de objeto ya escrita, que lleva grabada su propia fecha de vencimiento, y esa —en modo Compliance— no se puede acortar ni cambiar de modo. Solo ampliar.

La demostración es inmediata: pregunta por el valor por defecto del depósito y por el de un objeto concreto ya almacenado, y compáralos:

Comparar el defecto del depósito con la retención real de un objeto
mc retention info --default clockwork/backups
mc retention info clockwork/backups/kopia.repository
El depósito dirá ahora 1 día, pero el objeto seguirá mostrando su vencimiento original a 30 días vista. El atacante ha conseguido debilitar lo que se escriba a partir de ahora, y absolutamente nada de lo que ya estaba. Las copias existentes siguen siendo indestructibles durante todo su plazo.
Y en este montaje hay además una segunda barrera: Kopia no depende del valor por defecto del depósito, porque fija la retención objeto a objeto con el plazo guardado en su propia configuración de repositorio (el --retention-period de la sección 11). Aunque alguien rebaje el defecto del depósito, las copias que siga haciendo el agente nacerán igualmente con sus 30 días. Son dos capas independientes, y hace falta comprometer las dos para debilitar la protección futura.
No olvides deshacer este cambio antes de continuar, o el depósito se quedará con una retención por defecto de un solo día: mc retention set --default COMPLIANCE 30d clockwork/backups.
Y aquí la parte honesta, que conviene decir en voz alta: esto protege frente a un atacante que llega por la API de S3, que es el vector real del ransomware y el robo de credenciales. No protege frente a quien comprometa el sistema anfitrión donde corre MinIO, porque desde ahí se puede destruir el volumen de Docker por debajo, sin pasar por la API ni por sus reglas. Por eso en un despliegue real el almacenamiento de copias vive en una máquina separada y endurecida, con acceso administrativo restringido, y a ser posible replicado a un tercer sitio.

14. Restauración

Primero: comprobar si hace falta deshacer algo

La recuperación no empieza restaurando, sino mirando en qué estado quedó el repositorio. Dependiendo de hasta dónde llegara el atacante, el punto de partida es distinto, así que lo primero es preguntárselo al propio agente:

Ver si el agente alcanza su repositorio
sudo kopia repository status
Hay dos desenlaces posibles, y conviene entender por qué. Si el ataque se quedó en el borrado convencional, los marcadores siguen puestos, el repositorio está oculto y este comando falla: hay que retirarlos con el paso siguiente. Pero si el atacante llegó a intentar el borrado de versiones, se llevó por delante sus propios marcadores antes de chocar con el bloqueo, de modo que los objetos volvieron a ser visibles solos y este comando responde con normalidad. En ese caso no hay nada que deshacer y se puede saltar directamente a la restauración.
La salida es además una buena diapositiva: confirma Blob retention mode: COMPLIANCE y Blob retention period: 720h0m0s —las 720 horas son los 30 días—, es decir que el agente sigue escribiendo con protección activa incluso después del incidente.

Si el repositorio sigue oculto: retirar los marcadores

Solo en el primer caso. Los datos nunca se fueron, únicamente quedaron tapados, y mc lo revierte deshaciendo la última operación sobre los objetos versionados:

Deshacer el borrado del atacante
mc undo --recursive --force clockwork/backups/
mc ls clockwork/backups/
El listado normal vuelve a mostrar el contenido del repositorio. Los marcadores han desaparecido y los objetos originales vuelven a ser la versión actual. Ningún dato tuvo que recuperarse de ningún sitio: nunca se fue.
Si en su lugar recibes Undo command works only with S3 versioned-enabled buckets junto a una ruta del sistema de ficheros local, no es que falte el versionado: es que el alias no existe en ese equipo y mc está mirando en el disco. Revisa el aviso sobre alias de la sección 13.

Ahora la restauración propiamente dicha. Localiza la copia y recupérala en una carpeta nueva, para poder comparar antes de sobrescribir nada:

Listar las copias disponibles (Ubuntu)
sudo kopia repository status
sudo kopia snapshot list /datos/pruebas
sudo kopia snapshot list --all
Restaurar en una carpeta nueva
sudo kopia snapshot restore ID_DE_LA_COPIA /datos/restaurado
ls -lh /datos/restaurado
cat /datos/restaurado/informe.txt
Los ficheros originales vuelven a estar ahí, en claro y con su contenido intacto, mientras que en /datos/pruebas siguen los .locked del ataque. La restauración de extremo a extremo está demostrada: ese es el cero de la regla 3-2-1-1-0.
Sustituye ID_DE_LA_COPIA por el identificador que devuelve snapshot list (una cadena hexadecimal larga). Si prefieres explorar antes de restaurar, sudo kopia mount all /mnt/kopia monta todas las copias como un sistema de ficheros de solo lectura y permite navegarlas con ls y cp como si fueran carpetas normales.

15. Vigencia de los bloqueos y mantenimiento

Este es el punto que más instalaciones tiene mal configuradas, y el que decide si la protección sigue viva dentro de seis meses o se apagó sin que nadie se diera cuenta.

El bloqueo de un objeto no es perpetuo: tiene una fecha de expiración, la que se fijó al subirlo. Pasada esa fecha, el objeto vuelve a ser borrable. Como una copia útil necesita seguir protegida indefinidamente mientras se conserve, Kopia va renovando la fecha de los bloques que siguen formando parte de alguna copia activa. Esa renovación ocurre durante el mantenimiento completo, y solo si está activada la opción correspondiente:

Revisar la configuración de mantenimiento
sudo kopia maintenance info
Esta salida es la mejor prueba de que la protección está viva, y merece una pausa en cámara porque en ella se ve todo el mecanismo funcionando. Hay tres cosas que mirar: la línea Object Lock Extension: enabled, que confirma que la renovación está activada; el Owner, que identifica al equipo responsable de ejecutarla; y sobre todo el histórico de la tarea extend-blob-retention-time, donde cada ejecución deja constancia del trabajo real con mensajes del tipo Blob retention extension found 15 blobs and extended for 15 blobs, retention period 720h0m0s. Ese contador creciendo copia tras copia es la inmutabilidad renovándose sola.
En ese mismo histórico aparecen las demás tareas del mantenimiento, y ayudan a explicar qué hace Kopia por dentro: snapshot-gc distingue los contenidos en uso de los huérfanos, cleanup-logs aplica la retención a los registros, y las tareas de épocas (advance-epoch, compact-single-epoch) mantienen compactado el índice. Todas deben aparecer como SUCCESS.
La regla de oro: el intervalo de mantenimiento completo debe ser menor que el periodo de retención, con margen de sobra —al menos un día menos—. Con retención de 30 días y mantenimiento cada 24 horas, como se configuró en la sección 11, hay margen de sobra. Si el mantenimiento dejara de ejecutarse durante más de 30 días —porque el equipo estuvo apagado, porque falló el temporizador o porque el propietario del mantenimiento cambió—, los bloqueos caducarían y las copias volverían a ser borrables sin ningún aviso.
Kopia designa a un único cliente como propietario del mantenimiento para que dos equipos no lo ejecuten a la vez. En este montaje lo es el Ubuntu Server, que fue quien creó el repositorio. Compruébalo con kopia maintenance info: si algún día ese equipo desaparece, hay que traspasar la propiedad con kopia maintenance set --owner=usuario@equipo o el mantenimiento sencillamente dejará de ejecutarse, en silencio.
Forzar un mantenimiento completo
sudo kopia maintenance run --full

El riesgo del depósito lleno

La inmutabilidad tiene un efecto secundario que conviene prever: si nada se puede borrar, nada libera espacio antes de tiempo. Un atacante con las credenciales del agente no puede destruir tus copias, pero sí puede subir basura hasta llenar el almacenamiento y dejarlo inservible, sin que puedas limpiarla hasta que expire su retención. La mitigación es poner una cuota al depósito para que ese ataque tenga un techo:
Limitar el tamaño del depósito
mc quota set clockwork/backups --size 100gi
mc quota info clockwork/backups

Política de conservación de copias

Independientemente de la retención del almacenamiento, Kopia decide cuántas copias conserva. Ya viene con unos valores por defecto que la propia creación del repositorio mostró en pantalla —10 últimas, 48 horarias, 7 diarias, 4 semanales, 24 mensuales y 3 anuales—, generosos y pensados para conservar mucho historial. Conviene revisarlos y ajustarlos de forma consciente, sobre todo si el espacio es limitado y no se puede liberar antes de que expire el bloqueo:

Definir cuántas copias se conservan
sudo kopia policy set --global \
  --keep-latest=10 \
  --keep-daily=14 \
  --keep-weekly=4 \
  --keep-monthly=6

sudo kopia policy show --global
Conviene no confundir los dos plazos, porque suenan parecidos y hacen cosas distintas. La retención de S3 dice cuánto tiempo un bloque es indestructible; la política de Kopia dice cuántas copias siguen siendo necesarias. Cuando Kopia descarta una copia antigua, sus bloques dejan de renovarse y acaban expirando por sí solos, momento en el que el espacio se libera. Los dos mecanismos trabajan juntos: uno protege, el otro recicla.

16. Kopia en Windows 11

Con el ciclo completo demostrado en Linux —copia, programación, ataque y recuperación—, toca comprobar que el mismo repositorio da servicio a un sistema operativo distinto sin cambiar nada del servidor. Este apartado repite el recorrido entero en un Windows 11: instalación del agente, confianza en el certificado, conexión al repositorio ya existente, copia programada y restauración, incluida la de los datos que respaldó la otra máquina.

La instalación se resuelve con el gestor de paquetes integrado. Ejecuta PowerShell como administrador:

Instalar Kopia
winget install Kopia.KopiaCLI --accept-package-agreements --accept-source-agreements
kopia --version
Si kopia no se reconoce inmediatamente después de instalar, cierra y vuelve a abrir PowerShell: la variable PATH se actualiza al iniciar una sesión nueva. También existe el paquete Kopia.KopiaUI, que añade una interfaz gráfica de escritorio sobre el mismo motor, útil si prefieres enseñar el proceso visualmente en lugar de por línea de comandos.

Confiar en el certificado del servidor

En Linux el certificado se descargaba del propio servicio con openssl; en Windows es más cómodo traerlo por SCP, porque Windows 10 y 11 incluyen el cliente de OpenSSH de serie y scp funciona directamente desde PowerShell sin instalar nada. Pedirá la contraseña del usuario del servidor:

Traer el certificado desde el servidor
scp USUARIO_SERVIDOR@IP_HOST_DOCKER:/docker/minio/proxy-selfsigned.crt "$env:USERPROFILE\Downloads\proxy-selfsigned.crt"
Si responde Permission denied al leer el fichero, es porque el certificado pertenece a root y el usuario con el que entras no puede leerlo. Se resuelve dejando una copia accesible en el servidor antes de repetir el scp: sudo cp /docker/minio/proxy-selfsigned.crt ~ && sudo chown $USER: ~/proxy-selfsigned.crt, y apuntando entonces a ~/proxy-selfsigned.crt. Ten en cuenta también que muchos servidores tienen deshabilitado el acceso SSH directo como root, así que usa tu usuario habitual.

Antes de instalarlo, conviene mirar qué se ha traído. Este paso confirma de un vistazo que el fichero es el certificado correcto y no otra cosa:

Comprobar el certificado descargado
Get-PfxCertificate -FilePath "$env:USERPROFILE\Downloads\proxy-selfsigned.crt" | Format-List Subject, Issuer, NotAfter, Thumbprint
El Subject y el Issuer deben coincidir —es autofirmado, así que se emite a sí mismo— y mostrar la IP del servidor. La NotAfter debe estar a algo más de dos años vista, que son los 825 días con los que se generó.

Ya se puede añadir al almacén de entidades de certificación de confianza del equipo:

Importar el certificado
Import-Certificate -FilePath "$env:USERPROFILE\Downloads\proxy-selfsigned.crt" -CertStoreLocation Cert:\LocalMachine\Root
Tiene que ir a LocalMachine\Root y no a CurrentUser, porque más adelante la tarea programada puede ejecutarse en un contexto distinto al de tu sesión interactiva. Importarlo a nivel de equipo evita un fallo que solo aparecería de madrugada, cuando la tarea se lanza sola. Y por ese mismo motivo, esta consola de PowerShell debe estar abierta como administrador: escribir en el almacén del equipo requiere elevación, y sin ella el comando falla con un acceso denegado.

Conectar al repositorio existente

Aquí se usa connect, no create. El repositorio ya existe: lo creó el Ubuntu Server en la sección 11, y volver a crearlo sobre el mismo depósito daría error o, peor, dejaría dos estructuras conviviendo. Los parámetros de retención tampoco se repiten: quedaron grabados en el repositorio y los hereda cualquier cliente que se conecte.
Conectar Windows al repositorio
kopia repository connect s3 --bucket=backups --endpoint=IP_HOST_DOCKER:8447 --access-key=KOPIA_ACCESS_KEY --secret-access-key=KOPIA_SECRET_KEY --region=us-east-1 --password=KOPIA_REPO_PASSWORD
Datos de prueba y primera copia
New-Item -ItemType Directory -Force -Path C:\datos\pruebas
"Contrato de mantenimiento - Clockwork Computer" | Out-File C:\datos\pruebas\contrato.txt
"Presupuesto 2026" | Out-File C:\datos\pruebas\presupuesto.txt
kopia snapshot create C:\datos\pruebas
kopia snapshot list
El listado anterior muestra únicamente la copia de este equipo, y conviene entender por qué antes de pensar que algo ha fallado: kopia snapshot list filtra por defecto por el usuario y la máquina actuales. Las copias del Ubuntu Server están en el mismo repositorio, pero no se muestran salvo que se pidan expresamente.
Ver las copias de todos los equipos
kopia snapshot list --all
Ahora sí aparecen las copias de los dos equipos, identificadas con el formato usuario@maquina:ruta, conviviendo en el mismo repositorio. Kopia deduplica entre ellas: si ambos equipos guardaran el mismo fichero, se almacenaría una sola vez, aunque procedan de sistemas operativos distintos.

Programar la copia diaria con el Programador de tareas

Registrar la tarea diaria
$kopia = (Get-Command kopia).Source

$accion = New-ScheduledTaskAction -Execute $kopia -Argument "snapshot create C:\datos\pruebas"
$disparo = New-ScheduledTaskTrigger -Daily -At 02:00
$opciones = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries

Register-ScheduledTask -TaskName "Kopia - Copia diaria" -Action $accion -Trigger $disparo -Settings $opciones -User $env:USERNAME -RunLevel Highest
Probar la tarea sin esperar
Start-ScheduledTask -TaskName "Kopia - Copia diaria"
Get-ScheduledTaskInfo -TaskName "Kopia - Copia diaria"
La configuración de Kopia en Windows es por usuario: vive en el perfil de quien ejecutó repository connect. Por eso la tarea se registra con -User $env:USERNAME y no como SYSTEM. Si la registras como SYSTEM sin más, se ejecutará sin encontrar ninguna conexión al repositorio y fallará con un escueto repository is not connected. Si necesitas que corra sin sesión iniciada, hay que conectar el repositorio también en el contexto de esa cuenta.
Un último ajuste recomendable en Windows: activar las instantáneas de volumen (VSS) para que Kopia pueda copiar ficheros que estén abiertos por otras aplicaciones. Se hace con kopia policy set --enable-volume-shadow-copy=when-available C:\datos\pruebas y requiere ejecutar con privilegios elevados.

Restaurar en Windows y desde Windows

El procedimiento es idéntico al de Linux: se localiza la copia por su identificador y se recupera en una carpeta nueva.

Restaurar desde Windows
kopia snapshot list C:\datos\pruebas
kopia snapshot restore ID_DE_LA_COPIA C:\datos\restaurado
Get-ChildItem C:\datos\restaurado
Merece la pena enseñar también la restauración cruzada: desde el equipo Windows se pueden listar y restaurar las copias del Ubuntu Server, porque comparten repositorio y contraseña de cifrado. Hay que pedirlas con kopia snapshot list --all, ya que por defecto cada equipo solo ve las suyas, y después restaurar por identificador con normalidad. Es una demostración muy visual de que la copia sobrevive incluso a la pérdida total del equipo de origen, que es el escenario que de verdad importa.

17. Tabla de puertos y componentes

PuertoComponenteSentido de la conexiónPara qué sirve
8447/tcpminio-proxy (Nginx)Agentes → contenedorAPI compatible con S3: por aquí suben y bajan las copias, por HTTPS
8448/tcpminio-proxy (Nginx)Navegador → contenedorConsola web de MinIO, para consultar objetos y versiones
minio :9000 (Docker, interno)proxy → minioAPI S3 real; no publica ningún puerto al host
minio :9001 (Docker, interno)proxy → minioConsola web real; no publica ningún puerto al host
Kopia en cada endpointAgente → servidorNo escucha en ningún puerto: solo abre conexiones salientes
Una ventaja discreta pero real de este modelo: Kopia no abre ningún puerto de escucha en los equipos respaldados. A diferencia de los agentes de inventario o de monitorización, aquí no hay ni siquiera un puerto local que asegurar en el endpoint.

18. Errores comunes y cómo resolverlos

ProblemaCausa probableSolución
Todas las peticiones fallan con SignatureDoesNotMatch aunque las credenciales sean correctas El proxy está reescribiendo la cabecera Host, que forma parte de la firma S3 Usar proxy_set_header Host $http_host; y no $host ni $host:$server_port (sección 6)
Las peticiones se rechazan con AuthorizationHeaderMalformed tras cambiar la región La región configurada en el servidor y la que envía el cliente no coinciden, y la firma se deriva con ella Usar el mismo valor en ambos lados, o volver a us-east-1 en el --region y dejar el servidor sin región definida (sección 11)
La copia empieza y se corta con 413 Request Entity Too Large Los bloques que sube Kopia superan el límite por defecto de Nginx, que es de 1 MB Confirmar client_max_body_size 0; en el bloque del puerto 8447 (sección 6)
Kopia falla con x509: certificate signed by unknown authority El certificado autofirmado no está en el almacén de confianza del cliente Añadirlo con update-ca-certificates en Linux o Import-Certificate en Windows (secciones 11 y 16)
Al crear el repositorio, error de resolución de nombre en el endpoint Se ha escrito --endpoint=https://IP:8447 con el esquema incluido Indicar solo dirección y puerto: --endpoint=IP_HOST_DOCKER:8447 (sección 11)
No se puede activar el bloqueo sobre un depósito que ya existe Object Lock solo se habilita en el momento de crear el depósito Crear uno nuevo con mc mb --with-lock y migrar las copias (sección 9)
El depósito se creó desde el botón Create Bucket de la consola web y no admite retención El formulario de la edición Community solo pide el nombre y crea el depósito sin bloqueo de objetos Borrarlo y volver a crearlo con mc mb --with-lock, que es la única vía que activa el bloqueo (secciones 8 y 9)
mc rm parece borrar las copias sin ningún error Comportamiento normal del versionado: solo se han creado marcadores de borrado, el dato sigue intacto Verificarlo con mc ls --versions y revertirlo con mc undo (secciones 13 y 14)
Tras instalar mc, cualquier comando responde mc: line 32: a: No such file or directory La descarga se hizo sin -L: lo que se guardó como mc es el cuerpo de una redirección, no el binario Borrarlo con sudo rm /usr/local/bin/mc y repetir la descarga con curl -LO, verificándola con file mc (sección 8)
mc admin policy create no se reconoce como subcomando Versión antigua del cliente, que usaba otra nomenclatura Actualizar mc, o usar mc admin policy add y mc admin policy set en su lugar
La consola web no ofrece gestión de usuarios, políticas ni depósitos MinIO retiró esas funciones de la edición Community en 2025 Administrar con mc, que es la vía soportada para esta edición (sección 8)
mc responde Requested path /tmp/ALIAS/... not found o ... is not supported for 'filesystem' Ese alias no existe en el equipo desde el que lanzas el comando: los alias son locales, viven en ~/.mc/config.json. Al no reconocerlo, mc lo trata como una ruta del disco local Dar de alta el alias en ese equipo con mc alias set, o ejecutar el comando desde el servidor donde ya existe (secciones 8 y 13)
Un equipo no ve las copias del otro aunque compartan repositorio Comportamiento normal: kopia snapshot list filtra por el usuario y la máquina actuales Añadir --all para ver las copias de todos los equipos (secciones 14 y 16)
La copia funciona a mano con sudo pero el servicio de systemd falla con open repository: repository is not connected systemd no define $HOME en los servicios del sistema, así que Kopia no encuentra su configuración en /root/.config/kopia/ Añadir Environment=HOME=/root y el parámetro --config-file a la unidad (sección 12)
La tarea programada de Windows falla con repository is not connected La tarea corre bajo una cuenta distinta a la que conectó el repositorio, y la configuración de Kopia es por usuario Registrar la tarea con la misma cuenta, o conectar el repositorio en el contexto de la cuenta que la ejecuta (sección 16)
Pasado un tiempo, los objetos vuelven a poder borrarse El mantenimiento completo no se está ejecutando y los bloqueos han caducado sin renovarse Revisar kopia maintenance info, el propietario del mantenimiento y el temporizador (sección 15)
El almacenamiento se llena y no se puede liberar espacio Con Object Lock nada se borra antes de expirar la retención Poner cuota al depósito y ajustar retención y política de conservación (sección 15)
El contenedor minio no arranca y se queda en Restarting (1), con FATAL Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable: invalid hostname Se ha definido MINIO_BROWSER_REDIRECT_URL con una dirección IP. Esa variable solo admite nombres de host Quitarla del docker-compose.yml, o sustituir la IP por un nombre DNS que además conste en el certificado (sección 5)
El contenedor minio arranca y muere al instante por credenciales La contraseña de MINIO_ROOT_PASSWORD tiene menos de 8 caracteres, o el .env no se está leyendo Comprobar los valores ya resueltos con docker compose config y revisar el .env (sección 5)

19. Notas de ampliación

El montaje de esta guía cubre lo necesario para tener copias inmutables funcionando y verificadas en dos sistemas operativos. Para un uso continuado conviene ampliar con:

  • Un segundo destino fuera del emplazamiento, que es lo que completa la regla 3-2-1-1-0. Kopia admite varios repositorios, y MinIO puede replicar un depósito hacia otra instalación en otra ubicación.
  • Alertas de copia fallida, porque el peor fallo de un sistema de respaldo es el que no avisa. Kopia puede lanzar acciones tras cada ejecución, y un temporizador que falla es un evento que el sistema de monitorización debería recoger.
  • Integración con el SIEM, enviando el resultado de cada copia y los intentos de borrado rechazados por MinIO. Un rm denegado sobre el depósito de copias es, en sí mismo, un indicador de compromiso de primer nivel.
  • Apertura automática de tickets en la mesa de ayuda cuando una copia falle dos días seguidos, para que el aviso no se quede en un correo que nadie lee.
  • Cifrado y custodia de la contraseña del repositorio en un gestor de contraseñas propio, fuera de los equipos respaldados. Sin ella no hay restauración posible, y guardarla en la máquina que se respalda es un contrasentido.
  • Endurecimiento del servidor de copias, que a estas alturas es el activo más valioso de la infraestructura: acceso administrativo restringido, sin servicios adicionales, y a poder ser en una red separada del resto.
  • Pruebas de restauración periódicas y documentadas. Una copia que nunca se ha restaurado es una hipótesis, no una copia de seguridad.