A multi-tenant SaaS offering composes multiple applications. Applications use a shared IAM Session Broker library to scope user access to their tenant’s boundary. SaaS provider wants to build a shared IAM Session Broker application instead to reduce operational overhead and improve security posture. The application should initially support the ABAC authorization strategy.
${aws:PrincipalTag/
key
}
variable in policies for scoping accessDocumentsAPIDataAccess
)TenantID
)custom:tenant_id
)https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json
)Context
We need to identify application boundaries by describing stories and flows on technical level.
Scope the user access to their tenant’s boundary:
Decision
Create IAM Session Broker and Identity Provider applications. IAM Session Broker returns tenant-scoped temporary security credentials based on JWT claims for registered applications. Identity Provider is not described in this example for brevity.
Consequences
IAM Session Broker is on the critical path for upstream applications. Hence, it should maintain the agreed upon service level objectives (SLOs). Users drive the requests volume to IAM Session Broker, because the user-agent provides the JWT. Hence, IAM Session Broker performance characteristics should take interactive flows as the baseline.
Context
We need to define IAM Session Broker components by describing requirements on technical level.
Decision
Create the following components:
Gateway should authorize requests and throttle if needed to prevent the “noisy neighbor” problem. Gateway should proxy all authorized and non-throttled requests to Credentials Manager. Credentials Manager should 1/ fetch Access Metadata 2/ call Temporary Security Credentials Provider to assume the Service Role 3/ call Temporary Security Credentials Provider using the Service Role credentials to assume the access role 4/ return the scoped temporary security credentials.
Gateway
Use Amazon API Gateway HTTP API with IAM authorization. Use proxy integration for Credentials Manager. Leverage usage plans to prevent the “noisy neighbor” problem. Use the Lambda authorizer as the API key source (example) and application name as the API key.
Credentials Manager
Use Lambda function and Lambda Powertools for Python. Use Lambda provisioned concurrency to reduce latency. Cache applications scoped temporary security credentials to further reduce latency.
Request | Description | Request body | Response |
---|---|---|---|
POST /applications | Register an application | { “AccessRoleName”: “…”, “SessionTagKey”: “…”, “JWTClaimName”: “…”, “JWKSetURL”: “…“ } |
|
GET /credentials?jwt=X | Return scoped temporary security credentials | { “AccessKeyId”:”…”, “SecretAccessKey”:”…”, “SessionToken”:”…“ } |
Access Metadata
Use Amazon DynamoDB. Create an AccessMetadata
DynamoDB table. The table should store the registered access metadata.
Data model (designed using NoSQL WorkBench for DynamoDB):
Temporary Security Credentials Provider
Use AWS Session Token Service (AWS STS). Other options include AWS IAM Roles Anywhere and AWS IoT Core credential provider. Choose AWS STS because there is currently no requirement to support on-premises applications and/or certificate-based authentication use cases.
Service Role
Create IAMSessionBroker
IAM role. Creating a dedicated Service Role enables flexibility for the IAM Session Broker architecture. Applications should trust the Service Role to assume their access role. The service role doesn’t have any permissions. Per requirements, applications and IAM Session Broker should be in the same account. The applications access role’s trust policy acts as an IAM resource-based policy. When a resource-based policy grants access to a principal in the same account, no additional identity-based policy is required (documentation).
Note: Applications access role trust policy uses the IAMSessionBroker
role ID once saved. Deleting or altering the IAMSessionBroker
role will require applications to update their access role’s trust policy to apply the new IAMSessionBroker
role ID.
Consequences
To support cross-account scenarios, would need to replace API Gateway HTTP API by API Gateway REST API, because HTTP API doesn’t support resource policies at this time.
Credentials Manager uses role chaining: 1/ assume Service Role 2/ assume application access role. Role chaining limits the role session to a maximum of one hour. In the worst case scenario, the Credentials Manager will need to call Temporary Security Credentials Provider (AWS STS) every hour for a specific application.
Context
We need to decide on a service discovery strategy to allow applications discover IAM Session Broker API endpoint without managing configuration files.
Decision
Use DNS for service discovery. Use the following naming convention for domain hierarchy:
<application>.<region>.<environment>.<product>.<top-level domain>
Example:
iam-session-broker.eu-west-1.gamma.saas-platform.example.com
Delegate the product sub-domain to a dedicated AWS account so that each product team can manage DNS zones for their applications. Using the above approach, applications can construct the IAM Session Broker API endpoint at deployment time.
Consequences
This approach supports cross-environment (account and Region) use cases by relying on naming convention.
Python: 3.9.11
AWS CDK Toolkit (CLI) and AWS CDK Construct Library: 2.69.0
Project template: https://github.com/aws-samples/aws-cdk-project-structure-python
IAM Session Broker: iam-session-broker
IAM Session Broker
service/
access_metadata.py
class AccessMetadata:
dynamodb.Table
api/
<Powertools for AWS Lambda (Python) application>
compute.py
class Compute:
iam.Role
lambda.Function
lambda.Alias
lambda.Version
gateway.py
class Gateway:
apigatewayv2.Api
apigatewayv2.Model
apigatewayv2.Route
apigatewayv2.Stage
apigatewayv2.Authorizer
apigatewayv2.Deployment
service_role.py
class ServiceRole:
iam.Role
service_stack.py
class ServiceStack:
service_role.ServiceRole
access_metadata.AccessMetadata
credentials_manager.compute.Compute
gateway.Gateway
app.py
service.service_stack.ServiceStack("IAMSessionBroker-Service-Sandbox")
{
"version": "2.0",
"routeKey": "$default",
"rawPath": "/applications",
"rawQueryString": "",
"headers": {
"accept": "application/xml",
"accept-encoding": "gzip, deflate",
"authorization": "AWS4-HMAC-SHA256 Credential=ASIA3YC54HEJWX32ILMG/20230317/eu-west-1/execute-api/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=1b0063ba301f7668d5c7c982e505609db52ad83c3a879c311047acf6205cb760",
"content-length": "0",
"content-type": "application/json",
"host": "d0cfuu2ujg.execute-api.eu-west-1.amazonaws.com",
"user-agent": "python-requests/2.28.2",
"x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"x-amz-date": "20230317T170215Z",
"x-amz-security-token": "<redacted>",
"x-amzn-trace-id": "Root=1-64149d18-046d89152552d1da34770913",
"x-forwarded-for": "85.250.125.159",
"x-forwarded-port": "443",
"x-forwarded-proto": "https"
},
"requestContext": {
"accountId": "111111111111",
"apiId": "d0cfuu2ujg",
"authorizer": {
"iam": {
"accessKey": "ASIA3YC54HEJWX32ILMG",
"accountId": "111111111111",
"callerId": "AROA3YC54HEJ7ZID2KGLH:user@example.com",
"cognitoIdentity": null,
"principalOrgId": "aws:PrincipalOrgID",
"userArn": "arn:aws:sts::111111111111:assumed-role/AWSReservedSSO_DeveloperAccess_073t3cf358b80610/user@example.com",
"userId": "AROA3YC54HEJ7ZID2KGLH:user@example.com"
}
},
"domainName": "d0cfuu2ujg.execute-api.eu-west-1.amazonaws.com",
"domainPrefix": "d0cfuu2ujg",
"http": {
"method": "POST",
"path": "/applications",
"protocol": "HTTP/1.1",
"sourceIp": "85.250.125.159",
"userAgent": "python-requests/2.28.2"
},
"requestId": "B7170h_-DoEEPzA=",
"routeKey": "$default",
"stage": "$default",
"time": "17/Mar/2023:17:02:16 +0000",
"timeEpoch": 1679072536104
},
"isBase64Encoded": false
}