Skip to content

SOPS + age key rotation

Estado actual (verificado contra .sops.yaml 2026-08-01): 4 keypairs configurados como recipients — ramon (Mac), pmx-50, pmx-51, media-208 (VM 208). TODO en .sops.yaml: añadir pubkeys de LXC 280 (remote-pulse) y LXC 281 (wp-pulse) — sigue pendiente. Cada fichero de secrets/ se cifra a un conjunto de recipients distinto (key_groups + anclas YAML: p.ej. cloudflare = ramon+media-208; agents = solo ramon), no una lista global. No hay backup offline de los age keys — riesgo: si se pierde la key de Ramón, los secretos quedan inrecuperables. Mitigación pendiente: copia cifrada de las keys en KeePassXC personal.

hermesbot resuelto (2026-08-01)

LXC 101 (hermesbot) fue destruido el 2026-07-05 (ver memoria hermes_setup.md) y su age pubkey había quedado como recipient huérfano de secrets/agents.sops.yaml. Se quitó de .sops.yaml y se corrió sops updatekeys sobre el fichero — verificado que sigue descifrando bien con la key de ramon.

Custodia

Los archivos ~/.config/sops/age/keys.txt son el secreto raíz. Nunca commit. Backup manual offline (USB/papel) recomendado.

Generar un nuevo keypair age

# En la máquina destino (e.g. pmx-50 o un LXC)
age-keygen -o /root/.config/sops/age/keys.txt
chmod 600 /root/.config/sops/age/keys.txt
cat /root/.config/sops/age/keys.txt
# Apunta la línea "public key: age1xxxxxxxxxxxxxxxx"

Para la Mac de Ramón:

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt

Añadir a ~/.zshrc:

export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt"

Añadir un nuevo recipient a archivos existentes

1. Actualizar .sops.yaml

En la raíz del repo, edita .sops.yaml para añadir la nueva public key:

creation_rules:
  - path_regex: secrets/.*\.sops\.yaml$
    age: >-
      age1mac....,
      age1pmx50....,
      age1pmx51....,
      age1NEWKEY....    # <- añadir aquí

2. Re-encriptar archivos afectados

sops updatekeys re-aplica las reglas del .sops.yaml actual a cada archivo:

cd ~/docs-repo
sops updatekeys secrets/observability.sops.yaml
# repetir para cada secreto:
for f in secrets/*.sops.yaml; do sops updatekeys "$f"; done

3. Commit

git add .sops.yaml secrets/
git commit -m "sops: add <hostname> recipient to all secrets"
git push

Tras 15min (Ansible-pull) los nodos verán los nuevos archivos. El nuevo recipient podrá descifrar.

Rotación de un keypair comprometido

Si una clave privada se filtra (e.g. SSH leak, disco perdido sin LUKS):

1. Generar nueva keypair en el host afectado

ssh pmx-50 'mv /root/.config/sops/age/keys.txt /root/.config/sops/age/keys.txt.old'
ssh pmx-50 'age-keygen -o /root/.config/sops/age/keys.txt'
ssh pmx-50 'cat /root/.config/sops/age/keys.txt | grep "public key"'

2. Sustituir la public key en .sops.yaml

Reemplaza la línea vieja por la nueva. Commit + sops updatekeys igual que antes.

3. Re-cifrar archivos (quitando la vieja key)

updatekeys solo añade. Para quitar la vieja:

# Edición manual del header `sops.age` de cada archivo, o reencrypt entero:
for f in secrets/*.sops.yaml; do
  sops -d "$f" > /tmp/plain.yaml
  sops -e /tmp/plain.yaml > "$f"
  rm /tmp/plain.yaml
done

4. Rotar además los secretos que la key vieja podía leer

Una vez comprometida, asumir que todo lo descifrable por esa key está comprometido. Generar tokens nuevos (CF API, Telegram bot, HA long-lived, n8n webhook secret, etc.). Ver Rotating secrets.

Verificación

# Desde la Mac (con tu key)
sops -d secrets/observability.sops.yaml | head

# Desde un nodo (con su key)
ssh pmx-50 'cd /var/lib/ansible/homelab-infra && sops -d secrets/observability.sops.yaml | head'

Si falla con Failed to get the data key: 0 successful groups required, 0 succeeded, el host no tiene una key que sea recipient del archivo.

Backup offline

Recomendado (pendiente implementar):

  1. Imprimir keys.txt de la Mac en papel + meter en caja fuerte.
  2. Copia cifrada con passphrase fuerte (age -p) en un USB + cloud personal.
  3. Documentar el procedimiento de restauración (orden de re-generar nodos, etc.) en una nota offline.