Módulo 3: Secrets Management
2. El antipatrón: secretos en el repositorio
Descripción
cicd-and-gitops-on-aws-guide, Módulo 4, lección 2, ya demostró —con un experimento real, no con una afirmación— que un archivo "eliminado" de un commit de Git sigue siendo recuperable para siempre desde ese commit específico. Esta lección no repite ese experimento — lo retoma para llevarlo un paso más allá: no ya "qué pasa si un secreto entra a un commit", sino "por qué el propio diseño de .secrets —gitignoreado desde el primer commit, nunca expuesto en el historial— sigue sin ser aceptable el día que el destino deja de ser LocalStack y pasa a ser una cuenta AWS real".
Conexión con el módulo
La lección 1 ya adelantó la conclusión: .secrets es inofensivo contra LocalStack porque test/test no abre ninguna puerta real. Esta lección explica, con precisión técnica y no por intuición, las tres razones exactas por las que ese mismo diseño de archivo dejaría de ser aceptable en el momento en que las credenciales dentro de él dejaran de ser dummies. La lección 3 usa esas tres razones como el criterio de decisión entre SSM Parameter Store y Secrets Manager.
Analogía: la libreta del club, otra vez, pero con la caja fuerte de verdad
Vuelve a la libreta del club de cicd-and-gitops-on-aws-guide —el club que nunca arranca páginas, solo agrega—. Esa analogía explicó por qué "eliminar" una credencial de un commit no la elimina del historial. Esta lección agrega una segunda pieza a la misma escena: incluso si nunca llegaras a cometer ese error —incluso si .secrets estuviera perfectamente gitignoreado desde el primer commit, como de hecho lo está en Andes Cargo—, seguirías teniendo un archivo de texto plano, sobre un disco físico, sin ninguna de las tres propiedades que una caja fuerte de verdad tiene: nadie necesita ninguna combinación para leerlo (basta con acceso de lectura al filesystem), nadie deja registro de haberlo abierto, y cambiar lo que hay adentro no tiene ningún mecanismo asistido — es editar un archivo de texto y confiar en que todo lo que lo lee se entere del cambio a tiempo.
Ejemplo trabajado: el .secrets real de Andes Cargo, línea por línea
Este es, sin cambios, el archivo que cicd-and-gitops-on-aws-guide, Módulo 2, lección 7, creó en la raíz de andes-cargo-infra/:
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
Dos líneas, protegidas por una sola entrada de .gitignore (.secrets, agregada antes de que el archivo existiera — la práctica correcta que esa misma guía, Módulo 4, lección 2, distinguió de la incorrecta: gitignorar después del primer commit no protege nada retroactivamente). Contra LocalStack, funcionan exactamente como deberían: act --secret-file .secrets las inyecta como secrets.AWS_ACCESS_KEY_ID/secrets.AWS_SECRET_ACCESS_KEY en ci.yml, y LocalStack acepta cualquier valor no vacío sin validarlo contra ningún mecanismo real de autenticación.
Razón 1 — Ningún control de acceso propio
Un archivo en disco hereda los permisos del filesystem, nada más. Cualquier proceso, script, o persona con acceso de lectura a la carpeta del proyecto puede abrir .secrets con cat, sin que el archivo mismo imponga ninguna condición adicional. SSM Parameter Store y Secrets Manager, en cambio, evalúan una política IAM en cada lectura — ssm:GetParameter y secretsmanager:GetSecretValue son acciones que un rol necesita tener permitidas explícitamente, acotadas además al ARN exacto del secreto, exactamente el mismo principio de mínimo privilegio que el Módulo 2 aplicó a LambdaManifestProcessorRole y AppServerRole.
Razón 2 — Ningún registro de quién lo leyó, ni cuándo
Abrir .secrets con un editor de texto no genera ningún evento observable. Si mañana ese valor apareciera usado desde un lugar inesperado, no habría ningún registro que responder a la pregunta "¿quién lo leyó, y cuándo?". Una lectura de SSM Parameter Store o Secrets Manager, en cambio, es una llamada a la API de AWS — GetParameter, GetSecretValue — y cualquier llamada a la API de AWS es, por diseño, la clase de evento que CloudTrail registra (el Módulo 7 de esta guía retoma esto con precisión: un guardrail detectivo, registro después del hecho, frente a uno preventivo como el control de acceso de la Razón 1).
Razón 3 — Ningún mecanismo asistido de rotación
Cambiar el valor de .secrets significa: editar el archivo a mano, y confiar en que cada proceso que lo usa —cada desarrollador con una copia local, cada corrida de act— se entere del cambio y lo aplique antes de que el valor viejo deje de funcionar en el sistema externo. No existe ningún mecanismo que coordine ese cambio. Secrets Manager, en contraste, tiene rotación nativa —el tema exacto de la lección 6— justamente porque coordinar un cambio de credencial sin romper nada mientras se propaga es un problema real que un archivo de texto no resuelve, ni lo intenta.
Qué esperar (literal — las tres razones de arriba son propiedades de diseño de cada mecanismo, no dependen de ninguna ejecución; puedes verificarlas leyendo la documentación oficial de IAM policies, CloudTrail, y Secrets Manager rotation, citada al final de esta lección):
| Propiedad | .secrets (archivo plano) | SSM Parameter Store / Secrets Manager |
|---|---|---|
| Control de acceso | Ninguno propio (hereda del filesystem) | Política IAM evaluada en cada lectura |
| Auditoría de lectura | Ninguna | Cada GetParameter/GetSecretValue es una llamada a la API de AWS, registrable en CloudTrail |
| Rotación | Manual, sin coordinación | Secrets Manager: nativa (lección 6). SSM: manual, pero versionada |
| Cifrado en reposo | Ninguno (texto plano) | KMS, por defecto |
Contra LocalStack, ¿por qué .secrets sigue siendo aceptable?
Esta es la pregunta que la lección 1 ya adelantó, y vale la pena responderla con la misma honestidad explícita que usó cicd-and-gitops-on-aws-guide, Módulo 4, lección 7: test/test no es una credencial de AWS. Es un valor especial que LocalStack reconoce como "no valides nada, esto es un laboratorio local" — no existe ninguna cuenta AWS real donde esa cadena de texto abra una puerta. Ninguna de las tres razones de arriba importa cuando no hay nada real que proteger: no hay ningún acceso que controlar, ningún evento que valga la pena auditar, ninguna rotación que coordinar, porque el valor comprometido no otorga ninguna capacidad real en ningún sistema real.
El punto de esta lección no es que .secrets sea peligroso hoy. Es que el mismo diseño de archivo —texto plano, sobre disco, sin ninguna de las tres propiedades de arriba— sí sería inaceptable el día en que las dos líneas de ese archivo fueran, en cambio, credenciales reales de una cuenta AWS de producción, o —el caso que vas a construir en las lecciones 4 y 5— credenciales reales de un sistema externo como una API de despacho aduanero. La forma del error no depende de si el contenido es peligroso o no; el hábito correcto tiene que instalarse antes de trabajar con un contenido que sí importe, exactamente el mismo argumento que usó cicd-and-gitops-on-aws-guide al migrar ci.yml de valores hardcodeados a secrets.AWS_ACCESS_KEY_ID.
Errores comunes
Concluir que "gitignorado" es sinónimo de "seguro" (el error central de esta lección). Qué pasa: alguien ve que .secrets nunca aparece en el historial de Git —verificado, en cicd-and-gitops-on-aws-guide, con git log --all --full-history -- .secrets sin ninguna salida— y da por cerrado el tema de seguridad de ese archivo. Cómo detectarlo: si tu evaluación de qué tan seguro está un secreto se detiene en "¿está en .gitignore?". Cómo corregirlo: .gitignore resuelve exactamente un problema —que Git no lo rastree— y ninguno de los otros tres (control de acceso, auditoría, rotación). Un archivo perfectamente gitignoreado sigue siendo legible por cualquier proceso con acceso al filesystem, sin dejar rastro de quién lo leyó.
Asumir que esta lección dice "no uses LocalStack con credenciales dummy en archivos planos" (de alcance). Qué pasa: alguien, después de esta lección, quiere migrar el propio .secrets de LocalStack a SSM Parameter Store, generando exactamente el problema circular que la lección 1 ya descartó. Cómo detectarlo: si tu plan de acción después de esta lección incluye tocar .secrets de alguna forma. Cómo corregirlo: .secrets no cambia en este módulo — sigue siendo el mecanismo correcto para credenciales dummy de un laboratorio local. Lo que esta lección justifica es por qué el mismo diseño de archivo no sería aceptable con un contenido distinto — la aplicación práctica de esa lección llega en las lecciones 4 y 5, con un secreto que sí lo necesita.
Tratar la auditoría (Razón 2) como redundante si ya existe control de acceso (Razón 1) (de las tres propiedades). Qué pasa: alguien razona que, si ya nadie sin permiso puede leer un secreto, no importa si esa lectura queda registrada o no. Cómo detectarlo: si tu justificación para no habilitar CloudTrail sobre un secreto es "igual nadie no autorizado puede leerlo". Cómo corregirlo: el control de acceso responde "¿pudo pasar?"; la auditoría responde "¿pasó, y quién lo hizo?" — son la misma distinción preventivo/detectivo que el Módulo 7 de esta guía dedica un módulo entero a explicar. Un rol legítimo con acceso legítimo a un secreto puede, aun así, comportarse de forma anómala (leerlo con una frecuencia inusual, desde un contexto inesperado) — sin auditoría, esa señal simplemente no existe, sin importar cuán bien configurado esté el control de acceso.
Ejercicios
Ejercicio 1 — Aplica las tres razones a un caso nuevo. Un compañero de equipo, en otro proyecto, guarda la contraseña de una base de datos de producción en un archivo db-password.txt, gitignoreado desde el primer commit, en la raíz de su repositorio. Usando las tres razones de esta lección, explícale en qué se equivoca —incluso sin que el archivo esté nunca en el historial de Git.
Ver solución
Ninguna de las tres razones depende de si el archivo llegó a un commit. Razón 1: cualquier persona o proceso con acceso de lectura a esa carpeta —incluido, potencialmente, cualquier dependencia de terceros que el proyecto instale y que tenga permiso de leer archivos del disco— puede leer la contraseña sin ninguna condición adicional. Razón 2: si esa contraseña se usa de forma anómala en algún punto, no hay ningún registro de quién la leyó del archivo para poder investigar. Razón 3: cambiar la contraseña de la base de datos exige editar el archivo a mano y confiar en que todos los procesos que la usan se enteren a tiempo — sin ningún mecanismo que lo coordine. El hecho de que el archivo esté gitignoreado resuelve un problema real (que no termine en el historial de Git) pero ninguno de estos tres.
Ejercicio 2 — Defiende, con las tres razones, por qué .secrets sí es aceptable hoy. Un auditor de seguridad, sin contexto de esta guía, ve .secrets con AWS_ACCESS_KEY_ID=test y lo marca como un hallazgo crítico. Redacta, en dos o tres frases, la respuesta que justificaría por qué no lo es —sin negar ninguna de las tres razones de esta lección.
Ver solución
Una respuesta completa suena, más o menos, así: "Las tres razones son correctas en general, pero ninguna aplica aquí porque el contenido específico de este archivo no otorga ninguna capacidad real: test/test es un valor que solo LocalStack reconoce, y no existe ninguna cuenta AWS real donde esa cadena abra una puerta. El riesgo que las tres razones describen —lectura no controlada, sin auditoría, sin rotación coordinada— solo se materializa cuando el valor protegido tiene algún valor real que proteger. Este mismo archivo, con credenciales de una cuenta AWS de producción en vez de LocalStack, sí sería el hallazgo crítico que estás describiendo." La clave: no se descarta el hallazgo negando el principio general, se descarta mostrando que la condición que lo activaría —un secreto real— no está presente en este caso específico.
Ejercicio 3 — Predice cuál de las tres razones resuelve primero el Módulo 3. Basándote en el orden de las lecciones 4, 5 y 6 de este módulo, ¿cuál de las tres razones (control de acceso, auditoría, rotación) queda completamente resuelta con las lecciones 4 y 5, y cuál queda solo nombrada, sin ejecutarse de punta a punta?
Ver solución
Control de acceso (Razón 1) queda resuelto de verdad con las lecciones 4 y 5: declarar aws_ssm_parameter/aws_secretsmanager_secret y, en un proyecto real, la política IAM que acota quién puede leerlos, es exactamente construir ese control. Auditoría (Razón 2) queda parcialmente resuelta — cada llamada GetParameter/GetSecretValue es, por diseño, auditable, pero esta guía no construye el trail de CloudTrail que capturaría esos eventos hasta el Módulo 7, y de forma solo representativa. Rotación (Razón 3) es la que queda explícitamente sin ejecutar de punta a punta — la lección 6 la nombra con precisión, pero un ciclo completo de rotación automática no cabe en el alcance ejecutable de este módulo, con la razón exacta declarada en esa misma lección.
Resumen y siguiente paso
En esta lección profundizaste en el antipatrón que ya conocías de cicd-and-gitops-on-aws-guide: no solo que un secreto en Git es recuperable para siempre desde su commit, sino que incluso un archivo perfectamente gitignoreado —como el propio .secrets de Andes Cargo— carece de tres propiedades que un gestor de secretos gestionado sí tiene: control de acceso propio, auditoría de lectura, y un mecanismo asistido de rotación. Confirmaste, con la misma honestidad explícita que ya usó esta guía, por qué .secrets sigue siendo aceptable hoy —porque su contenido no protege nada real— y por qué esa misma forma de archivo dejaría de serlo con un contenido distinto.
Antes de avanzar deberías poder: nombrar las tres razones sin dudar, con un ejemplo concreto de cada una; explicar por qué "gitignorado" no es sinónimo de "seguro"; y defender, con evidencia técnica y no con una excepción vaga, por qué el .secrets actual de Andes Cargo no es un hallazgo de seguridad real.
La lección 3 convierte estas tres razones en un criterio de decisión concreto: cuándo un secreto de Andes Cargo va a SSM Parameter Store, y cuándo va a Secrets Manager.
Recursos
cicd-and-gitops-on-aws-guide, Módulo 4, lección 2 (02-why-credentials-never-belong-in-the-repo.md) — el experimento original de recuperación de un archivo "eliminado" desde el historial de Git, que esta lección retoma.- AWS Docs — Overview of managing access permissions — la base de la Razón 1: cómo IAM evalúa el acceso a un recurso en cada llamada.
- AWS Docs — Logging AWS Secrets Manager API calls with AWS CloudTrail — la base de la Razón 2, retomada de forma representativa en el Módulo 7 de esta guía.
- Este módulo, lección 6 — el desarrollo completo de la Razón 3, con el mecanismo real de rotación de Secrets Manager nombrado con precisión.