Secretos con Bitwarden

Ingenieria 19/03/2026

Uso Bitwarden para mis contraseñas, como todo el mundo, y también como almacén de los .env de cada proyecto y como fuente de secretos del pipeline de despliegue. Cero secretos en repositorios, rotación en un solo sitio, y un runner que los pide solo cuando despliega.

Categoría:

Ingenieria

Cliente:

Soft2Help (Internal)

Tecnologías:

Bash, Bitwarden CLI, jq, Docker, SSH, YAML

Resumen del proyecto

El problema

Los secretos de un proyecto (claves de API, contraseñas de base de datos, tokens) tienen la mala costumbre de acabar donde no deben: en un chat, en un README "temporal", en un pendrive, o directamente commiteados. Y cuando cambias una clave, empieza la caza: ¿en cuántas máquinas y proyectos estaba copiada?

La idea

Cada proyecto guarda sus ficheros .env como adjuntos cifrados de un elemento de Bitwarden. El proyecto solo conserva en local un fichero .bwitem (ignorado por git) con el ID de su elemento. El repositorio no ve un secreto jamás.

El día a día: dos comandos

# Subir los .env del proyecto actual a su elemento de Bitwarden
bw_push_secrets production

# Recuperarlos en una máquina nueva
bw_get_secrets production

El truco que los hace cómodos: si no pasas nombre de elemento, usan el nombre de la carpeta del proyecto. Entrar en el directorio y teclear el comando, sin recordar nada:

bw_push_secrets() {
  local ENVIRONMENT="${1:-development}"
  local ITEM_NAME="${2:-$(basename "$PWD")}"
  _bw_run push_secrets "$ENVIRONMENT" "$ITEM_NAME"
}

Al subir, el adjunto anterior se reemplaza (Bitwarden no sobreescribe adjuntos, así que hay que borrar el viejo y crear el nuevo):

OLD_ID=$(bw get item "$ITEM_ID" | \
  jq -r --arg file "$FILE" '.attachments // [] | .[] | select(.fileName == $file) | .id')
if [ -n "$OLD_ID" ]; then
  bw delete attachment "$OLD_ID" --itemid "$ITEM_ID" --quiet
fi
bw create attachment --file "$FILE" --itemid "$ITEM_ID" --quiet

Montar el entorno en una máquina nueva son dos comandos, no una tarde buscando claves por chats antiguos. Y rotar una clave se hace en un sitio: el siguiente bw_get_secrets (o el siguiente despliegue) ya la lleva.

En el pipeline de despliegue

La misma idea, sin humanos: el runner de CI recupera el .env.production de Bitwarden en el momento de desplegar, dentro de un contenedor efímero. Ni el runner ni el repositorio guardan secretos de forma persistente:

docker run --rm -i \
  -e BW_CLIENTID -e BW_CLIENTSECRET -e BW_PASSWORD \
  -v "$PWD:/work" -w /work \
  s2h-bw-cli <<'SCRIPT'
bw login --apikey
export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw)
bw sync
bw get attachment ".env.production" --itemid "$BW_ITEM" --output .env.production
SCRIPT

Lo aprendido a base de fallos

Bitwarden limita los logins con API key muy seguidos: dos despliegues en la misma tarde y bw unlock empieza a devolver sesiones vacías de forma intermitente. La solución que quedó en el pipeline, reintentos con espera creciente:

for i in 1 2 3 4; do
  bw logout >/dev/null 2>&1 || true
  bw login --apikey >/dev/null 2>&1 || true
  BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null || true)
  [ -n "$BW_SESSION" ] && break
  sleep $((i*15))
done

Detalle que también muerde: cada push crea el elemento si no existe. Práctico, hasta que un despiste te crea un elemento duplicado y el pipeline sigue leyendo el antiguo. Desde entonces, el .bwitem manda y se versiona el ID en la documentación interna del proyecto.

El resultado

  • Cero secretos en repositorios. Ni "de momento", ni "es solo el de pruebas". Cero.
  • Onboarding de máquina nueva en dos comandos.
  • Rotación de claves en un único sitio.
  • El pipeline pide los secretos al desplegar y no los conserva.
El resto de mi entorno (terminal, aliases, WSL) lo cuento en mi setup de desarrollo.