~/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
- English
- Read in English →

En este artículo
- La regla 3-2-1
- Qué tipo de respaldo necesitas
- Paso 1: el bucket
- Paso 2: un usuario IAM que solo puede subir
- Paso 3: el script de respaldo
- Paso 4: programarlo y enterarte cuando falla
- Paso 5: retención con reglas de ciclo de vida
- Paso 6: inmutabilidad con Object Lock
- Paso 7: probar la restauración (de verdad)
- ¿Y si no uso AWS?
- Checklist final
- 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=EnabledPaso 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/credentialsPaso 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 enset -euo pipefail) hace que el script falle si cualquier parte de la tubería falla. Sin eso, unpg_dumpque muere a la mitad sube un archivo truncado y el script termina "bien".pg_restore --listconfirma 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.shArchivo /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.targetsudo systemctl daemon-reload
sudo systemctl enable --now respaldo-pg.timer
systemctl list-timers | grep respaldoPersistent=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.jsonCon 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.comRevisa 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 pipefaily 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
$ ls ~/blog | grep relacionados
Sigue leyendo

Cómo autenticarse en el SII con certificado digital desde Node.js: semilla, token y mTLS
Guía práctica para autenticarse en los servicios del SII de Chile con un certificado digital desde Node.js: cómo leer el .pfx, firmar la semilla con XMLDSig, obtener el token, usar TLS mutuo y resolver los errores más comunes.

Tu propio Vercel en un VPS: despliegues con Coolify y Dokploy
Cómo montar una plataforma de despliegue propia en un VPS con Coolify o Dokploy: push a Git y despliegue automático, SSL con Let's Encrypt, bases de datos y previews, más una comparación honesta con Vercel y sus límites reales.

Bitácora de guerra: depurar CORS, refactorizar código propio e integrar Webpay sin perder la tarde
Problemas reales y cómo los resolví: un error de CORS que solo fallaba en producción, una fuga de memoria en el servidor, código mío de hace un año refactorizado sin vergüenza y una guía práctica para integrar Webpay Plus, Mercado Pago y Flow.