Multi-Tenant Application Security Cheat Sheet / 多租户应用安全速查表¶
Introduction / 介绍¶
Multi-tenant applications serve multiple customers (tenants) from a shared infrastructure, codebase, and often shared databases. This architecture is the foundation of modern SaaS platforms, offering cost efficiency and simplified operations.
多租户应用从共享的基础设施、代码库和通常共享的数据库中为多个客户(租户)提供服务。这种架构是现代SaaS平台的基础,提供成本效益和简化操作。
However, multi-tenancy introduces critical security challenges: a single vulnerability can expose all tenants' data, misconfigurations can leak data across tenant boundaries, and resource contention can impact availability.
然而,多租户引入了关键的安全挑战:单个漏洞可能会暴露所有租户的数据,配置错误可能会导致数据跨越租户边界泄露,资源争用可能会影响可用性。
This cheat sheet provides best practices to secure multi-tenant applications, ensure tenant isolation, and prevent cross-tenant attacks.
本速查表提供了确保多租户应用安全、确保租户隔离和防止跨租户攻击的最佳实践。
Key Risks / 主要风险¶
Use the Authorization Cheat Sheet for baseline access controls.
使用授权速查表进行基线访问控制。
- Cross-Tenant Data Leakage: Bugs or misconfigurations exposing one tenant's data to another.
中文:跨租户数据泄露:漏洞或配置错误导致一个租户的数据暴露给另一个租户。 - Tenant Impersonation: Attackers gaining access to another tenant's context or resources.
中文:租户伪造:攻击者获取对另一个租户的上下文或资源的访问权限。 - Broken Tenant Isolation: Insufficient separation at database, cache, storage, or compute layers.
中文:损坏的租户隔离:在数据库、缓存、存储或计算层存在不足的分离。 - Insecure Direct Object References (IDOR): Accessing resources by manipulating tenant/resource IDs.
中文:不安全的直接对象引用 (IDOR): 通过操纵租户/资源 ID 访问资源。 - Noisy Neighbor Attacks: One tenant exhausting shared resources, impacting others (DoS).
中文:噪声邻居攻击:一个租户耗尽共享资源,影响其他租户(DoS)。 - Privilege Escalation Across Tenants: Exploiting admin functions to access other tenants.
中文:跨租户权限提升:利用管理员功能访问其他租户。 - Tenant Context Injection: Manipulating tenant identifiers in requests, tokens, or headers.
中文:租户上下文注入:在请求、令牌或头中操纵租户标识符。 - Shared Resource Poisoning: Cache poisoning, queue injection, or storage pollution affecting other tenants.
中文:共享资源中毒:缓存中毒、队列注入或存储污染影响其他租户。 - Insecure Tenant Onboarding/Offboarding: Incomplete provisioning, unauthorized residual access, or retention beyond policy.
中文:不安全的租户注册/注销: 配置不完整、未经授权的残留访问或超出政策范围的保留。 - Audit & Compliance Gaps: Insufficient tenant-specific logging for regulatory requirements.
中文:审计与合规差距:对于监管要求,租户特定日志不足。
Best Practices / 最佳实践¶
1. Tenant Identification & Context Management / 1. 租户识别与上下文管理¶
- For tenant-scoped operations, establish tenant context early in the request lifecycle (middleware/interceptor).
中文:对于租户范围的操作,在请求生命周期早期(中间件/拦截器)建立租户上下文。 - Choose identifiers appropriate to their exposure risk. Opaque, random identifiers can reduce enumeration, but they are not authorization controls.
中文:选择适合其暴露风险的标识符。不透明的、随机的标识符可以减少枚举,但它们不是授权控制。 - Treat client-supplied tenant identifiers as selectors only. Verify that the authenticated principal is authorized to act in the selected tenant.
中文:将客户端提供的租户标识符仅作为选择器处理。验证已认证的主体是否有权在所选租户中操作。 - Bind tenant context to a server-verified identity and current tenant membership or service authorization.
中文:将租户上下文绑定到服务器验证的身份以及当前租户成员资格或服务授权。 - Propagate server-verified tenant context to components that need it for tenant-sensitive decisions or observability; do not let downstream components replace it with unverified input.
中文:将服务器验证的租户上下文传播到需要它进行租户敏感决策或可观察性的组件;不要让下游组件用未经验证的输入替换它。
Bad example: Trusting client-supplied tenant ID
# Dangerous: The header selects a tenant without checking the caller's membership.
# Query parameterization prevents injection, not cross-tenant access.
def get_tenant_data(request):
tenant_id = request.headers.get("X-Tenant-ID") # Attacker can modify!
return db.execute("SELECT * FROM data WHERE tenant_id = :tid", {"tid": tenant_id})
Good example: Verifying tenant context against authenticated identity
from functools import wraps
from contextvars import ContextVar
from typing import Optional
class TenantContext:
def __init__(self, tenant_id: str, user_id: str, roles: list):
self.tenant_id = tenant_id
self.user_id = user_id
self.roles = roles
self.is_validated = True
# Request-local tenant context
current_tenant: ContextVar[Optional[TenantContext]] = ContextVar(
'current_tenant', default=None
)
class TenantMiddleware:
"""Verify tenant context against authenticated identity and membership."""
async def __call__(self, request, call_next):
# A verified claim may select the tenant, but authorization still
# depends on the issuer's guarantees or a current membership check.
token_claims = request.state.verified_claims # Set by auth middleware
if not token_claims or "tenant_id" not in token_claims:
return JSONResponse(status_code=401, content={"error": "Missing tenant context"})
tenant_id = token_claims["tenant_id"]
principal_id = token_claims["sub"]
membership = await self.tenant_service.get_active_membership(
principal_id, tenant_id
)
if not membership:
return JSONResponse(status_code=403, content={"error": "Tenant access denied"})
# Set tenant context for this request
ctx = TenantContext(
tenant_id=tenant_id,
user_id=principal_id,
roles=membership.roles
)
token = current_tenant.set(ctx)
try:
response = await call_next(request)
return response
finally:
current_tenant.reset(token)
def require_tenant(func):
"""Decorator ensuring tenant context is present."""
@wraps(func)
async def wrapper(*args, **kwargs):
ctx = current_tenant.get()
if not ctx or not ctx.is_validated:
raise SecurityException("Tenant context required")
return await func(*args, **kwargs)
return wrapper
2. Database Isolation Strategies / 2. 数据库隔离策略¶
Choose an isolation strategy based on security requirements, compliance needs, and operational complexity:
根据安全要求、合规性需求和操作复杂性选择隔离策略:
| Strategy / 策略 | Potential Boundary / 潜在边界 | Conditions and Trade-Offs / 条件和权衡 |
|---|---|---|
| Separate Databases 分离数据库 |
Database and credential boundary 数据库和凭证边界 |
Strong when credentials, network access, administrative paths, and backups are isolated; higher operational cost 当凭证、网络访问、管理路径和备份被隔离时强;更高的运营成本 |
| Separate Schemas 分离模式 |
Namespace and database-role boundary 命名空间和数据库角色边界 |
Requires disciplined grants, role separation, search_path handling, and migrations需要纪律性的授权、角色分离、 search_path 处理和迁移 |
| Shared Tables (Row-Level) 共享表(行级) |
Policy and row boundary 策略和行边界 |
Requires enforceable tenant ownership, policy coverage for classified tenant-owned tables, constrained request roles, and negative-path tests 需要可执行的租户所有权、针对分类租户拥有表的策略覆盖、受限请求角色和负路径测试 |
| Hybrid 混合 |
Varies by workload or tier 根据工作负载或层级而异 |
Document and test the boundary used for each data class 记录并测试每个数据类使用的边界 |
Row-Level Security Implementation (PostgreSQL)
-- Enable RLS on tenant tables
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
-- Create policy that restricts access to current tenant
CREATE POLICY tenant_isolation_policy ON orders
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::uuid);
CREATE POLICY tenant_isolation_policy ON customers
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::uuid);
-- Force RLS for table owners (not superusers or BYPASSRLS roles)
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
ALTER TABLE customers FORCE ROW LEVEL SECURITY;
Do Not Use an RLS-Bypassing Role for Tenant-Scoped Request Paths / 不要为租户范围请求路径使用绕过RLS的角色¶
FORCE ROW LEVEL SECURITY applies policies to a table owner. It does not constrain a superuser or a role with the BYPASSRLS attribute; PostgreSQL documents that both always bypass row security.
FORCE ROW LEVEL SECURITY 将策略应用于表所有者。它不会约束超级用户或具有BYPASSRLS属性的角色的权限;PostgreSQL 文档指出两者始终绕过行安全。
- Connect the normal application request path with a least-privileged role that is neither a superuser nor a
BYPASSRLSrole.
中文:将正常应用程序请求路径与既不是超级用户也不是BYPASSRLS角色的最小权限角色连接起来。 - Reserve privileged connections for migrations and explicitly authorized administrative jobs.
中文:为迁移和明确授权的管理任务保留特权连接。 - Do not serve ordinary tenant-scoped requests through a privileged connection. A privileged job must explicitly authorize and constrain its tenant set or cross-tenant operation because RLS will not do so.
中文:不要通过特权连接处理普通租户范围的请求。特权任务必须明确授权并限制其租户集或跨租户操作,因为RLS不会这样做。 - Check the deployed request role, not only the role declared in source-controlled configuration. PostgreSQL exposes
rolsuperandrolbypassrlsinpg_roles.
中文:检查已部署的请求角色,而不仅仅是源控制配置中声明的角色。PostgreSQL在pg_roles中公开rolsuper和rolbypassrls。
Scope Tenant Context to the Transaction / 将租户上下文范围限定在事务中¶
The example policy depends on current_setting('app.current_tenant'). Because it omits the optional missing_ok => true argument, PostgreSQL raises an error if the setting does not exist instead of returning NULL; that is a deliberate fail-closed choice. The current_setting documentation describes both behaviors.
示例策略依赖于current_setting('app.current_tenant')。因为它省略了可选的missing_ok => true参数,所以当设置不存在时,PostgreSQL会引发错误而不是返回NULL;这是一个故意的关闭失败选择。[current_setting文档](https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET)描述了这两种行为。
A normal SET persists until the session ends after its transaction commits, while SET LOCAL lasts only for the current transaction. When an application or connection pool reuses a database session without resetting it, session-scoped tenant state can therefore be inherited by the next request.
正常的SET在事务提交后持续到会话结束,而[SET LOCAL`只持续当前事务](https://www.postgresql.org/docs/current/sql-set.html)。当应用程序或连接池重用未重置的数据库会话时,会话范围的租户状态可能会被下一个请求继承。
- Begin a transaction, set the tenant context with
SET LOCAL, or callset_config('app.current_tenant', tenant_id, true), then execute queries that depend on that setting in the same transaction. Thetrueargument makesset_configtransaction-local.
中文:开始一个事务,使用SET LOCAL设置租户上下文,或者调用set_config('app.current_tenant', tenant_id, true),然后在同一事务中执行依赖于该设置的查询。true参数使set_config成为事务局部。 - Re-establish the tenant context for every transaction. Never assume a newly borrowed connection has the correct setting.
中文:为每个事务重新建立租户上下文。永远不要假设新借用的连接具有正确的设置。 - Fail closed if the tenant context is missing or invalid; do not fall back to an unscoped query.
中文:如果租户上下文缺失或无效,则关闭失败;不要回退到无范围查询。 - Commit or roll back before returning the connection to the pool.
中文:在将连接返回到池之前提交或回滚。
SQLAlchemy deprecated QueryEvents.before_compile because it does not cover ORM-level attribute and relationship loads. The current recommended pattern combines SessionEvents.do_orm_execute with with_loader_criteria so criteria propagate to occurrences of the mapped entity, including eager and lazy relationship loads.
SQLAlchemy 已弃用 QueryEvents.before_compile 因为它不涵盖 ORM 层的属性和关系加载。当前的推荐模式结合了 SessionEvents.do_orm_execute 与 with_loader_criteria,以便标准传播到映射实体的出现,包括 eager 和 lazy 关系加载。
Application-Level Enforcement (Python/SQLAlchemy)
from sqlalchemy import event, Column, String, text
from sqlalchemy.orm import Session, declared_attr, with_loader_criteria
from contextlib import contextmanager
class TenantMixin:
"""Mixin that adds tenant_id to models that inherit it."""
@declared_attr
def tenant_id(cls):
return Column(String(36), nullable=False, index=True)
class TenantAwareSession(Session):
"""Session carrying tenant context for participating ORM operations."""
def __init__(self, *args, tenant_id: str | None = None, **kwargs):
super().__init__(*args, **kwargs)
self._tenant_id = tenant_id
@property
def tenant_id(self):
if not self._tenant_id:
raise SecurityException("Tenant ID not set on session")
return self._tenant_id
# Add tenant criteria to ORM SELECTs issued through TenantAwareSession.
# with_loader_criteria propagates this rule to eager and lazy relationship loads.
@event.listens_for(TenantAwareSession, "do_orm_execute")
def add_tenant_filter(execute_state):
if (
execute_state.is_select
and not execute_state.is_column_load
and not execute_state.is_relationship_load
):
tenant_id = execute_state.session.tenant_id
execute_state.statement = execute_state.statement.options(
with_loader_criteria(
TenantMixin,
lambda model: model.tenant_id == tenant_id,
include_aliases=True,
)
)
# Set and validate tenant_id for new mapped objects in this session.
@event.listens_for(TenantAwareSession, "before_flush")
def set_tenant_on_insert(session, flush_context, instances):
tenant_id = session.tenant_id
for target in session.new:
if isinstance(target, TenantMixin):
if target.tenant_id not in (None, tenant_id):
raise SecurityException("Object tenant does not match session")
target.tenant_id = tenant_id
# Secure session factory
@contextmanager
def tenant_session(tenant_id: str):
"""Create a tenant-scoped database session."""
ctx = current_tenant.get()
if not ctx or ctx.tenant_id != tenant_id:
raise SecurityException("Tenant session requires verified context")
session = TenantAwareSession(bind=engine, tenant_id=tenant_id)
try:
# The final true makes the tenant context transaction-local
session.execute(
text("SELECT set_config('app.current_tenant', :tenant_id, true)"),
{"tenant_id": tenant_id}
)
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
This ORM helper is defense in depth, not complete enforcement. It applies tenant criteria to ORM SELECTs issued through TenantAwareSession, with the criteria propagated to relationship loaders. Raw SQL, Core connections, bulk operations, and code using another session type require separate controls; use database policies or constrained roles as the final boundary where possible.
这个 ORM 辅助工具是深度防御,而不是完全执行。它将租户标准应用于通过 TenantAwareSession 发布的 ORM SELECT,并将标准传播到关系加载器。原始 SQL、核心连接、批量操作以及使用其他会话类型的代码需要单独的控制;尽可能使用数据库策略或受限角色作为最终边界。
Verify Tenant Isolation / 验证租户隔离¶
Test isolation through the same role, connection path, and pooling mode used by the application. A privileged test connection can make a correct policy appear broken, while a test that never exercises the deployed request role can miss a bypass.
通过应用程序使用的相同角色、连接路径和连接池模式来测试隔离。一个具有特权的测试连接可能会使正确的策略看起来是错误的,而一个从未执行部署请求角色的测试可能会错过绕过的情况。
- Test the authorization matrix. For each RLS-protected table, prove that expected cross-tenant operations are denied and expected same-tenant operations succeed. Where sharing or platform administration is intentional, test the exact permitted path and prove that it grants no broader access.
中文:测试授权矩阵。对于每个 RLS 受保护的表,证明预期的跨租户操作被拒绝,预期的同租户操作成功。在共享或平台管理是故意的场合,测试确切的允许路径,并证明它不会授予更广泛的访问权限。 - Discover coverage. Derive the expected tenant-scoped table inventory from the schema or an explicit classification, then compare it with PostgreSQL's
pg_class.relrowsecurityandrelforcerowsecurityand thepg_policiesview. Fail when a new table has no classification, RLS is disabled, or the expected policy is absent.
中文:发现覆盖率。从模式或显式分类中推导出预期的租户范围表清单,然后将其与PostgreSQL的pg_class.relrowsecurity和relforcerowsecurity以及pg_policies视图进行比较。如果新表没有分类、RLS被禁用或缺少预期策略,则失败。 - Assert the request role. In the deployed environment, fail a configuration test if the request-path role has
rolsuperorrolbypassrlsset.
中文:断言请求角色。在部署环境中,如果请求路径角色设置了rolsuper或rolbypassrls,则配置测试失败。 - Exercise connection reuse. Run requests for two tenants over reused connections and prove the second request cannot observe the first request's tenant context.
中文:练习连接重用。在重用的连接上运行两个租户的请求,并证明第二个请求无法观察到第一个请求的租户上下文。
A hand-maintained table list can drift: the same omission that leaves RLS off a new table can also leave that table out of the test. Prefer schema-derived discovery, or gate schema changes so each new table must be classified as tenant-scoped, intentionally shared, or otherwise isolated.
手动维护的表列表可能会漂移:导致 RLS 在新表上关闭的相同遗漏也可能导致该表被排除在测试之外。优先使用模式导出的发现,或者门控模式更改,以便每个新表都必须被分类为租户范围、有意共享或其他隔离。
3. Preventing Cross-Tenant Data Access (IDOR Prevention) / 3. 防止跨租户数据访问(IDOR 防范)¶
- For each tenant-scoped resource, verify that the authenticated principal can act in the resource's tenant.
中文:对于每个租户范围的资源,验证已认证主体是否可以在该资源的租户中执行操作。 - Include tenant scope in the lookup or authorization policy when ownership is tenant-specific. A composite key (
tenant_id+resource_id) is one option, not a universal requirement.
中文:当所有权特定于租户时,在查找或授权策略中包含租户范围。复合键(tenant_id+resource_id)是一种选择,但不是普遍要求。 - Enforce authorization at a boundary traversed by every tenant-owned access path. Add data-layer checks as defense in depth where the architecture supports them.
中文:在所有租户拥有的访问路径穿越的边界上强制执行授权。如果架构支持,添加数据层检查作为深度防御。 - Treat opaque or random identifiers as defense in depth against enumeration, not as a substitute for authorization.
中文:将不透明或随机标识符视为防御枚举,而不是授权的替代品。
Bad example: Direct object reference without tenant validation
# Dangerous: Only checks resource_id, not tenant ownership
@app.get("/api/documents/{document_id}")
async def get_document(document_id: str):
doc = db.query(Document).filter(Document.id == document_id).first()
if not doc:
raise HTTPException(404)
return doc # Could return another tenant's document!
Good example: Tenant-scoped resource access
from uuid import UUID
from typing import TypeVar, Generic, Type
T = TypeVar('T')
class TenantScopedRepository(Generic[T]):
"""Repository that scopes the operations provided below to one tenant."""
def __init__(self, model: Type[T], session: Session):
self.model = model
self.session = session
@property
def tenant_id(self) -> str:
ctx = current_tenant.get()
if not ctx:
raise SecurityException("Tenant context required")
return ctx.tenant_id
def get_by_id(self, resource_id: UUID) -> Optional[T]:
"""Get resource only if it belongs to current tenant."""
return self.session.query(self.model).filter(
self.model.id == resource_id,
self.model.tenant_id == self.tenant_id # Tenant-owned resource
).first()
def list_all(self, limit: int = 100, offset: int = 0) -> list[T]:
"""List resources for current tenant only."""
return self.session.query(self.model).filter(
self.model.tenant_id == self.tenant_id
).limit(limit).offset(offset).all()
def create(self, **kwargs) -> T:
"""Create resource with tenant_id automatically set."""
if 'tenant_id' in kwargs and kwargs['tenant_id'] != self.tenant_id:
raise SecurityException("Cannot create resource for different tenant")
kwargs['tenant_id'] = self.tenant_id
instance = self.model(**kwargs)
self.session.add(instance)
return instance
def delete(self, resource_id: UUID) -> bool:
"""Delete resource only if it belongs to current tenant."""
result = self.session.query(self.model).filter(
self.model.id == resource_id,
self.model.tenant_id == self.tenant_id
).delete()
return result > 0
# Usage
@app.get("/api/documents/{document_id}")
@require_tenant
async def get_document(document_id: UUID, db: Session = Depends(get_db)):
repo = TenantScopedRepository(Document, db)
doc = repo.get_by_id(document_id)
if not doc:
raise HTTPException(404, "Document not found") # Don't reveal if it exists for other tenant
return doc
4. Cache & Session Isolation / 4. 缓存与会话隔离¶
- Classify each cached value as global, tenant-scoped, or user-scoped.
中文:将每个缓存的值分类为全局、租户范围或用户范围。 - Include the tenant identifier in every cache key whose value or authorization varies by tenant.
中文:在所有值或授权随租户变化的缓存键中包含租户标识符。 - Give intentionally shared entries an explicit global namespace and document why they are safe to share.
中文:为有意共享的条目提供一个显式的全局命名空间,并说明为什么它们可以安全共享。 - Include every other attribute that changes the result, such as user, locale, feature set, or permission version.
中文:包括所有其他会改变结果的特征,例如用户、区域、功能集或权限版本。 - Authorize the request before reading a protected cached value; cache-key separation does not replace authorization.
中文:在读取受保护的缓存值之前授权请求;缓存键分离不能替代授权。 - Use separate cache instances for tenants that require stronger physical isolation.
中文:为需要更强物理隔离的租户使用单独的缓存实例。 - Choose TTL and invalidation behavior from freshness and authorization risk. Immutable, versioned global entries may not require expiry.
中文:从新鲜度和授权风险中选择TTL和失效行为。不可变、版本化的全局条目可能不需要过期。
Bad example: Tenant-scoped data stored under a shared key
# Dangerous: user IDs are not guaranteed to be unique across tenants,
# and the key incorrectly places tenant-scoped data in a global namespace.
def get_user_preferences(user_id: str):
cache_key = f"global:user-preferences:{user_id}"
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# ...
Good example: Explicit tenant and global cache scopes
import json
def tenant_cache_key(tenant_id: str, resource: str, item_id: str) -> str:
return f"tenant:{tenant_id}:{resource}:{item_id}"
def global_cache_key(resource: str, version: str) -> str:
return f"global:{resource}:{version}"
async def get_user_preferences(user_id: str):
# These application-specific helpers derive tenant context from the
# verified session and enforce access before the cache lookup.
tenant_id = require_authenticated_tenant()
await authorize_user_access(tenant_id, user_id)
# Preferences vary by tenant, so tenant_id is part of the key.
key = tenant_cache_key(tenant_id, "user-preferences", user_id)
if cached := redis.get(key):
return json.loads(cached)
preferences = await db.fetch_preferences(tenant_id, user_id)
redis.setex(key, 1800, json.dumps(preferences))
return preferences
async def get_country_codes():
# This versioned reference data is identical and authorized for all tenants.
key = global_cache_key("country-codes", "v1")
if cached := redis.get(key):
return json.loads(cached)
country_codes = await db.fetch_public_country_codes()
redis.setex(key, 86400, json.dumps(country_codes))
return country_codes
5. API, Asynchronous Work & Resource Controls / 5. API、异步工作与资源控制¶
API Security & Rate Limiting / API安全与速率限制¶
- When tenants share capacity or quotas, include tenant identity as one rate-limit dimension alongside any required global, endpoint, user, or IP limits.
中文:当租户共享容量或配额时,将租户身份作为一个速率限制维度,包括任何必需的全局、端点、用户或IP限制。 - Rate limits at the HTTP boundary do not constrain every shared resource. Where tenant load can affect other tenants, apply tenant-aware limits or scheduling to the relevant bottlenecks, such as concurrent work, queued messages, database connections, fan-out, CPU, or memory. Retain global safety limits, and isolate a worker or resource pool when the risk or service commitment justifies the operational cost. The AWS SaaS Lens describes scaling, throttling, and selective resource isolation as complementary noisy-neighbor controls.
中文:HTTP边界的速率限制并不约束每个共享资源。当租户负载可能影响其他租户时,将租户感知的限制或调度应用于相关瓶颈,例如并发工作、队列消息、数据库连接、扇出、CPU或内存。保留全局安全限制,并在风险或服务承诺足以证明运营成本时,隔离工作进程或资源池。AWS SaaS Lens描述了扩展、节流和选择性资源隔离作为补充的嘈杂邻居控制。 - Validate server-verified tenant context on every tenant-scoped API request. Public or global endpoints need no artificial tenant context.
中文:在每个租户范围的 API 请求中验证服务器验证的租户上下文。公共或全局端点无需人工租户上下文。 - Bind API credentials to explicit tenant sets, environments, and permission scopes. An intentionally cross-tenant service identity must be separately authorized and least privileged.
中文:将 API 凭证绑定到明确的租户集、环境和权限范围。一个有意跨租户的服务标识必须单独授权并具有最小权限。 - When B2B request signing is required, bind the signature to the security-relevant request context, such as tenant selection, target audience, method, path, body digest, and expiration, as applicable.
中文:当需要 B2B 请求签名时,将签名绑定到与安全相关的请求上下文,例如租户选择、目标受众、方法、路径、体摘要和过期时间,适用时。
The limits below are illustrative. Choose production limits from capacity, abuse risk, and contractual quotas, and mount tenant-aware middleware only on routes where tenant context is required.
以下限制仅供参考。从容量、滥用风险和合同配额中选择生产限制,仅在需要租户上下文的路由上安装租户感知中间件。
Tenant-Aware Rate Limiting
import time
from dataclasses import dataclass
from enum import Enum
class TenantTier(Enum):
FREE = "free"
STARTER = "starter"
BUSINESS = "business"
ENTERPRISE = "enterprise"
@dataclass
class RateLimitConfig:
requests_per_minute: int
requests_per_day: int
burst_size: int
TIER_LIMITS = {
TenantTier.FREE: RateLimitConfig(60, 1000, 10),
TenantTier.STARTER: RateLimitConfig(300, 10000, 50),
TenantTier.BUSINESS: RateLimitConfig(1000, 100000, 100),
TenantTier.ENTERPRISE: RateLimitConfig(5000, 1000000, 500),
}
class TenantRateLimiter:
"""One tenant dimension in a broader rate-limiting strategy."""
def __init__(self, redis_client):
self.redis = redis_client
async def check_rate_limit(self, tenant_id: str, tenant_tier: TenantTier) -> dict:
"""Check and update rate limit for tenant."""
config = TIER_LIMITS[tenant_tier]
now = time.time()
minute_key = f"rl:{tenant_id}:min:{int(now // 60)}"
day_key = f"rl:{tenant_id}:day:{int(now // 86400)}"
pipe = self.redis.pipeline()
# Increment counters
pipe.incr(minute_key)
pipe.expire(minute_key, 60)
pipe.incr(day_key)
pipe.expire(day_key, 86400)
results = pipe.execute()
minute_count = results[0]
day_count = results[2]
# Check limits
if minute_count > config.requests_per_minute:
return {
"allowed": False,
"reason": "minute_limit_exceeded",
"retry_after": 60 - (now % 60),
"limit": config.requests_per_minute
}
if day_count > config.requests_per_day:
return {
"allowed": False,
"reason": "daily_limit_exceeded",
"retry_after": 86400 - (now % 86400),
"limit": config.requests_per_day
}
return {
"allowed": True,
"remaining_minute": config.requests_per_minute - minute_count,
"remaining_day": config.requests_per_day - day_count
}
class RateLimitMiddleware:
"""Middleware for routes that require tenant-scoped rate limits."""
async def __call__(self, request, call_next):
ctx = current_tenant.get()
if not ctx:
return JSONResponse(
status_code=401,
content={"error": "Tenant context required"}
)
tenant = await self.tenant_service.get_tenant(ctx.tenant_id)
result = await self.rate_limiter.check_rate_limit(
ctx.tenant_id,
tenant.tier
)
if not result["allowed"]:
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded", "details": result},
headers={
"Retry-After": str(int(result["retry_after"])),
"X-RateLimit-Limit": str(result["limit"]),
"X-RateLimit-Remaining": "0"
}
)
response = await call_next(request)
# Add rate limit headers
response.headers["X-RateLimit-Remaining-Minute"] = str(result["remaining_minute"])
response.headers["X-RateLimit-Remaining-Day"] = str(result["remaining_day"])
return response
Tenant-Aware Asynchronous Work / 租户感知异步工作¶
Classify asynchronous work as global, tenant-scoped, or explicitly cross-tenant. A shared queue or topic is not itself a tenant-isolation boundary. Microsoft's multitenant messaging guidance describes the trade-off between shared and dedicated messaging infrastructure and the need for application-enforced isolation when infrastructure is shared.
将异步工作分类为全局、租户范围或明确跨租户。共享队列或主题本身不是租户隔离边界。Microsoft 的多租户消息指南描述了共享和专用消息基础设施之间的权衡,以及在基础设施共享时需要应用程序强制隔离的需求。
- For tenant-scoped work, derive tenant context from the authenticated producer and bind it to the message through trusted broker routing, authenticated metadata, or an integrity-protected payload. Do not let an unverified message field replace the producer's authorized scope.
中文:对于租户范围的工作,从认证的生产者中提取租户上下文,并通过受信任的代理路由、认证元数据或完整性保护的有效负载将其绑定到消息。不要让未经验证的消息字段替换生产者的授权范围。 - At the consumer, authenticate the producer or broker path, re-establish tenant context, and authorize the operation and target resource. Re-check time-sensitive membership or permission when delayed execution could make the original decision stale.
中文:在消费者端,验证生产者或代理路径,重新建立租户上下文,并授权操作和目标资源。当延迟执行可能导致原始决策过时时,重新检查时间敏感的成员资格或权限。 - Scope idempotency and deduplication keys, retry state, dead-letter access, and per-tenant ordering when their data or effects vary by tenant. Global jobs and authorized cross-tenant jobs should use explicit identities and scopes rather than a fabricated tenant.
中文:在数据或效果因租户而异时,范围幂等性和去重键、重试状态、死信访问以及按租户排序。全局作业和授权跨租户作业应使用显式标识符和范围,而不是伪造的租户。 - Apply tenant-aware queue depth, concurrency, and throughput controls when a shared worker fleet or broker is susceptible to noisy-neighbor load; retain service-wide limits for aggregate exhaustion.
中文:当共享工作员群集或代理容易受到噪声邻居负载的影响时,应用租户感知的队列深度、并发性和吞吐量控制;保留服务范围的聚合耗尽限制。
6. File Storage & Blob Isolation / 6. 文件存储与Blob隔离¶
- Classify stored objects as global, tenant-scoped, or user-scoped. Keep intentionally shared assets in an explicit global namespace.
中文:将存储对象分类为全局、租户范围或用户范围。将有意共享的资产保存在显式的全局命名空间中。 - Partition tenant-scoped objects with a tenant-aware key, bucket, account, or enforceable storage policy.
中文:使用租户感知的键、存储桶、账户或可执行存储策略对租户范围的对象进行分区。 - Authorize access to the exact object and operation before serving it or generating a signed URL.
中文:在提供服务或生成签名URL之前,授权访问确切的对象和操作。 - Limit signed URLs to the required object and method, with a lifetime appropriate to the operation and revocation model. The tenant identifier does not need to appear in the URL when authorization happened before signing.
中文:将签名URL限制在所需的对象和方法上,其生命周期适合于操作和撤销模型。当授权发生在签名之前时,租户标识符不需要出现在URL中。 - Use tenant-specific encryption keys when the risk or compliance model requires cryptographic isolation. A shared managed key with enforced access context can also be appropriate.
中文:当风险或合规性模型需要加密隔离时,使用特定于租户的加密密钥。具有强制访问上下文的共享管理密钥也可能适用。
Illustrative Tenant-Scoped File Storage
import boto3
from botocore.config import Config
from datetime import datetime
from pathlib import PurePosixPath
from typing import Optional
import re
class TenantFileStorage:
"""Illustrative S3 helper for tenant-scoped objects."""
def __init__(self, bucket_name: str, kms_key_id: str = None):
self.bucket = bucket_name
self.s3 = boto3.client('s3', config=Config(signature_version='s3v4'))
self.kms_key_id = kms_key_id
def _get_tenant_prefix(self, tenant_id: str) -> str:
"""Generate tenant-specific path prefix."""
# Naming is not authorization. Accept only the application's canonical
# tenant identifier format so it cannot alter the object-key structure.
if not re.fullmatch(r"[A-Za-z0-9_-]{1,128}", tenant_id):
raise ValueError("Invalid tenant identifier")
return f"tenants/{tenant_id}"
def _build_key(self, tenant_id: str, file_path: str) -> str:
"""Build full S3 key with tenant isolation."""
path = PurePosixPath(file_path)
if not path.parts or path.is_absolute() or ".." in path.parts or "\\" in file_path:
raise ValueError("Invalid object path")
return f"{self._get_tenant_prefix(tenant_id)}/{path.as_posix()}"
async def upload_file(self, tenant_id: str, file_path: str,
content: bytes, content_type: str) -> dict:
"""Upload file for tenant."""
await authorize_file_access(tenant_id, file_path, "put_object")
key = self._build_key(tenant_id, file_path)
extra_args = {
'ContentType': content_type,
'Metadata': {
'tenant-id': tenant_id,
'uploaded-at': datetime.utcnow().isoformat()
}
}
# Use tenant-specific KMS key if available
if self.kms_key_id:
extra_args['ServerSideEncryption'] = 'aws:kms'
extra_args['SSEKMSKeyId'] = self.kms_key_id
self.s3.put_object(
Bucket=self.bucket,
Key=key,
Body=content,
**extra_args
)
return {"key": key, "size": len(content)}
async def get_file(self, tenant_id: str, file_path: str) -> Optional[bytes]:
"""Get file only if it belongs to tenant."""
await authorize_file_access(tenant_id, file_path, "get_object")
key = self._build_key(tenant_id, file_path)
try:
response = self.s3.get_object(Bucket=self.bucket, Key=key)
# Verify tenant ownership from metadata
metadata_tenant = response.get('Metadata', {}).get('tenant-id')
if metadata_tenant != tenant_id:
raise SecurityException("Tenant mismatch in file metadata")
return response['Body'].read()
except self.s3.exceptions.NoSuchKey:
return None
async def generate_presigned_url(self, tenant_id: str, file_path: str,
expiration: int = 3600,
operation: str = 'get_object') -> str:
"""Authorize and sign one object operation for a verified tenant."""
await authorize_file_access(tenant_id, file_path, operation)
key = self._build_key(tenant_id, file_path)
url = self.s3.generate_presigned_url(
ClientMethod=operation,
Params={
'Bucket': self.bucket,
'Key': key,
},
ExpiresIn=expiration
)
return url
async def delete_tenant_data(self, tenant_id: str):
"""Delete current objects; versions and backups follow retention policy."""
# The delimiter prevents a tenant such as "acme" from matching
# another tenant's "acme-west" prefix.
prefix = f"{self._get_tenant_prefix(tenant_id)}/"
paginator = self.s3.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=self.bucket, Prefix=prefix):
objects = page.get('Contents', [])
if objects:
self.s3.delete_objects(
Bucket=self.bucket,
Delete={'Objects': [{'Key': obj['Key']} for obj in objects]}
)
7. Tenant Onboarding & Offboarding Security / 7. 客户上线与下线安全¶
- Implement secure tenant provisioning with isolated resources. Treat onboarding as an all-or-nothing operation: if a step fails, remove partially created resources and record the failure.
中文:实施安全客户配置,使用隔离资源。将上线视为全有或全无的操作:如果某个步骤失败,则删除部分创建的资源并记录失败信息。 - Generate unique encryption keys per tenant where required.
中文:根据需要为每个客户生成唯一的加密密钥。 - Generate tenant API credentials with a cryptographically secure random generator and at least 128 bits of entropy. Return each credential once and store only a hash, never the plaintext value. An approved hash function such as SHA-256 is enough for a high-entropy random key, as NIST SP 800-63B allows for look-up secrets. It does not protect low-entropy or user-chosen secrets; store those according to the Password Storage Cheat Sheet.
中文:使用密码学安全的随机生成器和至少128位的熵生成租户API凭证。一次性返回每个凭证,仅存储哈希值,永不存储明文值。批准的哈希函数,如SHA-256,对于高熵随机密钥就足够了,因为NIST SP 800-63B允许查找秘密。它不保护低熵或用户选择的秘密;根据密码存储作弊表存储这些秘密。 - Apply a documented retention and deletion policy to every place tenant data lives: active stores, caches, object versions, replicas, exports, backups, and encryption keys. Restrict access to records kept for legal reasons.
中文:对客户数据所在的所有位置应用文档化的保留和删除策略:活动存储、缓存、对象版本、副本、导出、备份和加密密钥。限制保留用于法律原因的记录的访问。 - During offboarding, put the tenant into a state that rejects new operations, then revoke all active sessions, API keys, and other tenant-scoped credentials.
中文:在下线期间,将客户置于拒绝新操作的状态,然后撤销所有活动会话、API密钥和其他客户范围凭证。 - When the retention policy permits deletion, remove the tenant's active data, cached data, and stored objects. Delete a tenant encryption key only after every copy encrypted under it, including backups, replicas, and records under legal hold, has passed its retention period, unless you intend to destroy those copies too.
中文:当保留策略允许删除时,删除客户的活动数据、缓存数据和存储对象。只有在包括备份、副本和法律保留下的记录在内的所有加密副本都过了保留期后,才删除客户加密密钥,除非您打算销毁这些副本。 - Maintain an audit trail of provisioning and deprovisioning, including failed and partial operations.
中文:维护配置和取消配置的审计跟踪,包括失败和部分操作。 - Provide tenant data export when contract, regulation, or product policy requires it.
中文:当合同、法规或产品政策要求时,提供客户数据导出。
Tenant Backup Restore / 租户备份恢复¶
A shared backup can contain data from several tenants. For a single-tenant recovery from a shared database, restore into a separate resource and selectively recover the target tenant's data, as described in Azure's multitenant storage guidance.
共享备份可以包含多个租户的数据。对于从共享数据库的单租户恢复,将数据恢复到单独的资源中,并选择性地恢复目标租户的数据,具体操作请参考Azure多租户存储指南。
- Authorize the operator for the target tenant and record the source backup, target, reason, and outcome.
中文:授权目标租户的操作员并记录源备份、目标、原因和结果。 - Check tenant ownership before copying records or objects into the live tenant scope. Do not give a tenant operator access to a shared backup.
中文:在将记录或对象复制到实时租户范围之前,检查租户所有权。不要给予租户操作员访问共享备份的权限。 - Reapply current access and retention rules; a backup can contain old memberships or data that should no longer be accessible.
中文:重新应用当前的访问和保留规则;备份可能包含旧的成员资格或不应再可访问的数据。 - Test a backup with two tenants: recover one tenant, verify the other tenant's records and objects are absent, and reject a restore request by an unauthorized actor.
中文:对两个租户进行备份测试:恢复一个租户,验证另一个租户的记录和对象不存在,并拒绝未经授权的恢复请求。
8. Logging, Monitoring & Audit / 8. 日志记录、监控与审计¶
- Include server-verified tenant context in tenant-scoped security and audit events; global infrastructure events may have no tenant.
中文:在租户范围的安全和审计事件中包含服务器验证的租户上下文;全局基础设施事件可能没有租户。 - A centralized audit store is acceptable when read access enforces tenant scope and cross-tenant access requires an explicit platform permission.
中文:当读取访问强制执行租户范围且跨租户访问需要显式平台权限时,集中式审计存储是可以接受的。 - Monitor denied or unexpected cross-tenant access attempts without treating explicitly authorized platform operations as violations.
中文:监控未授权或意外的跨租户访问尝试,但不应将明确授权的平台操作视为违规行为。 - Alert on tenant-isolation control failures and suspicious denial patterns.
中文:对租户隔离控制失败和可疑拒绝模式发出警报。 - Apply the documented access and retention policy for each audit-data class.
中文:为每个审计数据类应用记录的访问和保留策略。
Tenant-Aware Logging & Monitoring
import structlog
from typing import Any, Dict
from datetime import datetime
import secrets
class TenantAwareLogger:
"""Logger that includes verified tenant context when one is active."""
def __init__(self):
self.logger = structlog.get_logger()
def _enrich_with_tenant(self, event_data: dict) -> dict:
"""Add tenant context to log entry."""
ctx = current_tenant.get()
if ctx:
event_data["tenant_id"] = ctx.tenant_id
event_data["user_id"] = ctx.user_id
return event_data
def info(self, message: str, **kwargs):
self.logger.info(message, **self._enrich_with_tenant(kwargs))
def warning(self, message: str, **kwargs):
self.logger.warning(message, **self._enrich_with_tenant(kwargs))
def error(self, message: str, **kwargs):
self.logger.error(message, **self._enrich_with_tenant(kwargs))
def security_event(self, event_type: str, severity: str, **kwargs):
"""Log security-relevant events."""
self.logger.warning(
"security_event",
event_type=event_type,
severity=severity,
**self._enrich_with_tenant(kwargs)
)
class TenantAuditLog:
"""Audit API with tenant-aware reads and append-only writes."""
def __init__(self, db):
self.db = db
async def log(self, action: str, details: Dict[str, Any],
tenant_id: str = None):
"""Record audit entry."""
ctx = current_tenant.get()
if (
ctx
and tenant_id
and tenant_id != ctx.tenant_id
and not has_permission(ctx, "platform:audit:write")
):
raise SecurityException("Cannot write another tenant's audit log")
effective_tenant_id = tenant_id or (ctx.tenant_id if ctx else "system")
entry = {
"id": secrets.token_urlsafe(16),
"tenant_id": effective_tenant_id,
"user_id": ctx.user_id if ctx else None,
"action": action,
"details": details,
"timestamp": datetime.utcnow(),
"ip_address": get_client_ip(),
"user_agent": get_user_agent()
}
# This API only appends. Database permissions, tamper-evident storage,
# or WORM controls must enforce the required immutability properties.
await self.db.execute("""
INSERT INTO audit_log
(id, tenant_id, user_id, action, details, timestamp, ip_address, user_agent)
VALUES (:id, :tenant_id, :user_id, :action, :details, :timestamp, :ip_address, :user_agent)
""", entry)
async def get_tenant_audit_trail(self, tenant_id: str,
start_date: datetime,
end_date: datetime) -> list:
"""Retrieve audit trail for a specific tenant."""
ctx = current_tenant.get()
# Tenant audit readers and platform auditors are distinct permissions;
# a tenant-local admin is not implicitly a platform auditor.
can_read_tenant = (
ctx
and ctx.tenant_id == tenant_id
and has_permission(ctx, "tenant:audit:read")
)
can_read_platform = ctx and has_permission(ctx, "platform:audit:read")
if not (can_read_tenant or can_read_platform):
raise SecurityException("Audit access denied")
return await self.db.fetch_all("""
SELECT * FROM audit_log
WHERE tenant_id = :tenant_id
AND timestamp BETWEEN :start AND :end
ORDER BY timestamp DESC
""", {"tenant_id": tenant_id, "start": start_date, "end": end_date})
class CrossTenantAccessMonitor:
"""Monitor and alert on potential cross-tenant access attempts."""
def __init__(self, alert_service):
self.alerts = alert_service
self.violation_counts = {}
async def check_access(self, requested_tenant: str,
resource_type: str, resource_id: str):
"""Check for cross-tenant access attempts."""
ctx = current_tenant.get()
if not ctx or not await authorize_tenant_access(
ctx, requested_tenant, resource_type, resource_id
):
# Log a denied attempt, not an explicitly authorized platform action
logger.security_event(
"cross_tenant_access_attempt",
severity="HIGH",
requested_tenant=requested_tenant,
resource_type=resource_type,
resource_id=resource_id
)
# Track violations per user
principal_id = ctx.user_id if ctx else "anonymous"
source_tenant = ctx.tenant_id if ctx else "none"
key = f"{principal_id}:{source_tenant}"
self.violation_counts[key] = self.violation_counts.get(key, 0) + 1
# Alert on repeated attempts
if self.violation_counts[key] >= 3:
await self.alerts.send(
severity="CRITICAL",
message="Repeated cross-tenant access attempts detected",
details={
"user_id": principal_id,
"tenant_id": source_tenant,
"attempts": self.violation_counts[key]
}
)
raise SecurityException("Access denied")
Do's and Don'ts / 该做什么和不该做什么¶
Do:
- Derive tenant context from a server-verified identity and current membership or service authorization.
中文:从服务器验证的身份和当前成员资格或服务授权中推导租户上下文。 - Use an enforceable isolation boundary appropriate to the data and threat model; database controls such as RLS or schema and credential separation can provide defense in depth.
中文:使用适合数据和威胁模型的可强制隔离边界;数据库控制,如RLS或模式与凭证分离,可以提供多层次防御。 - Include tenant scope in queries, cache keys, and storage boundaries when the resource or result varies by tenant.
中文:当资源或结果因租户而异时,在查询、缓存键和存储边界中包含租户范围。 - Include tenant identity in rate limits and quotas when tenants share capacity or have tenant-level entitlements; retain any needed global, endpoint, user, or IP limits.
中文:当租户共享容量或具有租户级权益时,在速率限制和配额中包含租户身份;保留任何需要的全局、端点、用户或IP限制。 - Bound tenant consumption of other shared bottlenecks, such as queued work, concurrency, database connections, and compute, when those resources can create cross-tenant availability impact.
中文:其他共享瓶颈(如队列工作、并发、数据库连接和计算)的绑定租户消耗,当这些资源可能造成跨租户可用性影响时。 - Carry verified tenant context through tenant-scoped asynchronous work and re-establish authorization at the consumer.
中文:通过租户范围异步工作传递已验证的租户上下文,并在消费者处重新建立授权。 - Log verified tenant context for tenant-scoped security and audit events.
中文:记录租户范围的安全和审计事件中已验证的租户上下文。 - Enforce tenant ownership at a boundary traversed by every tenant-owned access path.
中文:在所有租户拥有的访问路径穿越的边界上强制执行租户所有权。 - Use separate encryption keys when the risk or compliance model requires cryptographic isolation.
中文:当风险或合规模型要求加密隔离时,使用单独的加密密钥。 - Apply and verify the documented retention and deletion policy during offboarding.
中文:在离职过程中应用和验证记录的保留和删除策略。 - Authorize and test tenant-scoped restores, especially when the source backup contains other tenants.
中文:授权并测试租户范围的恢复,特别是当源备份包含其他租户时。 - Monitor and alert on denied or unexpected cross-tenant access attempts.
中文:监控并警报被拒绝或意外的跨租户访问尝试。 - For shared-table PostgreSQL RLS that uses a tenant setting, prefer transaction-local context and re-establish it for each transaction.
中文:对于使用租户设置的共享表 PostgreSQL RLS,优先使用事务局部上下文,并在每个事务中重新建立它。 - For shared-table PostgreSQL RLS, inventory classified tenant-owned tables and test cross-tenant denial for each one.
中文:对于共享表 PostgreSQL RLS,对分类的租户拥有的表进行清单,并测试每个表的跨租户拒绝访问。 - Verify that the ordinary tenant-request database role cannot bypass row security when RLS is the isolation boundary.
中文:验证普通租户请求数据库角色在 RLS 是隔离边界时无法绕过行安全。
Don't:
- Treat tenant IDs from client headers or request parameters as authorization proof; they are selectors that require server-side verification.
中文:将客户端头部或请求参数中的租户 ID 视为授权证明;它们是需要服务器端验证的选择器。 - Use a shared cache key for data or authorization that varies by tenant.
中文:对于按租户变化的 数据或授权,使用共享缓存键。 - Rely on identifier complexity to prevent cross-tenant access.
中文:依靠标识符复杂性来防止跨租户访问。 - Allow an ordinary tenant-owned request path to perform an unscoped query; make any cross-tenant administrative path explicit, separately authorized, and auditable.
中文:允许普通租户拥有的请求路径执行无作用域查询;使任何跨租户管理路径明确、单独授权并可审计。 - Store tenant-owned data without an enforceable tenant association or isolation boundary; a literal
tenant_idcolumn is not required by every architecture.
中文:在不具有可强制执行的租户关联或隔离边界的条件下存储租户拥有的数据;并非每个架构都需要一个字面tenant_id列。 - Give a tenant-scoped credential access to other tenants. Explicitly cross-tenant service credentials require their own least-privileged scope and controls.
中文:授予租户范围的凭证以访问其他租户。显式跨租户服务凭证需要其自己的最小权限范围和控制。 - Skip authorization merely because a service is internal.
中文:仅因为服务是内部服务就跳过授权。 - Treat a tenant identifier in a queued message as authorization proof without authenticating the producer path and authorizing the consumer operation.
中文:将队列中的租户标识视为授权证明,而无需验证生产者路径和授权消费者操作。 - Allow one tenant unbounded consumption of a shared queue, worker pool, connection pool, or other resource that can degrade other tenants.
中文:允许一个租户无限制地消费共享队列、工作池、连接池或其他可能降低其他租户性能的资源。 - Retain tenant data beyond the documented retention policy without a contractual or legal basis and appropriate access restrictions.
中文:在没有合同或法律依据以及适当的访问限制的情况下,保留超出文档保留政策的租户数据。 - Restore a shared snapshot over live data to recover only one tenant.
中文:在实时数据上恢复共享快照,仅恢复一个租户。 - Log sensitive tenant data in plain text.
中文:以纯文本形式记录敏感租户数据。 - Serve ordinary tenant-scoped PostgreSQL RLS request paths with a superuser or
BYPASSRLSrole.
中文:使用超级用户或BYPASSRLS角色处理普通的租户范围PostgreSQL RLS请求路径。 - Use a session-scoped database tenant setting across pooled requests without reliable reset-on-checkout or equivalent isolation and connection-reuse tests; prefer transaction-local settings.
中文:在池化请求中跨会话范围数据库租户设置,没有可靠的检查出时重置或等效的隔离和连接重用测试;优先使用事务局部设置。
References / 参考¶
- AWS SaaS Lens: Tenant Isolation) (AWS SaaS 镜头:租户隔离)
- PostgreSQL: Row Security Policies) (PostgreSQL:行安全策略)
- Microsoft: Messaging in Multitenant Solutions) (Microsoft:多租户解决方案中的消息传递)