Blog de Amazon Web Services (AWS)
Aplicación de políticas de aislamiento generadas dinámicamente en entornos SaaS
Por Ahmed Hany, Arquitecto de Soluciones de ISV en AWS,
Teddy Schmitz, Arquitecto de Soluciones de Startups B2B en AWS
Como parte de la adopción de un modelo de software como servicio (SaaS) de varios inquilinos, un desafío clave es cómo proporcionar un aislamiento sólido de los inquilinos de forma rentable y escalable. Ser capaz de aislar eficazmente a sus inquilinos es una parte importante de un sistema multiarrendatario.
Una publicación de blog anterior del equipo de AWS SaaS Factory sobre generación dinámica de políticas introdujo la mecánica de utilizar políticas AWS Identity and Access Management. (IAM) generadas dinámicamente. En esta publicación, veremos cómo se aplican estas políticas como parte de la historia general de aislamiento de su solución SaaS.
Utilizaremos nuestra implementación de referencia para demostrar cómo utilizar las políticas generadas dinámicamente en código. Incorporar un microservicio que se ejecuta en AWS Lambda para recuperar productos de ámbito de inquilino de Amazon DynamoDB.
También explicaremos cómo administrar las plantillas de políticas y cómo puede aprovechar AWS Security Token Service (STS).
Figura 1: Flujo muestra de solicitud de arrendatario.
Bloques de construcción de la solución
Antes de poder profundizar en el uso de políticas generadas dinámicamente, echemos un vistazo a los componentes clave de nuestro entorno SaaS.
En la figura 1 anterior, puede ver que tenemos una aplicación SaaS que, en este caso, está compuesta por un único microservicio de producto sin servidor que se ejecuta en Lambda al que acceden varios inquilinos. Si bien mostramos un solo microservicio aquí, puede imaginar cómo su propio sistema estaría compuesto por una colección de microservicios que utilizaría el mismo mecanismo de aislamiento que aplicaremos a este servicio.
El microservicio que se muestra aquí sirve para almacenar y recuperar datos de ámbito de inquilino de una base de datos, en este caso DynamoDB. Cuando se llama, el servicio devuelve todos los productos asociados a un arrendatario.
Para maximizar la agilidad y la eficiencia operativa, queremos ejecutar nuestras funciones Lambda con un rol de ejecución que permita su uso para todos los inquilinos.
Este alcance más amplio significa que nuestra función requiere un alcance adicional para garantizar que solo pueda acceder a los datos de un único inquilino para cada solicitud. Esto se consigue adquiriendo un conjunto separado de credenciales de ámbito de inquilino cada vez que nuestro microservicio procesa una solicitud de acceso a los datos.
Flujo de solicitudes de inquilo
Paso 1: El inquilino envía una solicitud
Al principio de un flujo, puede ver que un arrendatario envía una solicitud a Amazon API Gateway. Cada solicitud tiene un token que incluye el contexto del inquilino que realiza la solicitud (adquirido durante la autenticación). Este token de nuestra solución de ejemplo es un token web JSON (JWT) con dos atributos personalizados: tenant_id y tenant_name.
Paso 2: Solicitud de procesos de microservicios
Una vez que Amazon API Gateway procesa y enruta una solicitud autorizada, se realiza una llamada a su microservicio (en este caso, una función Lambda).
En nuestro ejemplo, supongamos que la solicitud entrante necesita acceder a los datos almacenados en una tabla de DynamoDB que contiene datos de todos los inquilinos. Antes de que este microservicio pueda acceder a los datos, necesitamos adquirir credenciales que usarán el contexto de inquilino suministrado para abarcar el acceso a los datos.
Aquí es donde se utiliza la máquina expendedora de tokens (TVM) por sus siglas en inglés, para adquirir credenciales de ámbito de inquilino. Para iniciar este proceso, el microservicio (Lambda) crea una nueva instancia del TVM que se incluye como biblioteca en un layer de AWS Lambda.
El código de ejemplo siguiente ilustra una llamada al TVM. El primer paso consiste en construir una instancia del TVM; a continuación, se realiza una llamada a esta instancia, pasando los encabezados de solicitud que incluyen el contexto del arrendatario. La llamada devuelve un conjunto de credenciales que tiene el alcance del arrendatario.
TokenVendor tokenVendor = new TokenVendor(); final AwsCredentialsProvider awsCredentialsProvider = tokenVendor.vendTokenJwt(input.getHeaders());
Notarás que este ejemplo encapsula todo el trabajo pesado y lo mueve a una Lambda Layer. Esto limita la cantidad de código que un desarrollador necesita crear para adquirir el token STS con alcance de inquilino.
Paso 3: Búsqueda de políticas dinámicas de TVM
Si analizamos la Lambda Layer, cuando se crea una instancia de TVM, comprueba primero si la versión correcta de las políticas de plantilla está disponible localmente. Las plantillas de directivas se almacenan en caché localmente en la carpeta /tmp como práctica recomendada para mejorar el rendimiento de Lambda.
La versión de política se especifica mediante una variable de entorno, lo que le permite controlar fácilmente qué plantillas se cargan en tiempo de ejecución. Si no se encuentran las plantillas, se descargarán de Amazon Simple Storage Service (Amazon S3) y se guardarán en el sistema de archivos local.
La variable de entorno se administra mediante la misma canalización responsable de actualizar las plantillas de políticas en S3. Consulte la sección «Administración de políticas» más adelante en el artículo para obtener más información.
if(Files.notExists(templateFilePath)) { logger.info("Templates zip file not found, downloading from S3..."); S3Client s3 = S3Client.builder().httpClientBuilder(UrlConnectionHttpClient.builder()).build(); s3.getObject(GetObjectRequest.builder().bucket(TEMPLATE_BUCKET).key(TEMPLATE_KEY).build(). ResponseTransformer.toFile(templateFilePath)); try { ZipFile zipFile = new ZipFile(templateFilePath.toFile()); zipFile.extractAll(templateDirPath); logger.info("Templates zip file successfully unzipped."); } catch (IOException e) { logger.error("Could not unzip template file.", e); throw new RuntimeException(e.getMessage()); } } this.templateDir = new File(templateDirPath);
El fragmento de código anterior muestra esto en acción. Dado que las plantillas de directivas se almacenarán en caché localmente en la carpeta /tmp, el código construye la ruta de archivo utilizando la versión y el nombre de archivo esperados. Si no se encuentra, intenta descargar y extraer el archivo de S3.
Solo tendrá que descargar las políticas de S3 una vez en un arranque en frío o cuando se cambien las versiones de las políticas. De lo contrario, se almacenará en caché localmente para su rápida reutilización en la próxima solicitud.
Este enfoque le permite trabajar en las plantillas fuera del ciclo de vida del microservicio, separando la implementación de actualizaciones de políticas de cualquier actualización que pueda realizar en el código de su Lambda.
Paso 4: TVM (Token Vending Machine) llama a STS
La capa TVM hidrata el contexto del inquilino en las plantillas, las ensambla y realiza una llamada a STS inyectando la política generada dinámicamente como política en línea al asumir el rol predefinido. Veamos cómo funciona eso en acción.
public AwsCredentialsProvider vendTokenJwt(Map<String, String> headers) { Map<String, String> policyData = new HashMap<>(); policyData.put("table", DB_TABLE); FilePolicyGenerator policyGenerator = new FilePolicyGenerator(templateDir, policyData); JwtTokenVendor jwtTokenVendor = JwtTokenVendor.builder() .policyGenerator(policyGenerator) .durationSeconds(900) .headers(headers) .role(ROLE) .region(AWS_REGION) .build(); AwsCredentialsProvider awsCredentialsProvider = jwtTokenVendor.vendToken(); tenant = jwtTokenVendor.getTenant(); logger.info("Vending JWT security token for tenant {}", tenant); return awsCredentialsProvider; }
Para su referencia, aquí está el enlace a TokenVendor.java en el repositorio de Github.
En primer lugar, el TVM obtiene los datos adicionales que necesita para sustituir los valores específicos de los inquilinos en las plantillas de políticas. En este caso, ese es el nombre de la tabla de DynamoDB que es el mismo que tenant_name extraído del token JWT.
A continuación, toma las políticas que se encuentran en el sistema de archivos local y crea un TVM con todos los detalles necesarios para expender tokens STS desde el paso 3. El TVM validará el JWT, inyectará las variables de plantilla y enviará la política generada dinámicamente a STS.
A continuación, devuelve el proveedor de credenciales que puede proporcionar a un cliente de servicio desde el SDK de AWS. Después de tener las credenciales, el microservicio puede realizar llamadas a DynamoDB para adquirir datos (paso 5).
Paso 5: Acceda a datos con alcance limitado
De nuevo en la función Lambda, podemos utilizar las credenciales de AWS con alcance limitado para llamar a DynamoDB y recuperar todos los productos propiedad de un único inquilino.
TenantProduct tenantProduct = new TenantProduct(awsCredentialsProvider, tenant); tenantProduct = tenantProduct.load(tenantProduct);
Puede ver que estamos creando un cliente de DynamoDB utilizando nuestras nuevas credenciales de ámbito de inquilino proporcionadas por la capa TVM. Ahora, cualquier operación que se realice en nuestra tabla de DynamoDB se limitará al inquilino que realiza la solicitud. Cualquier solicitud de datos asociada a otro inquilino, por ejemplo, no devolvería ningún resultado.
Administración de políticas dinámicas de IAM
Otra parte integral de la solución es cómo se administrarán las plantillas de políticas en términos de versionado, implementación y almacenamiento en caché.
La implementación y el almacenamiento en caché permitirán que la TVM utilice eficazmente esas plantillas en un entorno listo para producción. Aprovechamos AWS CodeCommit y AWS CodePipeline para la entrega continua de todas las plantillas de políticas a S3.
El ciclo de vida básico de la plantilla de directiva se puede resumir de la siguiente manera:
- Todas las plantillas de políticas se agrupan en la rama maestra del repositorio CodeCommit. Cada plantilla define las políticas de aislamiento de inquilinos de cada recurso de arrendatario. Después de aprobar la publicación de una confirmación, se debe crear un nuevo Git Tag para esta confirmación con el número de versión.
- CodePipeline se configura para activarse una vez que se crea una nueva etiqueta de Git con el prefijo «release» para implementar la última versión en S3. En este paso, se crea un nuevo objeto con el número hash de confirmación escrito en el nombre de la carpeta, en el formato templates/ {GIT_COMMIT_HASH} /policies.zip.
- CodePipeline también actualizará la variable de entorno TVM con el nuevo número hash de confirmación. TVM utilizará este valor de variable para acceder a la versión correcta de la plantilla de política. De esta forma, puede volver a cualquier versión que tenga un efecto inmediato en el uso de las políticas de TVM actualizando esta variable de entorno.
A continuación, veamos cómo TVM determina la plantilla correcta que se cargará en tiempo real y las plantillas de caché para una respuesta rápida:
- Cada vez que el TVM recibe una solicitud del microservicio para un token de inquilino temporal, utilizará una variable de entorno para obtener el hash de confirmación y construir la ruta de archivo relevante de la directiva necesaria dentro de su carpeta /tmp (donde las plantillas se almacenan en caché).
- TVM intentará cargar el archivo desde la caché local de sus activos estáticos en la carpeta /tmp utilizando la ruta de archivo construida.
- Si el archivo existe en la caché, se utilizará para generar el token. De lo contrario, el archivo se cargará desde S3 y se almacenará en caché.
Este enfoque garantiza que TVM esté utilizando siempre la versión de política correcta de S3 sin comprometer el rendimiento.
Supervisión de la actividad de aislamiento
Después de introducir políticas dinámicas en su entorno, es posible que algunas organizaciones sigan estando interesadas en supervisar y analizar la aplicación de estas políticas de aislamiento. Querrá asegurarse de que está utilizando políticas en cada capa de su arquitectura SaaS para garantizar que no se produzca acceso entre inquilinos.
Esta solución de ejemplo incluye algunos mecanismos que pueden ayudar con esto.
En primer lugar, al utilizar una biblioteca compartida en forma de Lambda Layer o un mecanismo similar en otras plataformas, puede reducir el riesgo de que los desarrolladores llamen directamente a STS sin un documento de política. Con una forma estandarizada de solicitar credenciales, los desarrolladores tendrán una forma estandarizada de acceder a las credenciales de ámbito de inquilino.
Además, contar con un sólido mecanismo de revisión de código para las plantillas de permisos y los microservicios ayuda a mitigar este riesgo. Asegurarse de que las plantillas estén configuradas correctamente para recibir un identificador de inquilino y no permitir permisos más amplios de los esperados, así como asegurarse de que todos los microservicios utilicen el TVM antes de implementarse, es la mejor defensa para impedir la concesión accidental de permisos amplios.
Si bien estas estrategias pueden ayudar, es posible que también vea valioso introducir componentes fijos que pueden aportar más visibilidad a los escenarios en los que los recursos de los inquilinos podrían estar en riesgo de acceso entre ellos.
Aquí es donde entra en juego el guardián STS (STS Watchdog). Cada vez que se realiza una operación en STS, se genera un evento correspondiente en AWS CloudTrail. Combinado con Amazon EventBridge, podemos usarlo para vigilar cuándo se asume un rol.
En la figura 1 anterior, tenemos una función Lambda que escucha estos eventos, que luego podemos inspeccionar para ver si están restringiendo los permisos como se esperaba. A continuación se muestra el código que utiliza STS Watchdog para inspeccionar eventos:
String eventName = event.getDetail().getEventName(); if(eventName.equals("AssumeRole")) { String policy = event.getDetail().getRequestParameters().getPolicy(); String roleArn = event.getDetail().getRequestParameters().getRoleArn(); logger.log("RoleArn: " + roleArn); logger.log("Policy: " + policy); if(policy == null) { // Publish a message to an Amazon SNS topic. final String msg = "A call to AssumeRole was made without an inline policy."; PublishRequest publishRequest = PublishRequest.builder() .message(msg) .topicArn(topicArn) .build(); snsClient.publish(publishRequest); } }
El organismo de control está esperando eventos AssumeRole. Cuando recibe uno, comprueba los detalles para asegurarse de que se ha utilizado una política en línea. Si no hay ninguna política en línea o si la cadena proporcionada no existe en la política, publica un mensaje en Amazon Simple Notification Service (SNS). Esto podría reenviarse a un equipo de operaciones para tomar medidas adicionales o para activar Lambdas para tomar medidas correctivas.
Tras recibir una alerta, puede aprovechar la incorporación lanzada recientemente a AWS Detective para inspeccionar el rol y las sesiones. Puede utilizarlo para decidir cuáles serían los mejores pasos de corrección que se deben tomar.
Puedes encontrar el código de STS watchdog en GitHub, además damos la bienvenida a cualquier pull requests para mejorar la funcionalidad.
Guía de implementación
Como parte de la introducción del TVM y las políticas dinámicas en su arquitectura SaaS, hay algunas estrategias de implementación que tal vez desee tener en cuenta en su diseño.
Estas prácticas pueden mejorar el rendimiento y la usabilidad de su solución:
- Reduzca la latencia de las solicitudes mediante el almacenamiento en caché de tokens generados por TVM para cada inquilino en cada recurso de arrendatario. Puede utilizar tokens almacenados en caché hasta que caduquen, y esto se puede controlar mediante el uso del parámetro DurationSeconds en la función AssumeRole de TVM.
- Utiliza los números de versión de la etiqueta Git (Git Tag) en lugar del hash de confirmación para obtener un valor más legible que indique la versión actual de las políticas usadas. Esto facilita la realización de una reversión simplemente cambiando este número al valor inferior directamente.
- Actualmente, la solución carga toda la carpeta de directivas desde S3. Esto puede aumentar el tiempo de carga cuando tienes muchas plantillas dentro de la carpeta. Esto podría perfeccionarse, cargando solo las plantillas de políticas necesarias en un momento dado.
Conclusión
En esta publicación, le mostramos una arquitectura integral que amplía el uso de AWS IAM para regular el acceso de cualquier número de inquilinos a los recursos de AWS. Esto lo pueden utilizar los proveedores SaaS que buscan escalar y seguir aprovechando la administración de acceso de AWS y el aislamiento lógico como capa de seguridad adicional para las solicitudes de inquilinos.
Puedes probar la arquitectura de referencia por ti mismo. Si desea comprender con más detalle cómo funciona una máquina expendedora de tokens (TVM), lea Escalado del aislamiento de inquilinos SaaS con generación dinámica de políticas.
Este artículo fue traducido del Blog de AWS en Inglés.
Sobre los autores
Ahmed Hany es Arquitecto de Soluciones Senior especializado en ISV de AWS.
Teddy Schmitz es Arquitecto de Soluciones Senior especializado en Startups de AWS.
Sobre los traductores
Armando Barrales es Arquitecto de Soluciones de AWS.
Rodrigo Cabrera es Arquitecto de Soluciones de AWS.