todos los sistemas operativostodo operativo

apenaloza.cl · bitácora

SCL (UTC−3) · v2026.11

~/blog/respaldos-postgresql-s3-iam-restauracion.md

Respaldos de PostgreSQL en S3: regla 3-2-1, IAM mínima y restauraciones probadas

Cómo respaldar PostgreSQL en Amazon S3 (o un servicio compatible) de forma segura: pg_dump en streaming, un usuario IAM que solo puede subir, ciclo de vida para la retención, protección contra borrados y un script que prueba la restauración de forma automática.

autor
Alejandro Peñaloza
Publicado
Lectura
5 min
Portada: Respaldos de PostgreSQL en S3, regla 3-2-1, IAM mínima y restauraciones probadas, con una base de datos enviando datos a un bucket
En este artículo
  1. La regla 3-2-1
  2. Qué tipo de respaldo necesitas
  3. Paso 1: el bucket
  4. Paso 2: un usuario IAM que solo puede subir
  5. Paso 3: el script de respaldo
  6. Paso 4: programarlo y enterarte cuando falla
  7. Paso 5: retención con reglas de ciclo de vida
  8. Paso 6: inmutabilidad con Object Lock
  9. Paso 7: probar la restauración (de verdad)
  10. ¿Y si no uso AWS?
  11. Checklist final
  12. Conclusión

Todo el mundo tiene respaldos hasta que necesita restaurar uno. Ahí aparecen los problemas: el archivo está vacío, el cron dejó de correr hace tres meses, el respaldo vivía en el mismo disco que se dañó o, peor, el atacante que entró al servidor también borró los respaldos.

En este post armo un sistema de respaldos de PostgreSQL hacia S3 pensado para evitar justamente esos escenarios.

La regla 3-2-1

Es la base de cualquier estrategia de respaldo:

  • 3 copias de los datos (la original y dos respaldos).
  • 2 tipos de almacenamiento distintos.
  • 1 copia fuera del sitio.

Para una base de datos en un VPS, se traduce en: la base en producción, un respaldo local reciente para restauraciones rápidas y una copia en S3 (u otro proveedor) fuera del servidor. Hoy se suele agregar un punto más: una copia inmutable, que nadie pueda borrar durante un tiempo, ni siquiera tú.

Qué tipo de respaldo necesitas

Método Qué hace Recuperación Cuándo usarlo
pg_dump Exporta una base de forma lógica Al momento del respaldo Bases pequeñas y medianas, la mayoría de los proyectos
pg_dumpall --globals-only Exporta roles y permisos — Siempre, junto al pg_dump
pgBackRest / WAL-G Respaldo físico + archivado continuo de WAL A cualquier segundo (PITR) Bases grandes o cuando perder horas de datos no es aceptable

Este post se enfoca en pg_dump, que cubre la gran mayoría de los casos. Si tu negocio no tolera perder las transacciones desde el último respaldo, el siguiente paso es recuperación a un punto en el tiempo con pgBackRest o WAL-G.

Paso 1: el bucket

Crea un bucket dedicado, en la región más cercana a tu servidor, con:

  • Bloqueo de acceso público activado (viene así por defecto).
  • Cifrado del lado del servidor (SSE-S3 viene activo por defecto).
  • Versionado activado, para que sobrescribir o borrar un objeto no elimine la versión anterior.
aws s3api create-bucket --bucket mi-empresa-respaldos-pg \
  --region sa-east-1 \
  --create-bucket-configuration LocationConstraint=sa-east-1

aws s3api put-bucket-versioning --bucket mi-empresa-respaldos-pg \
  --versioning-configuration Status=Enabled

Paso 2: un usuario IAM que solo puede subir

Este es el punto que más se descuida. Si el servidor usa una llave con permisos de administrador o con s3:*, cualquiera que comprometa el servidor puede borrar todos tus respaldos. El servidor solo necesita escribir.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SoloSubirRespaldos",
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::mi-empresa-respaldos-pg/postgres/*"
    }
  ]
}

Sin s3:DeleteObject, sin s3:GetObject, sin listar. Para restaurar se usa otro usuario o rol, con permisos de lectura, cuyas credenciales no viven en el servidor de producción:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LeerRespaldos",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::mi-empresa-respaldos-pg/postgres/*"
    },
    {
      "Sid": "ListarRespaldos",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::mi-empresa-respaldos-pg",
      "Condition": { "StringLike": { "s3:prefix": ["postgres/*"] } }
    }
  ]
}

Configura las credenciales del usuario de escritura en el servidor con un perfil propio:

aws configure --profile respaldos
chmod 600 ~/.aws/credentials

Paso 3: el script de respaldo

Hace el pg_dump en formato personalizado (comprimido y restaurable de forma selectiva) y lo envía en streaming a S3, sin dejar el archivo completo en el disco. También respalda roles y deja una copia local corta para restauraciones rápidas.

Archivo /opt/scripts/respaldo-pg.sh:

#!/usr/bin/env bash
set -euo pipefail

DB="${DB_NAME:?Falta DB_NAME}"
BUCKET="s3://mi-empresa-respaldos-pg/postgres/$(hostname)"
LOCAL_DIR="/var/backups/postgres"
LOCAL_DIAS=3
FECHA="$(date -u +%Y/%m/%d)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
export AWS_PROFILE=respaldos

mkdir -p "$LOCAL_DIR"

pg_dump -Fc --no-owner -d "$DB" \
  | tee "$LOCAL_DIR/${DB}_${STAMP}.dump" \
  | aws s3 cp - "$BUCKET/$FECHA/${DB}_${STAMP}.dump" \
      --storage-class STANDARD_IA \
      --expected-size 10737418240   # ajusta si tu respaldo supera 50 GB

pg_dumpall --globals-only \
  | aws s3 cp - "$BUCKET/$FECHA/globals_${STAMP}.sql"

pg_restore --list "$LOCAL_DIR/${DB}_${STAMP}.dump" > /dev/null

find "$LOCAL_DIR" -name "*.dump" -mtime +"$LOCAL_DIAS" -delete

echo "$(date -Is) OK ${DB}_${STAMP}.dump"

Detalles importantes:

  • set -o pipefail (incluido en set -euo pipefail) hace que el script falle si cualquier parte de la tubería falla. Sin eso, un pg_dump que muere a la mitad sube un archivo truncado y el script termina "bien".
  • pg_restore --list confirma que el archivo es un dump válido y legible.
  • Las fechas van en UTC para no tener sorpresas con el cambio de horario.

Paso 4: programarlo y enterarte cuando falla

Con un temporizador de systemd tienes logs y reintentos gratis:

Archivo /etc/systemd/system/respaldo-pg.service:

[Unit]
Description=Respaldo PostgreSQL a S3
OnFailure=notificar-falla@%n.service

[Service]
Type=oneshot
User=postgres
Environment=DB_NAME=app
ExecStart=/opt/scripts/respaldo-pg.sh

Archivo /etc/systemd/system/respaldo-pg.timer:

[Unit]
Description=Respaldo diario de PostgreSQL

[Timer]
OnCalendar=*-*-* 06:15:00 UTC
Persistent=true
RandomizedDelaySec=10m

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now respaldo-pg.timer
systemctl list-timers | grep respaldo

Persistent=true ejecuta el respaldo pendiente si el servidor estaba apagado a la hora programada. La unidad notificar-falla@ puede ser un simple curl a ntfy, Slack o un correo; lo importante es que un respaldo fallido haga ruido.

Y para cubrir el caso en que el script ni siquiera corre, usa un monitor de tipo heartbeat (Healthchecks, Uptime Kuma en modo push): el script le avisa al terminar y, si no recibe aviso en 25 horas, te alerta.

Paso 5: retención con reglas de ciclo de vida

No borres respaldos desde el servidor: deja que S3 lo haga con reglas de ciclo de vida.

{
  "Rules": [
    {
      "ID": "retencion-respaldos-pg",
      "Status": "Enabled",
      "Filter": { "Prefix": "postgres/" },
      "Transitions": [{ "Days": 30, "StorageClass": "GLACIER_IR" }],
      "Expiration": { "Days": 365 },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket mi-empresa-respaldos-pg \
  --lifecycle-configuration file://ciclo-vida.json

Con esto, los respaldos pasan a un almacenamiento más barato a los 30 días y se eliminan al año. Ajusta los plazos a tus obligaciones: algunos datos (contables, tributarios o laborales) deben conservarse varios años. Revisa también la duración mínima de cobro de cada clase de almacenamiento antes de mover archivos a clases frías.

Paso 6: inmutabilidad con Object Lock

Para protegerte de un atacante que obtenga credenciales con más permisos (o de un error propio), S3 Object Lock impide borrar o sobrescribir objetos durante un periodo. Se activa al crear el bucket (o después, con el versionado activo):

aws s3api put-object-lock-configuration \
  --bucket mi-empresa-respaldos-pg \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": { "DefaultRetention": { "Mode": "GOVERNANCE", "Days": 30 } }
  }'
  • GOVERNANCE: solo usuarios con un permiso especial pueden saltarse el bloqueo.
  • COMPLIANCE: nadie puede borrar el objeto antes del plazo, ni siquiera la cuenta raíz. Úsalo con cuidado: también te obliga a ti.

Paso 7: probar la restauración (de verdad)

Un respaldo que nunca restauraste es una hipótesis. Este script, que corre en otra máquina con las credenciales de lectura, descarga el último respaldo, lo restaura en un PostgreSQL desechable y ejecuta una consulta de control:

Archivo /opt/scripts/probar-restauracion.sh:

#!/usr/bin/env bash
set -euo pipefail
export AWS_PROFILE=restauracion

PREFIJO="s3://mi-empresa-respaldos-pg/postgres/servidor-prod"
ULTIMO=$(aws s3 ls "$PREFIJO/" --recursive | grep '\.dump$' | sort | tail -1 | awk '{print $4}')
echo "Probando: $ULTIMO"

aws s3 cp "s3://mi-empresa-respaldos-pg/$ULTIMO" /tmp/prueba.dump

docker run -d --rm --name pg-prueba -e POSTGRES_PASSWORD=prueba postgres:16-alpine
until docker exec pg-prueba pg_isready -U postgres > /dev/null 2>&1; do sleep 1; done

docker exec pg-prueba createdb -U postgres restaurada
docker cp /tmp/prueba.dump pg-prueba:/tmp/prueba.dump
docker exec pg-prueba pg_restore -U postgres -d restaurada --no-owner /tmp/prueba.dump

FILAS=$(docker exec pg-prueba psql -U postgres -d restaurada -tAc \
  "SELECT count(*) FROM usuarios")
docker stop pg-prueba > /dev/null
rm -f /tmp/prueba.dump

if [ "$FILAS" -lt 1 ]; then
  echo "ERROR: restauración sin datos"; exit 1
fi
echo "OK: restauración con $FILAS usuarios"

Prográmalo una vez por semana. Además de confirmar que el respaldo sirve, te dice cuánto demora una restauración, un número que vas a querer conocer antes de una emergencia.

¿Y si no uso AWS?

Todo lo anterior funciona con servicios compatibles con S3, como Cloudflare R2, Backblaze B2, Wasabi o un MinIO propio, agregando el endpoint:

aws s3 cp archivo.dump s3://bucket/ruta/ --endpoint-url https://<cuenta>.r2.cloudflarestorage.com

Revisa qué soporta cada uno: no todos tienen las mismas reglas de ciclo de vida, clases de almacenamiento u Object Lock. Y si tus datos son sensibles, puedes cifrarlos antes de subirlos con age o gpg, de modo que ni el proveedor pueda leerlos (guardando la llave de descifrado en un lugar seguro y separado).

Checklist final

  • Bucket privado, cifrado y con versionado
  • Usuario IAM del servidor con solo s3:PutObject
  • Credenciales de lectura fuera del servidor de producción
  • Script con set -euo pipefail y verificación del dump
  • Respaldo de roles con pg_dumpall --globals-only
  • Temporizador + alerta de falla + monitor heartbeat
  • Reglas de ciclo de vida según tus plazos legales
  • Object Lock para una copia inmutable
  • Prueba de restauración automática semanal

Conclusión

Un buen sistema de respaldos no se mide por cuántas copias guarda, sino por la certeza de poder restaurar. Con permisos mínimos, retención automática, inmutabilidad y pruebas periódicas, un respaldo deja de ser un archivo que esperas que funcione y pasa a ser uno que sabes que funciona.

compartir:linkedinxblueskywhatsappreddithacker newsfacebookcorreo

Comentarios

Aún no hay comentarios. ¿Quieres dejar el primero?

Deja un comentario

Los comentarios se revisan antes de publicarse. No pido correo ni guardo tu IP.

$ ls ~/blog | grep relacionados

Sigue leyendo