AWS - Accediendo a servicios de cuentas diferentes
Hace poco tuvimos el desafío de hacer que un microservicio que estaba corriendo en una cuenta A pudiera acceder al pool de Cognito de una cuenta B. Si bien existen mecanismos rápidos para lograr esto, como por ejemplo hacer que el microservicio contenga keys para la cuenta B, decidimos seguir el camino recomendado por AWS.
Para aquellos profesionales que ya conocen mucho sobre AWS y estarán pensando: "esto se soluciona fácilmente con roles", el objetivo de este blog es mostrar la solución ideal contrastándola con aquellas otras soluciones que nos parecen intuitivas a profesionales de TI que no tienen mucha familiaridad con los conceptos específicos de AWS. Dicho de otra forma, vemos primero los problemas que tenemos al tratar de utilizar la solución menos óptima, para poder ver mejor las ventajas de la solución recomendada.
Sobre las opciones sencillas y rápidas
Lo primero que nos viene a la mente para lograr lo que buscamos es simplemente generar credenciales CLI (access_key_id y secret_access_key) y hacer que el microservicio, que está corriendo en ECS, las tome de alguna forma, por ejemplo mediante secrets, y las utilice para inicializar su entorno aws-cli.
Los problemas de este método son:
- Credenciales estáticas: estamos inyectando access keys de largo plazo dentro de un servicio que, idealmente, debería operar con credenciales temporales. Si alguien las extrae de alguna forma (logs, memory dump, mala configuración de secrets), queda con acceso directo a los servicios de la otra cuenta.
- Rotación manual: aunque usemos Secrets Manager, igual hay que rotar esas credenciales periódicamente y coordinar despliegues. En la práctica, nunca se hará una rotación a menos que el equipo de seguridad de la otra cuenta las dé de baja y por nuestro lado solo nos enteramos de esto cuando falla el servicio.
- Pérdida de trazabilidad real: Si usamos servicios como CloudTrail aquí todo aparece como “ese usuario IAM”, no como el rol específico del servicio en ejecución.
- Acoplamiento fuerte entre cuentas: el servicio A pasa a “depender” explícitamente de un usuario de la cuenta B, lo cual complica gobernanza y auditoría.
- Violación del principio de menor privilegio por conveniencia: es común que ese usuario termine teniendo más permisos de los necesarios “para que funcione y listo”.
En resumen el método funciona, y lo podemos usar para pruebas si nuestro foco está en el código de la propia aplicación, pero es frágil desde el punto de vista de seguridad y operación.
Hablando un poco de AWS roles
Uno de los primeros conceptos que aparece cuando empezamos a trabajar seriamente con AWS es el de los IAM Roles. Es una pieza central del modelo de seguridad, y entenderlo bien cambia completamente cómo diseñamos accesos entre servicios, cuentas y ambientes.
Podemos resumir su funcionamiento con la siguiente definición:
Un IAM Role es una identidad sin credenciales permanentes que puede ser asumida temporalmente por una entidad autorizada para obtener permisos definidos por políticas adjuntas.
Ahora expandamos esta definición para sacar lo importante.
Qué es realmente un Role
Un Role tiene tres partes fundamentales:
- Trust Policy (quién puede asumirlo): Define qué entidad (usuario, role, servicio o cuenta externa) puede ejecutar sts:AssumeRole.
- Permission Policies (qué puede hacer): Define las acciones y recursos permitidos una vez que el role fue asumido.
- Credenciales temporales generadas por STS: Cuando alguien asume el role, AWS Security Token Service (STS) emite credenciales temporales:
- access_key_id
- secret_access_key
- session_token
- tiempo de expiración
No hay credenciales estáticas asociadas al role. Ese es el punto clave de seguridad que debemos entender.
Diferencia entre User y Role
Un error común al empezar es pensar que un Role es “como un usuario IAM”. En realidad esto no es así si miramos el detalle de sus diferencias:
IAM User | IAM Role |
Tiene credenciales permanentes | No tiene credenciales permanentes |
Representa una persona o sistema fijo | Representa permisos que pueden ser asumidos |
Se autentica directamente | Se asume temporalmente vía STS |
En arquitecturas modernas, los IAM Users deberían ser cada vez menos frecuentes, especialmente para workloads. Los servicios (ECS, Lambda, EC2, EKS) deberían usar Roles.
Así, la imagen mental que debemos tener es que los usuarios son una identidad fija, o sea "quien sos", mientras que los roles son permisos temporales que se pueden delegar, o dicho de otro modo "qué podes hacer y durante cuanto tiempo".
Un paso adelante usando roles, dos pasos atrás en la seguridad
Ahora que entendemos el concepto de roles y cómo nos pueden ayudar para mejorar la seguridad, tenemos que dar un paso atrás y ver cómo nos pueden, al mismo tiempo, complicar en la seguridad.
Por ejemplo, si bien los roles nos permiten olvidarnos de credenciales estáticas, estos aún tienen el poder de dar permisos de accesos peligrosos si no los configuramos bien.
Volviendo a nuestro ejemplo de acceder a Cognito de una cuenta B desde una cuenta A, podríamos resolver nuestro problema creando un rol en la cuenta B con el siguiente trust-policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_A_ID:root"
},
"Action": "sts:AssumeRole"
}
]
}Este trust policy permite que cualquier role de la Cuenta A asuma el role en la Cuenta B. Técnicamente resuelve el problema, pero el riesgo está justamente en la palabra “cualquier”. Eso implica que no solo los roles actuales, sino también roles futuros que aún no existen, podrían asumir ese role y operar sobre recursos en la Cuenta B.
Desde el punto de vista de seguridad, esto puede resultar incluso peor que compartir credenciales estáticas, porque amplía la superficie de acceso de forma implícita y menos visible. Este es, evidentemente, un ejemplo exagerado, pero sirve para dejar claro el punto que los roles no son seguros por definición; son seguros cuando están bien diseñados, correctamente delimitados y aplicando estrictamente el principio de menor privilegio.
La solución correcta
Ahora que sabemos como NO usar roles, armemos un mapa conceptual de lo que tenemos que armar:
- Cuenta A (donde está el microservicio ficticio de pagos)
- Role "ms-pagos-task-role", que a su vez contiene:
- Trust-policy que permite ser asumido por una task en ECS
- Permisos para asumir el rol "cognito-desde-pagos" en la cuenta B
- Role "ms-pagos-task-role", que a su vez contiene:
- Cuenta B (donde está el Cognito)
- Role "cognito-desde-pagos", que a su vez contiene:
- Trust-policy que permite asumir roles en la cuenta A
- Permisos para consulta de Cognito
- Role "cognito-desde-pagos", que a su vez contiene:

Como podemos ver en el gráfico, existe una especie de circulo de confianza entre ambas cuentas, donde ocurre lo siguiente:
- La cuenta B permitirá que el role "ms-pagos-task-role" de la cuenta A se asuma el role "cognito-desde-pagos". Denotado por la flecha azul.
- La cuenta A permitirá que aquellos que asuman el role "ms-pagos-task-role" (en nuestro caso el microservicio de pagos) pueda asumir el role "cognito-desde-pagos" en la cuenta B. Denotado por la flecha verde.
Si uno de los puntos anteriores (flechas verde o azul) no se cumplen, el proceso fallará. Si convertimos el gráfico a roles y permisos reales, el resultado es el siguiente:
Cuenta A
Por el lado de la cuenta A, necesitamos crear el role "ms-pagos-task-role" con el siguiente permiso, que se traduce a "este role tiene permiso para asumir el role cognito-desde-pagos-a de la cuenta B".
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::ACCOUNT_B_ID:role/cognito-desde-pagos-a"
}
]
}Este role, tendra el siguiente trust-policy, que dice "solo las tasks en ECS podrán asumir este role".
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}Cuenta B
El role "cognito-desde-pagos" tiene el siguiente trust-policy, que se traduce a "Este role podrá ser asumido por el role ms-pagos-task-role de la cuenta A":
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_A_ID:role/ms-pagos-task-role"
},
"Action": "sts:AssumeRole"
}
]
}Este role tendrá los siguientes permisos, que se traducen a "los que tengan este role, podrán consultar mi Cognito".
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cognito-idp:AdminGetUser",
"cognito-idp:ListUsers"
],
"Resource": "arn:aws:cognito-idp:us-east-1:ACCOUNT_B_ID:userpool/us-east-1_usuarios"
}
]
}Con esta cadena de roles y permisos entrelazados entre sí, logramos que nuestro microservicio de la cuenta A pueda consultar el Cognito de la cuenta B sin necesidad de proveerle de ninguna credencial, ya que el task-role será aplicado automáticamente cuando este se levante dentro del servicio de ECS, la configuración de este task-role queda fuera del alcance de este blog, pero no pasa de ser una simple línea en su task-definition.
Conclusión
Esta cadena de roles, trust policies y permisos puede parecer muy compleja e innecesaria especialmente para los que no estamos acostumbrados a lidiar con productos muy interdependientes de AWS, sin embargo, esta complejidad se traduce en una gran cantidad de ventajas que solo son evidentes cuando todo está armado y funcionando, aquí empiezan a surgir ventajas como:
- No necesitamos proveer credenciales a ningún microservicio de la cuenta A.
- Los permisos que otorgamos sobre el Cognito o cualquier otro servicio de la cuenta B se administran en un solo lugar. Dentro de la propia cuenta B.
- No es necesario rotar secretos ni credenciales ya que todo es temporal.
- El acceso a la cuenta B es otorgado con muchísima precisión, limitado a un role específico y a acciones concretas sobre recursos definidos.
- Se reduce drásticamente el impacto de una posible filtración, ya que las credenciales expiran y no sirven fuera del contexto autorizado.
- El equipo de desarrollo del microservicio de "pagos" no necesita gestionar credenciales ni conocer detalles de cuentas cruzadas: desde el código, simplemente se invoca Cognito. STS y el SDK se encargan de obtener y renovar credenciales automáticamente.
Con este esquema bien diseñado e implementado lo que parece complejo al inicio termina siendo más simple de operar, más seguro de auditar y mucho más escalable cuando el número de servicios, cuentas y equipos empieza a crecer.