Módulo 3: Secrets Management

8. Proyecto: el inventario de secretos de Andes Cargo

Descripción

Este proyecto cierra el módulo integrando, en un solo lugar, el trabajo de las siete lecciones anteriores: secrets.tf completo —los dos secretos de las lecciones 4 y 5—, verificado con tflocal validate/tflocal plan de verdad, y confirmado libre de fugas con el mismo escaneo de Trivy que la lección 7 corrió sobre un caso con hallazgos. Cierra, además, la fila TM-05 de THREAT-MODEL.md y RISK-MAP.md —el mismo mecanismo de actualización que el Módulo 1, lección 8, ya anticipó como ejercicio para los módulos siguientes—.

Conexión con el módulo

Nada de lo que sigue es contenido nuevo — es la integración, verificada de punta a punta, de las lecciones 2 a 7. Si alguna pieza de este proyecto te resulta desconocida, esa es la señal de volver a la lección correspondiente antes de seguir.


Paso 1 — secrets.tf completo

En la raíz de andes-cargo-infra/, el archivo completo, combinando las lecciones 4 y 5 sin ningún cambio:

# secrets.tf -- Andes Cargo secrets management (Module 3)
# Replaces the flat .secrets file pattern (see THREAT-MODEL.md, finding TM-05).
# Governed by RISK-MAP.md, row 3 (TM-05, resolved here).

resource "aws_ssm_parameter" "customs_api_webhook_signing_key" {
  name        = "/andes-cargo/customs-api/webhook-signing-key"
  description = "HMAC signing key used to verify inbound webhook calls from the customs-clearance partner API"
  type        = "SecureString"
  value       = var.customs_api_webhook_signing_key

  tags = {
    Project = "andes-cargo"
  }
}

resource "aws_secretsmanager_secret" "customs_api_credentials" {
  name        = "andes-cargo/customs-api/credentials"
  description = "Username/password credential pair for the customs-clearance partner REST API"
}

resource "aws_secretsmanager_secret_version" "customs_api_credentials" {
  secret_id = aws_secretsmanager_secret.customs_api_credentials.id
  secret_string = jsonencode({
    username = var.customs_api_username
    password = var.customs_api_password
  })
}

Y las tres variables correspondientes, agregadas a variables.tf (el archivo que terraform-and-iac-guide, Módulo 3, ya dejó existente en este proyecto):

variable "customs_api_webhook_signing_key" {
  description = "HMAC signing key for the customs-clearance partner webhook (LocalStack test value)"
  type        = string
  sensitive   = true
  default     = "test-webhook-signing-key-do-not-use-in-prod"
}

variable "customs_api_username" {
  description = "Username for the customs-clearance partner REST API (LocalStack test value)"
  type        = string
  sensitive   = true
  default     = "andes-cargo-test-user"
}

variable "customs_api_password" {
  description = "Password for the customs-clearance partner REST API (LocalStack test value)"
  type        = string
  sensitive   = true
  default     = "test-password-do-not-use-in-prod"
}

Tres recursos nuevos, tres variables nuevas — ni un solo recurso de negocio tocado. iam.tf, s3.tf, lambda.tf, dynamodb.tf de terraform-and-iac-guide, y modules/oidc-provider/ del Módulo 2 de esta guía, quedan exactamente como estaban.


Paso 2 — Verificando el HCL completo, real

tflocal validate

Qué esperar (literal — ejecutado para escribir esta lección):

Success! The configuration is valid.
tflocal plan

Qué esperar (literal — ejecutado para escribir esta lección, sobre secrets.tf completo):

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_secretsmanager_secret.customs_api_credentials will be created
  + resource "aws_secretsmanager_secret" "customs_api_credentials" {
      + arn                            = (known after apply)
      + description                    = "Username/password credential pair for the customs-clearance partner REST API"
      + force_overwrite_replica_secret = false
      + id                             = (known after apply)
      + name                           = "andes-cargo/customs-api/credentials"
      + name_prefix                    = (known after apply)
      + policy                         = (known after apply)
      + recovery_window_in_days        = 30
      + region                         = "us-east-1"
      + tags_all                       = (known after apply)

      + replica (known after apply)
    }

  # aws_secretsmanager_secret_version.customs_api_credentials will be created
  + resource "aws_secretsmanager_secret_version" "customs_api_credentials" {
      + arn                  = (known after apply)
      + has_secret_string_wo = (known after apply)
      + id                   = (known after apply)
      + region               = "us-east-1"
      + secret_arn           = (known after apply)
      + secret_id            = (known after apply)
      + secret_string        = (sensitive value)
      + secret_string_wo     = (write-only attribute)
      + version_id           = (known after apply)
      + version_stages       = (known after apply)
    }

  # aws_ssm_parameter.customs_api_webhook_signing_key will be created
  + resource "aws_ssm_parameter" "customs_api_webhook_signing_key" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + description    = "HMAC signing key used to verify inbound webhook calls from the customs-clearance partner API"
      + has_value_wo   = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/andes-cargo/customs-api/webhook-signing-key"
      + region         = "us-east-1"
      + tags           = {
          + "Project" = "andes-cargo"
        }
      + tags_all       = {
          + "Project" = "andes-cargo"
        }
      + tier           = (known after apply)
      + type           = "SecureString"
      + value          = (sensitive value)
      + value_wo       = (write-only attribute)
      + version        = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Plan: 3 to add, 0 to change, 0 to destroy — los dos secretos de la lección 4 y 5, ningún efecto sobre nada más. tflocal apply sobre este mismo plan queda representativo, por la misma razón declarada desde la lección 1: sin LOCALSTACK_AUTH_TOKEN, LocalStack no responde en este entorno de escritura.


Paso 3 — El escaneo final, ejecutado

Con secrets.tf y variables.tf completos, corre el mismo comando de la lección 7 sobre el proyecto entero:

trivy fs --scanners secret .

Qué esperar (literal — ejecutado para escribir esta lección, sobre secrets.tf, variables.tf, versions.tf y providers.tf combinados):

INFO	[secret] Secret scanning is enabled
INFO	[secret] Please see https://trivy.dev/docs/v0.74/guide/scanner/secret#recommendation for faster secret detection
INFO	Number of language-specific files	num=0
INFO	[report] No issues detected with scanner(s).	scanners=[secret]

Report Summary

┌────────┬──────┬─────────┐
│ Target │ Type │ Secrets │
├────────┼──────┼─────────┤
│   -    │  -   │    -    │
└────────┴──────┴─────────┘
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

Cero hallazgos. Este es el verificador final del proyecto: no una promesa de que secrets.tf está bien escrito, sino la misma herramienta que la lección 7 usó para encontrar un hallazgo real, ahora confirmando —de forma ejecutada, no supuesta— que no hay ningún secreto en texto plano en el HCL que este módulo entrega. Si en algún punto futuro alguien agregara un valor literal a secrets.tf por error, este mismo comando lo encontraría, con la misma precisión de línea y offset que viste en la lección 7.


Paso 4 — Cerrando TM-05: la actualización de THREAT-MODEL.md y RISK-MAP.md

El Módulo 1, lección 7, ya anticipó exactamente este momento en su Ejercicio 3: "cada módulo que cierra una fila de RISK-MAP.md tiene que reflejar ese cierre aquí también". La fila de THREAT-MODEL.md que este módulo cierra:

Antes (Módulo 1, lección 7):

| TM-05 | Information disclosure | Credentials stored in plaintext on disk | `.secrets` (gitignored, but a flat plaintext file with no access control of its own) | Anyone with filesystem read access can read the credential, unlogged | Module 3 (SSM Parameter Store / Secrets Manager) |

Después (al cerrar este módulo):

| TM-05 | Information disclosure | Credentials stored in plaintext on disk | `.secrets` (gitignored, but a flat plaintext file with no access control of its own) | Anyone with filesystem read access can read the credential, unlogged | **Resolved in M3.8**: `secrets.tf` (`aws_ssm_parameter` + `aws_secretsmanager_secret`) replaces the flat-file pattern for any secret Andes Cargo needs going forward; verified secret-free via `trivy fs --scanners secret` (M3.7/M3.8) |

Y la fila correspondiente de RISK-MAP.md (Módulo 1, lección 8):

Antes:

| 3 | TM-05 | Information disclosure | Plaintext credentials on disk (`.secrets`) | SSM Parameter Store / Secrets Manager | M3 | Open |

Después:

| 3 | TM-05 | Information disclosure | Plaintext credentials on disk (`.secrets`) | SSM Parameter Store / Secrets Manager | M3 | **Resolved (M3.8)** |

Fíjate en lo que este cierre no dice, tan importante como lo que sí dice. No dice ".secrets fue eliminado" — sigue existiendo, como el mecanismo local de act --secret-file, sin cambios (la lección 1 de este módulo ya lo aclaró). Dice, con precisión, que el patrón que .secrets representaba —secreto en texto plano, sin control de acceso propio— ya tiene una alternativa construida, verificada, y lista para cualquier secreto real que Andes Cargo necesite de aquí en adelante.


El inventario completo de secretos de Andes Cargo, en un solo lugar

              INVENTARIO DE SECRETOS DE ANDES CARGO — cierre del Módulo 3

  ┌──────────────────────────────┐        ┌───────────────────────────────────┐
  │ .secrets (LOCAL, sin cambios)  │        │  SSM Parameter Store                 │
  │ AWS_ACCESS_KEY_ID=test         │        │  /andes-cargo/customs-api/            │
  │ AWS_SECRET_ACCESS_KEY=test     │        │    webhook-signing-key                │
  │ uso: act --secret-file, local  │        │  tipo: SecureString                   │
  │ NUNCA migra (sería circular,   │        │  rotación: manual (ver M3.6)          │
  │ ver lección 1)                 │        └───────────────────────────────────┘
  └──────────────────────────────┘
                                             ┌───────────────────────────────────┐
  ┌──────────────────────────────┐        │  Secrets Manager                     │
  │ TM-05 (THREAT-MODEL.md)        │        │  andes-cargo/customs-api/credentials │
  │ Status: Resolved (M3.8)        │───────▶│  campos: username, password           │
  │ Evidencia: secrets.tf +        │        │  rotación: nativa, no ejecutada       │
  │ trivy fs --scanners secret     │        │  (mecanismo nombrado en M3.6)         │
  │ (cero hallazgos)               │        └───────────────────────────────────┘
  └──────────────────────────────┘

Errores comunes

Marcar TM-05 como Resolved sin haber corrido el escaneo de la lección 7 (de evidencia, ver Módulo 1 lección 8). Qué pasa: alguien actualiza el Status de THREAT-MODEL.md apenas termina de escribir secrets.tf, sin correr trivy fs --scanners secret como verificación. Cómo detectarlo: si tu evidencia para "Resolved" es "escribí el HCL correcto", no "corrí una herramienta que lo confirma". Cómo corregirlo: el mismo principio que el Módulo 1, lección 8, ya advirtió para TM-01/TM-07 aplica aquí — la única evidencia válida de "resuelto" es la verificación real, no la intención. El Paso 3 de esta lección es exactamente esa verificación.

Buscar un aws_iam_role_policy que le dé a LambdaManifestProcessorRole permiso de leer estos secretos (de alcance, expectativa razonable pero fuera de este módulo). Qué pasa: alguien, después de construir secrets.tf, busca en este proyecto el resource "aws_iam_role_policy" que le otorgaría a los roles del Módulo 2 el permiso de leer estos dos secretos nuevos. Cómo detectarlo: si tu checklist de "seguridad completa" incluye ese permiso como parte de este módulo. Cómo corregirlo: es un paso real y necesario —cualquier código que efectivamente use estos secretos necesitaría ssm:GetParameter/secretsmanager:GetSecretValue acotado al ARN exacto—, pero es, precisamente, el tipo de trabajo de mínimo privilegio que el Módulo 2 ya enseñó cómo hacer sobre los roles existentes: aplicar ese mismo patrón a un ARN nuevo cuando la integración real con la API de aduanas exista. Este módulo entrega el almacén, no el permiso de leerlo — mezclar los dos alcances sería repetir, sin necesidad, el trabajo que el Módulo 2 ya resolvió como principio general.

Pensar que secrets.tf necesita todavía más secretos para estar "completo" (de alcance, sobre-generalización). Qué pasa: alguien, con el patrón recién aprendido, empieza a preguntarse qué otros secretos "debería" agregar Andes Cargo, aunque no exista ningún sistema real que los necesite todavía. Cómo detectarlo: si tu plan de expansión de secrets.tf no tiene ningún caso de uso concreto detrás. Cómo corregirlo: los dos secretos de este módulo existen porque responden a un caso concreto y nombrado —una futura integración con una API de aduanas—, no porque "más secretos gestionados es siempre mejor". Declarar secretos sin un consumidor real solo agrega superficie y costo (recuerda la tabla de pricing de la lección 3) sin ningún beneficio de seguridad real.


Ejercicios

Ejercicio 1 — Verifica el inventario completo de memoria. Sin mirar hacia atrás en este módulo, nombra los dos secretos que este proyecto entrega, su servicio correspondiente, y la razón —de la lección 3— por la que cada uno fue a ese servicio y no al otro.

Ver solución

La firma HMAC del webhook de aduanas → SSM Parameter Store, SecureString — un único valor de verificación, sin necesidad de rotación automática coordinada con un sistema externo (no es, en sí misma, una credencial de autenticación). El usuario/contraseña de la API de aduanas → Secrets Manager — es, literalmente, una credencial de autenticación frente a un sistema externo, candidata natural a rotación periódica, con una estructura de múltiples campos relacionados que encaja con el soporte nativo de Secrets Manager para JSON. Si nombraste los dos servicios y las dos razones sin mirar atrás, tienes el criterio de la lección 3 internalizado.

Ejercicio 2 — Explica por qué este módulo no elimina .secrets, a un compañero que esperaba que sí. Un colega, después de leer que este módulo "resuelve TM-05", pregunta por qué .secrets sigue apareciendo en git status de andes-cargo-infra/ al cerrar el módulo. Responde con precisión, sin usar la palabra "circular" (usa tus propias palabras para el mismo argumento).

Ver solución

Una respuesta completa suena, más o menos, así: ".secrets guarda la credencial que el propio pipeline necesita para hablar con LocalStack — y para leer un secreto de SSM Parameter Store o Secrets Manager, primero necesitas estar autenticado contra AWS. Si esa credencial de autenticación viviera dentro de uno de esos dos servicios, no habría forma de leerla la primera vez, porque leerla ya requeriría tenerla. Lo que resuelve este módulo es un tipo distinto de secreto: uno de un sistema que no es AWS, que sí puede vivir dentro de un servicio de AWS sin ningún problema circular, porque autenticarse contra AWS (Módulo 2, con OIDC) y autenticarse contra el sistema de aduanas son dos problemas completamente separados."

Ejercicio 3 — Diseña el criterio de aceptación para un secreto nuevo, hipotético. Andes Cargo va a integrar, en el futuro, un servicio de notificaciones por SMS para avisar a los destinatarios cuando un envío llega a destino. Usando todo lo aprendido en este módulo (lección 1: prueba del REDACTED; lección 3: criterio de decisión), decide: ¿el token de API de ese servicio de SMS es un secreto? Si lo es, ¿va a SSM Parameter Store o a Secrets Manager? Justifica ambas respuestas.

Ver solución

¿Es un secreto? Sí — aplicando la prueba del REDACTED de la lección 1: publicar ese token le daría a cualquiera la capacidad de enviar SMS en nombre de Andes Cargo (y, dependiendo del proveedor, potencialmente generar cargos), exactamente la definición de secreto que usa este módulo. ¿SSM Parameter Store o Secrets Manager? Secrets Manager — es una credencial de autenticación frente a un sistema externo (Pregunta 1 de la lección 3), y es razonable esperar que, como buena práctica, ese token se rote periódicamente si el proveedor lo permite (Pregunta 2). El mismo patrón exacto que el usuario/contraseña de la API de aduanas de este módulo: aws_secretsmanager_secret + aws_secretsmanager_secret_version, con su propia variable sensitive en variables.tf.


El cierre del Módulo 3

Con secrets.tf completo, verificado con tflocal validate/tflocal plan real y con el escaneo de Trivy confirmando cero secretos en texto plano, este módulo entrega exactamente lo que prometió en la lección 1: no una reescritura de .secrets —ese archivo sigue existiendo, sin cambios, para el propósito específico que siempre tuvo—, sino el patrón correcto, construido y verificado, para el tipo de secreto al que .secrets nunca debería haber pertenecido en primer lugar. TM-05 queda cerrado, con evidencia ejecutada, no supuesta.

Con este cierre, RISK-MAP.md queda así: identidad (M2, resuelto), secretos (M3, resuelto), y las cuatro filas restantes —no-public-buckets.rego, no-destroy-shipments.rego, la firma del artefacto con cosign, y CloudTrail— todavía abiertas, esperando los Módulos 4 a 7.


Resumen y siguiente paso

En este proyecto integraste el trabajo completo del módulo: secrets.tf con los dos secretos de las lecciones 4 y 5, verificado de verdad con tflocal validate/tflocal plan, y confirmado libre de fugas con el mismo Trivy que la lección 7 usó para encontrar un hallazgo real. Cerraste formalmente TM-05 en THREAT-MODEL.md y RISK-MAP.md, con la evidencia exacta que respalda ese cierre — no la intención, la verificación ejecutada.

Antes de cerrar este módulo deberías poder: recitar los dos secretos de Andes Cargo con su servicio y su justificación, sin mirar ningún documento; explicar por qué .secrets sigue existiendo sin contradecir el cierre de TM-05; y correr, de memoria, el comando de Trivy que confirmaría que cualquier secrets.tf futuro sigue libre de literales.

Con esto, el Módulo 3 de cloud-security-and-guardrails-guide queda completo: la distinción identidad/secreto aplicada de punta a punta, dos servicios gestionados de AWS construidos con criterio real, el mecanismo de rotación entendido con precisión aunque no ejecutado, y un escáner real integrado como verificación repetible. El Módulo 4 abre la siguiente fila de RISK-MAP.md: policy-as-code preventivo con conftest, reemplazando el grep artesanal que cicd-and-gitops-on-aws-guide construyó a mano.

Recursos

  1. Este módulo, lecciones 4 y 5 — la fuente completa de cada recurso de secrets.tf integrado en este proyecto.
  2. Este módulo, lección 7 — el escáner de Trivy que este proyecto usa como verificación final.
  3. Módulo 1, lección 7 (THREAT-MODEL.md) y lección 8 (RISK-MAP.md) — los documentos que este proyecto actualiza al cerrar TM-05.
  4. src/guides/cloud-security-and-guardrails-guide/DISENO.md — el diseño completo de esta guía, fuente de la secuencia M2→M3→M4 que RISK-MAP.md justifica.