When developing Software as a Service (SaaS) products, an important consideration is the isolation model used to restrict cross-tenant access. Many legacy applications utilise a siloed architecture that physically or virtually separates resources. However, a pooled isolation model offers greater flexibility and agility when it comes to developing and deploying your application. By leveraging serverless AWS services, an agile but secure architecture can be achieved.
API Gateway as an Identity Layer
One of the principal components of SaaS applications is the identity and authorisation layer. API Gateway provides a way to generate API keys that can be used to authorise access to an application. For specified endpoints in the API, tenants provide their private API key as a header when making a request. The key can then be automatically validated to allow or deny the request. Each API key has an ID that is also provided in the request headers to uniquely identify each tenant without exposing their private key. Additionally, API Gateway provides valuable features such as rate limiting and request quotas. SaaS products with tiered service plans can utilise these tools to provide varying levels of usage requirements to the tenants using the application.
API Gateway easily integrates with Lambda for the compute layer of the application. The API can be configured to invoke different Lambdas across multiple endpoints, or requests can be proxied through to a single Lambda function that can access the request context. When Lambda functions are invoked, a temporary execution context is launched that is isolated by nature. This makes Lambda an ideal platform for multi-tenanted applications at the compute layer, as tenant isolation is implicitly ensured. The serverless nature of Lambda also lends to multi-tenanted architectures, as the “noisy neighbour” problem is alleviated by inherent scalability.
Tenant Context and Data Partitioning
Requests to Lambda functions from API Gateway include the API key ID linked to the client’s API key, which can be used to set tenant context. DynamoDB can store tenant details with the API key ID used as the partition key. This can be initialised during the tenant on-boarding process – a table item can be added when tenants sign up for the application that contains information like the tenant name, email, contact details, etc. These details can then provide context to access additional resources.
Once a tenant context is established, partitioning further AWS resources becomes much easier. Serverless storage services like DynamoDB and S3 have simple ways of keeping data logically separated. Tables in DynamoDB can use partition keys that reference stored context information or the API key ID directly. S3 objects also use keys, and key prefixes can be used in a similar manner. Some tenants may require stricter isolation at the storage level than this pooled model, in which case entire resources can be created during the on-boarding process. For DynamoDB, this would mean creating a table per tenant with a unique prefix on the table name. For S3, a similar prefix can be added to the bucket name, though note that unlike DynamoDB tables, S3 bucket names must be globally unique.
Preventing Cross-Tenant Access
When these resources are partitioned using the methods described, access control to them can be tightly secured. For SaaS products that only allow indirect access to resources through the application layer, custom logic can easily validate access by only making requests to resources with names that match the current tenant context. However, for applications that provide direct access to AWS resources, STS can be leveraged to manage federated access. The AssumeRole action provides the ability to specify session tags when generating temporary security credentials. These session tags can then be referenced in IAM policies attached to the assumed role.
The example IAM policy shown above grants the user access to modify items in a DynamoDB table only when the partition key matches a session tag called “api-key-id”. When assuming a role that has this policy attached, a session tag with this key can be set with a value equal to the API key ID provided in the Lambda request. The generated security credentials will only be able to access table items where the partition key is equal to the tenant’s API key ID. These credentials can then be safely passed back to the user with the assurance that they can only access their own resources.
As multi-tenanted applications tend towards pooled isolation models, it becomes more difficult to ensure that cross-tenant access is ensured. By leveraging some of the serverless services provided by AWS, features such as authentication, authorisation, compute isolation, resource partitioning, and access control can be easily implemented in a scalable way.