Amazon S3 es un almacenamiento de objetos duradero y barato, adecuado para guardar copias de seguridad fuera del servidor. Con aws s3 sync solo se suben los archivos que han cambiado, y si activas el versionado del bucket S3 conserva también las versiones anteriores. En este tutorial prepararás un bucket privado con versionado y retención de 30 días, un usuario IAM que solo puede escribir en ese bucket y un script de backup en Ubuntu 24.04 que se ejecuta cada noche con un timer de systemd.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo.
  • Una cuenta de AWS y, en tu equipo local o en AWS CloudShell, la AWS CLI v2 configurada con un usuario con permisos de administración de S3 e IAM. Esos permisos solo se usan en los pasos 1 y 2; el servidor nunca los tendrá.
  • Opcional: MySQL o MariaDB en el servidor si quieres incluir un volcado de bases de datos.

En los ejemplos, sustituye your_bucket por un nombre de bucket único a nivel global (por ejemplo backups-miempresa-web01) y eu-west-1 por la región que prefieras.

Paso 1: Crear el bucket con versionado

Ejecuta estos comandos en el equipo con credenciales de administrador, no en el servidor. Crea el bucket:

aws s3 mb s3://your_bucket --region eu-west-1
make_bucket: your_bucket

Los buckets nuevos tienen bloqueado el acceso público y cifrado en reposo con SSE-S3 por defecto. Activa el versionado para que cada sobrescritura o borrado deje una copia de la versión anterior:

aws s3api put-bucket-versioning --bucket your_bucket --versioning-configuration Status=Enabled

Comprueba las tres propiedades:

aws s3api get-bucket-versioning --bucket your_bucket
aws s3api get-public-access-block --bucket your_bucket
aws s3api get-bucket-encryption --bucket your_bucket

La primera debe devolver "Status": "Enabled", la segunda BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy y RestrictPublicBuckets a true, y la tercera "SSEAlgorithm": "AES256".

Paso 2: Definir la retención con una regla de ciclo de vida

Con el versionado activo, las versiones antiguas se acumulan para siempre. Una regla de ciclo de vida las elimina a los 30 días, limpia las marcas de borrado huérfanas y aborta subidas multiparte incompletas. Crea el archivo lifecycle.json:

nano lifecycle.json
{
  "Rules": [
    {
      "ID": "retencion-30-dias",
      "Status": "Enabled",
      "Filter": {},
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
      "Expiration": { "ExpiredObjectDeleteMarker": true },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    }
  ]
}

Aplica la regla y compruébala:

aws s3api put-bucket-lifecycle-configuration --bucket your_bucket --lifecycle-configuration file://lifecycle.json
aws s3api get-bucket-lifecycle-configuration --bucket your_bucket

La salida debe mostrar la regla retencion-30-dias con "Status": "Enabled". Ajusta NoncurrentDays a la retención que necesites.

Paso 3: Crear un usuario IAM de mínimo privilegio

El servidor necesita credenciales propias que solo sirvan para este bucket. Crea el documento de política backup-policy.json, sustituyendo your_bucket:

nano backup-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": "arn:aws:s3:::your_bucket"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::your_bucket/*"
    }
  ]
}

La política no incluye s3:DeleteObjectVersion ni permisos sobre el versionado o el ciclo de vida. Si alguien roba estas claves y borra los objetos, S3 solo crea marcas de borrado y las versiones anteriores siguen disponibles durante el periodo de retención.

Crea el usuario, adjúntale la política como política en línea y genera sus claves de acceso:

aws iam create-user --user-name backup-web01
aws iam put-user-policy --user-name backup-web01 --policy-name s3-backup --policy-document file://backup-policy.json
aws iam create-access-key --user-name backup-web01
{
    "AccessKey": {
        "UserName": "backup-web01",
        "AccessKeyId": "AKIA...",
        "Status": "Active",
        "SecretAccessKey": "wJalr...",
        ...
    }
}

Copia AccessKeyId y SecretAccessKey: la clave secreta no se vuelve a mostrar.

Paso 4: Instalar la AWS CLI v2 en el servidor

A partir de aquí todo se hace en el servidor. Ubuntu 24.04 no incluye la AWS CLI v2 en sus repositorios, así que usa el instalador oficial de AWS:

sudo apt update
sudo apt install unzip curl
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip -q awscliv2.zip
sudo ./aws/install

En un servidor ARM (aarch64) descarga awscli-exe-linux-aarch64.zip en su lugar. Comprueba la versión:

aws --version
aws-cli/2.x.x Python/3.x.x Linux/6.8.0-xx-generic exe/x86_64.ubuntu.24

El backup se ejecutará como root, así que guarda las credenciales en el perfil backup de root:

sudo -H aws configure --profile backup

Introduce las claves del paso 3, la región del bucket y json como formato de salida. Las credenciales quedan en /root/.aws/credentials, legible solo por root. Comprueba que funcionan:

sudo -H aws s3 ls s3://your_bucket --profile backup

Un bucket vacío no devuelve nada y el comando termina sin error. Si ves AccessDenied, revisa el nombre del bucket en la política.

Paso 5: Escribir el script de backup

El script sincroniza /var/www y /etc, y sube un volcado comprimido de todas las bases de datos si MySQL o MariaDB están instalados. Como el volcado siempre tiene el mismo nombre, el versionado del bucket guarda automáticamente los de los últimos 30 días.

sudo nano /usr/local/bin/backup-s3.sh
#!/usr/bin/env bash
set -euo pipefail

BUCKET="your_bucket"
export AWS_PROFILE="backup"
DUMP_DIR="/var/backups/mysql"

echo "Inicio del backup: $(date --iso-8601=seconds)"

# Volcado de bases de datos (solo si hay MySQL/MariaDB)
if command -v mysqldump > /dev/null; then
  install -d -m 700 "$DUMP_DIR"
  mysqldump --all-databases --single-transaction --routines --events \
    | gzip > "$DUMP_DIR/all-databases.sql.gz"
  aws s3 cp "$DUMP_DIR/all-databases.sql.gz" "s3://$BUCKET/mysql/all-databases.sql.gz" --only-show-errors
fi

# Archivos web
aws s3 sync /var/www/ "s3://$BUCKET/www/" --delete --only-show-errors \
  --exclude "*.log" --exclude "*/cache/*"

# Configuración del sistema
aws s3 sync /etc/ "s3://$BUCKET/etc/" --delete --only-show-errors

echo "Backup completado: $(date --iso-8601=seconds)"

--delete mantiene el destino idéntico al origen; gracias al versionado, un archivo borrado por error en el servidor sigue recuperable durante 30 días. El volcado usa la autenticación por socket de root que Ubuntu configura por defecto en MySQL y MariaDB, así que no hace falta poner contraseñas en el script.

Hazlo ejecutable y lánzalo una vez a mano:

sudo chmod 750 /usr/local/bin/backup-s3.sh
sudo -H /usr/local/bin/backup-s3.sh
Inicio del backup: 2026-09-25T10:14:02+00:00
Backup completado: 2026-09-25T10:15:37+00:00

Comprueba lo que se ha subido:

sudo -H aws s3 ls s3://your_bucket/ --recursive --summarize --profile backup | tail -n 2
Total Objects: 1843
   Total Size: 412337914

La segunda ejecución será mucho más rápida porque sync solo sube los archivos nuevos o modificados.

Paso 6: Programar el backup con un timer de systemd

Un timer de systemd registra cada ejecución en el journal y recupera las ejecuciones perdidas si el servidor estaba apagado. Crea el servicio:

sudo nano /etc/systemd/system/backup-s3.service
[Unit]
Description=Backup del servidor a Amazon S3
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
Environment=HOME=/root
ExecStart=/usr/local/bin/backup-s3.sh

HOME=/root es necesario para que la AWS CLI encuentre /root/.aws. Crea el timer:

sudo nano /etc/systemd/system/backup-s3.timer
[Unit]
Description=Backup diario a Amazon S3

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15min
Persistent=true

[Install]
WantedBy=timers.target

Recarga systemd, activa el timer y lanza el servicio una vez para validarlo:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-s3.timer
sudo systemctl start backup-s3.service
systemctl list-timers backup-s3.timer
NEXT                        LEFT     LAST                        PASSED  UNIT             ACTIVATES
Sat 2026-09-26 03:07:41 UTC 16h left Fri 2026-09-25 10:20:05 UTC 1min ago backup-s3.timer backup-s3.service

Revisa el resultado de la ejecución:

sudo journalctl -u backup-s3.service -n 20 --no-pager

Debes ver las líneas Inicio del backup y Backup completado, y Finished backup-s3.service.

Paso 7: Restaurar desde S3

Un backup que no se ha probado a restaurar no es un backup. Descarga la copia de la web a un directorio temporal y compárala:

sudo -H aws s3 sync s3://your_bucket/www/ /tmp/restore-www/ --profile backup
sudo diff -rq /var/www/ /tmp/restore-www/ | head

Si solo aparecen los archivos excluidos (logs y caché), la copia es correcta. Para recuperar un volcado de un día anterior, lista sus versiones y descarga la que necesites por su VersionId:

sudo -H aws s3api list-object-versions --bucket your_bucket --prefix mysql/all-databases.sql.gz \
  --query 'Versions[].[VersionId,LastModified]' --output table --profile backup
sudo -H aws s3api get-object --bucket your_bucket --key mysql/all-databases.sql.gz \
  --version-id VERSION_ID /tmp/all-databases.sql.gz --profile backup

Restaura el volcado con zcat /tmp/all-databases.sql.gz | sudo mysql, idealmente primero en un servidor de pruebas.

Solución de problemas

An error occurred (AccessDenied): comprueba qué identidad está usando la CLI con sudo -H aws sts get-caller-identity --profile backup. Debe aparecer user/backup-web01. Si es correcta, revisa que el ARN de la política coincide exactamente con el nombre del bucket.

El timer se ejecuta pero falla con Unable to locate credentials: falta Environment=HOME=/root en el servicio, o las credenciales se guardaron en el HOME de tu usuario. Repite sudo -H aws configure --profile backup.

mysqldump: Access denied for user 'root'@'localhost': tu instalación usa contraseña para root. Crea /root/.my.cnf con una sección [mysqldump] que incluya user y password, con permisos 600.

El bucket crece más de lo esperado: consulta el tamaño con aws s3 ls --recursive --summarize y revisa que la regla de ciclo de vida está activa. Las versiones antiguas se borran de forma asíncrona, uno o dos días después de cumplir los 30 días.

Conclusión

Tu servidor sube cada noche sus archivos web, su configuración y sus bases de datos a un bucket S3 privado, con versiones de los últimos 30 días y credenciales que no pueden destruir ese historial. Como siguientes pasos puedes cifrar los volcados en el cliente con gpg antes de subirlos, mover las versiones antiguas a S3 Glacier Instant Retrieval con otra regla de ciclo de vida para abaratar costes, o activar S3 Object Lock en un bucket nuevo si necesitas copias inmutables.