Durante mucho tiempo, los servicios han dependido del modelo tradicional de autenticación username/password para conectarse a bases de datos. La mayoría de los frameworks de desarrollo funcionan muy bien con este patrón, proporcionando mecanismos integrados para leer credenciales desde variables de entorno, archivos de configuración o configuraciones a nivel de clase. Esto crea una experiencia de desarrollador simple y fluida — pero hay un trade-off: el verdadero desafío no es usar las credenciales, sino distribuirlas y protegerlas.
En nuestro caso, seguimos un enfoque común:
- Las credenciales se almacenaban en AWS Secrets Manager.
- El secreto se obtenía al inicio del servicio.
- Amazon RDS manejaba la rotación periódica del secreto y soportaba rotación bajo demanda cuando era necesario.
Esta configuración funcionaba “bien”… hasta que encontramos un error mientras operábamos a baja escala. Fue un incidente aislado:
SQLException. Message: Connect failed to authenticate: reached max connection retries
Solo con el mensaje de error no teníamos mucho contexto. Para entender qué pasó, correlacionamos eventos de CloudTrail, registros de aplicaciones y el historial de tareas de ECS para reconstruir la secuencia de eventos:
sequenceDiagram
participant ECS as ECS Cluster
participant Pod as Service Pod
participant Secrets as AWS Secrets Manager
participant RDS as Amazon RDS
ECS->>Pod: El servicio escala por carga elevada
activate Secrets
Note over Secrets: Inicia rotación de contraseña
loop Reintenta hasta alcanzar el máximo de intentos
Pod->>Secrets: Obtiene credenciales
Secrets-->>Pod: Devuelve credenciales obsoletas
Pod->>RDS: Intenta conectar
RDS-->>Pod: Autenticación fallida
end
Pod-->>ECS: Error en startup:<br/>máximo de reintentos alcanzado
Note over Secrets: Finaliza la rotación:<br/>DB y secreto sincronizados
deactivate Secrets
ECS->>Pod: Recreación del Pod
Después de revisar la línea de tiempo, destacaron varias observaciones clave:
- El error provenía de un solo pod; la salud general del servicio permaneció “ok”.
- Ningún otro servicio se vio afectado.
- Durante la rotación, hay una breve ventana donde la contraseña de la base de datos y el secreto almacenado pueden estar desincronizados.
- A mayor escala, las obtenciones frecuentes de secretos, reintentos de conexión y eventos de rotación pueden aumentar el riesgo de alcanzar límites de tasa de API, agregar costos e introducir throttling.
- Lo más importante: esto ocurrió durante un evento de escalado. El servicio necesitaba capacidad adicional para manejar la carga, pero el nuevo pod falló al iniciarse y tuvo que ser reemplazado — retrasando exactamente el momento en que se necesitaban recursos extra.
Dadas estas observaciones, el equipo decidió revisar el flujo de autenticación. Algunas opciones propuestas incluyeron:
- Extender el intervalo de rotación (por ejemplo, de días a semanas o meses) para reducir la frecuencia de esta ventana.
- Mejorar la lógica de reintentos con backoff exponencial o jitter.
Ambas eran soluciones razonables, pero se sentían como parches. Extender los intervalos de rotación debilita la postura de seguridad, mientras que reintentos más agresivos dificultan distinguir problemas de rotación de problemas reales de configuración o conectividad — especialmente a medida que crece el número de bases de datos y usuarios de aplicaciones.
Las credenciales son simples, pero nunca gratuitas
La autenticación tradicional de bases de datos tiene algunos problemas bien conocidos:
- Credenciales de larga duración aumentan el radio de explosión
- La rotación agrega complejidad operativa. RDS puede rotar la contraseña maestra, pero para usuarios de base de datos personalizados todavía necesitamos manejar la lógica de rotación nosotros mismos.
- La distribución de secretos se convierte en parte del camino de ejecución
- Depurar fallos de autenticación a menudo involucra múltiples sistemas
Incluso con servicios gestionados como Secrets Manager, las credenciales siguen siendo artefactos estáticos que debemos obtener, almacenar en caché y proteger.
La complejidad en nuestros sistemas es inevitable, pero dependiendo del contexto (el equipo, la arquitectura, las herramientas, etc.) podemos moverla para fortalecer algunas partes de nuestros sistemas.
La pregunta que quería responder era simple:
¿Podemos eliminar las contraseñas estáticas de base de datos con fricción mínima?
De credenciales a roles
El Control de Acceso Basado en Roles (RBAC) no es nuevo, empuja la complejidad de las credenciales a una capa inferior que las herramientas manejan de forma transparente. Es un patrón común en infraestructura, pero no ampliamente usado en desarrollo (creo que esto se debe a que en general aún hay una barrera entre equipos de infra y desarrollo, pero quizás eso sea para otro post).
En nuestro caso, la autenticación de base de datos IAM reemplaza contraseñas estáticas con tokens de autenticación de corta duración, generados bajo demanda. La complejidad se mueve de la distribución y rotación de contraseñas a la capa de IAM.
sequenceDiagram
participant Pod as Service Pod
participant RDS as Amazon RDS
participant IAM
Pod-->>Pod: Genera token IAM (SigV4)
Pod->>RDS: Conecta usando usuario + token
RDS->>IAM: Valida token y permisos
IAM-->>RDS: Firma válida
RDS-->>Pod: Conexión exitosa
Sobre el papel, ofrece:
- No hay contraseñas de base de datos almacenadas
- Expiración automática
- Control de acceso basado en IAM
- Revocación de credenciales más fácil
Pero siempre hay un pero.
La preocupación común que escucho es:
Suena bien, pero ¿no es más lento o complejo?
La configuración del experimento
Construí una pequeña aplicación en Java que se conecta a RDS de dos maneras:
- Enfoque tradicional
- Obtener credenciales de Secrets Manager
- Usar JDBC con username/password
- Autenticación IAM
- Generar un token de autenticación usando el SDK de AWS
- Usar el token como contraseña de la base de datos
El objetivo no era optimizar el rendimiento al extremo — solo observar comportamiento de conexión realista.
Midiendo el costo
Autenticación basada en secretos
Secret fetch time: 1260 ms
Connection time: 780 ms
SecretsManagerClient closed.
El overhead total se divide entre:
- Llamada de red a Secrets Manager
- Establecimiento de conexión a la base de datos
Autenticación basada en IAM
Token generation time: 877 ms
Connection time (JDBC connect): 760 ms
Connection closed.
Aquí, el costo se desplaza ligeramente:
- La generación de token reemplaza la obtención de secreto
- El tiempo de conexión a la base de datos permanece aproximadamente igual
Qué cambió
- No hay contraseñas de base de datos almacenadas
- No hay lógica de rotación de secretos
- Control de acceso movido a roles
- Auditoría y revocación más fáciles
Desde un punto de vista de seguridad y operativo, esto fue una clara victoria para nuestro caso de uso.
Qué no cambió mucho
- La latencia de conexión a la base de datos se mantuvo similar
- Los cambios en el código de la aplicación fueron mínimos
- Los desarrolladores todavía pasan un username y “password” (ahora un token)
Esto fue importante: adoptar autenticación IAM no requirió un cambio en el modelo mental para los equipos de aplicaciones.
El impacto en el código (menor de lo esperado)
Otra preocupación es:
Esto requerirá un gran refactor.
En realidad, el cambio fue localizado:
getPropertyFromSecret(prop) {
SecretsManagerClient smClient = SecretsManagerClient.builder().region("ap-northeast-1").credentialsProvider(DefaultCredentialsProvider.create()).build();
GetSecretValueRequest req = GetSecretValueRequest.builder().secretId(DB_SECRET_NAME).build();
GetSecretValueResponse resp = smClient.getSecretValue(req);
JSONObject json = new JSONObject(resp.secretString());
return json.getString(prop);
}
generateAuthToken() {
RdsUtilities utilities = RdsUtilities.builder().region("ap-northeast-1").credentialsProvider(DefaultCredentialsProvider.create()).build();
GenerateAuthenticationTokenRequest tokenRequest = GenerateAuthenticationTokenRequest.builder().hostname(DB_HOST).port(DB_PORT).username(DB_USER).build();
return utilities.generateAuthenticationToken(tokenRequest);
}
// String password = getPropertyFromSecret("password");
String password = generateAuthToken();
Connection conn = DriverManager.getConnection(jdbcUrl, DB_USER, password);
La diferencia principal es de dónde viene la contraseña, no cómo funciona la conexión.
La adopción importa más que la elegancia
Desde una perspectiva puramente técnica, la autenticación IAM no es revolucionaria.
Lo que la hace valiosa es esta combinación:
- Riesgo de credenciales reducido
- Fricción mínima para desarrolladores
- Límites operativos claros
Las mejoras de seguridad que son difíciles de adoptar suelen fallar.
Esta no necesita heroísmos.
Cuándo tiene sentido la autenticación IAM
Es una buena opción si:
- Ya ejecutas cargas de trabajo en AWS
- Usas roles IAM para cómputo (ECS, EC2, EKS, Lambda)
- Quieres reducir credenciales de larga duración
Consejo: Para cargas de trabajo con altas conexiones o serverless, combínalo con RDS Proxy — maneja la generación y renovación de tokens de forma transparente, sin código extra.
Podría no ser ideal si:
- Te conectas desde fuera de AWS
- Dependes mucho de pooling de conexiones sin lógica de renovación de tokens
El contexto importa.
Pensamientos finales
Este experimento no aceleró mágicamente las conexiones a la base de datos.
Ese no era el objetivo.
Lo que hizo fue eliminar toda una clase de riesgo sin hacer el sistema más difícil de operar.
Para mí, ese es el tipo de trade-off que vale la pena hacer: cambios pequeños, impacto medible y menos cosas que pueden fallar a las 3 a.m.