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:
- En el
user datade la plantilla de lanzamientolt-mercadofresco-tienda, que se puede leer desde el servicio de metadatos de cualquier instancia de la tienda. - En
/etc/mercadofresco/tienda.confdentro de cada instancia. - En un fichero
.envque Luis se pasó a sí mismo por chat cuando montó el entorno de desarrollo. - En el historial de comandos de la sesión en la que la usó por primera vez.
- 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
- Por qué las credenciales en el código son un problema estructural
- Qué pasa exactamente cuando un secreto llega a Git
- Configuración y secreto no son lo mismo
- Parameter Store: tipos de parámetro
- Jerarquía por rutas, versiones y niveles
- La configuración de MercadoFresco en Parameter Store
- Secrets Manager: secretos, versiones y etapas
- Cifrado con KMS y políticas de recurso
- Rotación automática: el ciclo de cuatro pasos
- Configurar la rotación de
mercadofresco/produccion/rds/mfadmin - Usuario único frente a alternancia de dos usuarios
- Tabla comparativa y la recomendación para MercadoFresco
- Consumo desde la aplicación: boto3 y caché
- Consumo desde Lambda, EC2, ECS y CloudFormation
- Permisos mínimos para leer un secreto
- La migración: sacar la contraseña del
user data - Qué no hay que guardar aquí
- Auditoría del acceso a secretos
- Detección de secretos filtrados en el repositorio
- 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 pushEsto 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:
- Rotar la credencial inmediatamente. Considérala comprometida desde el momento en que se escribió. Este paso es obligatorio y no es opcional.
- Reescribir el historial (
git filter-repo, BFG) y forzar el push. Esto rompe los clones de todo el equipo, así que hay que coordinarlo. - Buscar en los registros si la credencial se usó desde algún sitio inesperado.
- 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-devNotas sobre SecureString:
- Si no indicas
--key-id, se usa la clave gestionada por AWSalias/aws/ssm, que es gratuita pero no controlable. Para valores que importan, usa tu clave. - Para leer el valor descifrado hace falta
--with-decryptiony permisokms:Decryptsobre 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-devJerarquí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-devY 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-devVolver 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 | Sí |
| 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-devLa 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:
- Cachear el secreto (una lectura por petición es carísima y lenta).
- 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-devLos 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:
- Acceso de red a la base de datos. Debe estar en la VPC
vpc-mercadofresco, en las subredessnet-mercadofresco-app-a/-b, ysg-mercadofresco-basedatosdebe 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. - 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 parasecretsmanager, del mismo modo que hicimos convpce-mercadofresco-s3en 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. - Permisos. Su rol necesita
secretsmanager:GetSecretValue,PutSecretValue,UpdateSecretVersionStage,DescribeSecret,GetRandomPassword, máskms:Decryptykms:GenerateDataKeysobrealias/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-devPrueba 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 | Sí, 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 | Sí | No |
| Acceso entre cuentas | Sí | No directamente |
| Jerarquía por rutas | No (pero se usan / por convención) |
Sí, con lectura en bloque |
| Versiones | Sí, con etapas | Sí, con etiquetas |
| Integración con RDS | Nativa, credenciales gestionadas | No |
| Réplica entre regiones | Sí, 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-devConsumo 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 reintentaEl 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:
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 —AccessDeniedcon una política aparentemente correcta— desconcierta a cualquiera. La alternativa es terminar en-*, algo más laxa. kms:ViaServicelimitado asecretsmanager. La tienda puede descifrar a través de Secrets Manager y de S3 (por la declaración de 04-02), pero no puede llamar akms:Decryptdirectamente contra cualquier blob que consiga.ssm:GetParametersByPathacotado 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-tiendaEl 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-tiendaLa 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-devY por fin el paso 10, la limpieza, que es la parte que se olvida:
- Borrar
/etc/mercadofresco/tienda.confde las instancias antiguas (que se destruyen solas con la renovación). - Borrar el
.envde 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-devLo que Marta vigila:
- Lecturas desde principales inesperados —cualquiera que no sea
rol-mercadofresco-tiendao el rol de rotación. - Lecturas fuera de horario o desde IP desconocidas.
- Un pico de lecturas, que sugiere que alguien está enumerando secretos.
DeleteSecretoPutResourcePolicy, 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:
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-devCuidado 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:
- Clave de API de producción de la pasarela de pago (rota cada 90 días por política del proveedor).
- URL base de la API de la pasarela:
https://api.pasarela.example/v2. - Identificador de comercio:
MF-2026-ES-0042. - Secreto de firma de los webhooks entrantes de la pasarela.
- Tiempo máximo de espera de la pasarela: 8 segundos.
- Usuario y contraseña del SFTP del proveedor de mensajería.
- Lista de códigos postales con reparto en 24 h.
- 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-AbCdEfY 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.
StringListconget-parametery--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:
- Red. La Lambda no está en
vpc-mercadofresco, o está en las subredes equivocadas, osg-mercadofresco-basedatosno 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 condescribe-security-groups. - La contraseña de
AWSCURRENTno es la real. Si al crear el secreto se puso una contraseña inventada en lugar de la que realmente tienemfadmin, la Lambda no puede autenticarse. Comprobación: intentar conectarse manualmente conpsqlusando el valor deAWSCURRENTdesde un bastión. - Los campos del JSON no son los esperados. Si falta
dbname,portoengine, 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. - 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-devDespué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ónsecretsmanager: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. GetRandomPasswordno admite ARN de recurso; por eso"Resource": "*". No es peligroso: genera cadenas aleatorias y no da acceso a nada.- KMS acotado con
kms:ViaServicea 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 sobreec2:Subnetyec2:SecurityGroupsi 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ón
—AWSCURRENT, AWSPENDING, AWSPREVIOUS— y el ciclo de cuatro pasos createSecret →
setSecret → testSecret → finishSecret, 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
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
