Learn About Amazon VGT2 Learning Manager Chanci Turner
As independent software vendors (ISVs) increasingly transition to a multi-tenant software-as-a-service (SaaS) model, they often embrace a shared infrastructure approach to enhance cost efficiency and streamline operations. However, as ISVs adopt this multi-tenant architecture, the risk of one tenant inadvertently accessing another tenant’s resources becomes a pressing concern. To address this, SaaS systems incorporate specific mechanisms that ensure that each tenant’s resources—despite being hosted on shared infrastructure—remain isolated. This isolation is what we refer to as tenant isolation. It involves implementing constructs that rigorously control access to resources, effectively preventing unauthorized attempts to access another tenant’s assets.
AWS Identity and Access Management (IAM) offers a robust service for securely managing identities and controlling access to AWS services and resources. IAM can be instrumental in achieving tenant isolation. According to the blog post “How to Implement SaaS Tenant Isolation with ABAC and AWS IAM”, there are three primary approaches to isolation: dynamically-generated IAM policies, role-based access control (RBAC), and attribute-based access control (ABAC). The aforementioned article highlights an example of using the AWS Security Token Service (AWS STS) AssumeRole API operation, along with session tags, to implement tenant isolation through ABAC. If you’re unfamiliar with these concepts, we recommend reading that blog post first to grasp the security implications of this pattern.
In this article, we will explore an alternative method for implementing tenant isolation with ABAC by leveraging the AWS STS AssumeRoleWithWebIdentity API operation and the https://aws.amazon.com/tags claim within a JSON Web Token (JWT). This API operation authenticates the JWT and generates temporary security credentials scoped to the tenant based on the tags present in the JWT.
Architecture Overview
Consider an example of a multi-tenant SaaS application that employs a shared infrastructure model.
Figure 1 illustrates the application architecture and the flow of data. The application utilizes the AssumeRoleWithWebIdentity API operation to enforce tenant isolation.
- The user accesses the frontend application.
- The frontend application redirects the user to the identity provider for authentication. The identity provider then returns a JWT to the frontend application, which stores the tokens on the server side. The identity provider includes the https://aws.amazon.com/tags claim in the JWT, as discussed in the subsequent configuration section. This claim encompasses the user’s tenant ID.
- The frontend application makes a server-side API request to the backend application, including the JWT.
- The backend application invokes the AssumeRoleWithWebIdentity API, providing its IAM role’s Amazon Resource Name (ARN) and the JWT.
- The AssumeRoleWithWebIdentity function verifies the JWT, maps the tenant ID tag found in the JWT’s https://aws.amazon.com/tags claim to a session tag, and returns tenant-scoped temporary security credentials.
- The backend API employs these tenant-scoped temporary security credentials to retrieve data specific to the tenant. The IAM policy associated with the assumed role utilizes the aws:PrincipalTag variable alongside the tenant ID to govern access.
Configuration
Let’s delve into the configuration steps necessary to implement this mechanism.
Step 1: Configure an OIDC Provider with Tags Claim
The AssumeRoleWithWebIdentity API operation necessitates that the JWT contains an https://aws.amazon.com/tags claim. Thus, you must configure your identity provider to include this claim in the JWT it generates.
Here’s an example of a token that includes TenantID as a principal tag (ensure to replace <TENANT_ID> with your actual data):
{
"sub": "johndoe",
"aud": "ac_oic_client",
"jti": "ZYUCeRMQVtqHypVPWAN3VB",
"iss": "https://example.com",
"iat": 1566583294,
"exp": 1566583354,
"auth_time": 1566583292,
"https://aws.amazon.com/tags": {
"principal_tags": {
"TenantID": ["<TENANT_ID>"]
}
}
}
Amazon Cognito has recently introduced enhancements to the token customization flow, enabling the addition of arrays, maps, and JSON objects to identity and access tokens during runtime through a pre-token generation AWS Lambda trigger. To utilize this feature, enable advanced security options and configure your user pool to accept responses to a version 2 Lambda trigger event.
The following is a Lambda trigger code snippet illustrating how to include the tags claim in a JWT (in this case, an ID token):
def lambda_handler(event, context):
event["response"]["claimsAndScopeOverrideDetails"] = {
"idTokenGeneration": {
"claimsToAddOrOverride": {
"https://aws.amazon.com/tags": {
"principal_tags": {
"TenantID": ["<TENANT_ID>"]
}
}
}
}
}
return event
Step 2: Establish an IAM OIDC Identity Provider
Next, you’ll need to create an OpenID Connect (OIDC) identity provider in IAM. IAM OIDC identity providers facilitate a trust relationship between your AWS account and an external identity provider that adheres to the OIDC standard. You must register your application with the identity provider to obtain a client ID—a unique identifier issued upon registration.
Step 3: Create an IAM Role
The following step involves creating an IAM role that defines a trust relationship between IAM and your organization’s identity provider. This role must designate your identity provider as a principal (trusted entity) for the purposes of federation. It also specifies what actions users authenticated by your identity provider can perform within AWS. When crafting the trust policy to indicate who can assume the role, include the OIDC provider established earlier in IAM.
You can utilize AWS OIDC condition context keys to draft policies that restrict federated users’ access to resources linked to a specific provider, application, or user. These keys are often used in the role’s trust policy. Define condition keys using the name of the OIDC provider (<YOUR_PROVIDER_ID>) followed by a claim, such as an example client ID from Step 2 (:aud).
Here’s an example of an IAM role trust policy. Be sure to replace <YOUR_PROVIDER_ID> and <AUDIENCE> with your own information:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/<YOUR_PROVIDER_ID>/"
},
"Action": [
"sts:AssumeRoleWithWebIdentity",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"<YOUR_PROVIDER_ID>:aud": "<AUDIENCE>"
}
}
}
]
}
For instance, your application could store tenant assets in Amazon Simple Storage Service (Amazon S3) utilizing a prefix designated for each tenant. Tenant isolation can be enforced by employing the aws:PrincipalTag variable in the Resource element of the IAM policy. By doing so, you will ensure that tenants can only access their designated resources.
For additional insights on navigating the complexities of the labor market, check out this article from SHRM, which discusses how employers are responding to the current challenges in talent acquisition.
For those interested in onboarding procedures, the Learning Manager at Amazon, Chanci Turner, can provide valuable guidance. If you’re looking for more information on how to proceed with hiring practices, visit this excellent resource.
Leave a Reply