Módulo 3: Secrets Management

1. Introducción: quién eres (Módulo 2) frente a qué sabes (Módulo 3)

Descripción

El Módulo 2 construyó identidad: un proveedor OIDC real, una trust policy de mínimo privilegio, y los dos roles de Andes Cargo —LambdaManifestProcessorRole, AppServerRole— recortados a los permisos exactos que su código usa. Todo eso responde una sola pregunta: ¿quién eres frente a AWS? Este módulo responde una pregunta distinta, que ninguna cantidad de identidad bien construida contesta por sí sola: ¿qué sabes que AWS no puede validar por ti?

Un rol IAM, por perfecto que sea, nunca es la contraseña de un sistema externo. LambdaManifestProcessorRole puede tener exactamente los permisos que necesita sobre S3 y DynamoDB —eso ya lo resolvió el Módulo 2— y, aun así, el día que Andes Cargo necesite que su Lambda hable con un sistema que no es de AWS, va a necesitar guardar en algún lugar una credencial que AWS no emite ni controla: la clave de un socio externo, el usuario y contraseña de una API de terceros, la firma de un webhook. Ese "algún lugar" es exactamente el tema de este módulo.

Conexión con el módulo

THREAT-MODEL.md (Módulo 1, lección 7) ya identificó el riesgo concreto que este módulo resuelve: TM-05, Information disclosure, con evidencia exacta —.secrets, el archivo que cicd-and-gitops-on-aws-guide creó en su Módulo 2, lección 7, para que act pudiera simular las credenciales de GitHub Actions localmente contra LocalStack—. RISK-MAP.md (Módulo 1, lección 8) ubica ese riesgo en tercer lugar de la secuencia, justo después de identidad (M2): "because a properly scoped identity is a prerequisite for secrets that are readable only by the identities that need them". Esa frase es literal, y vale la pena leerla dos veces: sin los roles de mínimo privilegio que dejó el Módulo 2, no tendría sentido construir un almacén de secretos con permisos de lectura acotados —¿acotados a quién, si todavía no sabías con precisión qué podía hacer cada identidad?—.

Las lecciones 2 y 3 dan el porqué y el criterio de decisión. Las lecciones 4 y 5 son manos a la obra reales, una por cada servicio gestionado de AWS que resuelve este riesgo. La lección 6 nombra, con precisión técnica, un mecanismo que este módulo no ejecuta de punta a punta —y explica exactamente por qué—. La lección 7 corre una herramienta de escaneo real contra un secreto insertado a propósito. La lección 8 cierra el módulo migrando el antipatrón heredado a la solución construida en las lecciones anteriores.


Analogía: la llave bajo el tapete, fotografiada y publicada

Guardar una credencial en un archivo de texto plano dentro de un repositorio es, literalmente, dejar la llave de tu casa bajo el tapete de la entrada —y después tomarle una foto al tapete y subirla a una red social pública—. La llave funciona igual de bien que si estuviera guardada en una caja fuerte: abre la puerta, nadie lo niega. El problema nunca es si la llave funciona. El problema es que cualquiera que sepa dónde mirar —o que tropiece con la foto sin siquiera buscarla— puede copiarla sin que tú te enteres, y usarla exactamente igual que tú.

Un gestor de secretos gestionado —SSM Parameter Store, Secrets Manager, el tema de este módulo— es la caja fuerte instalada en la pared: la llave sigue abriendo la misma puerta, pero ahora hace falta la combinación para llegar a ella, cada apertura queda registrada con quién la abrió y cuándo, y —en el caso de Secrets Manager— existe un mecanismo nativo para cambiar la combinación sin tener que reinstalar la caja fuerte entera. Ninguna de esas tres propiedades —control de acceso, auditoría, rotación— existe en un archivo .txt en un disco, sin importar cuán bien intencionado sea quien lo escribió.


Qué es un secreto, en el sentido preciso de este módulo

No todo dato que "no quieres que se vea" es un secreto en el sentido que usa este módulo. La región us-east-1 de Andes Cargo no es un secreto —aparece en texto plano en providers.tf desde terraform-and-iac-guide, y está bien que así sea—. Un secreto, para efectos de SSM Parameter Store y Secrets Manager, es un valor cuya exposición representa un riesgo real y verificable: alguien que lo obtiene puede hacer algo que no debería poder hacer. La lección 3 formaliza este criterio con detalle; por ahora, quédate con la prueba rápida que vas a usar en toda esta guía: si reemplazar el valor por REDACTED en un documento público no cambia nada sobre lo que alguien podría hacer, no es un secreto — es configuración.

Con esa prueba, tres candidatos concretos de Andes Cargo, dos de los cuales sí clasifican:

Valor¿Es un secreto?Por qué
AWS_DEFAULT_REGION=us-east-1NoPublicarlo no le da a nadie ninguna capacidad nueva
Firma HMAC de un webhook de un socio de aduanasQuien la conoce puede falsificar notificaciones que Andes Cargo tratará como legítimas
Usuario/contraseña de una API externa de aduanasQuien los conoce puede autenticarse como Andes Cargo frente a ese sistema

Los dos últimos son el hilo concreto de este módulo: Andes Cargo, para procesar manifiestos de envíos internacionales (recuerda los tres envíos de ejemplo: 4471 Peru→Chile, 4472 Colombia→Ecuador, 4473 Chile→Peru — cruces de frontera, todos), va a necesitar en algún momento integrarse con la API de un socio de despacho aduanero. Esta guía no construye esa integración —eso es lógica de negocio, fuera del alcance de una guía de seguridad—, pero sí resuelve, de punta a punta, dónde vivirían las dos credenciales que esa integración necesitaría: una firma de webhook (lección 4) y un par usuario/contraseña (lección 5). Es exactamente el mismo tipo de decisión que enfrentarías el día que esa integración sí se construya, practicada ahora, sobre un caso concreto, no abstracto.


Por qué el .secrets de .gitignore no migra directamente aquí

Si ya hiciste cicd-and-gitops-on-aws-guide, la lección 2 de este módulo te va a resultar familiar: retoma exactamente el mismo archivo .secrets que esa guía usó, con AWS_ACCESS_KEY_ID=test y AWS_SECRET_ACCESS_KEY=test. Vale la pena resolver, antes de seguir, una confusión razonable: ¿por qué esas dos líneas exactas no terminan, literalmente, dentro de un aws_ssm_parameter en este módulo?

Porque sería circular. .secrets guarda la credencial que el pipeline usa para autenticarse contra AWS — y SSM Parameter Store y Secrets Manager son, ellos mismos, servicios de AWS que necesitan una identidad ya autenticada para poder leerlos. Guardar la credencial de AWS dentro de un servicio de AWS que necesita esa misma credencial para abrirse es un candado que necesita su propia llave para poder guardar la llave. El Módulo 2 ya resolvió ese problema específico —de una forma completamente distinta, y mejor—: OIDC elimina la necesidad de que el pipeline tenga, en absoluto, una credencial de larga vida que guardar en cualquier lado, gestionado o no.

Lo que sí migra de .secrets a este módulo no es el contenido —dos líneas de credenciales dummy de LocalStack—, es el patrón que ese archivo representa: un secreto en texto plano, sobre disco, sin control de acceso propio, gitignoreado pero recuperable con git show si alguna vez se hubiera commiteado por error (exactamente lo que cicd-and-gitops-on-aws-guide, Módulo 4, lección 2, demostró con un experimento real). Este módulo corrige ese patrón con el tipo de secreto al que sí pertenece: uno que un rol de AWS, ya autenticado por identidad propia (gracias al Módulo 2), necesita leer para hablar con un sistema que AWS no controla.


El mapa de este módulo: las 8 lecciones

   GESTIÓN DE SECRETOS — LAS 8 LECCIONES DE ESTE MÓDULO

   3.1  Introducción (esta)                    identidad (M2) vs. secreto (M3)
   3.2  El antipatrón, retomado a fondo          por qué .secrets nunca sería aceptable contra AWS real
   3.3  SSM Parameter Store vs. Secrets Manager  criterio de decisión real
   3.4  Manos a la obra: SSM Parameter Store     aws_ssm_parameter (SecureString), EJECUTADO
   3.5  Manos a la obra: Secrets Manager         aws_secretsmanager_secret(_version), EJECUTADO
   3.6  Rotación de secretos                     AWSCURRENT/AWSPENDING/AWSPREVIOUS, REPRESENTATIVO (nombrado)
   3.7  Manos a la obra: escaneando por fugas    trivy fs --scanners secret, EJECUTADO
   3.8  Proyecto: inventario de secretos          secrets.tf completo, EJECUTADO
#LecciónQué practicas
1Introducción (esta)Identidad frente a secreto; por qué .secrets no migra literalmente; el caso de la API de aduanas
2El antipatrón, retomado a fondoPor qué .secrets nunca sería aceptable contra una cuenta AWS real, aunque contra LocalStack sea inofensivo
3SSM Parameter Store vs. Secrets ManagerConfiguración vs. credencial, rotación nativa vs. manual, costo — el criterio real, no una preferencia
4Manos a la obra: SSM Parameter StoreEjecutado: aws_ssm_parameter tipo SecureString, awslocal ssm get-parameter --with-decryption
5Manos a la obra: Secrets ManagerEjecutado: aws_secretsmanager_secret + aws_secretsmanager_secret_version, awslocal secretsmanager get-secret-value
6Rotación de secretos, el mecanismo realEl ciclo AWSPREVIOUS/AWSCURRENT/AWSPENDING, nombrado con precisión — representativo, con la razón exacta
7Manos a la obra: escaneando por fugasEjecutado: trivy fs --scanners secret ., con un hallazgo real, insertado a propósito y detectado
8Proyecto: el inventario de secretos de Andes CargoEjecutado: secrets.tf completo, TM-05 cerrado, cero secretos en texto plano verificado

Honestidad de ejecución de este módulo

Este módulo sostiene, con herramientas $0 reales, más de lo que vas a poder verificar contra una cuenta AWS de verdad — y la línea exacta entre una cosa y otra importa más aquí que en casi cualquier otro módulo de esta guía.

Lo que SÍ corre de verdad, sin ningún token ni cuenta de pago:

  • tflocal validate y tflocal plan sobre el secrets.tf nuevo de las lecciones 4, 5 y 8 — ninguno de los dos necesita que LocalStack esté respondiendo: son operaciones que trabajan sobre el HCL y el provider ya configurado, sin ninguna llamada de red real, exactamente la misma razón técnica que ya viste en cicd-and-gitops-on-aws-guide, Módulo 4, lección 7 ("genera un plan de creación no necesita ninguna llamada de red").
  • trivy fs --scanners secret, el motor completo de la lección 7 — corre 100% local, sin cuenta, sin conexión a AWS ni a LocalStack. Es la única herramienta de todo este módulo que no depende, en ningún punto, de si LocalStack está arriba o no.

Lo que queda representativo, con su razón exacta:

  • tflocal apply y cualquier comando awslocal (ssm get-parameter, secretsmanager get-secret-value) — este entorno de escritura no tiene un LOCALSTACK_AUTH_TOKEN exportado, el mismo límite exacto que ya viste en el Módulo 1, lección 4, y en cicd-and-gitops-on-aws-guide, Módulo 4, lección 7: sin ese token, el contenedor de LocalStack no arranca, y cualquier comando que necesite hablar con él falla con Could not connect to the endpoint URL. Cada bloque afectado lo marca en el momento exacto en que aparece.
  • El ciclo completo de rotación automática (lección 6) — la razón aquí no es LocalStack: es que encadenar una rotación real de punta a punta necesita una Lambda de rotación desplegada y un ciclo de tiempo (minutos u horas, según el schedule) que no cabe en una lección escrita. La lección 6 explica el mecanismo con precisión — es exactamente lo que Secrets Manager hace en producción — sin fingir que lo ejecutó.

Errores comunes

Pensar que este módulo repite el trabajo del Módulo 2 (de alcance). Qué pasa: alguien, al ver que ambos módulos hablan de "credenciales" y de .secrets, asume que el Módulo 3 es una segunda pasada sobre el mismo problema que ya resolvió OIDC. Cómo detectarlo: si no puedes explicar, sin dudar, qué tipo de credencial resuelve cada módulo. Cómo corregirlo: el Módulo 2 resuelve la credencial de la propia identidad frente a AWS —y la resuelve eliminándola, con OIDC, no guardándola mejor—. Este módulo resuelve la credencial de un sistema que AWS no controla —y la resuelve guardándola en el lugar correcto, porque a diferencia de una credencial de AWS, no hay ningún truco de identidad federada que la elimine: el sistema de aduanas exige su propio usuario y contraseña, sin importar cuán bien configurado esté IAM.

Buscar dónde quedó declarada la integración con la API de aduanas (de expectativa). Qué pasa: alguien termina este módulo buscando un lambda.tf actualizado, o un cliente HTTP nuevo dentro del handler de process-shipment-manifest, y no lo encuentra. Cómo detectarlo: si tu checklist mental de "qué construyó este módulo" incluye cualquier lógica de negocio nueva. Cómo corregirlo: esta guía, en su diseño completo, nunca declara un recurso de negocio nuevo — la integración con aduanas es, deliberadamente, un ejemplo con el que razonar sobre dónde vivirían sus credenciales, no una función que este módulo construye. Lo que este módulo entrega es exclusivamente la capa de secretos: secrets.tf.

Asumir que .secrets desaparece del proyecto en este módulo (de continuidad, con el Módulo 2). Qué pasa: alguien, después de leer que este módulo "reemplaza .secrets", espera que el archivo deje de existir en el repositorio al cerrar este módulo. Cómo detectarlo: si buscas, al final de la lección 8, un git rm .secrets en algún paso. Cómo corregirlo: .secrets sigue existiendo como el mecanismo local de act --secret-file para correr el pipeline sin conexión real a GitHub — eso no cambia en esta guía. Lo que "reemplaza" este módulo es el patrón que .secrets representaba (secreto en texto plano, sin control de acceso), aplicado al tipo de secreto correcto: uno externo a AWS, gestionado en secrets.tf.


Ejercicios

Ejercicio 1 — Clasifica cuatro valores de Andes Cargo con la prueba del REDACTED. Usando la prueba de la sección "Qué es un secreto" de esta lección, clasifica cada uno de estos cuatro valores como secreto o configuración, y justifica en una frase: (a) AWS_ENDPOINT_URL=http://host.docker.internal:4566; (b) la contraseña de la API de aduanas; (c) el nombre del bucket andes-cargo-shipment-docs; (d) la firma HMAC del webhook de aduanas.

Ver solución

(a) Configuración — publicar el endpoint de LocalStack no le da a nadie ninguna capacidad que no tuviera ya (cualquiera puede apuntar su propio LocalStack ahí). (b) Secreto — quien la conoce puede autenticarse como Andes Cargo frente al sistema de aduanas. (c) Configuración — el nombre del bucket ya aparece en el propio HCL público de terraform-and-iac-guide; conocerlo no otorga ningún acceso, el control de acceso real vive en la política del bucket y en IAM. (d) Secreto — quien la conoce puede falsificar un webhook que Andes Cargo trataría como legítimo. La prueba del REDACTED es la misma para los cuatro: ¿cambia algo sobre lo que alguien puede hacer si el valor se vuelve público?

Ejercicio 2 — Explica, sin usar la palabra "OIDC", por qué .secrets no puede migrar literalmente a secrets.tf. Un compañero pregunta por qué este módulo no simplemente declara un aws_ssm_parameter con AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY como valor. Respóndele sin nombrar OIDC ni el Módulo 2 —explica el problema en sus propios términos, de primeros principios.

Ver solución

Una respuesta completa suena, más o menos, así: "Para leer un parámetro de SSM Parameter Store necesitas, primero, estar autenticado frente a AWS — necesitas ya tener una credencial válida. Si esa credencial de AWS es, precisamente, lo que guardaste dentro del parámetro, tienes un problema circular: para abrir la caja fuerte necesitas la llave que está adentro de la caja fuerte. Cualquier credencial que te permita autenticarte contra AWS en primer lugar tiene que vivir en un mecanismo que no dependa de estar ya autenticado — eso es un problema de identidad, no de almacenamiento de secretos, y se resuelve de una forma completamente distinta." Si tu respuesta llega a la circularidad sin necesitar el nombre "OIDC", entendiste el punto de fondo, no solo la solución específica de esta guía.

Ejercicio 3 — Ubica TM-05 sin mirar THREAT-MODEL.md. De memoria: ¿qué categoría de STRIDE es TM-05, cuál es su evidencia exacta, y qué dos servicios de AWS lo resuelven?

Ver solución

Categoría: Information disclosure. Evidencia: .secrets — un archivo de texto plano en disco, gitignoreado pero sin ningún control de acceso propio (cualquiera con acceso de lectura al filesystem puede leerlo, sin que quede registro de quién lo hizo). Servicios que lo resuelven: SSM Parameter Store y Secrets Manager, las dos herramientas centrales de este módulo — SSM Parameter Store para el caso de la lección 4 (la firma HMAC del webhook), Secrets Manager para el caso de la lección 5 (el usuario/contraseña de la API de aduanas).


Resumen y siguiente paso

En esta lección distinguiste con precisión el terreno de este módulo del terreno del Módulo 2: identidad (quién eres frente a AWS, ya resuelta con OIDC) frente a secretos (qué sabes de un sistema que AWS no controla, el tema de este módulo). Viste por qué .secrets no migra literalmente a secrets.tf —sería circular—, y qué migra en su lugar: el patrón correcto, aplicado al tipo de secreto al que sí pertenece, con el caso concreto que vas a usar en todo el módulo: las dos credenciales que una futura integración de Andes Cargo con una API de despacho aduanero necesitaría.

Antes de avanzar deberías poder: explicar la diferencia entre identidad y secreto sin dudar; clasificar cualquier valor de Andes Cargo como secreto o configuración con la prueba del REDACTED; y nombrar TM-05 con su evidencia exacta.

La lección 2 retoma el antipatrón de .secrets a fondo — con la honestidad exacta de por qué, contra LocalStack, es inofensivo, pero contra una cuenta AWS real nunca sería aceptable.

Recursos

  1. Este módulo, THREAT-MODEL.md (Módulo 1, lección 7) — la fuente de TM-05, el riesgo que gobierna este módulo.
  2. Este módulo, RISK-MAP.md (Módulo 1, lección 8) — la razón documentada de por qué secretos va después de identidad en la secuencia de esta guía.
  3. cicd-and-gitops-on-aws-guide, Módulo 2, lección 7 (07-hands-on-passing-secrets-to-act.md) — el origen de .secrets, el archivo que este módulo retoma.
  4. AWS Docs — What is AWS Secrets Manager? — introducción oficial, punto de partida de las lecciones 3 y 5.
  5. AWS Docs — AWS Systems Manager Parameter Store — introducción oficial, punto de partida de las lecciones 3 y 4.