数据库基础体系 · 第 128/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Elasticsearch 安全与多租户:TLS、角色、文档权限、审计和隔离

Elasticsearch 的安全问题通常不是“是否打开了认证”这么简单。一个可用的多租户安全模型至少要回答以下问题:

  1. 客户端和节点之间的通信是否保密、是否验证了对端身份?
  2. 请求来自哪个用户、服务账号或 API key?
  3. 该身份可以执行哪些集群级和索引级操作?
  4. 用户只能看到哪些文档、字段和聚合结果?
  5. 写入操作是否也被限制在自己的租户范围内?
  6. 谁在什么时间访问了什么资源,审计记录能否被信任?
  7. 租户之间是逻辑隔离、索引隔离,还是集群和网络层面的物理隔离?
  8. 节点故障、证书过期、权限变更和快照恢复时,隔离边界是否仍然成立?

本文以 Elasticsearch 官方稳定版本公开语义为基础。具体配置项、许可证级别和云产品界面可能随发行版、部署方式及版本变化,生产环境应以当前版本文档和授权能力为准。


一、安全模型:TLS、认证、授权和审计解决的是不同问题

可以把一次 Elasticsearch 请求抽象为以下流程:

请求TLS 建链身份认证授权检查文档过滤执行与审计\text{请求} \rightarrow \text{TLS 建链} \rightarrow \text{身份认证} \rightarrow \text{授权检查} \rightarrow \text{文档过滤} \rightarrow \text{执行与审计}

其中每一步解决的问题不同。

1. TLS 解决通信机密性和对端身份

TLS 主要解决:

  • 请求内容是否会被网络窃听;
  • 请求是否可能被中间人篡改;
  • 客户端是否确认自己连接的是正确的 Elasticsearch 服务;
  • 在双向 TLS 中,服务端是否确认客户端持有受信任证书。

TLS 不负责决定用户能否搜索某个索引,也不负责决定用户能否删除文档。

2. 认证解决“你是谁”

Elasticsearch 可以通过内置用户、原生或文件型用户库、LDAP、Active Directory、PKI 证书、SAML、OIDC、Kerberos 等机制完成认证,具体能力取决于部署方式和授权级别。

认证成功后,安全模块需要得到一个身份,例如:

username = alice
realm     = native
roles     = [tenant_acme_reader]

或者:

principal = service-orders
credential = API key

API key 不是匿名访问令牌。它代表创建时关联的权限描述,并且可以设置过期时间、撤销和轮换。

3. 授权解决“你能做什么”

授权通常由角色(role)描述。一个角色可以包含:

  • 集群权限(cluster privileges);
  • 索引权限(indices privileges);
  • 应用权限(application privileges);
  • run_as 权限,用于允许一个身份模拟另一个用户;
  • 文档级安全(DLS,Document Level Security);
  • 字段级安全(FLS,Field Level Security)。

一个用户可以拥有多个角色,最终权限通常是这些角色权限的合并结果。正因为如此,“额外绑定一个宽权限角色”可能完全抵消另一个角色中的 DLS 或 FLS 限制。

4. 审计解决“发生过什么”

审计记录通常可以回答:

  • 谁发起了认证;
  • 谁访问了哪个索引或 API;
  • 请求是成功还是失败;
  • 哪个 IP 或节点参与了请求;
  • 权限检查是否失败。

审计并不等于业务数据备份,也不一定包含完整请求体和完整文档内容。审计日志本身还需要保护、转发、保留和校验。


二、TLS:HTTP 层和传输层必须分别考虑

Elasticsearch 有两条重要通信路径:

客户端、Kibana、应用
        │
        ▼
   HTTP/HTTPS 层
        │
Elasticsearch 节点之间
        │
        ▼
 transport 层

1. HTTP TLS 保护客户端请求

HTTP TLS 保护以下内容:

  • 用户名和密码;
  • API key;
  • 查询条件;
  • 写入的文档;
  • 返回的搜索结果;
  • 管理 API 请求。

在没有 HTTPS 的情况下,即使 Elasticsearch 启用了认证,凭据和数据仍可能以明文经过网络。

自建集群中,HTTP TLS 的配置通常涉及:

xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.certificate: /path/to/http.crt
xpack.security.http.ssl.key: /path/to/http.key
xpack.security.http.ssl.certificate_authorities:
  - /path/to/ca.crt

不同版本和部署方式可能推荐使用 PKCS#12、Elasticsearch keystore 或自动生成的证书材料,而不是直接把私钥路径写入配置文件。配置私钥时还要保证操作系统权限足够严格。

客户端测试可以使用 CA 文件验证服务端证书:

curl \
  --cacert http-ca.crt \
  -u elastic:'密码' \
  https://es.example.com:9200/

预期结果是返回集群名称、版本和节点信息。若省略 --cacert,可以临时使用 -k 跳过证书验证,但这只适合定位问题,不适合生产环境,因为它仍然容易受到中间人攻击。

2. transport TLS 保护节点间通信

transport 层承载节点之间的内部通信,包括:

  • 集群状态发布;
  • 分片恢复;
  • 节点发现和集群加入;
  • 节点间请求转发;
  • 某些安全相关的内部通信。

生产环境不应只启用 HTTP HTTPS 而忽略 transport TLS。否则客户端到入口节点的链路虽然加密,节点间链路仍可能暴露集群数据和内部协议。

常见配置结构类似:

xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: full

verification_mode 的语义很重要:

  • none:不验证证书,不应作为生产方案;
  • certificate:验证证书链,但通常不严格验证主机名;
  • full:验证证书链,并验证证书中的主机名或 IP 与连接目标匹配。

如果使用 full,节点证书的 SAN(Subject Alternative Name)必须包含节点实际使用的 DNS 名称或 IP。只把名称写入证书的旧式 CN(Common Name)通常不足以满足现代 TLS 主机名验证要求。

3. 双向 TLS 和证书认证

HTTP TLS 默认主要是服务端向客户端证明身份。若还需要客户端通过证书认证,则需要:

  1. 客户端持有私钥和客户端证书;
  2. Elasticsearch 信任签发该证书的 CA;
  3. 配置 PKI 认证域;
  4. 将证书主体映射到 Elasticsearch 用户及角色。

证书认证的逻辑是:

客户端证书
  ├─ 证书链有效
  ├─ 未过期、未被撤销(取决于部署的校验能力)
  ├─ 用途符合客户端认证
  └─ 证书主体映射到某个用户

证书本身只证明“持有某个私钥的实体”。权限仍由角色绑定决定。一个证书被映射到管理员用户,就拥有管理员权限;被映射到只读用户,就只能执行只读操作。

4. TLS 的故障边界

常见错误可以先按阶段区分:

表现 更可能的原因
SSL certificate problem 客户端不信任 CA、证书链不完整
hostname verification failed 证书 SAN 不包含连接地址
handshake_failure TLS 版本、密码套件、客户端证书或协议不匹配
transport 节点无法加入 节点证书、CA、主机名验证或 transport TLS 配置不一致
HTTP 返回 401 TLS 已建立,但认证失败或缺少凭据
HTTP 返回 403 已认证,但授权失败

因此,不能用“返回 401”来判断 TLS 是否配置正确;401 表示 HTTP 层已经成功收到并处理了请求。

5. TLS 不等于静态数据加密

TLS 保护传输中的数据,不自动保护:

  • 数据目录中的分片文件;
  • Elasticsearch 节点磁盘;
  • 快照仓库;
  • 备份介质;
  • 日志文件;
  • 操作系统交换分区。

静态数据保护通常依赖云盘加密、主机磁盘加密、文件系统加密、快照仓库权限和密钥管理系统。快照尤其重要:如果攻击者能够读取快照仓库,就可能绕过在线 Elasticsearch 的角色控制直接获得数据。


三、角色模型:从集群权限到索引权限

1. 集群权限和索引权限不是一回事

集群权限控制集群级 API,例如:

  • 查看集群状态;
  • 管理索引模板;
  • 管理生命周期策略;
  • 管理用户、角色和 API key;
  • 执行集群管理操作。

索引权限控制某些索引上的操作,例如:

  • read:搜索、获取等读取行为;
  • write:写入、更新、删除等写操作;
  • create_index:创建索引;
  • view_index_metadata:读取索引元数据;
  • manage:更广泛的索引管理操作。

实际可用的权限名称和组合应以当前版本为准,不应凭记忆把某个权限名写入生产配置。最小权限的基本形式是:

{
  "cluster": [],
  "indices": [
    {
      "names": ["tenant-acme-*"],
      "privileges": ["read"]
    }
  ]
}

这表示该角色不拥有集群级权限,只能读取名称匹配 tenant-acme-* 的索引。

2. 角色合并可能放大权限

假设用户拥有两个角色:

role_a:
  tenant-acme-* 上的 read

role_b:
  * 上的 read

用户最终可以读取所有索引,而不是只能读取 tenant-acme-*

再例如:

role_a:
  tenant-acme-* 上带 DLS 的 read

role_b:
  tenant-acme-* 上不带 DLS 的 read

role_b 可能使用户拥有不受文档过滤的读取权限。设计权限时,必须检查用户、组、角色映射、API key 的角色描述以及 run_as 权限的合并结果,而不能只检查某一个角色文件。

3. 内置超级用户不是应用身份

elastic 等内置超级用户适合初始化和故障恢复,不适合被应用长期使用。原因不是“名字不好看”,而是它拥有的能力通常远超业务需求:

  • 可以访问所有索引;
  • 可以管理安全配置;
  • 可以修改角色;
  • 可能绕过普通租户边界;
  • 一旦凭据泄露,影响整个集群。

应用、数据同步任务、CI/CD、监控和运维人员应使用不同的用户、服务账号或 API key。

4. API key 的生命周期

一个合理的服务 API key 生命周期包括:

申请
  ↓
授予限定索引和操作
  ↓
部署到应用密钥存储
  ↓
使用和审计
  ↓
轮换
  ↓
撤销旧 key
  ↓
过期或删除

创建 API key 需要调用方本身具有相应的 API key 管理权限。示例:

curl \
  --cacert http-ca.crt \
  -u security-admin:'密码' \
  -H 'Content-Type: application/json' \
  -X POST https://es.example.com:9200/_security/api_key \
  -d '{
    "name": "orders-acme-readonly",
    "expiration": "30d",
    "role_descriptors": {
      "orders_acme_reader": {
        "cluster": [],
        "indices": [
          {
            "names": ["orders-acme-*"],
            "privileges": ["read"]
          }
        ]
      }
    }
  }'

响应通常包含 API key 的标识和一段编码后的凭据。完整凭据只应在创建时安全保存,不能写入源代码、镜像或普通日志。

轮换时不要先删除旧 key 再创建新 key,否则应用在切换期间可能中断。更安全的顺序是:

  1. 创建新 key;
  2. 将新 key 部署到应用;
  3. 验证新 key 的读写行为;
  4. 删除旧 key;
  5. 检查审计日志确认旧 key 不再被使用。

四、文档级安全:DLS 限制“能看到哪些文档”

文档级安全(DLS,Document Level Security)是对读取结果应用的查询过滤器。它不是把物理分片切成多个租户,而是在授权后的查询执行过程中限制可见文档。

1. 一个完整的 DLS 示例

假设所有租户共用索引 orders,每个文档包含租户字段:

{
  "_id": "o-1001",
  "tenant_id": "acme",
  "customer": "Alice",
  "amount": 120
}

为 Acme 创建只读角色:

curl \
  --cacert http-ca.crt \
  -u security-admin:'密码' \
  -H 'Content-Type: application/json' \
  -X PUT https://es.example.com:9200/_security/role/tenant_acme_reader \
  -d '{
    "cluster": [],
    "indices": [
      {
        "names": ["orders"],
        "privileges": ["read"],
        "query": {
          "term": {
            "tenant_id": "acme"
          }
        }
      }
    ]
  }'

然后使用该角色的用户或 API key 查询:

curl \
  --cacert http-ca.crt \
  -u acme-reader:'密码' \
  -H 'Content-Type: application/json' \
  https://es.example.com:9200/orders/_search \
  -d '{
    "query": {
      "match_all": {}
    },
    "sort": [
      { "amount": "desc" }
    ]
  }'

请求本身没有显式写 tenant_id = acme,但 DLS 会将角色查询与请求查询组合。直观上可以表示为:

Qeffective=QrequestQDLSQ_{\text{effective}} = Q_{\text{request}} \land Q_{\text{DLS}}

本例中:

match_all AND tenant_id = acme

因此结果只能包含 tenant_idacme 的文档。聚合也建立在这个可见文档集合上,所以租户只能看到其可见文档产生的聚合结果。

2. DLS 的安全边界

DLS 主要保护读取路径,包括常见的搜索和获取操作。它不是写入约束

如果一个角色同时拥有:

{
  "names": ["orders"],
  "privileges": ["read", "write"],
  "query": {
    "term": {
      "tenant_id": "acme"
    }
  }
}

不能据此推断该角色只能写入 tenant_id = acme 的文档。DLS 对读取生效,而写入、更新和删除权限仍然作用于该角色允许的索引范围。攻击者可能写入:

{
  "tenant_id": "other-tenant",
  "amount": 999999
}

如果业务要求“调用者只能写自己的租户”,需要在 Elasticsearch 之外增加可靠的写入约束,例如:

  • 每个租户使用独立 API key,应用服务端根据已认证身份填充 tenant_id
  • 应用层拒绝客户端直接提交或修改 tenant_id
  • 使用每租户独立索引,并严格限制 API key 的索引名;
  • 让写入服务成为唯一的 Elasticsearch 写入方;
  • 对写入请求做服务端校验、签名或不可伪造的租户上下文传递。

不要把浏览器或客户端提交的 tenant_id 当成可信身份信息。

3. DLS 查询必须覆盖所有读取入口

只过滤普通 _search 不够,还要验证:

  • _get
  • mget
  • search 的聚合;
  • 按 ID 查询;
  • 跨索引模式查询;
  • alias 访问;
  • 相关的导出、报表或后台任务;
  • Elasticsearch SQL、EQL 或其他查询入口;
  • 跨集群搜索场景。

不同 API 的授权行为和 DLS 支持细节可能受版本影响,不能只用一个接口验证后就宣布“租户隔离完成”。

4. DLS 模板和用户元数据

对于大量租户,为每个租户创建一个角色可能带来角色数量和配置管理压力。可以考虑使用查询模板,从当前用户或身份元数据中取租户值。

但这里有三个风险:

  1. 模板语法和可用变量必须以当前版本为准;
  2. 用户元数据的写权限必须严格控制;
  3. 必须测试特殊字符、缺失字段、多租户身份和模板渲染失败。

“租户 ID 存在用户 metadata 中”并不天然安全。如果普通用户能够修改自己的 metadata,DLS 就可能被改成另一个租户。

生产使用模板时,应至少建立以下测试:

用户 acme       → 只能得到 acme 文档
用户 globex     → 只能得到 globex 文档
缺少 tenant_id  → 请求失败或得到空结果
tenant_id 含特殊字符 → 不得改变查询结构
多角色合并      → 不得出现无 DLS 的宽权限角色

5. DLS 与查询能力的取舍

DLS 越复杂,查询越依赖额外过滤条件和底层字段。常见问题包括:

  • 租户字段没有正确映射;
  • tenant_id 被定义为 text,导致精确 term 查询行为不符合预期;
  • DLS 查询引用了不存在的字段;
  • 角色匹配的索引范围过宽;
  • 应用使用了不同索引别名,绕过了预期的命名约束;
  • DLS 与复杂查询、脚本或运行时字段组合后难以诊断。

租户标识通常应使用适合精确匹配的字段类型,例如 keyword。但字段映射正确并不代表写入可信;“字段可过滤”和“字段不可伪造”是两个不同问题。


五、字段级安全:FLS 限制“能看到哪些字段”

字段级安全(FLS,Field Level Security)用于隐藏字段。例如订单中可能包含:

{
  "tenant_id": "acme",
  "order_id": "o-1001",
  "amount": 120,
  "internal_cost": 70,
  "customer_phone": "..."
}

只允许读取公开字段的角色可以写成:

{
  "cluster": [],
  "indices": [
    {
      "names": ["orders"],
      "privileges": ["read"],
      "field_security": {
        "grant": [
          "order_id",
          "amount",
          "status",
          "created_at"
        ]
      }
    }
  ]
}

也可以使用排除字段的方式:

"field_security": {
  "except": [
    "internal_cost",
    "customer_phone"
  ]
}

1. FLS 与 DLS 可以组合

二者的作用不同:

可访问文档集合 = 请求结果 ∩ DLS 过滤结果
可返回字段集合 = 查询字段 ∩ FLS 允许字段

例如:

  • DLS 只允许 Acme 的订单;
  • FLS 隐藏内部成本和电话号码。

这样可以同时实现租户隔离和敏感字段最小披露。

2. 字段权限合并也可能放大权限

和普通索引权限一样,多个角色的字段权限会合并。一个角色排除了 customer_phone,另一个角色却允许读取整个索引,最终用户可能仍能读取该字段。

因此,以下配置不能简单相加后认为更安全:

角色 A:orders 上 grant [order_id, amount]
角色 B:orders 上 read,无 FLS 限制

还要注意 FLS 通常是读取控制,不是写入字段约束。用户是否能写入敏感字段,取决于写权限、应用逻辑和索引设计。

3. FLS 不等于脱敏

FLS 是“不给字段”,不是“把字段变成掩码”。如果业务需要显示:

138****1234

而不是完全不返回手机号,应在索引前、应用层或专门的数据处理流程中完成脱敏。把明文手机号存储在 Elasticsearch 中,再靠普通查询逻辑临时隐藏,并不能覆盖:

  • 管理员查询;
  • 快照;
  • reindex;
  • 导出任务;
  • 日志;
  • 其他拥有宽权限的服务;
  • 直接访问底层存储。

六、共享索引中的多租户设计

1. 基本数据模型

共享索引通常至少包含一个不可缺少的租户字段:

{
  "tenant_id": "acme",
  "document_id": "order-1001",
  "created_at": "2025-01-10T12:00:00Z",
  "amount": 120
}

字段映射示例:

PUT orders
{
  "mappings": {
    "properties": {
      "tenant_id": {
        "type": "keyword"
      },
      "document_id": {
        "type": "keyword"
      },
      "amount": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

这里的 keywordterm 查询能够进行精确匹配;scaled_float 适合表示具有固定精度的金额。实际金额精度和币种规则仍应由业务定义。

2. 共享索引的优点和缺点

优点:

  • 索引数量较少;
  • 模板、分片和生命周期管理集中;
  • 适合租户数量多且单个租户数据量小的场景。

缺点:

  • DLS 配置复杂;
  • 写入隔离不能仅靠 DLS 完成;
  • 一个租户的热点可能影响其他租户;
  • 删除某个租户数据需要可靠的租户过滤;
  • 映射、分析器和生命周期策略通常难以按租户独立定制。

3. 独立索引的多租户设计

可以为每个租户使用独立索引:

orders-acme-000001
orders-globex-000001

权限限制为:

{
  "names": ["orders-acme-*"],
  "privileges": ["read", "write"]
}

这种方式把部分隔离下沉到索引命名和角色授权中。即便写入接口没有 DLS,Acme key 也不能直接访问 orders-globex-*,前提是:

  • 用户没有其他宽权限角色;
  • 索引名不能被任意模式绕过;
  • alias、data stream、远程索引和管理 API 也被正确限制;
  • 应用不能使用更高权限的共享凭据。

独立索引的代价是索引、分片、模板、生命周期和监控对象增多。租户数量很大时,过度细分可能造成大量小分片和集群状态膨胀。

4. 独立集群或独立部署

对于强监管、租户之间需要独立升级或独立故障域的场景,可以使用:

  • 独立 Elasticsearch 集群;
  • 独立部署单元;
  • 独立网络和账号体系;
  • 独立快照仓库和密钥;
  • 在更高层使用独立云项目或账户。

这通常提供更强隔离,但成本也最大。它不能消除所有问题:运维人员、备份系统、日志系统和云控制面仍可能成为共享的高权限入口。


七、路由和分片隔离:性能优化不是安全控制

Elasticsearch 的自定义 routing 可以让同一租户的文档尽量落到同一组分片。典型写入:

PUT orders/_doc/order-1001?routing=acme
{
  "tenant_id": "acme",
  "amount": 120
}

查询时也可以带 routing:

GET orders/_search?routing=acme
{
  "query": {
    "term": {
      "tenant_id": "acme"
    }
  }
}

路由的核心计算可以抽象为:

shard=hash(routing value)modN\text{shard} = \operatorname{hash}(\text{routing value}) \bmod N

其中 NN 是主分片数量。路由值相同的文档会被定位到相同的主分片。

但 routing 不是授权:

  • 知道 routing=acme 不代表拥有 Acme 权限;
  • 不带 routing 的请求可能访问更多分片;
  • 若路由值与文档中的 tenant_id 不一致,可能产生难以发现的数据定位问题;
  • 单一大租户可能形成热点分片;
  • 改变主分片数量会影响路由结果和索引规划;
  • 应用必须确保所有写入、读取和删除请求使用一致的 routing 规则。

因此:

routing = 性能和分片定位机制
DLS/FLS/RBAC = 授权机制

不能用路由参数代替角色和文档权限。


八、审计:记录安全事件,而不是自动生成完整操作录像

1. 审计日志关注哪些事件

自建 Elasticsearch 的安全审计能力通常可以记录部分安全相关事件,例如:

  • 认证成功和失败;
  • 用户或 API key 身份;
  • 授权成功或失败;
  • 访问索引;
  • 管理 API;
  • 用户、角色、API key 变更;
  • 连接来源和节点信息。

可记录事件取决于版本、部署方式、授权能力和审计配置。Elastic Cloud、Serverless 和自建发行版的配置方式也可能不同。

审计日志常见字段包括:

timestamp
event.action
principal
authentication.type
realm
origin.address
request.id
indices
event.outcome
node.name

具体字段名和事件类型不应被业务程序当成永久稳定的公共数据模型,升级时需要重新核对。

2. 审计配置的原则

自建部署中,审计能力通常通过 Elasticsearch 安全配置开启,并配置事件选择和输出 appender。启用后要验证:

  1. 成功认证是否出现记录;
  2. 错误密码是否出现失败记录;
  3. 无权限访问是否出现授权失败;
  4. 查询和管理操作是否按预期记录;
  5. 审计输出是否影响节点磁盘;
  6. 日志是否被可靠转发到外部系统。

审计日志不应只留在 Elasticsearch 自己的本地磁盘上,否则攻击者若取得集群管理权限,可能同时修改数据和删除审计记录。常见做法是将日志转发到外部日志系统或不可变存储,并限制删除、覆盖和修改权限。

3. 审计日志不等于文档内容记录

如果审计记录只包含:

alice 搜索 orders

它未必包含:

alice 读取了 order-1001 的完整 JSON

这不是缺陷,而是审计粒度、隐私和日志成本之间的取舍。若合规要求追踪到业务对象,应在应用层记录:

  • 业务用户;
  • 租户;
  • 订单或客户 ID;
  • 操作类型;
  • 请求关联 ID;
  • 结果;
  • Elasticsearch 用户或 API key 标识。

应用日志和 Elasticsearch 安全审计应通过 trace ID 或 request ID 关联,而不是试图让 Elasticsearch 审计日志承担全部业务审计职责。

4. 审计的验证算例

可以建立一个最小验证流程:

1. acme-reader 成功读取 orders
2. acme-reader 尝试读取其他索引
3. 使用错误密码认证
4. 使用已撤销 API key 请求
5. security-admin 修改角色
6. 检查外部审计系统是否得到对应事件

预期至少应能区分:

认证失败       → 身份没有建立
授权失败       → 身份建立,但权限不足
DLS 结果为空   → 请求合法,但文档过滤后没有可见数据
角色变更事件   → 安全配置发生变化

九、租户隔离的形式化条件

设:

  • TT 是租户集合;
  • DtD_t 是租户 tt 应拥有的文档集合;
  • utu_t 是租户 tt 的身份;
  • P(ut)P(u_t) 是该身份拥有的权限;
  • R(ut)R(u_t) 是请求查询;
  • F(ut)F(u_t) 是 DLS 过滤器;
  • W(ut)W(u_t) 是写入允许集合;
  • SS 是用户可访问的索引、别名和数据流集合。

读取隔离要求:

Read(ut)=Eval(R(ut)F(ut))Dt\operatorname{Read}(u_t) = \operatorname{Eval}(R(u_t) \land F(u_t)) \subseteq D_t

写入隔离要求:

Write(ut)Dt\operatorname{Write}(u_t) \subseteq D_t

问题在于,标准 DLS 通常能帮助实现:

Read(ut)Dt\operatorname{Read}(u_t) \subseteq D_t

但不能自动保证:

Write(ut)Dt\operatorname{Write}(u_t) \subseteq D_t

因此,如果共享索引允许应用直接写入,完整隔离条件至少需要:

身份绑定正确
AND 角色索引范围正确
AND 读取 DLS 正确
AND 写入路径可信
AND tenant_id 不可由不可信客户端任意伪造
AND alias、导出、快照和管理入口不绕过边界

一个反例是:

DLS:tenant_id = acme
写权限:orders 上的 write
客户端:可以自由提交 tenant_id

结果可能是:

读取:只能看到 acme
写入:可以创建 globex 文档

这就是“读隔离成立、写隔离失败”。在多租户系统中,这种配置不能称为完整隔离。


十、典型失败表现和诊断顺序

1. 返回 401

优先检查:

  • Authorization 头是否发送;
  • Basic Auth 的用户名和密码是否正确;
  • API key 是否完整;
  • API key 是否过期或已撤销;
  • 证书认证用户映射是否成功;
  • 请求是否被代理层改写。

2. 返回 403

优先检查:

  • 角色是否包含目标索引;
  • 索引名是否被通配符匹配;
  • 是否缺少 view_index_metadata
  • 是否访问了管理 API,而角色只有数据读取权限;
  • 用户是否同时拥有 run_as 或其他角色;
  • API key 的角色描述是否与预期一致。

3. 查询返回零文档

这不一定是索引中没有数据,还可能是:

  • DLS 条件不匹配;
  • tenant_id 字段映射错误;
  • 角色模板没有得到预期的用户元数据;
  • 用户身份和租户绑定错误;
  • 查询访问了错误的 alias 或索引;
  • 读请求中的 routing 错误;
  • 文档实际写入了另一个租户字段值。

诊断时应使用安全测试用户和受控测试文档,不要直接授予超级用户权限后宣称问题解决。可以分别验证:

普通用户搜索 → 结果为空
受控管理员检查文档 → 确认文档确实存在
查看角色定义 → 确认 DLS
检查身份元数据 → 确认租户值
检查索引映射 → 确认 tenant_id 类型

4. 能查到文档,但看不到字段

优先检查 FLS:

  • 字段是否位于 grant 列表;
  • 是否被 except 排除;
  • 多个角色合并后是否出现冲突;
  • 查询、排序、聚合是否引用了受限字段;
  • 应用是否把 _source 过滤和 FLS 混为一谈。

FLS 的缺失字段可能使某些查询、排序或聚合行为失败,而不是简单返回空字段。必须用真实业务查询验证。

5. transport 节点无法加入

不要先修改角色。应先检查:

  1. 节点时间是否明显偏差;
  2. CA 是否相同且受信;
  3. 节点证书是否包含实际连接地址;
  4. transport TLS 是否在所有节点一致启用;
  5. 私钥文件权限和格式是否正确;
  6. 节点是否连接到了预期集群;
  7. TLS 错误日志中的失败阶段。

十一、生产隔离方案的选择

可以用以下方式进行选择:

方案 主要控制 优点 主要风险
共享索引 + DLS/FLS 角色和文档过滤 资源利用率高 写入隔离复杂,配置易错
每租户独立索引 索引权限和命名空间 写读边界更直观 小分片、索引数量和运维成本增加
每租户独立集群 网络、账号、数据和故障域 隔离最强 成本、运维和升级复杂
混合模式 大租户独立,小租户共享 兼顾成本和隔离 策略和自动化更复杂

选择依据应包括:

  • 租户数量;
  • 单租户数据规模;
  • 是否允许租户独立生命周期;
  • 是否存在强监管要求;
  • 是否需要独立恢复和删除;
  • 单租户热点是否会影响其他租户;
  • 备份和快照是否能按租户隔离;
  • 运维人员和平台管理员的权限边界。

不能只根据“DLS 已经开启”判断隔离强度。真正的隔离边界还包括:

网络入口
HTTP TLS
transport TLS
认证域
角色和 API key
DLS/FLS
索引、alias、data stream
写入服务
快照仓库
日志和审计
管理员和运维权限

十二、一次可验证的最小安全方案

以“Acme 只读订单”为例,完整验证链可以是:

第一步:准备 HTTPS

curl --cacert http-ca.crt https://es.example.com:9200/

确认:

  • 证书链被验证;
  • 主机名匹配;
  • 请求不是通过 -k 绕过验证。

第二步:创建只读角色

角色只允许:

cluster: []
indices:
  names: ["orders"]
  privileges: ["read"]
  DLS: tenant_id = acme

第三步:创建专用身份

可以创建专用用户或 API key,不能复用超级用户凭据。

第四步:写入受控测试数据

测试数据至少包含:

tenant_id = acme
tenant_id = globex

写入由受信服务完成,而不是让被测只读身份写入。

第五步:验证读取

Acme 身份执行:

{
  "query": {
    "match_all": {}
  }
}

预期:

只能看到 acme 文档
看不到 globex 文档
聚合只统计 acme 文档

第六步:验证越权入口

逐一测试:

/orders/_search
/*/_search
/orders/_doc/id
/orders/_mget
alias 查询
数据导出任务

预期是未授权入口返回 403、DLS 后结果为空,或只能返回 Acme 可见内容。

第七步:验证字段权限

如果启用 FLS,确认:

公开字段存在
内部成本不存在
手机号不存在
不能通过排序、聚合或脚本间接暴露敏感字段

第八步:验证审计

检查:

成功登录
失败登录
无权限访问
角色修改
API key 撤销

是否都进入受保护的外部审计系统。

第九步:验证恢复路径

测试快照恢复时,检查:

  • 恢复操作需要哪些权限;
  • 快照仓库是否被独立保护;
  • 恢复后的索引是否继承预期角色边界;
  • 是否出现临时恢复索引绕过命名规则;
  • 删除租户数据时是否覆盖别名、快照和缓存副本。

Elasticsearch 的安全设计不能归结为一个开关。TLS 保护通信,认证确认身份,角色决定 API 和索引权限,DLS 限制可见文档,FLS 限制可见字段,审计记录安全事件,而多租户隔离还必须覆盖写入、索引命名、路由、快照、日志和管理员路径。

其中最容易被误解的边界是:DLS 是读取过滤,不是通用的文档写入约束;routing 是分片定位,不是权限控制;FLS 是字段隐藏,不是数据脱敏;审计日志是事件证据,不是天然不可篡改的完整录像。 只有把这些机制分别放在正确的位置,并通过认证、授权、越权和故障测试验证,租户隔离才具有可证明的安全边界。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。