Saltar a contenido

Vaultwarden — bootstrap y gestión de secretos

Procedimientos para poner en marcha Vaultwarden y gestionar los secretos del cluster a través de VKS.

Bootstrap inicial

Antes de exponer el servicio, se accede por port-forward:

kubectl -n vaultwarden port-forward svc/vaultwarden 8080:80

1. Crear el usuario admin

En http://localhost:8080, crear la cuenta gorkarevilla@gmail.com con una master password fuerte. Será el owner de la organización.

2. Verificar el panel /admin

En http://localhost:8080/admin, entrar con el ADMIN_TOKEN (SealedSecret vaultwarden-admin-token; el valor en claro está en el .env local como VAULTWARDEN_ADMIN_TOKEN).

3. Crear la organización "Familia"

En la web vault del usuario admin: crear organización Familia.

4. Crear las colecciones

Dentro de Familia, crear 2 colecciones: Familia (compartida) y Homelab.

Las cajas fuertes personales de Gorka y Miren ya sirven como "secretos de Gorka" y "secretos de Miren"; no necesitan colección propia.

5. Invitar a Miren

Invitar a miren (email de Miren) con acceso a la colección Familia. La invitación sale por el SMTP configurado.

6. Crear el usuario de servicio vaultwarden-sync

Crear un usuario de servicio con email gorkarevilla+vaultwarden-sync@gmail.com (alias de Gmail: así el correo de verificación/invitación llega a tu inbox) y master password generada (guardarla en el .env como VKS_MASTER_PASSWORD). Invitarlo a Familia con acceso solo lectura a Homelab.

La colección debe llamarse exactamente Homelab (VKS está configurado con VAULTWARDEN__COLLECTIONNAME=Homelab).

7. Generar la API key de vaultwarden-sync

En la cuenta de vaultwarden-sync (Settings → Security → Keys → API key), generar la API key. Guardar en el .env:

VKS_BW_CLIENTID=<client_id>
VKS_BW_CLIENTSECRET=<client_secret>

La API key debe ser de usuario, no de organización (la API de Vaultwarden no soporta las de organización).

8. Sellar las credenciales de VKS

kubectl create secret generic vaultwarden-kubernetes-secrets \
  --namespace vaultwarden-kubernetes-secrets \
  --from-literal=BW_CLIENTID="$VKS_BW_CLIENTID" \
  --from-literal=BW_CLIENTSECRET="$VKS_BW_CLIENTSECRET" \
  --from-literal=VAULTWARDEN__MASTERPASSWORD="$VKS_MASTER_PASSWORD" \
  --dry-run=client -o yaml \
  | kubeseal --controller-name sealed-secrets \
      --controller-namespace kube-system --format yaml \
  > clusters/luka/config/vaultwarden-kubernetes-secrets/sync-creds-sealed.yaml

9. Desactivar el registro abierto

Una vez creados el admin y los usuarios (y antes de exponer externamente), desactivar el registro abierto: cambiar SIGNUPS_ALLOWED a "false" en clusters/luka/config/vaultwarden/deployment.yaml (commit separado). Durante el bootstrap estaba "true" para poder crear la primera cuenta por la web (solo accesible vía port-forward, no expuesta).

Añadir un secreto de cluster

Un secreto que debe llegar a k8s es un ítem de Vaultwarden en la colección Homelab con el campo custom namespaces (separado por comas si son varios):

  • Login → produce un Secret con claves username y password (o las que indiques con secret-key-username / secret-key-password).
  • Secure Note con YAML válido en notes → se aplica tal cual (útil para claves arbitrarias como pgpass).

Campos custom útiles: namespaces, secret-name, secret-type, secret-key-username, secret-key-password, secret-annotation, secret-label.

VKS sincroniza cada 5 minutos (o al reiniciar el pod para forzar).

Ejemplo

Ítem Login test password con campo namespaces=default → crea el Secret test-password (el nombre sale del nombre del ítem, espacios → -) en el namespace default, con claves username y password.

Gotchas

  • El campo custom se llama namespaces (plural). namespace (singular) no se reconoce y el ítem se ignora.
  • El nombre del Secret se sanea desde el nombre del ítem (espacios y caracteres especiales → -). Para un nombre exacto, usa el campo secret-name.
  • Verificar: kubectl get secret <nombre> -n <namespace>.

Importar claves en bloque (Bitwarden JSON)

Para pasar varias claves de una vez (p. ej. al migrar), mejor que crear ítems a mano: se genera un fichero JSON de importación de Bitwarden con los valores del .env y se importa en la colección Homelab.

El fichero es vaultwarden-import.json (gitignored; contiene secretos).

Formato de cada ítem

{
  "type": 1,
  "name": "cloudflare-api-token",
  "login": { "username": null, "password": "<valor>" },
  "fields": [
    { "name": "namespaces", "value": "cert-manager", "type": 0 },
    { "name": "secret-name", "value": "cloudflare-api-token", "type": 0 },
    { "name": "secret-key-password", "value": "api-token", "type": 0 }
  ]
}
  • Ítems de cluster (se sincronizan): type: 1 (Login) con los campos custom namespaces, secret-name, secret-key-password/secret-key-username (tipo 0 = text).
  • Ítems de referencia (CI/Terraform, no se sincronizan): mismo formato sin namespaces → VKS los ignora. Sirven para tenerlos a mano en Vaultwarden.

Importar

  1. Org Familia → Tools → Import data.
  2. Formato Bitwarden (json).
  3. Destino: colección Homelab.
  4. Subir vaultwarden-import.json.

⚠️ Importar en la colección Homelab (no en la caja fuerte personal), si no VKS no ve los ítems.

Generar el fichero

Un script Python lee el .env y escribe el JSON (patrón repetible):

set -a; source .env; set +a
python3 - <<'EOF'
import json, os
def login(u=None, p=None, uri=None):
    return {"username": u, "password": p, "uris": [{"uri": uri}] if uri else [], "totp": None}
def f(n, v): return {"name": n, "value": v, "type": 0}
items = [
    {"type": 1, "name": "cloudflare-api-token",
     "login": login(password=os.environ["CERT_MANAGER_CLOUDFLARE_TOKEN"]),
     "fields": [f("namespaces", "cert-manager"), f("secret-name", "cloudflare-api-token"), f("secret-key-password", "api-token")]},
]
json.dump({"encrypted": False, "folders": [], "items": items}, open("vaultwarden-import.json", "w"), indent=2, ensure_ascii=False)
EOF

Migrar un servicio del legacy al cluster (corte/rollback)

El corte de un servicio expuesto (p. ej. heimdall) es un flip de CNAME:

  1. Añadir el hostname al túnel del cluster (Terraform) y su HTTPRoute en Traefik.
  2. Cambiar el CNAME de heimdall.gorkarevilla.com de hometunnel.gorkarevilla.com a homelabtunnel.gorkarevilla.com.
  3. Rollback: volver a apuntar el CNAME a hometunnel.gorkarevilla.com.

Con Cloudflare proxied (TTL automático) el cambio es casi instantáneo.