摘要

AI Agent 处理的数据中,API Key、用户Token、对话记录、配置密钥等敏感信息随处可见。本文从数据安全的分层模型出发,系统讲解 OpenClaw 中的加密策略——包括传输层 TLS 终结、存储层 AES-256 加密、环境变量脱敏、Secret 管理最佳实践。通过三个实战场景(配置加密存储、对话记录脱敏归档、多环境密钥轮转),你将掌握让 Agent 既能高效工作又不泄露敏感数据的完整方案。读完你会发现:安全不是"事后补救",而是设计之初就嵌入的能力。


1. 引言:你的 Agent 在裸奔吗?

1.1 先看一个触目惊心的场景

你的 OpenClaw Gateway 配置文件里写着:

model:
  providers:
    openai:
      api_key: sk-proj-abc123def456...
channels:
  feishu:
    app_secret: x8Kj2mP9qR5v...
  wecom:
    corp_secret: wL7nQ3tY6bV1...

如果这个文件被泄露——无论是误传到 GitHub、被攻击者读取、还是运维人员误操作——你的 API Key 就裸奔在网上了。

更隐蔽的是:Agent 的对话记录中可能包含用户手机号、身份证、银行卡信息。日志文件里可能躺着完整 Token。环境变量 printenv 一下就能看到所有密钥。

1.2 数据安全分层模型

🛡️ 数据安全四层模型

第1层:传输安全
TLS/HTTPS 加密通信

第2层:存储安全
AES-256 加密落盘

第3层:访问控制
RBAC + Secret 隔离

第4层:审计追踪
操作日志 + 异常告警

层级 保护范围 如果没有这一层
传输安全 网络间的数据传输 中间人攻击可直接截获敏感信息
存储安全 落盘的数据 硬盘被盗或日志泄露直接暴露原文
访问控制 谁能读写数据 任何内部人员都能读到所有密钥
审计追踪 事后追查 数据泄露后不知道是谁、何时、做了什么

2. 传输层安全:TLS 终结配置

2.1 TLS 基础配置

# openclaw.yaml - TLS 配置段
gateway:
  port: 18789
  
  # TLS 配置
  tls:
    enabled: true
    cert_file: /etc/openclaw/tls/cert.pem
    key_file: /etc/openclaw/tls/key.pem
    
    # 最低 TLS 版本
    min_version: "1.2"
    
    # 支持的加密套件(白名单模式)
    cipher_suites:
      - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
      - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    
    # 证书自动续期
    auto_renew:
      enabled: true
      provider: letsencrypt
      domains:
        - openclaw.your-domain.com
      email: admin@your-domain.com
      renew_before_days: 30

2.2 自签名证书(开发环境)

# 生成自签名证书(仅开发/测试用)
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
  -keyout /etc/openclaw/tls/key.pem \
  -out /etc/openclaw/tls/cert.pem \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

# 验证证书
openssl x509 -in /etc/openclaw/tls/cert.pem -text -noout | head -20

💡 生产环境务必使用受信任的 CA 证书(Let’s Encrypt 或企业 CA),自签名证书仅用于本地开发。


3. 存储层加密:AES-256 实战

3.1 加密工具实现

"""
secure_storage.py
OpenClaw 安全存储模块 - AES-256-GCM 加密

设计原则:
1. AES-256-GCM 提供认证加密(同时保证机密性和完整性)
2. 每次加密生成随机 IV(初始化向量),防止相同明文产生相同密文
3. 密钥从环境变量读取,绝不硬编码
4. 支持密钥版本化,方便轮转
"""

import os
import json
import base64
import hashlib
from typing import Dict, Optional, Any
from cryptography.hazmat.primitives.ciphers.aead import AESGCM


class SecureStorage:
    """
    敏感数据加密存储管理器
    
    使用 AES-256-GCM 认证加密算法:
    - GCM 模式同时提供加密和完整性验证
    - 256 位密钥,当前计算能力下暴力破解不可行
    - 每次加密使用随机 Nonce(12字节)
    """
    
    def __init__(self, key_version: str = "v1"):
        """
        初始化加密管理器
        
        密钥从环境变量 OPENCLAW_ENCRYPTION_KEY 读取
        支持多版本密钥(key_version)用于密钥轮转
        """
        # 从环境变量读取主密钥
        master_key_hex = os.environ.get("OPENCLAW_ENCRYPTION_KEY")
        if not master_key_hex:
            raise ValueError(
                "❌ 未设置 OPENCLAW_ENCRYPTION_KEY 环境变量\n"
                "💡 生成密钥: python3 -c \"import os; print(os.urandom(32).hex())\""
            )
        
        # 多版本密钥派生
        self.key_version = key_version
        self._keys = self._derive_keys(bytes.fromhex(master_key_hex))
        self._aesgcm = AESGCM(self._keys[key_version])
    
    def _derive_keys(self, master_key: bytes) -> Dict[str, bytes]:
        """从主密钥派生多版本密钥(支持密钥轮转)"""
        keys = {}
        for version in ["v1", "v2", "v3"]:
            salt = f"openclaw-key-{version}".encode()
            derived = hashlib.pbkdf2_hmac(
                "sha256", master_key, salt, 100000, dklen=32
            )
            keys[version] = derived
        return keys
    
    def encrypt(self, plaintext: str) -> str:
        """
        加密字符串
        
        返回格式: base64(version || nonce || ciphertext)
        前缀 version 用于解密时选择正确的密钥版本
        """
        # 生成 12 字节随机 Nonce
        nonce = os.urandom(12)
        
        # 加密(AESGCM 自动附加认证标签)
        plaintext_bytes = plaintext.encode("utf-8")
        ciphertext = self._aesgcm.encrypt(nonce, plaintext_bytes, None)
        
        # 打包:版本(2字节) + Nonce(12字节) + 密文
        packed = (
            self.key_version.encode() +
            nonce +
            ciphertext
        )
        
        return base64.b64encode(packed).decode("ascii")
    
    def decrypt(self, encrypted: str) -> str:
        """
        解密密文
        
        自动识别密钥版本,支持密钥轮转期间的平滑过渡
        """
        try:
            packed = base64.b64decode(encrypted)
            
            # 解析版本(前2字节)
            version = packed[:2].decode()
            if version not in self._keys:
                raise ValueError(f"不支持的密钥版本: {version}")
            
            nonce = packed[2:14]
            ciphertext = packed[14:]
            
            aesgcm = AESGCM(self._keys[version])
            plaintext = aesgcm.decrypt(nonce, ciphertext, None)
            
            return plaintext.decode("utf-8")
        except Exception as e:
            raise DecryptionError(f"解密失败: {e}")

    def encrypt_dict(self, data: Dict[str, Any]) -> str:
        """加密字典(自动序列化为 JSON)"""
        return self.encrypt(json.dumps(data, ensure_ascii=False))
    
    def decrypt_dict(self, encrypted: str) -> Dict[str, Any]:
        """解密密文并反序列化为字典"""
        return json.loads(self.decrypt(encrypted))


class DecryptionError(Exception):
    """解密异常"""
    pass


# ============================================
# 使用示例
# ============================================

if __name__ == "__main__":
    # 1. 生成主密钥(一次性操作)
    # python3 -c "import os; print(os.urandom(32).hex())"
    
    store = SecureStorage()
    
    # 2. 加密敏感数据
    api_key = "sk-proj-this-is-a-secret-key-12345"
    encrypted = store.encrypt(api_key)
    print(f"🔐 加密后: {encrypted[:50]}...")
    
    # 3. 解密验证
    decrypted = store.decrypt(encrypted)
    print(f"🔓 解密后: {decrypted}")
    assert decrypted == api_key, "加解密不匹配!"
    print("✅ 加解密验证通过")
    
    # 4. 加密结构化数据
    config = {
        "openai_key": "sk-xxx",
        "feishu_secret": "abc123",
        "db_password": "p@ssw0rd"
    }
    encrypted_config = store.encrypt_dict(config)
    print(f"📦 加密配置: {encrypted_config[:50]}...")
    
    # 5. 相同明文产生不同密文(随机Nonce保证)
    encrypted2 = store.encrypt(api_key)
    print(f"🔀 再次加密: {encrypted2[:50]}...")
    print(f"   与上次不同: {encrypted != encrypted2}")  # True

3.2 配置文件加密存储

# openclaw.yaml - 使用加密占位符
model:
  providers:
    openai:
      # 使用 ENC() 标记加密值
      api_key: "ENC(A8f3kL9mX2...加密后的密文...)"

channels:
  feishu:
    app_secret: "ENC(pQ7wR2sY5b...)"
"""
config_decrypt.py
配置文件解密加载器

在 Gateway 启动时自动解密 ENC() 占位符
"""

import re
import os
from secure_storage import SecureStorage

ENC_PATTERN = re.compile(r'^ENC\((.*)\)$')

def load_config_with_decrypt(config_path: str) -> dict:
    """
    加载配置文件并自动解密集 ENC() 标记的值
    
    支持递归解密嵌套结构
    """
    import yaml
    
    with open(config_path, "r") as f:
        config = yaml.safe_load(f)
    
    store = SecureStorage()
    
    def decrypt_values(obj):
        """递归解密所有 ENC() 标记的值"""
        if isinstance(obj, dict):
            return {k: decrypt_values(v) for k, v in obj.items()}
        elif isinstance(obj, list):
            return [decrypt_values(v) for v in obj]
        elif isinstance(obj, str):
            match = ENC_PATTERN.match(obj)
            if match:
                return store.decrypt(match.group(1))
            return obj
        else:
            return obj
    
    return decrypt_values(config)

4. 对话记录脱敏归档

4.1 脱敏策略

"""
conversation_anonymizer.py
对话记录脱敏归档模块

支持的脱敏类型:
- 手机号: 138****1234
- 身份证: 3101**********1234
- 邮箱: ab****@domain.com
- API Key: sk-****...****
- IP地址: 192.168.*.*
- 自定义关键字列表
"""

import re
import json
import hashlib
from typing import List, Dict, Optional
from datetime import datetime


class ConversationAnonymizer:
    """
    对话脱敏器——归档前自动清理敏感信息
    
    设计原则:
    1. 不可逆脱敏(脱敏后无法恢复原文)
    2. 保留语义(脱敏后的内容仍可用于分析和搜索)
    3. 可配置规则(不同业务场景使用不同规则)
    """
    
    DEFAULT_RULES = [
        # (名称, 正则, 替换函数)
        ("手机号", r'1[3-9]\d{9}',
         lambda m: m.group()[:3] + "****" + m.group()[-4:]),
        ("身份证", r'\d{17}[\dXx]',
         lambda m: m.group()[:4] + "**********" + m.group()[-4:]),
        ("邮箱", r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
         lambda m: m.group().split("@")[0][:2] + "****@" + m.group().split("@")[1]),
        ("API Key", r'(?:sk|api[_-]?key|token)[-=:]\s*["\']?([\w-]{20,})["\']?',
         lambda m: m.group(0).replace(m.group(1), m.group(1)[:3] + "***..." + m.group(1)[-4:])),
        ("IPv4", r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',
         lambda m: ".".join(m.group().split(".")[:2]) + ".*.*"),
    ]
    
    def __init__(self, extra_keywords: Optional[List[str]] = None):
        self.rules = self.DEFAULT_RULES.copy()
        self.keywords = set(extra_keywords or [])
        self.stats = {"total_processed": 0, "total_redacted": 0}
    
    def anonymize(self, text: str) -> Dict:
        """对单条文本进行脱敏"""
        clean = text
        redactions = []
        
        # 正则规则脱敏
        for rule_name, pattern, mask_fn in self.rules:
            for match in re.finditer(pattern, clean):
                original = match.group()
                masked = mask_fn(match)
                clean = clean.replace(original, masked)
                redactions.append({
                    "type": rule_name,
                    "position": match.start(),
                    "length": len(original)
                })
        
        # 关键词脱敏
        for kw in self.keywords:
            if kw in clean:
                clean = clean.replace(kw, "*" * len(kw))
                redactions.append({"type": "keyword", "keyword": kw})
        
        self.stats["total_processed"] += 1
        if redactions:
            self.stats["total_redacted"] += 1
        
        return {
            "original_length": len(text),
            "anonymized_text": clean,
            "redaction_count": len(redactions),
            "redaction_types": list(set(r["type"] for r in redactions))
        }
    
    def anonymize_conversation(
        self, messages: List[Dict], archive_path: str
    ) -> str:
        """
        脱敏并归档完整对话
        
        输出 JSONL 格式,每条一行,方便后续分析
        """
        anonymized = []
        for msg in messages:
            content = msg.get("content", "")
            result = self.anonymize(content)
            anonymized.append({
                "role": msg.get("role", "unknown"),
                "content": result["anonymized_text"],
                "redacted": result["redaction_count"] > 0,
                "timestamp": msg.get("timestamp", datetime.now().isoformat())
            })
        
        # 写入归档文件
        with open(archive_path, "w", encoding="utf-8") as f:
            for record in anonymized:
                f.write(json.dumps(record, ensure_ascii=False) + "\n")
        
        return archive_path

4.2 脱敏效果对比

# 测试脱敏效果
anonymizer = ConversationAnonymizer(
    extra_keywords=["内部机密", "工资"]
)

test_conversation = [
    {"role": "user", "content": "我的手机号是13812345678,帮我查下订单"},
    {"role": "assistant", "content": "好的,已查到您的订单。发货地址是北京市朝阳区xx路xx号。"},
    {"role": "user", "content": "我的身份证310101199001011234,帮我验证下"},
    {"role": "user", "content": "API Key 是 sk-abcdefghijklmn1234567890"},
    {"role": "assistant", "content": "您的token已激活,这是内部机密请勿外传"},
]

for msg in test_conversation:
    result = anonymizer.anonymize(msg["content"])
    status = "🟡" if result["redaction_count"] > 0 else "🟢"
    print(f"{status} [{msg['role']}] {result['anonymized_text']}")

print(f"\n📊 脱敏统计: 处理{anonymizer.stats['total_processed']}条, "
      f"脱敏{anonymizer.stats['total_redacted']}条")

预期输出

🟡 [user] 我的手机号是138****5678,帮我查下订单
🟢 [assistant] 好的,已查到您的订单。发货地址是北京市朝阳区xx路xx号。
🟡 [user] 我的身份证3101**********1234,帮我验证下
🟡 [user] API Key 是 sk-***...7890
🟡 [assistant] 您的token已激活,这是****请勿外传

📊 脱敏统计: 处理5条, 脱敏4条

📸 截图位置:终端运行脱敏测试的输出,展示五种敏感信息类型的脱敏效果。


5. 密钥轮转策略

5.1 轮转流程

新密钥(v2) 旧密钥(v1) 加密存储 OpenClaw Gateway 管理员 新密钥(v2) 旧密钥(v1) 加密存储 OpenClaw Gateway 管理员 触发密钥轮转 生成新密钥(v2) 用v1解密所有数据 读取加密数据 返回解密数据 用v2重新加密 原子切换密钥版本 标记为待废弃(保留24h) 轮转完成报告

5.2 轮转脚本

"""
key_rotation.py
密钥轮转工具——平滑切换加密密钥
"""

import os
import json
import glob
from datetime import datetime, timedelta
from secure_storage import SecureStorage


def rotate_keys(data_dir: str = "/data/openclaw/encrypted"):
    """
    密钥轮转主流程
    
    1. 用旧密钥解密所有数据
    2. 用新密钥重新加密
    3. 验证新密钥可解密
    4. 保留旧密钥24小时(应急回滚)
    """
    print("🔄 开始密钥轮转...")
    
    # 使用旧密钥解密
    old_store = SecureStorage(key_version="v1")
    new_store = SecureStorage(key_version="v2")
    
    encrypted_files = glob.glob(f"{data_dir}/**/*.enc", recursive=True)
    print(f"📂 找到 {len(encrypted_files)} 个加密文件")
    
    rotated = 0
    errors = []
    
    for filepath in encrypted_files:
        try:
            # 读入 → 解密 → 重新加密
            with open(filepath, "r") as f:
                old_cipher = f.read()
            
            plaintext = old_store.decrypt(old_cipher)
            new_cipher = new_store.encrypt(plaintext)
            
            # 验证
            verified = new_store.decrypt(new_cipher)
            assert verified == plaintext, f"验证失败: {filepath}"
            
            # 写入(原子替换)
            tmp_path = filepath + ".tmp"
            with open(tmp_path, "w") as f:
                f.write(new_cipher)
            os.replace(tmp_path, filepath)
            
            rotated += 1
            print(f"  ✅ {os.path.basename(filepath)}")
            
        except Exception as e:
            errors.append((filepath, str(e)))
            print(f"  ❌ {os.path.basename(filepath)}: {e}")
    
    # 报告
    print(f"\n📊 轮转完成: 成功 {rotated}/{len(encrypted_files)}")
    if errors:
        print(f"⚠️ 失败 {len(errors)} 个文件:")
        for f, e in errors:
            print(f"   - {f}: {e}")
    
    return {"rotated": rotated, "errors": len(errors)}

6. 安全最佳实践清单

类别 实践 为什么重要
密钥管理 绝不在代码或配置中硬编码密钥 Git 历史会永久保留
密钥管理 使用环境变量或 Secret Manager 代码和密钥分离
密钥管理 定期轮转密钥(建议90天) 降低密钥泄露的影响范围
传输 强制 TLS 1.2+,禁用旧协议 防止降级攻击
传输 API 通信使用 HTTPS 防止中间人抓包
存储 敏感数据落盘前加密 硬盘被盗也不泄露
存储 日志自动脱敏 日志是最容易被忽略的泄露点
访问 最小权限原则 减少内部泄露风险
审计 记录所有敏感操作 事后可追溯

7. 总结

核心要点

  1. 四层安全模型:传输(TLS) → 存储(AES-256) → 访问(RBAC) → 审计(日志),层层递进

  2. AES-256-GCM 认证加密:同时保证机密性和完整性,每次加密使用随机 Nonce

  3. 密钥派生 + 版本化:从主密钥派生多版本子密钥,支持无缝密钥轮转

  4. 对话脱敏归档:正则规则 + 关键词匹配,脱敏后保留语义可供分析

  5. 配置加密占位符ENC(...) 标记让敏感配置在文件中以密文存在,Gateway 启动时自动解密

思考题

  1. AES-256-GCM 提供了认证加密,如果有人篡改了密文,解密时会发生什么?这种机制如何保护数据完整性?

  2. 密钥轮转期间(部分数据用 v1 加密、部分用 v2),如果 Gateway 重启,如何确保两种版本的密钥同时可用?你有什么设计思路?

  3. 如果你的 OpenClaw 部署在多云环境中(AWS + 阿里云),密钥管理策略应该如何调整?是统一用一个 KMS,还是各云用各的?


参考资料

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐