Terminamos 04-02 con la base de datos mercadofresco-pedidos cifrada con AES-256 y una clave de KMS con política, rotación y separación de funciones. Y aun así, cualquiera que se siente delante del portátil de Luis puede conectarse a esa base de datos, porque la contraseña del usuario mfadmin está escrita en texto plano en al menos cinco sitios:

  1. En el user data de la plantilla de lanzamiento lt-mercadofresco-tienda, que se puede leer desde el servicio de metadatos de cualquier instancia de la tienda.
  2. En /etc/mercadofresco/tienda.conf dentro de cada instancia.
  3. En un fichero .env que Luis se pasó a sí mismo por chat cuando montó el entorno de desarrollo.
  4. En el historial de comandos de la sesión en la que la usó por primera vez.
  5. En la cabeza de Marta, que la eligió hace catorce meses y no ha cambiado desde entonces.

El cifrado no ayuda aquí. Como vimos en la primera tabla de 04-02, el cifrado en reposo protege del disco robado y del snapshot compartido por error, pero no protege de unas credenciales válidas en manos equivocadas. Esta lección resuelve exactamente ese problema.

AWS ofrece dos servicios para custodiar valores de configuración y credenciales: AWS Systems Manager Parameter Store y AWS Secrets Manager. No son competidores, son complementarios, y la mitad de esta lección consiste en saber cuál usar para qué.

Advertencia. Los ejemplos son didácticos y las credenciales, ficticias. Toda gestión real de credenciales, rotación y cumplimiento (RGPD, PCI DSS) debe ser revisada por un profesional de seguridad antes de aplicarse a un entorno con datos de clientes. Un secreto mal migrado —especialmente una rotación mal configurada— deja la aplicación fuera de servicio y sin forma evidente de volver atrás. Nunca uses credenciales reales en un entorno de pruebas.

Contenido

  1. Por qué las credenciales en el código son un problema estructural
  2. Qué pasa exactamente cuando un secreto llega a Git
  3. Configuración y secreto no son lo mismo
  4. Parameter Store: tipos de parámetro
  5. Jerarquía por rutas, versiones y niveles
  6. La configuración de MercadoFresco en Parameter Store
  7. Secrets Manager: secretos, versiones y etapas
  8. Cifrado con KMS y políticas de recurso
  9. Rotación automática: el ciclo de cuatro pasos
  10. Configurar la rotación de mercadofresco/produccion/rds/mfadmin
  11. Usuario único frente a alternancia de dos usuarios
  12. Tabla comparativa y la recomendación para MercadoFresco
  13. Consumo desde la aplicación: boto3 y caché
  14. Consumo desde Lambda, EC2, ECS y CloudFormation
  15. Permisos mínimos para leer un secreto
  16. La migración: sacar la contraseña del user data
  17. Qué no hay que guardar aquí
  18. Auditoría del acceso a secretos
  19. Detección de secretos filtrados en el repositorio
  20. Coste, cuotas y limpieza

Por qué las credenciales en el código son un problema estructural

No es descuido, es un problema de diseño. Una credencial escrita en un fichero de configuración tiene cuatro propiedades que la condenan:

Propiedad Consecuencia
Se copia Cada despliegue, cada backup, cada portátil nuevo multiplica las copias
No caduca La contraseña de mfadmin lleva catorce meses igual, y podría llevar diez años
No deja rastro Nadie sabe quién la ha leído; no hay registro posible
No se puede revocar sin romper algo Cambiarla implica actualizar todos los sitios a la vez

La cuarta es la que perpetúa el problema. Marta sabe que la contraseña debería cambiarse, pero cambiarla significa editar la plantilla de lanzamiento, reiniciar todas las instancias del ASG, avisar a Luis y cruzar los dedos. Como el riesgo de la operación parece mayor que el de no hacer nada, no se hace nunca. La rotación automática rompe ese círculo, y por eso es el corazón de esta lección.

Qué pasa exactamente cuando un secreto llega a Git

Conviene ser concreto, porque el error más caro es creer que basta con borrarlo en el commit siguiente:

# Luis se da cuenta y "lo arregla"
git rm --cached .env
git commit -m "Quitar el .env del repositorio"
git push

Esto no elimina nada. El fichero sigue en el historial, accesible con git show <commit-anterior>:.env. Y si el repositorio estuvo en un servidor compartido o alguien hizo un clone, esas copias también lo tienen. El único procedimiento correcto es:

  1. Rotar la credencial inmediatamente. Considérala comprometida desde el momento en que se escribió. Este paso es obligatorio y no es opcional.
  2. Reescribir el historial (git filter-repo, BFG) y forzar el push. Esto rompe los clones de todo el equipo, así que hay que coordinarlo.
  3. Buscar en los registros si la credencial se usó desde algún sitio inesperado.
  4. Añadir un escáner que impida que vuelva a pasar.

El orden importa: rotar primero. Reescribir el historial mientras la credencial sigue siendo válida es tratar el síntoma. Si el repositorio es público, los bots que rastrean GitHub encuentran credenciales de AWS en cuestión de minutos, y el coste de una cuenta usada para minar criptomonedas se cuenta en miles de dólares por día.

Configuración y secreto no son lo mismo

Esta distinción decide qué servicio usar:

Configuración Secreto
Ejemplo Nombre del bucket, tamaño de página, tiempo de espera Contraseña, clave de API, token
¿Puede aparecer en un registro? Sí, sin consecuencias Nunca
¿Necesita rotar? No Sí, periódicamente
¿Quién puede verla? Todo el equipo técnico Solo quien la necesita
Servicio Parameter Store Secrets Manager

Casos frontera que conviene resolver de antemano:

  • El nombre de usuario de la base de datos (mfadmin) es configuración. La contraseña es secreto. Pero se guardan juntos, porque la rotación cambia ambos en el caso de alternancia.
  • El punto de enlace de RDS es configuración: no es secreto y ya es público en tu VPC.
  • Una clave de API de un proveedor de pagos es un secreto, aunque sea de un entorno de pruebas.
  • El identificador de la distribución de CloudFront (E2QWERTY123ABC) es configuración.

Parameter Store: tipos de parámetro

Parameter Store es un componente de AWS Systems Manager. Guarda pares clave-valor con tres tipos:

Tipo Cifrado Uso
String No Valores simples: nombres, URL, números
StringList No Lista separada por comas: eu-west-1a,eu-west-1b
SecureString Sí, con KMS Valores sensibles de bajo perfil
# Un parámetro simple
aws ssm put-parameter \
  --name "/mercadofresco/produccion/tienda/nombre-bucket-fotos" \
  --value "mercadofresco-catalogo-fotos" \
  --type String \
  --description "Bucket del catalogo de fotos de la tienda" \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
  --profile mercadofresco-dev

# Una lista
aws ssm put-parameter \
  --name "/mercadofresco/produccion/red/zonas" \
  --value "eu-west-1a,eu-west-1b" \
  --type StringList \
  --profile mercadofresco-dev

# Un valor cifrado con nuestra clave de 04-02
aws ssm put-parameter \
  --name "/mercadofresco/produccion/tienda/clave-api-mensajeria" \
  --value "ficticia-1234567890" \
  --type SecureString \
  --key-id alias/mercadofresco-datos \
  --profile mercadofresco-dev

Notas sobre SecureString:

  • Si no indicas --key-id, se usa la clave gestionada por AWS alias/aws/ssm, que es gratuita pero no controlable. Para valores que importan, usa tu clave.
  • Para leer el valor descifrado hace falta --with-decryption y permiso kms:Decrypt sobre la clave. Es la misma doble autorización de SSE-KMS.
aws ssm get-parameter \
  --name "/mercadofresco/produccion/tienda/clave-api-mensajeria" \
  --with-decryption \
  --query 'Parameter.Value' --output text \
  --profile mercadofresco-dev

Jerarquía por rutas, versiones y niveles

La jerarquía por rutas es la característica que hace útil a Parameter Store. Los nombres se estructuran como rutas y se pueden leer en bloque:

/mercadofresco/
├── produccion/
│   ├── tienda/
│   │   ├── nombre-bucket-fotos
│   │   ├── url-cdn
│   │   ├── pedidos-por-pagina
│   │   └── tiempo-espera-segundos
│   ├── basedatos/
│   │   ├── punto-enlace
│   │   ├── punto-enlace-lectura
│   │   ├── nombre-bd
│   │   └── usuario
│   └── red/
│       └── zonas
└── desarrollo/
    └── tienda/
        └── ...

Con una sola llamada la aplicación carga toda su configuración:

aws ssm get-parameters-by-path \
  --path "/mercadofresco/produccion/tienda/" \
  --recursive --with-decryption \
  --query 'Parameters[].[Name,Value]' --output table \
  --profile mercadofresco-dev

Y la jerarquía se convierte directamente en control de acceso: una política que concede ssm:GetParametersByPath sobre /mercadofresco/produccion/* deja fuera todo el entorno de desarrollo sin enumerar un solo parámetro.

Versiones. Cada put-parameter --overwrite crea una versión nueva y conserva las anteriores. Se puede leer una versión concreta con nombre:numero, y etiquetar una versión con un alias:

aws ssm put-parameter --name "/mercadofresco/produccion/tienda/pedidos-por-pagina" \
  --value "50" --type String --overwrite --profile mercadofresco-dev

aws ssm label-parameter-version \
  --name "/mercadofresco/produccion/tienda/pedidos-por-pagina" \
  --parameter-version 3 --labels estable --profile mercadofresco-dev

# Leer una versión concreta o una etiqueta
aws ssm get-parameter --name "/mercadofresco/produccion/tienda/pedidos-por-pagina:2" \
  --profile mercadofresco-dev
aws ssm get-parameter --name "/mercadofresco/produccion/tienda/pedidos-por-pagina:estable" \
  --profile mercadofresco-dev

Volver atrás tras un cambio desafortunado es cambiar la etiqueta estable a la versión anterior. No hay despliegue de por medio.

Niveles.

Estándar Avanzado
Parámetros por cuenta y región 10.000 100.000
Tamaño del valor 4 KB 8 KB
Políticas de parámetro (caducidad, avisos) No
Coste de almacenamiento Gratis 0,05 USD por parámetro y mes
Coste de las llamadas Gratis hasta 40/s 0,05 USD por cada 10.000

MercadoFresco cabe de sobra en el nivel estándar: cero euros por toda su configuración. El nivel avanzado se justifica cuando necesitas más de 4 KB —un certificado, por ejemplo— o las políticas de caducidad, que avisan cuando un parámetro lleva demasiado tiempo sin cambiar.

Hay además un modo de rendimiento alto (--parameter-tier, con ssm:GetParameters a 3.000 peticiones por segundo) que se factura aparte; solo hace falta si la aplicación lee parámetros en el camino crítico de cada petición, cosa que no debería hacer: para eso está la caché.

La configuración de MercadoFresco en Parameter Store

crear() {
  aws ssm put-parameter --name "$1" --value "$2" --type "${3:-String}" --overwrite \
    --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
           Key=Componente,Value=tienda Key=Propietario,Value=marta \
           Key=CentroCoste,Value=tecnologia \
    --profile mercadofresco-dev
}

crear "/mercadofresco/produccion/basedatos/punto-enlace" \
      "mercadofresco-pedidos.abc123.eu-west-1.rds.amazonaws.com"
crear "/mercadofresco/produccion/basedatos/punto-enlace-lectura" \
      "mercadofresco-pedidos-lectura.abc123.eu-west-1.rds.amazonaws.com"
crear "/mercadofresco/produccion/basedatos/nombre-bd" "pedidos"
crear "/mercadofresco/produccion/basedatos/usuario" "mfadmin"
crear "/mercadofresco/produccion/tienda/nombre-bucket-fotos" "mercadofresco-catalogo-fotos"
crear "/mercadofresco/produccion/tienda/url-cdn" "https://d111111abcdef8.cloudfront.net"
crear "/mercadofresco/produccion/tienda/pedidos-por-pagina" "50"
crear "/mercadofresco/produccion/tienda/tiempo-espera-segundos" "30"
crear "/mercadofresco/produccion/tienda/tema-alertas" \
      "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco"

Fíjate en que el usuario mfadmin está aquí y la contraseña no. El nombre de usuario es configuración; la contraseña va al otro servicio. Y observa el efecto colateral: el user data de lt-mercadofresco-tienda, que hasta ahora era una lista de valores incrustados, pasa a ser una llamada genérica idéntica en todos los entornos.

Secrets Manager: secretos, versiones y etapas

Un secreto de Secrets Manager es un valor —normalmente un JSON— cifrado con KMS, con versiones y un mecanismo de rotación. La pieza que hay que entender bien son las etapas de versión.

Cada versión de un secreto lleva una o varias etiquetas de etapa:

Etapa Significado
AWSCURRENT La versión vigente. Es la que devuelve get-secret-value por defecto
AWSPENDING Versión candidata durante una rotación en curso, aún no validada
AWSPREVIOUS La versión anterior, conservada para poder volver atrás

Estas tres etiquetas son el mecanismo que hace que la rotación no corte el servicio. Durante unos segundos coexisten la contraseña vieja (AWSCURRENT) y la nueva (AWSPENDING), y solo cuando la nueva se ha probado se intercambian las etiquetas. Ninguna aplicación se queda sin credencial válida en ningún momento.

Crear el secreto de MercadoFresco:

aws secretsmanager create-secret \
  --name "mercadofresco/produccion/rds/mfadmin" \
  --description "Credenciales del usuario mfadmin de mercadofresco-pedidos" \
  --kms-key-id alias/mercadofresco-datos \
  --secret-string '{
    "engine": "postgres",
    "host": "mercadofresco-pedidos.abc123.eu-west-1.rds.amazonaws.com",
    "port": 5432,
    "dbname": "pedidos",
    "username": "mfadmin",
    "password": "ContrasenaFicticiaTemporal2026",
    "dbInstanceIdentifier": "mercadofresco-pedidos"
  }' \
  --tags Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
         Key=Componente,Value=basedatos Key=Propietario,Value=marta \
         Key=CentroCoste,Value=tecnologia \
  --profile mercadofresco-dev

La estructura del JSON no es arbitraria: esos nombres de campo (engine, host, port, dbname, username, password) son exactamente los que espera la función de rotación gestionada de AWS para PostgreSQL. Si los cambias, la rotación no funcionará. Es uno de los detalles que más tiempo hace perder.

--kms-key-id alias/mercadofresco-datos conecta esta lección con la anterior: el secreto se cifra con la clave que construimos en 04-02, y por tanto queda sujeto a su política. Quien no pueda usar la clave no podrá leer el secreto aunque tenga secretsmanager:GetSecretValue. Otra vez la doble autorización.

Cifrado con KMS y políticas de recurso

Como bucket, cola o clave, un secreto admite política de recurso. Sirve para dos cosas: acceso entre cuentas y refuerzo con Deny.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SoloLaTiendaYMartaLeenEsteSecreto",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalArn": [
            "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda",
            "arn:aws:iam::111122223333:user/marta",
            "arn:aws:iam::111122223333:role/rol-rotacion-mfadmin"
          ]
        }
      }
    }
  ]
}

Es un Deny con StringNotEquals: «deniega la lectura a todo el que no sea uno de estos tres». Recuerda de 04-01 que un Deny gana siempre, así que ni una política de identidad con secretsmanager:* sobre * puede saltárselo. Es la forma más robusta de blindar el secreto más sensible de la cuenta, y hay que incluir en la lista el rol de rotación o la rotación fallará.

aws secretsmanager put-resource-policy \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --resource-policy file:///tmp/politica-secreto.json \
  --block-public-policy \
  --profile mercadofresco-dev

--block-public-policy rechaza la política si concediera acceso público. Úsalo siempre.

Rotación automática: el ciclo de cuatro pasos

Aquí está el valor diferencial de Secrets Manager. La rotación la ejecuta una función Lambda, que AWS proporciona ya escrita para los motores de base de datos habituales, y que se invoca cuatro veces con un paso distinto cada vez.

sequenceDiagram
    participant SM as Secrets Manager
    participant L as Lambda de rotacion<br/>SecretsManagerRDSPostgreSQL...
    participant DB as mercadofresco-pedidos
    participant A as Tienda (aplicacion)

    Note over SM,A: Dia 30: toca rotar
    SM->>L: Step 1 - createSecret
    L->>L: Genera contrasena aleatoria
    L->>SM: PutSecretValue(etapa=AWSPENDING)
    Note over SM: AWSCURRENT = vieja<br/>AWSPENDING = nueva

    SM->>L: Step 2 - setSecret
    L->>DB: ALTER USER mfadmin PASSWORD 'nueva'
    Note over DB: Ahora la BD acepta la NUEVA

    SM->>L: Step 3 - testSecret
    L->>DB: Conectar con AWSPENDING y hacer SELECT
    DB-->>L: OK
    Note over L: Si falla aqui, se aborta<br/>y AWSCURRENT sigue intacta

    SM->>L: Step 4 - finishSecret
    L->>SM: Mover AWSCURRENT a la version nueva
    Note over SM: nueva = AWSCURRENT<br/>vieja = AWSPREVIOUS

    A->>SM: GetSecretValue (proxima lectura)
    SM-->>A: contrasena nueva

Los cuatro pasos, con lo que importa de cada uno:

Paso Qué hace Qué pasa si falla
createSecret Genera la contraseña nueva y la guarda como AWSPENDING. No toca la base de datos Nada cambia; se reintenta
setSecret Cambia la contraseña en la base de datos Momento delicado: la BD puede tener la nueva y el secreto no haberla promocionado
testSecret Se conecta con la nueva y ejecuta una consulta de prueba Se aborta la rotación; AWSCURRENT sigue siendo la vieja
finishSecret Mueve la etiqueta AWSCURRENT a la versión nueva Se reintenta

La consecuencia práctica más importante es la que casi nadie anticipa: entre setSecret y finishSecret hay una ventana en la que la base de datos ya tiene la contraseña nueva pero AWSCURRENT todavía devuelve la vieja. Si tu aplicación cachea el secreto durante una hora y abre una conexión nueva justo en esa ventana, recibirá un error de autenticación.

Por eso el patrón correcto en la aplicación es:

  1. Cachear el secreto (una lectura por petición es carísima y lenta).
  2. Al recibir un error de autenticación, invalidar la caché y reintentar una vez.

Ese reintento de una línea es lo que convierte la rotación en algo transparente. Sin él, cada rotación produce un puñado de errores en los registros.

Configurar la rotación de mercadofresco/produccion/rds/mfadmin

La forma más sencilla es dejar que Secrets Manager cree la Lambda desde su plantilla gestionada:

aws secretsmanager rotate-secret \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --rotation-lambda-arn arn:aws:lambda:eu-west-1:111122223333:function:rotacion-mfadmin \
  --rotation-rules '{"AutomaticallyAfterDays": 30, "Duration": "2h",
                     "ScheduleExpression": "cron(0 3 ? * TUE *)"}' \
  --profile mercadofresco-dev

Los parámetros de --rotation-rules:

  • AutomaticallyAfterDays: 30: cada 30 días.
  • ScheduleExpression: cuándo exactamente. Aquí, los martes a las 3 de la madrugada. Es deliberado: nunca un jueves ni un viernes, por el pico de pedidos.
  • Duration: "2h": ventana de dos horas dentro de la cual puede empezar.

La Lambda de rotación necesita tres cosas que se olvidan constantemente:

  1. Acceso de red a la base de datos. Debe estar en la VPC vpc-mercadofresco, en las subredes snet-mercadofresco-app-a/-b, y sg-mercadofresco-basedatos debe aceptar el 5432 desde su grupo de seguridad. Aquí se aplica lo que vimos en 03-02: referenciar el SG de origen en lugar de un CIDR.
  2. Acceso a la API de Secrets Manager. Como está en subredes privadas y su salida a internet pasa por nat-mercadofresco-a, o bien se acepta ese coste o bien se crea un endpoint de VPC de interfaz para secretsmanager, del mismo modo que hicimos con vpce-mercadofresco-s3 en 03-01. Sin una de las dos cosas, la Lambda se queda colgada hasta agotar el tiempo de espera y el síntoma no dice nada.
  3. Permisos. Su rol necesita secretsmanager:GetSecretValue, PutSecretValue, UpdateSecretVersionStage, DescribeSecret, GetRandomPassword, más kms:Decrypt y kms:GenerateDataKey sobre alias/mercadofresco-datos.

Rotación manual inmediata, muy útil para probar antes de confiar en el calendario:

aws secretsmanager rotate-secret \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --rotate-immediately \
  --profile mercadofresco-dev

# Ver el resultado
aws secretsmanager describe-secret \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --query '{Rotacion:RotationEnabled, Ultima:LastRotatedDate, Proxima:NextRotationDate,
            Versiones:VersionIdsToStages}' \
  --profile mercadofresco-dev

Prueba la rotación en desarrollo antes de activarla en producción. Es el consejo más importante de esta lección: una rotación mal configurada deja la tienda sin base de datos, y el error de setSecret a medias puede requerir cambiar la contraseña a mano para recuperarse.

Usuario único frente a alternancia de dos usuarios

Existen dos estrategias de rotación, y la elección tiene consecuencias reales de disponibilidad:

Usuario único Alternancia de dos usuarios
Cómo funciona Cambia la contraseña del mismo usuario Alterna entre mfadmin_a y mfadmin_b
Ventana sin credencial válida Existe, de segundos No existe
Complejidad Baja Media: hay que crear y mantener dos usuarios
Conexiones abiertas Siguen funcionando hasta reconectar Igual
Cuándo Aplicaciones con reintento correcto Cargas críticas sin margen de error

MercadoFresco empieza con usuario único porque es más simple y la tienda implementa el reintento. Cuando el volumen crezca —o cuando entren en juego contenedores que arrancan y mueren constantemente, en el módulo 10— la alternancia de dos usuarios será la opción sensata. La plantilla gestionada de AWS existe para ambos casos: SecretsManagerRDSPostgreSQLRotationSingleUser y ...RotationMultiUser.

Tabla comparativa y la recomendación para MercadoFresco

Secrets Manager Parameter Store
Rotación automática , gestionada, con Lambda No
Coste 0,40 USD por secreto y mes + 0,05 USD/10.000 llamadas Gratis en nivel estándar
Tamaño máximo 64 KB 4 KB (8 KB avanzado)
Cifrado Siempre, con KMS Solo SecureString
Políticas de recurso No
Acceso entre cuentas No directamente
Jerarquía por rutas No (pero se usan / por convención) , con lectura en bloque
Versiones Sí, con etapas Sí, con etiquetas
Integración con RDS Nativa, credenciales gestionadas No
Réplica entre regiones , automática No
Acceso desde Parameter Store Sí: /aws/reference/secretsmanager/<nombre>
Cuándo usarlo Credenciales que deben rotar Configuración y secretos de bajo perfil

La recomendación para MercadoFresco, escrita como norma del equipo:

Todo lo que sea una credencial capaz de dar acceso a datos de clientes va a Secrets Manager, con rotación activada. Todo lo demás va a Parameter Store.

Aplicado:

Valor Dónde Por qué
Contraseña de mfadmin Secrets Manager Da acceso a datos personales; debe rotar
Clave de API de la pasarela de pago Secrets Manager Credencial de terceros; debe rotar
Usuario mfadmin Parameter Store Es configuración
Punto de enlace de RDS Parameter Store No es secreto
Nombres de bucket, URL del CDN Parameter Store Configuración pura
Clave de API de mensajería de pedidos Parameter Store SecureString Sensible pero de bajo impacto y sin rotación automática disponible

Coste de esta división: 2 secretos × 0,40 = 0,80 USD al mes, más unos céntimos de llamadas. Toda la configuración, gratis. Si lo hubiéramos metido todo en Secrets Manager, los 25 valores costarían 10 USD mensuales; si lo hubiéramos metido todo en Parameter Store, no tendríamos rotación. La división no es purismo, es economía.

Un detalle práctico muy cómodo: Parameter Store puede leer secretos de Secrets Manager usando el prefijo /aws/reference/secretsmanager/. Así la aplicación usa una sola API para todo:

aws ssm get-parameter \
  --name "/aws/reference/secretsmanager/mercadofresco/produccion/rds/mfadmin" \
  --with-decryption --query 'Parameter.Value' --output text \
  --profile mercadofresco-dev

Consumo desde la aplicación: boto3 y caché

La versión ingenua —leer el secreto en cada petición— tiene tres problemas: latencia de decenas de milisegundos, coste por llamada y riesgo de superar la cuota de peticiones. La correcta usa caché con invalidación ante error de autenticación:

import boto3
import json
import psycopg2
from botocore.exceptions import ClientError

SECRETO = "mercadofresco/produccion/rds/mfadmin"

sesion = boto3.Session(region_name="eu-west-1")
sm = sesion.client("secretsmanager")

_cache = {"valor": None}


def obtener_credenciales(forzar_recarga: bool = False) -> dict:
    """Devuelve las credenciales, usando la cache salvo que se fuerce la recarga."""
    if _cache["valor"] is None or forzar_recarga:
        respuesta = sm.get_secret_value(SecretId=SECRETO)
        _cache["valor"] = json.loads(respuesta["SecretString"])
    return _cache["valor"]


def conectar():
    """Conecta a la base de datos y reintenta UNA vez si la credencial ha rotado."""
    for intento in (1, 2):
        credenciales = obtener_credenciales(forzar_recarga=(intento == 2))
        try:
            return psycopg2.connect(
                host=credenciales["host"],
                port=credenciales["port"],
                dbname=credenciales["dbname"],
                user=credenciales["username"],
                password=credenciales["password"],
                connect_timeout=5,
            )
        except psycopg2.OperationalError as error:
            if "authentication" not in str(error).lower() or intento == 2:
                raise
            # La contrasena ha rotado: se invalida la cache y se reintenta

El bucle for intento in (1, 2) es toda la lógica que hace falta: primer intento con la caché, segundo con recarga forzada, y solo se reintenta si el error es de autenticación. Un error de red o de tiempo de espera se propaga tal cual, porque recargar el secreto no lo arreglaría.

Para producción existe la biblioteca oficial, que añade caducidad por tiempo y es más completa:

pip install aws-secretsmanager-caching
import boto3
from aws_secretsmanager_caching import SecretCache, SecretCacheConfig

cliente = boto3.client("secretsmanager", region_name="eu-west-1")
cache = SecretCache(
    config=SecretCacheConfig(
        secret_refresh_interval=3600,   # refrescar cada hora
        max_cache_size=1024,
    ),
    client=cliente,
)

credenciales = json.loads(cache.get_secret_string("mercadofresco/produccion/rds/mfadmin"))

Con secret_refresh_interval=3600 se hacen 24 llamadas al día por instancia en lugar de una por petición: de decenas de miles a dos docenas.

Consumo desde Lambda, EC2, ECS y CloudFormation

Lambda: la extensión de parámetros y secretos. AWS publica una capa que arranca un pequeño servidor HTTP local dentro del entorno de ejecución y cachea por ti. Se añade la capa, se configura con variables de entorno y se consulta por localhost:

import os
import json
import urllib.request

PUERTO = os.environ.get("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT", "2773")
TOKEN = os.environ["AWS_SESSION_TOKEN"]


def leer_secreto(nombre: str) -> dict:
    url = f"http://localhost:{PUERTO}/secretsmanager/get?secretId={nombre}"
    peticion = urllib.request.Request(url)
    peticion.add_header("X-Aws-Parameters-Secrets-Token", TOKEN)
    with urllib.request.urlopen(peticion) as respuesta:
        return json.loads(json.loads(respuesta.read())["SecretString"])


def handler(evento, contexto):
    credenciales = leer_secreto("mercadofresco/produccion/rds/mfadmin")
    ...

La extensión mantiene la caché entre invocaciones que reutilizan el mismo entorno de ejecución —recuerda el modelo de arranques en frío y en caliente de 02-05—, así que en la práctica una función muy invocada hace una llamada cada cinco minutos en lugar de una por invocación.

EC2. Con el rol rol-mercadofresco-tienda asignado, boto3 encuentra las credenciales solo. No hay nada que configurar salvo los permisos.

ECS. Las definiciones de tarea permiten inyectar secretos directamente como variables de entorno, sin que el código llame a la API. Se verá en 10-01.

CloudFormation: referencias dinámicas. Se puede referenciar un secreto sin escribirlo en la plantilla:

Resources:
  BaseDatos:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: mercadofresco-pedidos
      MasterUsername: '{{resolve:ssm:/mercadofresco/produccion/basedatos/usuario}}'
      MasterUserPassword: '{{resolve:secretsmanager:mercadofresco/produccion/rds/mfadmin:SecretString:password}}'

La sintaxis {{resolve:secretsmanager:<secreto>:SecretString:<campo>}} se resuelve en el momento del despliegue y el valor nunca aparece en la plantilla ni en los eventos de la pila. Se desarrolla en 09-01.

Permisos mínimos para leer un secreto

Esta es la política que se adjunta a rol-mercadofresco-tienda:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LeerSoloElSecretoDeLaBaseDeDatos",
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
      "Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:mercadofresco/produccion/rds/mfadmin-??????"
    },
    {
      "Sid": "DescifrarConLaClaveDeDatos",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "secretsmanager.eu-west-1.amazonaws.com"
        }
      }
    },
    {
      "Sid": "LeerLaConfiguracionDeProduccion",
      "Effect": "Allow",
      "Action": ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
      "Resource": "arn:aws:ssm:eu-west-1:111122223333:parameter/mercadofresco/produccion/*"
    }
  ]
}

Tres detalles que importan:

  • Los seis interrogantes finales del ARN. Secrets Manager añade un sufijo aleatorio de 6 caracteres al ARN de cada secreto (...mfadmin-AbCdEf). Si escribes el ARN sin sufijo, la política no coincide con nada. -?????? usa el comodín de un carácter para cubrirlo. Es el error número uno con Secrets Manager y su síntoma —AccessDenied con una política aparentemente correcta— desconcierta a cualquiera. La alternativa es terminar en -*, algo más laxa.
  • kms:ViaService limitado a secretsmanager. La tienda puede descifrar a través de Secrets Manager y de S3 (por la declaración de 04-02), pero no puede llamar a kms:Decrypt directamente contra cualquier blob que consiga.
  • ssm:GetParametersByPath acotado a /mercadofresco/produccion/*. La tienda de producción no ve la configuración de desarrollo, y viceversa.

Y lo que no hay: secretsmanager:ListSecrets, que revelaría los nombres de todos los secretos de la cuenta, ni acceso a ningún otro secreto. La tienda lee exactamente uno. Si mañana se añade el secreto de la pasarela de pago, hará falta una decisión explícita para concederlo.

La migración: sacar la contraseña del user data

Estado inicial del user data de lt-mercadofresco-tienda, tal como quedó en 02-01:

#!/bin/bash
cat > /etc/mercadofresco/tienda.conf <<'EOF'
DB_HOST=mercadofresco-pedidos.abc123.eu-west-1.rds.amazonaws.com
DB_USER=mfadmin
DB_PASSWORD=ContrasenaEnClaroQueTodosVen
EOF
systemctl start mercadofresco-tienda

El plan de migración, en orden y sin cortar el servicio:

flowchart TD
    A["1. Crear el secreto con la<br/>contrasena ACTUAL"] --> B["2. Crear los parametros<br/>de configuracion"]
    B --> C["3. Anadir permisos a<br/>rol-mercadofresco-tienda"]
    C --> D["4. Cambiar el codigo:<br/>leer del secreto con reintento"]
    D --> E["5. Nueva version de<br/>lt-mercadofresco-tienda SIN la clave"]
    E --> F["6. Renovacion progresiva<br/>del ASG"]
    F --> G["7. Verificar que todo funciona"]
    G --> H["8. Rotacion manual de prueba"]
    H --> I["9. Activar rotacion cada 30 dias"]
    I --> J["10. Limpiar: .env, historial,<br/>ficheros de configuracion"]

El paso 1 es crucial y contraintuitivo: se guarda la contraseña actual, sin cambiarla. Así los pasos 1 a 7 no alteran nada funcionalmente y se pueden revertir en cualquier momento. La contraseña solo cambia en el paso 8, cuando todo lo demás ya está probado.

Nuevo user data, sin una sola credencial:

#!/bin/bash
set -euo pipefail

REGION=eu-west-1
RUTA=/mercadofresco/produccion

# Configuración no sensible desde Parameter Store
DB_HOST=$(aws ssm get-parameter --region $REGION \
  --name "$RUTA/basedatos/punto-enlace" --query 'Parameter.Value' --output text)
DB_USER=$(aws ssm get-parameter --region $REGION \
  --name "$RUTA/basedatos/usuario" --query 'Parameter.Value' --output text)

cat > /etc/mercadofresco/tienda.conf <<EOF
DB_HOST=$DB_HOST
DB_USER=$DB_USER
SECRETO_BD=mercadofresco/produccion/rds/mfadmin
REGION=$REGION
EOF

systemctl start mercadofresco-tienda

La contraseña no se escribe en ningún fichero: el user data deja el nombre del secreto, y la aplicación lo lee en memoria al arrancar y cuando lo necesita. El user data, que es visible desde los metadatos de la instancia, ya no contiene nada aprovechable.

Despliegue con renovación progresiva, como en 02-01:

aws ec2 create-launch-template-version \
  --launch-template-name lt-mercadofresco-tienda \
  --source-version '$Latest' \
  --launch-template-data "{\"UserData\":\"$(base64 -w0 /tmp/user-data-nuevo.sh)\"}" \
  --profile mercadofresco-dev

aws autoscaling start-instance-refresh \
  --auto-scaling-group-name asg-mercadofresco-tienda \
  --preferences '{"MinHealthyPercentage": 90, "InstanceWarmup": 120}' \
  --profile mercadofresco-dev

Y por fin el paso 10, la limpieza, que es la parte que se olvida:

  • Borrar /etc/mercadofresco/tienda.conf de las instancias antiguas (que se destruyen solas con la renovación).
  • Borrar el .env de los portátiles y reescribir el historial de Git si llegó a subirse.
  • Purgar el historial de comandos de las sesiones donde se usó.
  • Y, sobre todo, rotar: la contraseña anterior debe considerarse comprometida.

Qué no hay que guardar aquí

Ni Secrets Manager ni Parameter Store son un almacén de propósito general:

No guardes Por qué Dónde va
Claves de acceso de IAM de un rol No deberían existir Roles y credenciales temporales (04-01)
Ficheros o binarios Límite de 64 KB / 4 KB, y coste S3 cifrado con KMS
Datos personales de clientes No es una base de datos RDS cifrada
Certificados TLS públicos Hay un servicio dedicado y gratuito ACM
Claves privadas SSH de larga vida Deberían ser efímeras EC2 Instance Connect, Session Manager
Configuración que cambia por petición Latencia y cuotas Caché en memoria o base de datos

Un caso que merece mención: para RDS existen además las credenciales gestionadas (--manage-master-user-password), donde RDS crea y rota el secreto automáticamente sin que configures nada. Es lo más sencillo si la base de datos es nueva; MercadoFresco no lo usa porque mercadofresco-pedidos ya existía, pero para una instancia nueva es la primera opción a considerar.

Auditoría del acceso a secretos

Cada llamada a GetSecretValue queda registrada en CloudTrail (05-03), con quién, cuándo, desde qué IP y con qué resultado. Es la propiedad que ningún fichero .env puede ofrecer:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=GetSecretValue \
  --start-time 2026-08-01T00:00:00Z \
  --query 'Events[].[EventTime,Username,CloudTrailEvent]' \
  --output text \
  --profile mercadofresco-dev

Lo que Marta vigila:

  • Lecturas desde principales inesperados —cualquiera que no sea rol-mercadofresco-tienda o el rol de rotación.
  • Lecturas fuera de horario o desde IP desconocidas.
  • Un pico de lecturas, que sugiere que alguien está enumerando secretos.
  • DeleteSecret o PutResourcePolicy, que deberían ser rarísimos.

En 05-01 convertiremos esto en una alarma real de CloudWatch que avise al tema alertas-mercadofresco; por ahora basta con saber que el rastro existe.

Detección de secretos filtrados en el repositorio

Prevenir es más barato que rotar. Dos capas:

En el portátil, antes del commit. git-secrets instala un hook que rechaza commits con patrones de credenciales:

git secrets --install
git secrets --register-aws
# Patrones propios de MercadoFresco
git secrets --add 'mfadmin.*password'
git secrets --add --allowed 'ContrasenaFicticia'   # excepción para la documentación
git secrets --scan-history

--scan-history revisa todo el historial, no solo el estado actual. Ejecutarlo la primera vez en un repositorio con años de vida suele dar sorpresas.

En el pipeline. El escaneo local se puede saltar; el del pipeline no. Herramientas como gitleaks, trufflehog o el escaneo de secretos de la plataforma se ejecutan en cada git push y detienen la construcción. Lo integraremos en la fase de compilación en 08-02, junto con las pruebas y el análisis estático.

Y una regla de higiene que vale más que cualquier herramienta:

.env
.env.*
*.pem
*.key
credentials
config/secrets.yml

Coste, cuotas y limpieza

Concepto Precio
Secrets Manager: por secreto 0,40 USD al mes (prorrateado por hora)
Secrets Manager: llamadas a la API 0,05 USD por cada 10.000
Secrets Manager: réplica en otra región 0,40 USD adicionales por región
Parameter Store estándar Gratis (10.000 parámetros, 40 peticiones/s)
Parameter Store avanzado 0,05 USD por parámetro y mes + 0,05 USD/10.000 llamadas
Rotación (invocaciones de Lambda) Precio normal de Lambda: céntimos al año
KMS Ya contabilizado en 04-02

Cálculo para MercadoFresco:

Concepto Cantidad Coste mensual
Secretos (mfadmin + pasarela de pago) 2 0,80 USD
Llamadas a Secrets Manager (6 instancias × 24 lecturas/día × 30) ~4.320 0,02 USD
Parámetros en Parameter Store estándar 25 0,00 USD
Llamadas a Parameter Store (arranques de instancia) ~2.000 0,00 USD
Lambda de rotación 1 invocación ×4 pasos al mes ~0,00 USD
Total 0,82 USD/mes

Menos de un dólar al mes por eliminar el riesgo más común y más caro de una infraestructura. Compáralo con el coste de una brecha notificable bajo el RGPD y la discusión se cierra sola.

Limpieza. Un secreto no se borra de inmediato: hay un periodo de recuperación de entre 7 y 30 días, por la misma razón que en KMS.

# Programar el borrado con 30 días de recuperación (recomendado)
aws secretsmanager delete-secret \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --recovery-window-in-days 30 --profile mercadofresco-dev

# Arrepentirse
aws secretsmanager restore-secret \
  --secret-id mercadofresco/produccion/rds/mfadmin --profile mercadofresco-dev

# Borrado inmediato: irreversible, solo en pruebas
aws secretsmanager delete-secret \
  --secret-id secreto-de-prueba --force-delete-without-recovery --profile mercadofresco-dev

# Parámetros: borrado inmediato, sin red de seguridad
aws ssm delete-parameter --name "/mercadofresco/desarrollo/tienda/prueba" \
  --profile mercadofresco-dev

Cuidado con la asimetría: los parámetros se borran al instante y no se recuperan. Antes de borrar en bloque una ruta de Parameter Store, exporta una copia.

Errores Comunes y Consejos

Olvidar el sufijo de seis caracteres en el ARN del secreto. Es el error más frecuente con Secrets Manager. El ARN real es ...secret:mercadofresco/produccion/rds/mfadmin-AbCdEf, y una política que termine en mfadmin sin más no coincide con nada. Usa -?????? o -*.

Cachear el secreto sin invalidar la caché ante un error de autenticación. Cada rotación produce entonces una tanda de errores. El reintento único es cuatro líneas de código y lo resuelve.

Poner la Lambda de rotación fuera de la VPC. No podrá conectarse a mercadofresco-pedidos y la rotación fallará en setSecret, que es el peor momento posible. Debe estar en las subredes de aplicación y sg-mercadofresco-basedatos debe aceptar su grupo de seguridad.

Olvidar la salida a la API de Secrets Manager desde la subred privada. La Lambda se queda colgada hasta agotar el tiempo de espera y el mensaje de error no dice nada útil. Necesita NAT o un endpoint de interfaz.

Cambiar los nombres de los campos del JSON del secreto. La función de rotación gestionada espera engine, host, port, dbname, username, password. Si los renombras «para que se lean mejor», la rotación no funciona.

Activar la rotación en producción sin haberla probado. Pruébala primero en una copia de la base de datos, y con --rotate-immediately para no esperar 30 días a descubrir que algo falla.

Guardar todo en Secrets Manager porque es «el sitio seguro». 25 valores son 10 USD al mes en lugar de 0,80. La configuración no sensible va a Parameter Store.

Programar la rotación un jueves por la noche. El pico de MercadoFresco es el viernes. Rota los martes de madrugada, cuando hay dos días laborables por delante para reaccionar.

Consejo: usa una convención de nombres desde el primer día. <proyecto>/<entorno>/<servicio>/<recurso> permite escribir políticas por prefijo y ver de un vistazo a qué entorno pertenece cada cosa.

Consejo: guarda el punto de enlace dentro del secreto además de en Parameter Store. Es lo que espera la rotación gestionada, y evita que la aplicación tenga que combinar dos fuentes para abrir una conexión.

Consejo: pon una alarma sobre NextRotationDate. Una rotación que lleva 45 días sin ejecutarse en un secreto configurado a 30 es una rotación rota que nadie ha visto.

Consejo: audita cada trimestre quién puede leer cada secreto. El acceso crece solo, y list-secrets con las políticas de recurso da la foto en un minuto.

Ejercicios

Ejercicio 1: repartir valores entre los dos servicios

MercadoFresco integra una pasarela de pago y un proveedor de mensajería. Aparecen estos ocho valores:

  1. Clave de API de producción de la pasarela de pago (rota cada 90 días por política del proveedor).
  2. URL base de la API de la pasarela: https://api.pasarela.example/v2.
  3. Identificador de comercio: MF-2026-ES-0042.
  4. Secreto de firma de los webhooks entrantes de la pasarela.
  5. Tiempo máximo de espera de la pasarela: 8 segundos.
  6. Usuario y contraseña del SFTP del proveedor de mensajería.
  7. Lista de códigos postales con reparto en 24 h.
  8. Contraseña de una segunda base de datos de informes, de solo lectura.

Decide para cada uno el servicio, el tipo y el nombre completo, siguiendo la convención de MercadoFresco, y justifica los tres casos que consideres más discutibles. Calcula el coste mensual resultante.

Ejercicio 2: diagnosticar una rotación que falla

Marta activa la rotación de mercadofresco/produccion/rds/mfadmin. La rotación se ejecuta y falla. En los registros de la Lambda aparece:

[ERROR] setSecret: Unable to connect to database with previous secret
of secret arn mercadofresco/produccion/rds/mfadmin-AbCdEf

Y en Secrets Manager, describe-secret muestra una versión con etapa AWSPENDING que lleva ahí tres horas, mientras AWSCURRENT sigue apuntando a la versión antigua.

Responde: (a) ¿en qué paso del ciclo ha fallado y qué significa exactamente ese mensaje?; (b) enumera en orden las cuatro causas más probables y cómo comprobar cada una; (c) ¿está la aplicación caída en este momento y por qué?; (d) ¿qué hay que limpiar antes de reintentar?

Ejercicio 3: escribir la política del rol de rotación

Escribe la política de permisos completa del rol rol-rotacion-mfadmin, que asume la Lambda de rotación. Debe poder: leer y escribir el secreto mercadofresco/produccion/rds/mfadmin (y solo ese), mover etapas de versión, generar contraseñas aleatorias, usar alias/mercadofresco-datos a través de Secrets Manager, escribir sus propios registros y funcionar dentro de vpc-mercadofresco.

Soluciones

Solución 1

# Valor Servicio Tipo Nombre
1 Clave de API de pago Secrets Manager mercadofresco/produccion/pasarela/clave-api
2 URL de la pasarela Parameter Store String /mercadofresco/produccion/pasarela/url-base
3 Identificador de comercio Parameter Store String /mercadofresco/produccion/pasarela/id-comercio
4 Secreto de firma de webhooks Secrets Manager mercadofresco/produccion/pasarela/secreto-webhook
5 Tiempo de espera Parameter Store String /mercadofresco/produccion/pasarela/tiempo-espera-segundos
6 Credenciales SFTP Secrets Manager mercadofresco/produccion/mensajeria/sftp
7 Códigos postales de 24 h Parameter Store StringList /mercadofresco/produccion/reparto/codigos-postales-24h
8 Contraseña de la BD de informes Secrets Manager mercadofresco/produccion/rds/informes

Casos discutibles:

  • El identificador de comercio (3). Parece sensible porque identifica a la empresa ante el proveedor, pero por sí solo no autoriza nada: sin la clave de API no sirve de nada, y aparece en las facturas. Es configuración. Si el proveedor lo tratara como credencial, cambiaría la decisión.
  • El secreto de firma de webhooks (4). Podría parecer configuración porque no «abre» nada: solo se usa para verificar firmas entrantes. Pero quien lo conozca puede falsificar notificaciones de pago confirmado, es decir, conseguir pedidos gratis. Es un secreto de primer orden.
  • Los códigos postales (7). No es secreto en absoluto —está publicado en la web— pero es configuración que cambia a menudo y conviene tenerla fuera del código para poder ampliar la cobertura sin desplegar. StringList con get-parameter y --query 'Parameter.Value' devuelve la cadena separada por comas.

Coste: 4 secretos nuevos × 0,40 = 1,60 USD/mes, más los 2 anteriores = 2,40 USD/mes en Secrets Manager. Parameter Store: 4 parámetros más, 0 USD. Si se hubieran metido los ocho valores en Secrets Manager: 3,20 USD solo por estos ocho, cuatro veces más, sin ninguna ventaja para los cuatro que no rotan.

Solución 2

(a) Ha fallado en el paso setSecret, el segundo del ciclo. El mensaje concreto —«unable to connect with previous secret»— dice que la Lambda ha intentado conectarse a la base de datos usando la credencial actual (AWSCURRENT) para poder ejecutar el ALTER USER, y no ha podido. Es decir: el problema no es la contraseña nueva, es que la Lambda no consigue autenticarse con la vieja.

(b) Cuatro causas, en orden de probabilidad:

  1. Red. La Lambda no está en vpc-mercadofresco, o está en las subredes equivocadas, o sg-mercadofresco-basedatos no acepta el 5432 desde su grupo de seguridad. Comprobación: aws lambda get-function-configuration --query 'VpcConfig' y revisar las reglas de entrada del SG de la base de datos con describe-security-groups.
  2. La contraseña de AWSCURRENT no es la real. Si al crear el secreto se puso una contraseña inventada en lugar de la que realmente tiene mfadmin, la Lambda no puede autenticarse. Comprobación: intentar conectarse manualmente con psql usando el valor de AWSCURRENT desde un bastión.
  3. Los campos del JSON no son los esperados. Si falta dbname, port o engine, o si están escritos de otra forma, la Lambda construye mal la cadena de conexión. Comprobación: leer el secreto y comparar campo a campo con el formato esperado.
  4. El punto de enlace es incorrecto —por ejemplo, quedó el antiguo tras la migración a la instancia cifrada de 04-02, que cambió el DNS. Comprobación: aws rds describe-db-instances --query 'DBInstances[].Endpoint.Address' y comparar.

(c) No, la aplicación no está caída. Y esa es exactamente la virtud del diseño de etapas: AWSCURRENT sigue apuntando a la versión antigua, que es la que sigue funcionando en la base de datos, y es la que devuelve get-secret-value. La rotación fallida es un problema operativo urgente, pero no un incidente de servicio. Si hubiera fallado después de setSecret —con la contraseña ya cambiada en la base de datos pero AWSCURRENT sin promocionar— entonces sí habría corte.

(d) Antes de reintentar hay que eliminar la versión colgada en AWSPENDING, o Secrets Manager intentará continuar la rotación a medias en lugar de empezar de cero:

aws secretsmanager describe-secret --secret-id mercadofresco/produccion/rds/mfadmin \
  --query 'VersionIdsToStages' --profile mercadofresco-dev

aws secretsmanager update-secret-version-stage \
  --secret-id mercadofresco/produccion/rds/mfadmin \
  --version-stage AWSPENDING \
  --remove-from-version-id <id-de-la-version-pendiente> \
  --profile mercadofresco-dev

Después, corregir la causa, probar en desarrollo y reintentar con --rotate-immediately.

Solución 3

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GestionarSoloEsteSecreto",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:DescribeSecret",
        "secretsmanager:GetSecretValue",
        "secretsmanager:PutSecretValue",
        "secretsmanager:UpdateSecretVersionStage"
      ],
      "Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:mercadofresco/produccion/rds/mfadmin-??????",
      "Condition": {
        "StringEquals": {
          "secretsmanager:resource/AllowRotationLambdaArn":
            "arn:aws:lambda:eu-west-1:111122223333:function:rotacion-mfadmin"
        }
      }
    },
    {
      "Sid": "GenerarContrasenasAleatorias",
      "Effect": "Allow",
      "Action": "secretsmanager:GetRandomPassword",
      "Resource": "*"
    },
    {
      "Sid": "UsarLaClaveSoloViaSecretsManager",
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
      "Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "secretsmanager.eu-west-1.amazonaws.com"
        }
      }
    },
    {
      "Sid": "Registros",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/rotacion-mfadmin:*"
    },
    {
      "Sid": "InterfazDeRedEnLaVPC",
      "Effect": "Allow",
      "Action": [
        "ec2:CreateNetworkInterface",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DeleteNetworkInterface"
      ],
      "Resource": "*"
    }
  ]
}

Justificación de cada bloque:

  • El secreto, uno solo, con el sufijo -?????? y la condición secretsmanager:resource/AllowRotationLambdaArn, que asegura que este rol solo puede operar sobre secretos cuya rotación está asignada precisamente a esta Lambda. Es una defensa extra frente a que alguien reutilice el rol.
  • GetRandomPassword no admite ARN de recurso; por eso "Resource": "*". No es peligroso: genera cadenas aleatorias y no da acceso a nada.
  • KMS acotado con kms:ViaService a Secrets Manager: el rol no puede descifrar objetos de S3 con la misma clave.
  • Los registros del propio grupo de la función, con el :* final.
  • Las tres acciones de ec2: son obligatorias para cualquier Lambda dentro de una VPC: crea y destruye una interfaz de red elástica en cada arranque en frío. No admiten ARN concreto porque la interfaz aún no existe cuando se comprueba el permiso. Es una de las poquísimas veces en que "Resource": "*" es inevitable, y se puede acotar con condiciones sobre ec2:Subnet y ec2:SecurityGroup si se quiere apurar.

No aparece secretsmanager:ListSecrets, que revelaría el inventario de secretos de la cuenta, ni ninguna acción de RDS: la Lambda cambia la contraseña conectándose por SQL, no por la API de AWS.

Conclusión

La contraseña de mfadmin ya no está donde no debía. Ha salido del user data de lt-mercadofresco-tienda, del fichero de configuración de las instancias, del .env de Luis y del historial de comandos, y vive en mercadofresco/produccion/rds/mfadmin, un secreto cifrado con alias/mercadofresco-datos, protegido por una política de recurso que deniega su lectura a todo principal que no sea rol-mercadofresco-tienda, marta o el rol de rotación, y que rota sola cada 30 días, los martes de madrugada, sin que nadie tenga que acordarse ni asumir el riesgo de hacerlo a mano.

Entiendes por qué la rotación no corta el servicio: el mecanismo de etapas de versiónAWSCURRENT, AWSPENDING, AWSPREVIOUS— y el ciclo de cuatro pasos createSecretsetSecrettestSecretfinishSecret, donde la contraseña nueva se crea, se aplica y se prueba antes de promocionarse, y donde un fallo en testSecret aborta todo dejando la vieja intacta. Sabes también dónde está la ventana peligrosa —entre setSecret y finishSecret— y por qué la aplicación debe invalidar la caché y reintentar una vez ante un error de autenticación: cuatro líneas que convierten la rotación en algo invisible.

Distingues configuración de secreto, y con ello cuál de los dos servicios usar. La configuración de MercadoFresco vive en Parameter Store bajo /mercadofresco/produccion/..., con jerarquía por rutas que permite cargarla entera con una llamada y escribir permisos por prefijo, con versiones y etiquetas para volver atrás sin desplegar, y gratis en el nivel estándar. Los secretos viven en Secrets Manager, a 0,40 USD cada uno. El total de la lección es 0,82 USD al mes, y sabes argumentar por qué meterlo todo en el servicio caro habría costado diez veces más sin aportar nada.

En lo práctico sabes leer un secreto desde boto3 con caché e invalidación, desde Lambda con la extensión de parámetros y secretos que cachea entre invocaciones, desde EC2 con el rol y sin configurar nada, y desde CloudFormation con referencias dinámicas {{resolve:secretsmanager:...}} que nunca escriben el valor en la plantilla (09-01); en ECS se inyectan en la definición de tarea y lo veremos en 10-01. Conoces los permisos mínimos para leer un secreto —incluido el sufijo de seis caracteres del ARN, el error que más tiempo hace perder— y sabes que el rastro de cada lectura queda en CloudTrail (05-03) y que el escaneo de secretos filtrados se automatiza en el pipeline (08-02).

MercadoFresco tiene ya la casa en orden hacia dentro: identidades mínimas, datos cifrados y credenciales rotadas. Pero todo eso protege de amenazas que pasan por la puerta de la API. Queda la otra mitad del problema, la que llega por la puerta de delante y no necesita ninguna credencial. Recuerda la regla DENY numerada 50 con la que en 03-02 bloqueamos aquella IP que hacía miles de peticiones por minuto: funcionó porque era una dirección. Si mañana llegan peticiones desde diez mil direcciones distintas repartidas por todo el mundo, esa regla no sirve de nada, la NACL tiene un límite duro de entradas, y el grupo de Auto Scaling reaccionará como está diseñado para reaccionar —arrancando instancias— convirtiendo el ataque en una factura. En la lección 04-04, «AWS Shield», veremos qué es realmente un ataque de denegación de servicio distribuida, en qué se diferencian los ataques volumétricos de los de capa de aplicación, qué protección tienes ya activada y gratis en CloudFront, Route 53 y el ALB, qué añade Shield Advanced por 3.000 USD al mes —y si eso tiene sentido para una empresa del tamaño de MercadoFresco—, y cómo se distingue en caliente un ataque de un pico legítimo de un viernes por la tarde.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados