ADR-0007 零信任与 mTLS 强制
- 状态(Status):已接受
- 日期:2024-09
- 决策者:EnerOS 安全架构组
- 相关能力:零信任与安全增强、合规映射
- 相关 ADR:ADR-0002 Power-Native AgentOS
背景(Context)
决策时机
2024 年下半年,EnerOS 即将进入省级电网试点部署阶段。试点环境涉及调控中心、变电站、发电厂三类生产区域,需同时满足多套严苛的安全合规标准。在试点启动前的安全评估中,第三方测评机构指出 EnerOS v0.30 的内部通信采用明文 gRPC,依赖网络边界(防火墙 + VLAN)做隔离,存在横向移动风险,无法通过等保 2.0 三级测评。本 ADR 即为解决该评估发现而发起。
合规要求
EnerOS 部署于电力关键基础设施,需同时满足以下三套合规标准:
| 标准 | 适用范围 | 关键要求 | 期限 |
|---|---|---|---|
| IEC 62443 SL-3 | 工业控制系统 | 通信加密、身份认证、审计追溯 | 试点前 |
| NERC CIP | 北美电力系统 | 电子安全边界、访问控制、事件日志 | 海外部署前 |
| 等保 2.0 三级 | 中国关键信息基础设施 | 通信完整性、身份鉴别、安全审计 | 试点前 |
三套标准在以下方面有共同要求:
- 通信机密性与完整性:所有控制指令传输必须加密且防篡改
- 双向身份认证:通信双方必须互相验证身份,不可单向
- 细粒度访问控制:基于身份与上下文授权,非基于网络位置
- 完整审计链:每条遥控命令可回溯至签发身份与时间
- 持续验证:不信任任何隐式信任,每次访问都需验证
现状痛点
EnerOS v0.30 之前内部服务采用明文 gRPC,存在以下问题:
痛点 1:边界一旦突破即可横向移动
┌─────────────────────────────────────────┐
│ 调控中心 VLAN(被信任) │
│ ┌────────┐ 明文 gRPC ┌────────┐ │
│ │ Agent │ ──────────► │Topology│ │
│ │ Runtime│ │ Engine │ │
│ └────────┘ └────────┘ │
│ │ 攻击者一旦进入 VLAN │
│ ▼ 即可嗅探/伪造 RPC │
└─────────────────────────────────────────┘
痛点 2:审计链断裂
明文 RPC 无法证明命令来源,审计日志只能记录”某 IP 发起了某操作”,无法绑定到具体身份。一旦发生事故,取证时无法证明”是哪个工程师/Agent 下达的指令”。
痛点 3:合规不达标
等保 2.0 三级要求”通信完整性”与”身份鉴别”,明文 gRPC 直接不符合。测评机构明确指出需在试点前整改。
触发决策的具体事件
2024 年 8 月,第三方测评机构对 EnerOS v0.30 进行等保预评估,给出”通信安全不达标”的发现项,要求在 2 个月内完成整改,否则无法进入试点。本次 ADR 即为响应该发现项而紧急发起。
决策时的约束
| 约束 | 描述 |
|---|---|
| 性能 | mTLS 握手增加延迟需 < 10ms,长期连接复用后接近 0 |
| 兼容 | 必须与现有 IEC 61850 MMS、IEC 60870-5-104 协议互通 |
| 运维 | 证书轮转对运维透明,无需人工干预 |
| 合规 | 一次性满足 IEC 62443 SL-3、NERC CIP、等保 2.0 三级 |
| 可用性 | CA 故障时业务可继续运行至少 24 小时 |
| 时限 | 2 个月内完成整改并通过复测 |
决策(Decision)
落地 零信任架构:所有内部服务间通信强制 mTLS 双向认证,由内置 CA 签发与轮转证书;审计采用 HMAC 链式结构并写入 WORM(Write Once Read Many)存储;身份认证绑定 SPIFFE ID 与 tenant_id,授权采用 RBAC + ABAC。
核心理由
理由 1:零信任契合电力关键基础设施
零信任的核心原则”永不信任,始终验证”(Never Trust, Always Verify)与电力系统的安全理念一致——任何操作都需身份验证与权限校验,不因位于”安全区域”而放松。
理由 2:mTLS 是双向认证的工业标准
mTLS 在通信握手阶段即完成双向身份验证,无需应用层额外编码,且与 gRPC、HTTP/2 天然兼容。相比应用层加密,mTLS 由传输层处理,对业务代码侵入最小。
理由 3:内置 CA 实现证书全生命周期自动化
电力部署环境通常网络隔离,无法依赖外部 CA 服务。内置 CA 配合自动轮转,使证书管理对运维透明。
理由 4:SPIFFE 提供统一身份框架
SPIFFE(Secure Production Identity Framework for Everyone)为云原生与混合环境设计的身份框架,其 SPIFFE ID 可跨服务、跨租户唯一标识工作负载,与 EnerOS 的多租户模型契合。
理由 5:HMAC 链式审计防篡改
链式审计日志一旦写入即不可篡改,满足合规取证要求,且可证明日志完整性。
零信任架构总览
┌──────────────────────────┐
│ 控制平面(Control Plane)│
│ ┌────┐ ┌─────┐ ┌────┐ │
│ │ CA │ │SPIRE│ │PDP │ │
│ └────┘ └─────┘ └────┘ │
└──────────────────────────┘
│ 签发证书
▼
┌─────────── 数据平面(Data Plane)──────────────┐
│ │
│ ┌────────┐ mTLS 双向认证 ┌────────┐ │
│ │ Agent │ ◄─────────────► │Topology│ │
│ │Runtime │ SPIFFE ID │ Engine │ │
│ └────────┘ 互相验证 └────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────┐ │
│ │ Audit │ ◄── HMAC 链 ──► │ WORM │ │
│ │ Logger │ │ Store │ │
│ └────────┘ └────────┘ │
└────────────────────────────────────────────────┘
mTLS 配置示例
EnerOS 内部服务的 mTLS 配置通过 eneros.toml 统一管理:
[security.mtls]
enabled = true # 强制开启,不可关闭
min_version = "TLS1.3" # 最低 TLS 1.3,禁用旧版本
cert_rotation = "24h" # 证书有效期 24 小时,自动轮转
cert_overlap = "1h" # 新旧证书重叠期 1 小时,平滑切换
[security.mtls.cipher_suites]
allowed = ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"]
denied = ["TLS_RSA_*", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"] # 禁用弱套件
[security.ca]
type = "internal" # 内置 CA
key_algorithm = "ECDSA-P256" # 椭圆曲线签名
key_storage = "hsm" # 私钥存入 HSM 或软 HSM
cluster_mode = "raft" # CA 集群采用 Raft 保证高可用
backup_interval = "6h" # CA 数据每 6 小时备份
[security.identity]
framework = "spiffe" # 采用 SPIFFE 身份框架
trust_domain = "eneros.example.com"
id_format = "spiffe://{tenant}/{service}/{instance}"
Rust 代码层面的 mTLS 客户端配置:
use rustls::{ClientConfig, ServerConfig};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
/// 构建 mTLS 客户端配置
pub fn build_mtls_client_config(
cert: CertificateDer<'static>,
key: PrivateKeyDer<'static>,
ca_cert: CertificateDer<'static>,
) -> Result<ClientConfig> {
let mut root_store = rustls::RootCertStore::empty();
root_store.add(ca_cert)?;
Ok(ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(vec![cert], key)?
.with_protocol_versions(&[&rustls::version::TLS13])? // 强制 TLS 1.3
.build())
}
/// 构建 mTLS 服务端配置
pub fn build_mtls_server_config(
cert: CertificateDer<'static>,
key: PrivateKeyDer<'static>,
ca_cert: CertificateDer<'static>,
) -> Result<ServerConfig> {
let mut client_root = rustls::RootCertStore::empty();
client_root.add(ca_cert)?;
Ok(ServerConfig::builder()
.with_no_client_auth() // 占位,下方覆盖为强制客户端认证
.with_client_cert_verifier(Arc::new(SpiffeVerifier::new(client_root)))
.with_single_cert(vec![cert], key)?
.build())
}
/// 基于 SPIFFE ID 的客户端证书验证器
pub struct SpiffeVerifier {
roots: rustls::RootCertStore,
}
impl ServerCertVerifier for SpiffeVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer,
intermediates: &[CertificateDer],
server_name: &ServerName,
now: UnixTime,
) -> Result<ServerCertVerified, Error> {
// 1. 验证证书链签名
self.verify_chain(end_entity, intermediates, now)?;
// 2. 提取并验证 SPIFFE ID
let spiffe_id = extract_spiffe_id(end_entity)?;
if !self.is_authorized(&spiffe_id) {
return Err(Error::General(format!(
"unauthorized SPIFFE ID: {}", spiffe_id
)));
}
Ok(ServerCertVerified::assertion())
}
}
证书生命周期管理
内置 CA 自动管理证书的签发、轮转、撤销:
pub struct CertificateLifecycle {
ca: Arc<CertificateAuthority>,
rotation_interval: Duration,
overlap: Duration,
}
impl CertificateLifecycle {
/// 启动证书轮转循环
pub async fn run_rotation_loop(self: Arc<Self>) {
let mut ticker = tokio::time::interval(self.rotation_interval - self.overlap);
loop {
ticker.tick().await;
if let Err(e) = self.rotate_certificate().await {
tracing::error!("certificate rotation failed: {}", e);
self.alert_ops_team(e).await;
}
}
}
/// 轮转单个服务的证书
async fn rotate_certificate(&self) -> Result<()> {
let new_cert = self.ca.issue_certificate(&self.service_identity()).await?;
let new_key = self.ca.generate_key_pair()?;
// 原子替换:新证书生效,旧证书保留 overlap 期后撤销
self.credential_store.swap(CredentialPair {
cert: new_cert,
key: new_key,
})?;
// 调度旧证书撤销
let ca = self.ca.clone();
let old_serial = self.credential_store.previous_serial()?;
tokio::spawn(async move {
tokio::time::sleep(ca.overlap).await;
ca.revoke(old_serial).await.ok();
});
Ok(())
}
}
身份与授权模型
每个内部服务在启动时获取 SPIFFE ID,绑定 tenant_id 与角色:
pub struct ServiceIdentity {
pub spiffe_id: SpiffeId, // spiffe://eneros.example.com/tenant-a/topology/node-3
pub tenant_id: TenantId,
pub service: ServiceName,
pub instance: InstanceId,
pub roles: Vec<Role>, // RBAC 角色
pub attributes: Vec<Attribute>, // ABAC 属性
}
/// 授权决策点(PDP)
pub struct AuthorizationPdp {
rbac: RbacEngine,
abac: AbacEngine,
}
impl AuthorizationPdp {
pub fn decide(&self, identity: &ServiceIdentity, action: &Action) -> Decision {
// 先 RBAC 粗粒度过滤
if !self.rbac.allow(identity, action) {
return Decision::Deny("RBAC denied".into());
}
// 再 ABAC 细粒度校验
match self.abac.evaluate(identity, action) {
AbacResult::Allow => Decision::Allow,
AbacResult::Deny(reason) => Decision::Deny(reason),
AbacResult::NeedMoreInfo => Decision::Deny("insufficient context".into()),
}
}
}
链式审计日志
每条审计日志包含前一条的 HMAC 摘要,形成链式结构,一旦写入 WORM 存储即不可篡改:
pub struct AuditEntry {
pub sequence: u64,
pub timestamp: NanosecondTimestamp,
pub actor_spiffe_id: String,
pub actor_tenant_id: TenantId,
pub action: String,
pub target: String,
pub result: AuditResult,
pub prev_hash: [u8; 32], // 前一条日志的 HMAC-SHA256
pub this_hash: [u8; 32], // 本条日志的 HMAC-SHA256
}
impl AuditEntry {
pub fn compute_hash(&self, secret: &HmacKey) -> [u8; 32] {
let mut buf = Vec::new();
buf.extend_from_slice(&self.sequence.to_le_bytes());
buf.extend_from_slice(&self.timestamp.to_le_bytes());
buf.extend_from_slice(self.actor_spiffe_id.as_bytes());
buf.extend_from_slice(self.actor_tenant_id.as_bytes());
buf.extend_from_slice(self.action.as_bytes());
buf.extend_from_slice(self.target.as_bytes());
buf.extend_from_slice(self.result.as_bytes());
buf.extend_from_slice(&self.prev_hash);
hmac_sha256(secret, &buf)
}
}
后果(Consequences)
好处
1. 边界被突破后攻击者无法横向移动
即使攻击者攻破某个 VLAN,没有合法的 SPIFFE ID 与证书,无法与任何内部服务建立 mTLS 连接。横向移动从”突破边界即可”变为”需攻破 CA 并窃取私钥”。
2. 任一遥控命令可完整回溯至签发身份
每条命令的审计日志绑定 SPIFFE ID 与 tenant_id,配合链式 HMAC 与 WORM 存储,满足合规取证的”不可抵赖”要求。
3. 一套技术基座同时满足多套合规标准
| 标准 | 要求 | mTLS + SPIFFE + 链式审计 的覆盖 |
|---|---|---|
| IEC 62443 SL-3 SR 1.1 | 通信完整性 | TLS 1.3 AEAD 加密 |
| IEC 62443 SL-3 SR 1.2 | 通信机密性 | TLS 1.3 强制加密 |
| IEC 62443 SL-3 SR 1.4 | 身份认证 | mTLS 双向认证 + SPIFFE |
| 等保 2.0 三级 8.1.4 | 通信完整性 | TLS MAC + 链式审计 |
| NERC CIP-005 R1 | 电子安全边界 | 零信任取代边界 |
4. 证书轮转自动化降低运维负担
证书 24 小时自动轮转,运维无需手动管理。即使私钥泄露,攻击窗口最多 24 小时。
5. 多租户隔离强化
每个 Agent 与服务的身份绑定 tenant_id,跨租户访问需显式授权,从架构层面防止租户间越权。
代价
1. mTLS 握手增加约 5ms 延迟
实测 TLS 1.3 握手(含双向认证)耗时约 5ms,已通过会话恢复(Session Resumption)与连接复用优化至可接受范围。长期连接的握手开销摊销后接近 0。
2. 内部 CA 高可用需独立保障
CA 故障会导致新证书无法签发,是系统的关键单点。已通过 Raft 集群 + HSM 备份保障,但仍需独立监控。
3. 证书轮转流程需配套监控与告警
轮转失败可能导致服务在证书过期后无法通信。已部署轮转失败告警,并保留 1 小时重叠期作为缓冲。
4. 调试复杂度增加
抓包查看 RPC 内容需配置密钥日志(SSLKEYLOGFILE),生产环境需通过审计日志而非流量抓包排查问题。
5. 与遗留系统互通需网关
部分老旧 IED(智能电子设备)不支持 TLS 1.3,需通过协议网关代理 mTLS。
后续工作
- 引入 IDS 覆盖 OT 攻击特征库(已在 v0.40 落地)
- 支持后量子(PQC)混合密钥协商,应对长期威胁(计划 v0.55)
- 完善 HSM 硬件适配,支持国密 SM2/SM3/SM4 算法(计划 v0.50)
- 引入 SPIRE 联邦,支持多集群跨信任域通信(计划 v0.60)
- 详见 零信任架构与 mTLS 合规映射
备选方案(Alternatives)
方案 A:服务网格 Sidecar(如 Istio)
描述:引入 Istio 服务网格,由 Sidecar 代理自动处理 mTLS,应用代码无感知。
优点:
- 应用零代码改动,mTLS 完全透明
- 提供丰富的流量管理、熔断、可观测能力
- 社区成熟,文档齐全
否决原因:
- Sidecar 每跳增加 1-3ms 延迟,对实时域(< 100μs)不可接受
- 引入 Envoy 依赖,二进制体积增加 100MB+
- Istio 控制平面复杂,电力运维团队难以承接
- 与 EnerOS 的内核原生理念冲突——安全应在内核而非 Sidecar
方案 B:应用层加密
描述:每个 RPC 调用自行加密签名,应用代码显式处理密钥与认证。
优点:
- 不依赖传输层,灵活性高
- 可针对单条消息精细控制加密策略
否决原因:
- 每条 RPC 重复造轮子,易出错(密钥管理、随机数、协议设计)
- 应用代码侵入大,每条 RPC 需加 5-10 行加密代码
- 性能差,应用层加密无法利用 TLS 硬件加速
- 难以统一审计,加密实现散落各处
方案 C:保留边界 + 加固
描述:维持现有的明文 gRPC,通过加固防火墙规则、VLAN 隔离、入侵检测等手段提升边界安全。
优点:
- 改动最小,开发成本低
- 不引入新组件,运维熟悉
否决原因:
- 无法应对内部威胁与零日漏洞,边界一旦突破即失守
- 等保 2.0 三级明确要求通信加密,该方案无法通过测评
- 审计链仍依赖 IP,无法绑定身份
- 与零信任理念背道而驰
方案 D:WireGuard 全隧道
描述:使用 WireGuard 在节点间建立加密隧道,所有内部流量走隧道。
优点:
- 性能优秀,WireGuard 内核态实现,延迟增加 < 1ms
- 配置简单,密钥管理轻量
- 现代加密协议(ChaCha20、Curve25519)
否决原因:
- WireGuard 是节点级隧道,不区分服务/进程,无法实现工作负载级身份
- 无法与 SPIFFE 身份框架集成,租户隔离弱
- 证书/密钥轮转需手动配置或额外工具,自动化程度低
- 仍以”节点信任”为基础,非真正的零信任
性能影响评估
mTLS 对性能的影响实测数据(标准测试环境:4 核 / 8GB / Ubuntu 22.04):
| 操作 | 明文 gRPC | mTLS(首次握手) | mTLS(连接复用) | 影响 |
|---|---|---|---|---|
| RPC 延迟(1KB 载荷) | 0.3ms | 5.3ms | 0.35ms | 复用后 +17% |
| RPC 吞吐(1000 RPS) | 1000 | 980 | 998 | 可忽略 |
| CPU 占用(满载) | 35% | 52% | 38% | +3% |
| 内存占用 | 80MB | 95MB | 95MB | +15MB |
| 证书轮转期间 | - | - | 0.5ms 抖动 | 可接受 |
关键优化措施:
- TLS 1.3 会话恢复:复用已建立的 TLS 会话,避免完整握手
- 连接池:长期保持连接,避免频繁握手
- ECDSA-P256:相比 RSA,签名验证快 3-5 倍
- AES-NI 硬件加速:利用 CPU 指令集加速对称加密
实施路径
整改分四个阶段执行,总计 8 周:
阶段 1(第 1-2 周):CA 与身份基础设施
├─ 实现内置 CA(基于 Raft 的高可用集群)
├─ 集成 SPIFFE 身份框架
├─ 实现 HSM 适配层
└─ 完成单元测试与安全审计
阶段 2(第 3-4 周):mTLS 接入
├─ 在 eneros-gateway 实现 mTLS 服务端
├─ 在 eneros-agent 实现 mTLS 客户端
├─ 改造所有内部 gRPC 为 mTLS
└─ 强制开关:无法关闭 mTLS
阶段 3(第 5-6 周):审计与授权
├─ 实现链式审计日志
├─ 部署 WORM 存储
├─ 实现 RBAC + ABAC 授权引擎
└─ 集成到所有系统调用
阶段 4(第 7-8 周):测试与验收
├─ 等保 2.0 三级复测
├─ 性能基准测试
├─ 故障注入(CA 宕机、证书过期)
└─ 试运行与运维培训
渐进式启用策略
为降低风险,mTLS 在试点环境采用渐进式启用:
# 阶段 1:仅审计模式(不强制,仅记录未加密连接)
[security.mtls]
mode = "audit" # audit / permissive / enforce
# 阶段 2:宽容模式(允许未加密但告警)
[security.mtls]
mode = "permissive"
# 阶段 3:强制模式(拒绝未加密连接)
[security.mtls]
mode = "enforce"
对应代码实现:
pub enum MtlsMode {
Audit, // 仅记录,不阻断
Permissive, // 告警,但不阻断
Enforce, // 强制,拒绝未加密
}
impl MtlsEnforcer {
pub fn check(&self, conn: &Connection) -> Result<()> {
match self.mode {
MtlsMode::Audit => {
if !conn.is_mtls() {
self.audit_log.record("unencrypted connection", conn);
}
Ok(())
}
MtlsMode::Permissive => {
if !conn.is_mtls() {
self.metrics.increment("unencrypted_connection_warning");
tracing::warn!("unencrypted connection from {:?}", conn.peer);
}
Ok(())
}
MtlsMode::Enforce => {
if !conn.is_mtls() {
return Err(SecurityError::UnencryptedConnectionRejected);
}
Ok(())
}
}
}
}
参考(References)
- ADR 总览 — ADR 索引与编写指南
- ADR-0002 Power-Native AgentOS — Power-Native 设计决策
- 零信任与安全增强 — 能力文档
- 零信任架构与 mTLS 合规映射 — 合规文档
- NIST SP 800-207, “Zero Trust Architecture”
- IEC 62443-3-3, “System security requirements and security levels”
- NERC CIP-005, “Electronic Security Perimeter(s)”
- SPIFFE specification, https://spiffe.io
- RFC 8446, “The Transport Layer Security (TLS) Protocol Version 1.3”
- 相关 crate:
eneros-trust、eneros-gateway、eneros-audit、eneros-ids