入职 Web3 运维日记 · 第 14 日:铸造无形钥匙 —— OIDC 与 CI/CD 施工实录

时间 :入职第 14 天,上午 10:00 天气 :多云,代码审查室里的气氛有些焦灼 事件:发现开发团队使用个人电脑直连主网部署合约,并深度剖析 Web3 的"草台班子"现状

上午 10 点,智能合约开发组长在 Slack 核心群里发了一条消息:"新版 Vault (资金库) 合约本地测试完毕,10 分钟后我准备把它发到主网 (Mainnet)。"

作为一个 Web2 摸爬滚打出来的老运维,我对"发主网(生产环境)"这三个字有着天然的敬畏。我立刻端着咖啡走到他工位旁,随口问了一句:"咱们发主网的流程是啥?你用的哪个平台的流水线?"

组长头也没抬,切到了他的 VS Code 终端:"流水线?不用那么麻烦。我在我的 Mac 上直接跑 forge script script/Deploy.s.sol --rpc-url $MAINNET_RPC --broadcast 命令就行了。秒上链。"

我眼皮一跳:"那你部署用的私钥 (Private Key) 放在哪?"

他指着编辑器左侧的目录树,理直气壮地说:"写死在本地的 .env 文件里啊。里面这个地址有 5 个 ETH 当手续费,部署完剩下的还能用。你放心,我已经把 .env 加进 .gitignore 了,绝对不会传到 GitHub 上,很安全!"

我感觉血压瞬间飙升。这不叫安全,这叫"裸奔"。

但我忍住了直接拔网线的冲动。我看着他,问出了那个直击灵魂的问题: "兄弟,如果你以前在美团或者阿里,敢把生产环境数据库的 Root 密码写在你这台 Mac 的本地文本里,然后坐在星巴克连着公共 Wi-Fi 敲回车发版,你今天下午就会被安全部和 HR 架出大楼。为什么到了 Web3,你觉得这就理所当然了?"

组长愣了一下,挠了挠头:"可是......大家都是这么干的啊。教程里也是这么教的。"

🕵️‍♂️ 为什么 Web2 的"死罪",变成了 Web3 的"常态"?

我拉了把椅子坐下,跟他(也是跟我自己)彻底理清了这个荒谬现状背后的三大原因:

原因一:物理边界的消亡(公网无墙)

  • Web2 时代 :生产环境的数据库藏在 VPC(虚拟私有云)深处,外面有防火墙、WAF、堡垒机。你在星巴克是物理上无法连接生产库的,除非你经过层层 VPN 认证。

  • Web3 时代 :以太坊主网是 Permissionless(无许可) 的公共网络。没有任何防火墙。对区块链来说,你坐在公司内网发出的交易,和你坐在星巴克公共 Wi-Fi 下发出的交易,没有任何区别。只要你的签名对得上,节点就会处理。这种"极度开放"的网络环境,给了开发者一种"随时随地都能发版"的错觉和便利。

原因二:"极客英雄主义"的开发工具链

  • Web3 起源于极客和密码朋克文化。早期大部分项目都是两三个人的草台班子在黑客松上搞出来的。

  • 他们没有专业的 DevOps,所以早期的开发框架(Truffle, Hardhat,甚至现在的 Foundry)在设计时,默认的使用场景就是单兵作战

  • 你去翻看官方文档的"部署教程",第一步永远是:"新建一个 .env 文件,粘贴你的 MetaMask 私钥"。工具层的设计导向,硬生生把整个行业的开发者培养出了极其糟糕的安全习惯。

原因三:身份与资产的高度绑定(Deployer = God)

  • 在 Web2,发版的账号(比如 Jenkins Service Account)和管钱的账号是分开的。

  • 但在 Web3,那个放在 .env 里的私钥,不仅是用来付 Gas 费的,它通常还会被智能合约自动识别为 Owner(超级管理员)

  • 这意味着,这把躺在 Mac 硬盘上的私钥,未来可能直接拥有铸造代币、暂停合约、甚至提取用户几亿美金的最高权限!

🛑 刺破谎言:叫停部署

听我分析完,组长的表情终于变了。但他还在辩解:"但我 Mac 上装了杀毒软件,而且我不乱点链接的。"

我直接按住了他的键盘:"停!这笔部署立刻取消。"

"你的 .env 谎言到此为止。"我给他下了最后通牒。

  1. 单点故障 (SPOF):只要你的电脑中了一次钓鱼木马,那个装着 5 个 ETH 和最高权限的私钥瞬间被盗,公司直接上新闻头条。

  2. 代码投毒防不住:你从本地编译部署,我怎么向审计公司证明,你部署的机器码 (Bytecode) 就是 GitHub 上的那份源码?万一你私自在本地改了一行代码,留了个后门呢?

  3. 环境差异:你的 Node.js 版本、甚至 Mac 的 M系列芯片,都可能导致编译出来的 Hash 值和线上验证不一致。

我站起身,走向 CTO 办公室,留下一句话: "从今天起,彻底废除任何形式的本地主网部署。 下午 5 点前,我会交付一套零信任的 CI/CD 流水线。以后所有上主网的合约,必须通过机器自动化部署,私钥必须锁在硬件保险柜(KMS)里!"


🏗️ 施工第一步:在 AWS 建立"门卫大爷" (OIDC 信任策略)

我登录到 AWS IAM 控制台。我要教 AWS 认识 GitHub 这个"外来机构",并定下死规矩。

1. 添加身份提供商 (IdP) 我先在 AWS 里添加了 GitHub 作为可信的 OpenID Connect (OIDC) 实体:

  • Provider URL : https://token.actions.githubusercontent.com

  • Audience : sts.amazonaws.com

2. 编写信任策略 (Trust Policy - 最核心的防线) 接着,我创建了一个 IAM 角色,命名为 Bybit-GitHub-Deploy-Role。这个角色就是给流水线准备的临时"工牌"。 但这块工牌不是谁都能领的,我给门卫大爷(AWS STS)写了一段极其严苛的 JSON 过滤规则:

复制代码
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::1234567890:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          // 【绝杀卡点】:必须是 Bybit 仓库的 main 分支!
          // 其他仓库、其他分支(比如 dev)来请求,直接 Access Denied!
          "token.actions.githubusercontent.com:sub": "repo:Bybit/smart-contracts:ref:refs/heads/main"
        }
      }
    }
  ]
}

3. 限制行为范围 (Permissions Policy) 领到工牌进门后,流水线能干啥?我只给了它两个极其可怜的权限:看公钥,和请求签名。

复制代码
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kms:Sign",          // 允许请求签名
        "kms:GetPublicKey"   // 允许获取公钥地址
      ],
      // 只能碰这一把指定的冷钱包部署私钥,别的资源看都看不见
      "Resource": "arn:aws:kms:ap-southeast-1:1234567890:key/mrk-abc123deploykey"
    }
  ]
}

📜 施工第二步:编写流水线大脑 (deployment.yml)

AWS 那边的门卫调教好了。现在我回到 GitHub 仓库,在 .github/workflows/ 目录下新建了 deployment.yml

这份 YAML 文件,就是彻底终结那个危险的 .env 文件的最终武器。我把它分成了三个严密的阶段。

复制代码
name: Compliant Mainnet Deployment

# 1. 触发条件:绝对禁止自动发版
on:
  workflow_dispatch:
    inputs:
      network:
        description: 'Target Network (mainnet / sepolia)'
        required: true
        default: 'mainnet'

# 2. 赋予流水线申请 OIDC Token 的权限 (极其重要的一句!)
permissions:
  id-token: write   # 必须有这个,GitHub 才会生成用来骗过 AWS 门卫的 JWT 证件
  contents: read

jobs:
  # 阶段一:无状态编译
  build:
    name: 🏗️ Compile with Foundry
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
      - name: Build Smart Contracts
        run: forge build

  # 阶段二:Slither 死亡安检 (防线)
  security-audit:
    name: 🛡️ Slither Security Scan
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Slither Analyzer
        uses: crytic/slither-action@v0.3.0
        with:
          target: 'src/'
          fail-on: high  # 只要查出高危漏洞(如重入攻击),立刻中断!

  # 阶段三:无私钥签名部署
  deploy:
    name: 🚀 Sign & Broadcast via KMS
    needs: security-audit # 只有安全审计全绿,才能走到这里
    runs-on: ubuntu-latest
    environment: production # 触发 GitHub 页面上的审批按钮 (Approve)
    
    steps:
      - uses: actions/checkout@v3
      
      # 魔法时刻:向 AWS 出示 OIDC 证件,换取 15 分钟临时凭证
      - name: Authenticate to AWS KMS (No Passwords!)
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: arn:aws:iam::1234567890:role/Bybit-GitHub-Deploy-Role
          aws-region: ap-southeast-1
          
      # 执行部署脚本 (此时环境里已经有了 AWS 临时凭证)
      - name: Execute KMS Deployment
        run: |
          echo "Connecting to AWS KMS..."
          # 这里通常是一个封装好的 TS/Python 脚本,调用 KMS API 签名并向 RPC 发送交易
          node scripts/deploy_kms.js --network ${{ github.event.inputs.network }}

最后三步:在 KMS 内部"孕育"私钥

我们不导入私钥,我们命令 AWS KMS 的底层硬件(HSM 防弹芯片)原生生成一把私钥。它从出生的那一刻起,就被焊死在芯片里。

操作步骤如下:

  1. 选择算法 : 在 AWS KMS 点击"创建密钥"。选择 非对称 (Asymmetric) ,用途选 签名和验证 (Sign and verify)

  2. 选择以太坊专用的"灵魂曲线" : 这一步极其关键。以太坊使用的密码学曲线叫 secp256k1。你在 AWS KMS 的下拉菜单里,必须精准地选中 ECC_SECG_P256K1。如果选错了,生成的签名以太坊根本不认。

  3. 完成创建 : 点击确定。就这么简单,私钥诞生了。

第四步:发起"权力移交"交易

你需要用你原来的本地旧私钥 (就是那个 .env 里的)执行一次合约调用。你可以用 Foundry 的命令行工具 cast 快速完成:

复制代码
# 命令解释:
# cast send [合约地址] "transferOwnership(address)" [新KMS地址] --private-key [旧私钥] --rpc-url [主网RPC]

cast send 0xVaultContractAddress "transferOwnership(address)" 0xNew_KMS_Address \
  --private-key $OLD_PRIVATE_KEY \
  --rpc-url $MAINNET_RPC
第五步:验证权力移交

去 Etherscan 上查看你的合约,点击 "Read Contract" ,查看 owner 变量。

  • 如果显示的地址已经是 0xNew_KMS_Address,那么恭喜你,移交成功!

下午 4 点,我把这段 YAML 代码推到了 main 分支。

这时候的架构已经完美了:

  1. 没有密码 :无论是本地电脑还是 GitHub 仓库,都没有明文的 Access Key 或私钥。

  2. 双重锁死 :开发人员想发主网,必须推代码到 main 分支;推上来之后,必须通过 Slither 的安全扫描;全绿之后,还要等我和 CTO 在网页上点击 Approve。


🚨 16:15 PM:触雷!Slither 的无情绞杀

组长点击了 Run workflow。 我们俩并排站在屏幕前,盯着 GitHub Actions 的运行日志。

  • 第一步:🏗️ Compile with Foundry。用时 12 秒,绿灯 ✅。 组长得意地说:"看吧,我本地编译没问题,云端肯定也没问题。"

  • 第二步:🛡️ Slither Security Scan。日志开始疯狂滚动。

就在这时,页面突然一闪,一个刺眼的红色叉号 (❌) 弹了出来。构建失败!流水线被强制熔断。 第三步的 🚀 Sign & Broadcast via KMS 直接变成了灰色的 Skipped(跳过)。

组长急了:"Alen,你这什么破流水线!我本地跑 forge test 测试用例明明全过了(100% Pass),怎么到你这就挂了?"

我没有说话,点开了 Slither 阶段的红色报错日志。屏幕上赫然印着几行冰冷的英文:

复制代码
INFO:Detectors:
High Risk Vulnerability Detected: Reentrancy
Contract: Vault.sol
Function: Vault.withdraw(uint256)
Details: External call made before state variable update.
         - msg.sender.call{value: amount}("") (Vault.sol#42)
         - balances[msg.sender] -= amount (Vault.sol#44)

Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
Error: Slither found 1 High severity issues! Failing build.

🧠 16:20 PM:为什么本地测试抓不出这个 Bug?

看到 Reentrancy(重入攻击)这个词,组长的脸色瞬间煞白。 这是以太坊历史上最臭名昭著的漏洞。当年 The DAO 就是因为这个漏洞被黑了 5000 万美金,直接导致了以太坊主网硬分叉出 ETC。

他的代码是这么写的:

复制代码
function withdraw(uint256 amount) public {
    require(balances[msg.sender] >= amount, "Not enough balance");
    
    // 【致命错误】:先给钱,把执行权交给了外部!
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
    
    // 然后才扣减数据库里的余额
    balances[msg.sender] -= amount; 
}

我拍了拍他的肩膀:"兄弟,本地的单元测试(Unit Test)往往只测正常的业务逻辑 。你测了存 100 块,取 50 块,余额剩 50 块,当然是绿的。 但黑客不会按套路出牌。黑客会写一个恶意合约,在收到你这笔钱的瞬间(触发 fallback 函数),再次回头调用你的 withdraw 函数。 因为你还没执行到 balances -= amount 这一步,系统认为黑客账户里还有钱,于是又给他发了一笔......无限循环,直到把你资金库里的几千万美金抽干。"

组长擦了擦头上的冷汗:"如果上午我用本地电脑直连主网部署了......明天公司可能就破产了。"

🔧 17:00 PM:修复与二次闯关

组长飞奔回工位,老老实实地重写了逻辑。 他遵循了智能合约最核心的安全铁律:CEI 模式 (Checks-Effects-Interactions) 。 先把余额扣掉(Effects),再去给用户转钱(Interactions)。并且引入了 OpenZeppelin 的 nonReentrant 互斥锁。

代码重新 Push,流水线再次触发。 这次,Slither 扫描:绿灯 ✅!

🚀 17:30 PM:核按钮与完美点火 (Environment Approval)

流水线走到了最后一步:🚀 Sign & Broadcast via KMS。 但这步并没有直接运行,而是变成了黄色的等待状态 (Waiting for Review)

CTO 的手机和我电脑上的 Slack 同时响了。

🤖 GitHub Actions 提醒 Compliant Mainnet Deployment 正在请求部署到 production 环境。 请管理员审核。

这是我在 YAML 里配置的最后一道防线:环境审批 。 我走到 CTO 办公室,他看了一眼 Slither 的全绿报告,我们在各自的 GitHub 页面上郑重地点击了 "Approve and Deploy"

接下来,就是见证 OIDC 与 KMS 密码学魔法的时刻了。

我们在日志里清晰地看到了这一切的发生,而黑客在网线另一端只能绝望地看着:

  1. Authenticating via OIDC... Success. (拿到了只活 15 分钟的 AWS 临时凭证)

  2. Connecting to AWS KMS HSM... (连接防弹保险箱)

  3. Sending bytecode hash to KMS for signing... (把编译好的机器码送进保险箱)

  4. Received signature (v, r, s) from KMS. (印章盖好,拿到了谁也看不懂但全网都认的数字签名)

  5. Broadcasting transaction to Ethereum Mainnet...

  6. Transaction confirmed at Block #18452901! Contract Address: 0x...

部署成功。

至此,那台完成了历史使命的 GitHub 临时 Ubuntu 虚拟机,像完成了刺杀任务的特工一样,瞬间销毁,不留一丝痕迹。

相关推荐
数智联AI团队1 小时前
AI搜索发布新一代智能体平台,以技术创新重塑自动化AI交互体验
运维·人工智能·自动化
上海合宙LuatOS1 小时前
LuatOS核心库API——【ioqueue】IO序列化操作
linux·运维·服务器·网络·嵌入式硬件·物联网·硬件工程
xcs194051 小时前
AI 自动化编程 trae 分析 包在那里引入
运维·自动化
白云偷星子1 小时前
云原生笔记2
运维·笔记·云原生
Eine .2 小时前
从入门到实战:HAProxy 负载均衡与代理全解析
运维·负载均衡·haproxy
️️(^~^)2 小时前
HAPORXY实验环境
linux·运维·服务器
红豆子不相思2 小时前
Nginx 全栈实操
运维·nginx
不像程序员的程序媛2 小时前
阿里云负载均衡器知多少?
运维·服务器·负载均衡
_OP_CHEN2 小时前
【Linux系统编程】(四十)线程控制终极指南:从资源共享到实战操控,带你吃透线程全生命周期
linux·运维·操作系统·线程·进程·c/c++·线程控制