Ir al contenido

Sincronización de secretos entre múltiples hosts con Consul y rotación de certificados sin downtime

Rogelio Guerra Riverón
Autor
Rogelio Guerra Riverón
Construyendo mi propia infraestructura web desde cero. Aquí documento cada paso: servidores, redes, contenedores y lo que vaya surgiendo.

El problema
#

Tengo varios hosts en mi infraestructura doméstica corriendo servicios Docker. Cada uno necesita certificados SSL válidos y credenciales compartidas. Renovar certificados manualmente en cada máquina es un dolor de cabeza, y hacer downtime cada vez es impractical.

La solución: Consul como almacén centralizado de secretos con sincronización automática y un sistema de hooks en systemd para recargar servicios sin parar.

Arquitectura
#

  • Consul: almacena certificados y secretos con replicación automática
  • systemd: monitorea cambios y ejecuta scripts de actualización
  • inotify: detecta cambios en archivos de certificados
  • Docker: reinicia servicios sin downtime usando health checks

Paso 1: Configuración inicial de Consul
#

Primero, instalé Consul en mi servidor principal y lo configuré como servidor:

# /etc/consul.d/server.hcl
server = true
ui = true
bootstrap_expect = 1
client_addr = "0.0.0.0"
bind_addr = "192.168.x.x"
data_dir = "/var/lib/consul"
encrypt = "tu_clave_encriptacion_aqui"

En los hosts secundarios, configuré Consul como cliente:

# /etc/consul.d/client.hcl
server = false
client_addr = "0.0.0.0"
bind_addr = "192.168.x.x"
retry_join = ["192.168.x.x"]
data_dir = "/var/lib/consul"
encrypt = "tu_clave_encriptacion_aqui"

Paso 2: Almacenar secretos en Consul
#

Usé curl para cargar los certificados:

#!/bin/bash
# guardar-secretos.sh

CERT=$(cat /etc/ssl/certs/mi-dominio.crt | base64 -w0)
KEY=$(cat /etc/ssl/private/mi-dominio.key | base64 -w0)

consul kv put secrets/ssl/cert "$CERT"
consul kv put secrets/ssl/key "$KEY"
consul kv put secrets/db/password "tu_password_aqui"

Paso 3: Sincronización con systemd
#

Creé un servicio systemd que vigila y sincroniza cambios:

# /etc/systemd/system/consul-sync-certs.service
[Unit]
Description=Sincronizar certificados desde Consul
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/consul-sync-watcher.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

El script watcher:

#!/bin/bash
# /usr/local/bin/consul-sync-watcher.sh

CERT_PATH="/etc/ssl/certs/mi-dominio.crt"
KEY_PATH="/etc/ssl/private/mi-dominio.key"
CERT_HASH_FILE="/tmp/cert.hash"

while true; do
    # Obtener certificado desde Consul
    CERT=$(consul kv get secrets/ssl/cert | base64 -d)
    KEY=$(consul kv get secrets/ssl/key | base64 -d)
    
    # Calcular hash
    CURRENT_HASH=$(echo "$CERT" | sha256sum | awk '{print $1}')
    STORED_HASH=$(cat $CERT_HASH_FILE 2>/dev/null || echo "")
    
    # Si cambió, actualizar y recargar servicios
    if [ "$CURRENT_HASH" != "$STORED_HASH" ]; then
        echo "$CERT" > "$CERT_PATH"
        echo "$KEY" > "$KEY_PATH"
        chmod 600 "$KEY_PATH"
        
        echo "$CURRENT_HASH" > "$CERT_HASH_FILE"
        
        # Recargar servicios sin downtime
        systemctl reload nginx
        docker exec mi-app nginx -s reload 2>/dev/null || true
        
        logger -t consul-sync "Certificados actualizados y servicios recargados"
    fi
    
    sleep 60
done

Paso 4: Rotación sin downtime
#

Para Docker, aseguré health checks funcionales:

# docker-compose.yml
services:
  web:
    image: nginx:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 10s
      timeout: 5s
      retries: 3
    volumes:
      - /etc/ssl/certs/mi-dominio.crt:/etc/nginx/certs/cert.crt:ro
      - /etc/ssl/private/mi-dominio.key:/etc/nginx/certs/cert.key:ro

Cuando el script detecta cambios, ejecuta:

docker exec mi-app nginx -s reload

Nginx recarga la configuración sin cerrar conexiones existentes.

Paso 5: Automatizar renovación de Let’s Encrypt
#

Integré Certbot con el flujo:

# /etc/letsencrypt/renewal-hooks/deploy/sync-certs.sh
#!/bin/bash
DOMAIN=$RENEWED_LINEAGE
CERT_PATH="${RENEWED_LINEAGE}/fullchain.pem"
KEY_PATH="${RENEWED_LINEAGE}/privkey.pem"

CERT=$(cat $CERT_PATH | base64 -w0)
KEY=$(cat $KEY_PATH | base64 -w0)

consul kv put secrets/ssl/cert "$CERT"
consul kv put secrets/ssl/key "$KEY"

Resultado
#

Ahora cuando renuevo un certificado con Certbot, Consul se sincroniza automáticamente con todos los hosts en 60 segundos. Los servicios se recargan sin downtime ni intervención manual.

Monitoreo cambios en los logs de systemd:

journalctl -u consul-sync-certs.service -f

Este setup me ahorró horas de administración manual. La replicación automática de Consul garantiza consistencia incluso si un host falla temporalmente.