Saca tus secretos del .zshrc con 1Password CLI
Mantengo mi configuración de shell en un repo de dotfiles para poder rastrear
cambios en git. Pero algunas API keys necesitaban estar disponibles al inicio de
la shell, y hardcodearlas en .zshrc no era una opción. Así que las cargué
desde un archivo .env separado — uno que se mantenía fuera del repo.
Los secretos seguían en texto plano, solo que en un archivo diferente. Un
git add equivocado y estarían en el historial. Entonces pensé: 1Password
podría encargarse de esto.
Moví las claves allí y ahora las cargo al inicio de la shell con op inject —
una llamada, desbloqueo biométrico, sin archivos en texto plano por ningún
sitio.
El problema con los secretos en .env y .zshrc
La mayoría de desarrolladores almacenan secretos de una de estas dos formas: un
archivo .env o sentencias export dispersas por las configuraciones de la
shell. De cualquier manera, los valores están en texto plano y terminan en
copias de seguridad, repos de dotfiles o en el historial de la shell.
Escribí sobre almacenamiento seguro de credenciales para Node.js con cross-keychain. Las variables de entorno de la shell son un problema diferente — se cargan antes de que se ejecute cualquier aplicación, y cada proceso en tu terminal necesita acceso a ellas.
1Password CLI (op) resuelve esto a nivel de shell.
Crea una bóveda y habilita el desbloqueo biométrico
Instala primero la 1Password CLI, luego configura dos cosas antes de tocar tu configuración de shell.
Una bóveda para los secretos. Creé una bóveda llamada development con
múltiples entradas para cada servicio. Cada secreto es el campo de credenciales
para ese servicio. Esto te da URIs limpias tipo
op://development/<NOMBRE_SERVICIO>/credentials. Puedes organizarlo como
quieras, pero recomiendo una estructura consistente para facilitar la
referencia.
Desbloqueo biométrico. Sin esto, op te pide tu contraseña maestra cada vez
que abres una terminal. Habilítalo en la aplicación de escritorio de 1Password
bajo Ajustes → Desarrollador → "Integrar con 1Password CLI". Después de eso,
op autentica vía Touch ID o el keychain de tu sistema.
Divide tu shell en dos archivos
Con la bóveda lista, reemplaza tu archivo .env con dos archivos de shell. Uno
para configuración no secreta que zsh carga (source) automáticamente. Otro para
secretos que op inject procesa antes de cargar.
.zshenv — configuración plana, sin secretos
export EDITOR="nvim"export LANG="en_US.UTF-8"export PATH="$HOME/.local/bin:$PATH"export NODE_ENV="development"
Zsh carga este archivo automáticamente en cada invocación de la shell — interactiva, no interactiva, scripts, comandos SSH. Aquí no hay secretos, nunca.
.zshsecrets — referencias a secretos, no valores
export GITHUB_TOKEN="{{ op://development/github/credentials }}"export ANTHROPIC_API_KEY="{{ op://development/anthropic/credentials }}"export NPM_TOKEN="{{ op://development/npm/credentials }}"
Es seguro hacer commit a tu repo de dotfiles. Cualquiera que lo lea ve URIs
op://, no credenciales.
.zshrc — conéctalo todo
if command -v op &>/dev/null; theneval "$(op inject -i ~/.zshsecrets 2>/dev/null)" || echo "⚠ op: secrets not loaded"fi
op inject lee la plantilla, resuelve cada referencia {{ op://... }} contra
tu bóveda de 1Password, y saca el resultado con valores reales. eval ejecuta
los exports. Si op no está autenticado o no está disponible, la shell arranca
igual — simplemente no tendrás los secretos cargados hasta que ejecutes
op signin y vuelvas a cargar (re-source).
Por qué op inject en lugar de op read
Dos formas de obtener secretos de op. Lecturas por variable:
export GITHUB_TOKEN="$(op read 'op://development/github/credentials' 2>/dev/null)"
O inyección de plantillas, que es lo que uso yo. La diferencia es el
rendimiento. Cada op read genera un subproceso y hace una llamada a la API.
Con más de 5 secretos, el inicio de la shell crece 1–2 segundos. op inject
resuelve todo en una sola llamada — alrededor de 200–400ms con desbloqueo
biométrico habilitado.
La limpieza que no puedes saltarte
Después de migrar, hice tres cosas:
Eliminé cada secreto hardcodeado de mis archivos de shell. Un grep rápido
encuentra los rezagados:
grep -rn 'export.*KEY\|export.*TOKEN\|export.*SECRET' ~/.zsh*
Limpié el historial de git de mis dotfiles. Si alguna vez hiciste commit de secretos — incluso si los borraste luego — todavía están en el historial. git filter-repo o BFG Repo-Cleaner pueden purgarlos.
Roté cada token que había estado en texto plano. Incluso si tus secretos nunca tocaron un repo git, pueden haber vivido en copias de Time Machine o en el historial de la shell. Si no estás seguro de tu exposición total, rótalos. No es opcional.
Compromisos
Latencia de inicio. La llamada a op inject añade 200–400ms al inicio de la
shell. Sin desbloqueo biométrico, op pide tu contraseña maestra — doloroso si
abres terminales frecuentemente. Incluso habilitado, la petición de Touch ID
interrumpe el flujo al abrir terminales en sucesión rápida.
Brechas de autenticación. Si op no está autenticado, tus secretos no
cargarán. Te darás cuenta cuando un comando falle, entonces ejecuta op signin
y source ~/.zshrc. Es una fricción menor y un intercambio razonable por no
tener secretos en texto plano.
Sin soporte remoto. Las sesiones SSH a máquinas remotas no tendrán op. Si
necesitas secretos allí, necesitarás un mecanismo diferente — cuentas de
servicio de op, o reenviar variables específicas a través de la configuración
SSH.
Un repo de dotfiles que puedes hacer público
Mi repo de dotfiles es ahora seguro para el público. Cada secreto vive en
1Password, un archivo de plantilla con URIs op:// los carga al inicio, y Touch
ID maneja el resto.
Si necesitas secretos en máquinas remotas, la documentación de 1Password CLI cubre cuentas de servicio y reenvío de agente SSH.