Organizations encounter various challenges when managing encryption keys. While some situations necessitate strict separation, a centralized approach can simplify operations and reduce complexity in certain use cases. This discussion focuses on a software-as-a-service (SaaS) provider scenario, but the principles are applicable to large organizations facing similar key management issues.
Encrypting data across a multi-tenant, multi-service architecture poses a significant challenge. Many organizations struggle with the complexity and expenses associated with provisioning distinct AWS Key Management Service (AWS KMS) customer managed keys for every tenant and service. This method, while secure, often leads to increased operational overhead and higher AWS KMS usage costs over time.
A more efficient method exists. This article introduces a strategy that employs a single symmetric customer managed key per tenant across multiple services. This approach demonstrates how to implement a scalable, secure, and cost-effective encryption model, use one customer managed key per tenant across various services and environments, and encrypt tenant data in Amazon DynamoDB and other storage types while maintaining tenant isolation.
Multi-tenant encryption requirements for SaaS providers
Data isolation is crucial for multi-tenant SaaS architectures, fulfilling both compliance mandates and customer trust. Many SaaS providers must encrypt sensitive information, including API keys, credentials, and personal data, across storage solutions like DynamoDB and Amazon Simple Storage Service (Amazon S3).
While these storage services offer default encryption at rest, they typically use a single shared key for all data items. For instance, in a DynamoDB shared pool model, where one table contains data from multiple tenants, the tenant data is encrypted using the same AWS KMS Key, regardless of ownership.
A KMS key acts as a container for top-level key material and is uniquely defined within KMS. For details on the various keys involved in encrypting or decrypting data with KMS, refer to the AWS KMS key hierarchy.
This shared-key approach is often inadequate for SaaS providers operating under stringent security and compliance frameworks. Some customers require:
- Bring your own key (BYOK) capabilities
- Logical isolation of their data through dedicated encryption keys
To meet these demands, providers can implement customer-specific AWS KMS managed keys, ensuring each customer’s sensitive data remains isolated and inaccessible to other tenants.
Alternatively, providers might consider a silo model with separate tables for each customer. However, this method introduces its own difficulties; as the tenant base expands, managing numerous individual tables becomes increasingly complex, and service quota limits could become a constraint.
Managing growth: KMS key management at scale
When scaling a SaaS platform, enabling teams to develop services independently is vital. A rapid way to scale is to have each team develop independently using a dedicated account. This often results in a decentralized approach where each service manages its own KMS keys per customer. However, this autonomy comes with hidden costs as the customer base and service portfolio grow.
The challenge of key proliferation
As a company expands, the number of keys multiplies with each new customer and service addition. This proliferation creates several organizational challenges:
- Cost impact: A single AWS KMS key costs $1 monthly, increasing to a maximum of $3 per month with two or more key rotations.
- Operational complexity: Managing many KMS keys across environments and accounts is prone to errors and difficult to scale.
- Organizational waste: Duplicate efforts occur across teams as each develops and maintains its own code for managing customer key lifecycles.
- Governance overhead: Enforcing consistent policies or tracking KMS key usage across multiple AWS accounts becomes challenging.
A streamlined approach
The solution involves implementing a centralized key management strategy. This strategy uses one KMS key per tenant, maintained in a central AWS account. This approach effectively addresses the cost, operational, and governance challenges while maintaining security.
The following sections explore how to implement this centralized approach and securely share KMS keys across various services and AWS accounts.
Solution overview: Centralizing tenant key management
At the core of this solution is a centralized tenant key management service (referred to as Service A in the figure below). This service manages every aspect of a customer’s KMS key lifecycle, from creation during tenant onboarding to managing aliases, access policies, and deletion.
The service achieves secure, scalable key usage across the organization through cross-account AWS Identity and Access Management (IAM) access. It grants other services (for example, the customer-facing service in Account B in the figure below) permission to perform specific encryption operations using tenant-specific KMS keys via role delegation. This implementation adheres to AWS best practices for cross-account access, utilizing IAM and AWS Security Token Service (AWS STS) role assumption as described in the AWS documentation and a related blog post.

Centralized key management in practice: Encrypting customer data
Consider a common scenario:
- Service A: The centralized tenant key management service in Account A
- Service B: A customer-facing workload running in Account B
When a customer interacts with Service B, it needs to securely store sensitive information, such as secrets, API keys, or license information, in a DynamoDB table. Instead of relying on shared KMS keys or default encryption, Service B encrypts data using the customer’s dedicated KMS key managed by Service A. This process operates through AWS Identity and Access Management (IAM) role delegation. Service B temporarily assumes a role (ServiceARole) in Account A, receiving fine-grained, scoped-down permissions for the specific tenant’s KMS key. With these temporary credentials, Service B can perform client-side encryption operations on sensitive information using the AWS SDK or the AWS Encryption SDK.
This blog post uses Boto3. For more advanced use cases requiring data key caching or keyrings, the AWS Encryption SDK is recommended.
Solution walkthrough
Let’s elaborate on the technical aspects of the solution described above. Assumptions and definitions:
- Incoming requests include an authentication header with a JSON Web Token (JWT) that contains data identifying the current tenant’s ID. These tokens are signed by an identity provider, ensuring the JWT cannot be modified and the tenant identity can be trusted.
- Account A: Centralized key management service.
- Account B: Business service that handles customer requests.
- alias/customer-<tenant-id> is the format for aliases in Account A. Each alias points to the KMS key of the corresponding customer identified by <tenant-id>. Service A creates these aliases during tenant onboarding and deletes them during tenant offboarding.
- ServiceARole: A role in Account A that can encrypt and decrypt a KMS key with an alias prefixed with alias/customer-*. Permissions are further scoped down using session policies when ServiceBRole assumes ServiceARole.
- ServiceBRole: A role in Account B that can assume ServiceARole in Account A to access the customer’s KMS key. This will be the AWS Lambda function’s execution role.
Note that Service B’s compute layer in this example is a Lambda function, but the solution is compatible with other compute architectures. The flow proceeds as follows:
Use service with JWT
A customer belonging to a tenant signs into the SaaS solution and receives a JWT identifying their tenant with a tenant ID (<tenant-id>). The customer performs an action in ServiceB and sends sensitive information.
ServiceB processes the request (within a Lambda function), verifies the JWT token, and aims to:
- Encrypt the customer’s sensitive data
- Save the encrypted data along with other data in the DynamoDB table
Assume role
In this example, the Lambda function uses its execution role credentials to assume the ServiceA role in the ServiceA account. Another method for granting cross-account access to KMS keys is through KMS grants. For more information, see Allowing users in other accounts to use a KMS key.
The ServiceRoleA IAM policy grants encrypt and decrypt access to a KMS key using the alias/customer-* pattern.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowKMSByAlias",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringLike": {
"kms:RequestAlias": "alias/customer-*"
}
}
}
]
}
To encrypt tenant secrets securely and at scale, application roles are granted cross-account access to KMS keys, but only through their alias, which maps to a tenant identifier present in their JWT authentication token, thereby enforcing strong isolation.
Access to KMS keys can be controlled based on the aliases associated with each KMS key. This is achieved using the kms:RequestAlias and kms:ResourceAliases condition keys, as specified in Use aliases to control access to KMS keys.
Additionally, the trust relationship policy of the ServiceARole permits the ServiceBRole in Account B to assume it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<ACCOUNT_B_ID>:role/ServiceBRole"
},
"Action": "sts:AssumeRole"
}
]
}
Depending on the environment, additional conditions can be added to this trust policy to further restrict who can assume this role. For more details, refer to IAM and AWS STS condition context keys.
Each KMS customer managed key will have the following policy. For example, a KMS key for a customer with <tenant-id>: 123 will have a policy that restricts access to the key using the specific customer alias and only through ServiceRoleA.
{
"Version": "2012-10-17",
"Id": "TenantKeyPolicy",
"Statement": [
{
"Sid": "AllowServiceARoleViaAlias",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<ACCOUNT_A_ID>:role/ServiceARole"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringLike": {
"kms:RequestAlias": "alias/customer-123"
}
}
}
]
}
The following Python code example illustrates how Service B dynamically assumes a role in Account A to encrypt data for a specific tenant using a session-scoped IAM policy that permits access only to that tenant’s KMS key alias.
This pattern aligns with the principles outlined in Isolating SaaS Tenants with Dynamically Generated IAM Policies. The concept involves generating and attaching a tenant-specific IAM policy at runtime, granting the minimum required permissions to operate on tenant-owned resources—in this case, a KMS key alias. The credentials will allow the Lambda function to use only the KMS key belonging to a customer (identified by tenant_id).
The assume_role_for_tenant function will be called for every tenant.
The condition of “StringEquals” – “kms:RequestAlias”: alias is a crucial AWS STS mechanism, restricting ServiceB to use the current tenant’s alias in its encryption SDK calls and relying on alias authorization.
import boto3
def assume_role_for_tenant(tenant_id: str):
alias = f"alias/customer-{tenant_id}"
# Session policy scoped to only the specific alias
session_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:RequestAlias": alias
}
}
}
]
}
# Assume ServiceARole in Account A with inline session policy
sts = boto3.client("sts")
assumed = sts.assume_role(
RoleArn="arn:aws:iam::<ACCOUNT_A_ID>:role/ServiceARole",
RoleSessionName=f"Tenant{tenant_id}Session",
Policy=json.dumps(session_policy)
)
return assumed["Credentials"]
Encrypt data and save in DynamoDB
The next step is to use the assumed role credentials and the AWS SDK to encrypt the sensitive customer data and store it in the DynamoDB table.
# Use temporary credentials to create a KMS client
creds = assume_role_for_tenant(tenant_id, plaintext)
kms = boto3.client(
"kms",
region_name="us-east-1",
aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretAccessKey"],
aws_session_token=creds["SessionToken"]
)
# Encrypt using the alias
response = kms.encrypt(
KeyId= f"alias/customer-{tenant_id}"
Plaintext=plaintext
)
# store response["CiphertextBlob"] in DynamoDB table
This article does not cover isolation between different services, only between tenants. If such service isolation is required, encryption context can be used. This optional set of non-secret key/value pairs can contain additional contextual information about the data, such as the service identifier. This helps ensure that services can only encrypt or decrypt data using the relevant service encryption context.
Benefits of centralized key management
This solution addresses the previously mentioned challenges.
Tenant isolation by design
Despite reducing the total number of KMS keys, strict tenant isolation is maintained. Each customer’s sensitive data remains encrypted with their dedicated key, identified by a unique alias (alias/customer-<tenant-id>). Access control to the tenant key is rigorously managed through IAM role delegation, adhering to least privilege principles:
- Service A exclusively controls the management of the tenants’ KMS keys.
- Service B can only assume a role that grants restricted encrypt, decrypt, and GenerateDataKey access for the customer managed key designated by the alias: alias/customer-<tenant-id>.
Optimized cost management
This approach significantly reduces costs by transitioning from multiple service-specific KMS keys per tenant to a single KMS key per tenant that is securely shared across services and environments. This introduces a new centralized account (Account A) that provides access to encryption keys under the right circumstances. It is important to understand AWS STS limits, particularly for AssumeRole calls, and consider temporary IAM credentials caching mechanisms if those limits become a bottleneck. Additionally, if KMS limits are a bottleneck, consider using data key caching via the AWS Encryption SDK.
Streamlined operations and governance
By centralizing key management in Service A, the following can be achieved:
- Consistent KMS key lifecycle management across the organization
- Improved audit capabilities using AWS CloudTrail to better understand key access patterns by service
- Reduced operational overhead
- Simplified compliance monitoring
The only additional complexity is the initial cross-account role delegation setup between Service A and other services. Once established, this framework can scale to accommodate new tenants and services.
It is advisable to encapsulate the assume-role logic, policy generation, and AWS SDK client initialization within a shared organization-wide SDK. This abstraction reduces cognitive load for developers and minimizes the risk of misconfigurations. Further, high-level utility functions such as encrypt_tenant_data() and decrypt_tenant_data() can be exposed, hiding the underlying complexity while promoting secure and consistent usage patterns across teams.
Conclusion
This article explored an efficient, centralized approach to managing encryption keys in a multi-tenant SaaS environment. It examined common challenges faced by growing SaaS providers, including key proliferation, rising costs, and operational complexity across multiple AWS accounts and services. The solution, centralizing key management, leverages AWS best practices for IAM role delegation and cross-account access, enabling organizations to maintain security and compliance while reducing operational overhead. By implementing this approach, SaaS providers or large organizations facing similar challenges can effectively manage their encryption infrastructure as they scale, without compromising security or increasing complexity.

