Migración de PostgreSQL

Objetivo

Esta guía explica cómo actualizar un servidor PostgreSQL y conservar los datos existentes mediante la instalación paralela de ambas versiones y la migración automática de los clústeres con pg_upgradecluster. El ejemplo utiliza PostgreSQL 14 y 17, pero el procedimiento puede aplicarse también a otras versiones.

Requisitos previos

  • Acceso con privilegios de root o sudo.

  • PostgreSQL 14 en funcionamiento.

  • Espacio en disco suficiente para alojar temporalmente ambos clústeres.

  • Ventana de mantenimiento: el servicio se detendrá durante la migración.

Procedimiento

Instalar PostgreSQL 17 (sin desinstalar la 14)

Actualizar el índice de paquetes e instalar la nueva versión en paralelo:

apt update
apt install postgresql-17

Nota: No se debe desinstalar la versión 14 todavía, ya que contiene los datos a migrar.

Detener el servicio de PostgreSQL

Detener el servicio para evitar escrituras durante la migración:

systemctl stop postgresql

Verificar los clústeres existentes

Listar los clústeres reconocidos por el sistema:

pg_lsclusters

Se espera ver el clúster 14 main (con datos) y un clúster 17 main vacío, creado automáticamente al instalar el paquete.

Eliminar el clúster vacío de la versión 17

Eliminar el clúster recién creado por la instalación (sin datos) para liberar el nombre main:

pg_dropcluster 17 main

Ejecutar la migración automática del clúster 14 al 17

Migrar los datos del clúster de la versión 14 al de la 17:

pg_upgradecluster 14 main

Nota: Este paso puede tardar según el tamaño de las bases de datos. Al finalizar, el clúster 17 main contendrá los datos migrados.

Iniciar el servicio con la nueva versión

systemctl start postgresql

Verificar la versión activa

Confirmar que el servidor está ejecutando PostgreSQL 17:

sudo -u postgres psql -c "SELECT version();"

Eliminar el clúster y el paquete antiguos (solo tras validar)

Una vez confirmado que todo funciona correctamente, eliminar el clúster de la versión 14 y su paquete:

pg_dropcluster 14 main --stop
apt remove postgresql-14

Advertencia: Este paso es irreversible. Realizarlo únicamente después de verificar el correcto funcionamiento de la versión 17 y de contar con un respaldo de los datos.

Solución de problemas de permisos

Si durante la migración o el arranque aparecen errores relacionados con el archivo de clave SSL (ssl-cert-snakeoil.key), aplicar las siguientes correcciones:

Corregir propietario y permisos del archivo de clave

Debe ser legible únicamente por el usuario postgres (permisos 600):

sudo chown postgres:postgres /etc/ssl/private/ssl-cert-snakeoil.key
sudo chmod 600 /etc/ssl/private/ssl-cert-snakeoil.key

Asegurar los permisos del directorio contenedor

El directorio padre debe tener permisos 700 y ser propiedad de postgres o root:

sudo chown postgres:root /etc/ssl/private
sudo chmod 700 /etc/ssl/private

Verificar los permisos aplicados

ls -la /etc/ssl/private/ssl-cert-snakeoil.key

El resultado debe mostrar al usuario postgres como propietario y permisos -rw——-.

Script para corregir permisos

#!/usr/bin/env bash
#
# corregir-permisos.sh
# Corrige propietario y permisos de los directorios de PostgreSQL
# y del directorio de claves SSL privadas (/etc/ssl/private).
# Uso: sudo ./corregir-permisos.sh
#

set -euo pipefail

if [[ $EUID -ne 0 ]]; then
    echo "ERROR: este script debe ejecutarse como root (usa sudo)." >&2
    exit 1
fi

DIRS=(
    "/var/log/postgresql"
    "/var/lib/postgresql"
    "/etc/postgresql"
)

echo "== Estado actual =="
for d in "${DIRS[@]}"; do
    if [[ -e "$d" ]]; then
        ls -ld "$d"
    else
        echo "AVISO: $d no existe, se omite."
    fi
done
ls -ld /etc/ssl/private 2>/dev/null || echo "AVISO: /etc/ssl/private no existe."
ls -l /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || echo "AVISO: la clave snakeoil no existe."
echo

echo "== Corrigiendo propietario (postgres:postgres) =="
for d in "${DIRS[@]}"; do
    if [[ -d "$d" ]]; then
        echo "-> chown -R postgres:postgres $d"
        chown -R postgres:postgres "$d"
    fi
done
echo

echo "== Ajustando permisos de directorios =="

if [[ -d /var/lib/postgresql ]]; then
    echo "-> chmod 700 /var/lib/postgresql"
    chmod 700 /var/lib/postgresql
fi

if [[ -d /etc/postgresql ]]; then
    echo "-> chmod 755 /etc/postgresql"
    chmod 755 /etc/postgresql
fi

if [[ -d /var/log/postgresql ]]; then
    echo "-> chmod 775 /var/log/postgresql"
    chmod 775 /var/log/postgresql
fi

echo

echo "== Corrigiendo directorio y clave SSL privada =="

if [[ -d /etc/ssl/private ]]; then
    # El directorio padre debe ser accesible (x) por el grupo ssl-cert.
    # 710: root acceso total, ssl-cert puede entrar, otros nada.
    echo "-> chown root:ssl-cert /etc/ssl/private"
    chown root:ssl-cert /etc/ssl/private
    echo "-> chmod 710 /etc/ssl/private"
    chmod 710 /etc/ssl/private

    # Asegurar que postgres pertenece al grupo ssl-cert
    if ! id -nG postgres | grep -qw ssl-cert; then
        echo "-> usermod -aG ssl-cert postgres"
        usermod -aG ssl-cert postgres
        echo "   (postgres añadido a ssl-cert; se aplicará en procesos nuevos)"
    else
        echo "   postgres ya pertenece al grupo ssl-cert."
    fi
else
    echo "AVISO: /etc/ssl/private no existe, se omite."
fi

if [[ -f /etc/ssl/private/ssl-cert-snakeoil.key ]]; then
    echo "-> Corrigiendo clave SSL"
    chown postgres:postgres /etc/ssl/private/ssl-cert-snakeoil.key
    chmod 600 /etc/ssl/private/ssl-cert-snakeoil.key
fi

echo

echo "== Verificando acceso real de postgres a la clave SSL =="
if [[ -f /etc/ssl/private/ssl-cert-snakeoil.key ]]; then
    if runuser -u postgres -- test -r /etc/ssl/private/ssl-cert-snakeoil.key; then
        echo "OK: postgres puede leer la clave SSL."
    else
        echo "ERROR: postgres NO puede leer la clave SSL. Revisa /etc/ssl/private y grupos." >&2
    fi
fi
echo

echo "== Detectando archivos con UID/GID sin resolver =="
for d in "${DIRS[@]}"; do
    [[ -d "$d" ]] || continue
    echo "-- $d --"
    find "$d" \( -nouser -o -nogroup \) -ls || true
done
echo

echo "== Buscando archivos con permisos peligrosos =="
for d in "${DIRS[@]}"; do
    [[ -d "$d" ]] || continue
    echo "-- Archivos con permiso de escritura para todos en $d --"
    find "$d" -type f -perm -o+w -ls || true
done
echo

echo "== Estado final =="
for d in "${DIRS[@]}"; do
    [[ -e "$d" ]] && ls -ld "$d"
done
ls -ld /etc/ssl/private 2>/dev/null || true
ls -l /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true

echo
echo "Listo. Ejecuta 'pg_lsclusters' para verificar."
echo "Luego reintenta: pg_upgradecluster 14 main"

Notas importantes

Directorios de PostgreSQL

  • /var/lib/postgresql en 700: es la práctica recomendada de Debian para el directorio raíz de datos. Los subdirectorios de cada clúster (/var/lib/postgresql/17/main, /var/lib/postgresql/14/main) mantienen 700 también de manera predeterminada.

  • /etc/postgresql en 755: los archivos de configuración (postgresql.conf, pg_hba.conf) deben ser legibles; solo pg_hba.conf suele requerir 640 con grupo postgres.

  • /var/log/postgresql en 775: permite que el grupo postgres escriba los logs sin necesidad de ser propietario del directorio.

  • El script no toca los subdirectorios de cada versión (17/main, 14/main) más allá de unificar el propietario; sus permisos internos (700 para datos, 640 para configs) se dejan como están para no romper el arranque del servidor.

Directorio y clave SSL (/etc/ssl/private)

  • /etc/ssl/private en 710 con propietario root:ssl-cert: es la configuración estándar de Debian/Ubuntu. El bit x para el grupo ssl-cert es imprescindible: sin él, ningún usuario (incluido postgres) puede «atravesar» el directorio para llegar al archivo de la clave, aunque el archivo mismo tenga permisos correctos. Este es precisamente el motivo del error no se pudo acceder al archivo de la llave privada … Permiso denegado que aparece al arrancar el clúster durante el upgrade.

  • ssl-cert-snakeoil.key en 600 con propietario postgres:postgres: la clave privada nunca debe ser legible por otros usuarios. Si el archivo ya está en 600 postgres:postgres pero el arranque falla igual, el problema no está en el archivo sino en el directorio padre.

  • Pertenencia al grupo ssl-cert: el script verifica que el usuario postgres sea miembro del grupo ssl-cert y lo agrega con usermod -aG si no lo es. Debian/Ubuntu otorgan el acceso a la clave a través de este grupo, no mediante permisos directos sobre el archivo. Un chmod 600 postgres:postgres aislado no basta si el directorio padre no deja entrar.

  • Aplicación del nuevo grupo: usermod -aG no afecta a sesiones ya abiertas. Como pg_upgradecluster lanza un proceso nuevo vía pg_ctl, normalmente toma el grupo actualizado sin reiniciar nada. Si la verificación sigue fallando, conviene cerrar sesión o reiniciar systemd-logind antes de reintentar el upgrade.

Verificación real de acceso

  • El script usa runuser -u postgres -- test -r /etc/ssl/private/ssl-cert-snakeoil.key. Esto simula exactamente lo que hará el proceso de PostgreSQL al arrancar y confirma de forma fiable si el acceso está resuelto.

Archivos con UID/GID sin resolver y con permisos de escritura para todos los usuarios

  • El script detecta archivos con propietario o grupo inexistentes (-nouser -o -nogroup) y archivos con permiso de escritura para todos (-perm -o+w). Ambos son indicios de configuraciones heredadas o migraciones incompletas que pueden romper el arranque o suponer un riesgo de seguridad.

  • No modifica estos archivos automáticamente: solo los lista para revisión manual, ya que un cambio indiscriminado de permisos podría alterar el comportamiento esperado de la instalación.

Alcance y orden de ejecución

  • El script es idempotente: puede ejecutarse varias veces sin efectos adversos. Primero unifica propietarios, luego ajusta permisos de directorios, después corrige el bloque SSL y finalmente verifica.

  • Se recomienda ejecutarlo antes de pg_upgradecluster, y volver a ejecutarlo si el upgrade falla por errores de permisos, ya que algunos pasos del upgrade pueden alterar temporalmente permisos de archivos auxiliares.