跳到主内容

ADR-0007 零信任 mTLS

架构决策记录

ADR-0007 零信任与 mTLS 强制

背景(Context)

决策时机

2024 年下半年,EnerOS 即将进入省级电网试点部署阶段。试点环境涉及调控中心、变电站、发电厂三类生产区域,需同时满足多套严苛的安全合规标准。在试点启动前的安全评估中,第三方测评机构指出 EnerOS v0.30 的内部通信采用明文 gRPC,依赖网络边界(防火墙 + VLAN)做隔离,存在横向移动风险,无法通过等保 2.0 三级测评。本 ADR 即为解决该评估发现而发起。

合规要求

EnerOS 部署于电力关键基础设施,需同时满足以下三套合规标准:

标准适用范围关键要求期限
IEC 62443 SL-3工业控制系统通信加密、身份认证、审计追溯试点前
NERC CIP北美电力系统电子安全边界、访问控制、事件日志海外部署前
等保 2.0 三级中国关键信息基础设施通信完整性、身份鉴别、安全审计试点前

三套标准在以下方面有共同要求:

  1. 通信机密性与完整性:所有控制指令传输必须加密且防篡改
  2. 双向身份认证:通信双方必须互相验证身份,不可单向
  3. 细粒度访问控制:基于身份与上下文授权,非基于网络位置
  4. 完整审计链:每条遥控命令可回溯至签发身份与时间
  5. 持续验证:不信任任何隐式信任,每次访问都需验证

现状痛点

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):

操作明文 gRPCmTLS(首次握手)mTLS(连接复用)影响
RPC 延迟(1KB 载荷)0.3ms5.3ms0.35ms复用后 +17%
RPC 吞吐(1000 RPS)1000980998可忽略
CPU 占用(满载)35%52%38%+3%
内存占用80MB95MB95MB+15MB
证书轮转期间--0.5ms 抖动可接受

关键优化措施:

  1. TLS 1.3 会话恢复:复用已建立的 TLS 会话,避免完整握手
  2. 连接池:长期保持连接,避免频繁握手
  3. ECDSA-P256:相比 RSA,签名验证快 3-5 倍
  4. 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)