OpenCode 安全事件:1.4% 的控制台用户、被关联的厂商密钥,以及今天该做什么

·11 分钟阅读·Evergreen Tools Team

2026 年 10 月 18 日,OpenCode 披露了一起安全事件。声明发在 X 上,StepSecurity 当天在分析里原文引用,内容很短:一次大规模蒸馏攻击影响了 1.4% 的 OpenCode 控制台用户,当天早上已通过邮件通知全部受影响用户,公司正与 Anthropic、OpenAI、Google 一起处置,事件仍在进行中。这就是目前全部的公开记录。没有根因,没说访问了哪些数据,也没有确认厂商 API 密钥是否泄露。下面讲的是:在只有这些信息的情况下,能做的事有哪些——而它比看上去要多。

深色电路板上叠加的挂锁图标

公布的影响范围很窄,但善后工作一点不窄。

厂商到底说了什么,没说什么

已经确定的有四点:影响范围限于 OpenCode 控制台用户,不是所有装了 CLI 的人;受影响的那部分用户关联了外部厂商,OpenCode 点名了 Anthropic、OpenAI 和 Google;10 月 18 日上午已给每位受影响用户发了邮件;事件仍在进行,根因未公布。

没有确定的有四点:访问了哪些数据;被关联的厂商凭据是否泄露;攻击者实际拿走了什么;以及随着调查推进,1.4% 这个数字是否还站得住。StepSecurity 明确写出了这个空白,而不是用推理去填满它——这是第三方分析该有的做法,也值得你在内部沟通里照做。

实际结论是:不要等根因出来才开始干活,因为这些活无论根因是什么都值得做。如果某位开发者把厂商凭据关联到了 agent 控制台,而那些凭据没有轮换,那么不管这次事件有没有碰到他,暴露现在存在,事后复盘之后也依然存在。

#!/usr/bin/env bash
# Inventory first. You cannot respond to a breach on machines you cannot list.
# Checks the binary, the config directory, and the MCP server config on each host.

set -u
echo "host: $(hostname)"

# macOS / Linux
for p in   "$HOME/.local/share/opencode"   "$HOME/.config/opencode"   "$HOME/.opencode"
do
  [ -e "$p" ] && echo "found dir: $p"
done

command -v opencode >/dev/null 2>&1 && echo "binary: $(command -v opencode)"

# Windows (run in Git Bash or WSL with the user profile mounted)
# %USERPROFILE%\.local\share\opencode

# version matters for scoping: pin it in your inventory
opencode --version 2>/dev/null || true

先在自己机器上找到它,再谈范围

大多数安全团队答不上一个简单问题:哪些开发者装了哪个 AI 编码代理?这是第一个要补的洞,而且它更像文档问题而不是工具问题。一个周末试验装完就忘的 agent,仍然是磁盘上带着凭据的 agent。

OpenCode 的状态文件位置是可预期的。macOS 和 Linux 上相关目录在用户的 local share 与配置路径下,Windows 上对应用户配置文件里的等价位置。CLI 二进制可以在 PATH 里找到。版本很重要,因为按版本划范围,是厂商之后公布细节时判断「谁可能受影响」的唯一办法。

要做全网可见性,StepSecurity 的 Dev Machine Guard 是开源的,可以单机一次性扫描,能检测 macOS、Windows、Linux 上的 CLI 二进制、配置目录和 MCP 服务器配置。它报告的是「装了什么」,无法告诉你有谁开了控制台账号——所以盘点清单之后,必须去问人。

# Check which providers are linked, WITHOUT printing the secrets themselves.
# The vendor's statement says affected users are the ones with external providers
# linked, so this file is the first thing to look at.

import json, os, pathlib

paths = [
    pathlib.Path.home() / ".local/share/opencode/auth.json",
    pathlib.Path.home() / ".config/opencode/auth.json",
]

for p in paths:
    if not p.exists():
        continue
    data = json.loads(p.read_text())
    print(f"file: {p}  providers: {len(data)}")
    for provider, cred in data.items():
        kind = "oauth" if isinstance(cred, dict) and cred.get("type") == "oauth" else "api-key"
        # print the shape, never the value
        keys = sorted(cred.keys()) if isinstance(cred, dict) else ["<string>"]
        print(f"  - {provider:14} type={kind:8} fields={keys}")

# Never paste the file into a ticket, a chat, or a screenshot. Copy the provider
# names and rotate; the values themselves are the thing you are trying to retire.

决定一切的那个文件:auth.json

OpenCode 把厂商凭据存在本地 auth 文件里,StepSecurity 给出了路径:macOS 和 Linux 在 local share 目录下,Windows 在用户配置文件下的等价路径。文件里列出的每一个厂商,都是一个值得轮换的凭据。这句话就是全部善后工作,也正是人们第一个犯错的地方——把整个文件贴进工单。

只打印结构,不打印值。厂商名、凭据类型、字段名、数量。这足够让你列出轮换清单,也足够验证「只有关联了外部厂商的人才在范围内」这条说法。值本身不该离开这台机器,因为这个动作的目的就是让它们变得没用。

还有第二个常被忘掉的凭据库:MCP 服务器配置。MCP server 往往自带连接目标所需的 token,所以只查到 auth.json 就收工的排查是不完整的。把 MCP 配置和你上一次的已知良好副本做 diff,看最近加了什么。

#!/usr/bin/env bash
# Rotation order matters. Rotate the key that can spend money on inference first,
# then the one with the widest data reach. Verify the new key works before you
# revoke the old one, and keep an overlap window you can reason about.

set -euo pipefail

rotate() {
  local provider="$1"
  echo "== $provider =="
  echo "1. create new key in the provider console (do not reuse the old name)"
  echo "2. write it to the secret store, not to a dotfile:"
  echo "     your-secret-tool set ${provider}_API_KEY"
  echo "3. smoke test with the NEW key:"
  echo "     curl -sS -o /dev/null -w '%{http_code}' "$2" -H "Authorization: Bearer \$NEW_KEY""
  echo "4. revoke the OLD key in the console; confirm it now returns 401"
  echo "5. record rotation time in the incident timeline"
  echo
}

rotate anthropic "https://api.anthropic.com/v1/models"
rotate openai    "https://api.openai.com/v1/models"
rotate google    "https://generativelanguage.googleapis.com/v1beta/models"

# Step 4 is the one people skip. A key you rotated but never revoked is still a
# valid credential sitting in an attacker's list.
暗色屏幕上的代码特写

auth.json 这个本地文件列出了你关联过的每个厂商,请把它当凭据库对待。

按正确顺序轮换,然后去读账单

轮换顺序应该按影响面排。能直接花推理钱的密钥先换,因为滥用会先出现在账单上,而不是告警里。其次是数据可达范围最广的那个。先建新的、写进密钥管理而不是 dotfile、做冒烟测试,然后才吊销旧的,并确认复用现在返回 401。

最后这步是时间紧时最常被跳过的。轮换但没吊销的密钥,仍然是别人清单里有效的凭据,而事件收尾会上发现不了,因为新密钥工作正常。要一把一把显式验证吊销,并记下时间戳。

然后去读用量历史,因为轮换只能阻止未来的滥用,对过去的滥用一字不提。看形状而不是看量:一串很短的请求、不寻常的模型、不寻常的区域、在开发者不工作的时间有活动。各家厂商导出格式不同,先归一化再判断。这也是检查 agent 控制台账号里有没有开发者不认识的会话的时机。

# Usage anomaly review. Rotation stops new abuse; usage history tells you whether
# abuse already happened. Look for shape, not volume: a burst of short requests
# with unusual model or region, at hours the developer does not work.

import json
from datetime import datetime
from collections import Counter

rows = [json.loads(l) for l in open("provider-usage.jsonl")]

def hour(dt): return datetime.fromisoformat(dt).hour

peaks = Counter(hour(r["ts"]) for r in rows)
models = Counter(r["model"] for r in rows)
regions = Counter(r.get("region", "unknown") for r in rows)

print("top hours:", peaks.most_common(3))
print("top models:", models.most_common(3))
print("regions:", regions.most_common(3))

# Flag anything a human would recognise as odd:
for r in rows:
    odd = (
        r.get("region") not in {"your-usual-region"}
        or r["model"] not in {"models-you-actually-use"}
        or r.get("prompt_tokens", 0) > 100_000
    )
    if odd:
        print("REVIEW", r["ts"], r["model"], r.get("region"), r.get("prompt_tokens"))

# Provider export formats differ. Normalise first, then judge. And do this before
# you conclude "we rotated, so we are fine".

让下一次披露对你更便宜一点

真正耐久的方法不是更强的应急响应,而是更短的长期凭据清单。厂商支持的话优先用短期 token;优先用可限定范围、可限流、可集中吊销的网关密钥,而不是把原始厂商密钥粘到工具配置里;优先维护一份持续更新的清单,而不是事发时才临时拼。

第二个耐久项是对范围有一个诚实的定义。「没收到邮件就是没受影响」是厂商的说法,它可能是对的,但这是在没有公布根因、事件仍在进行时给出的说法。你自己的控制不该取决于厂商的邮件送达是否正常。

第三个是把「不知道的东西」写下来。一页纸的事件说明,带一个明确的「未知项」小节,比一份语气笃定的总结更有价值,因为未知项才告诉你该补哪些监控。下面这份清单故意写得很笨,而正是「笨」让它能活过一个周五下午。

#!/usr/bin/env bash
# Incident closure checklist. Cheap to run, and it is the difference between
# "we handled it" and "we hope we handled it".

set -euo pipefail
cat <<'EOF'
[ ] vendor statement archived (timestamp + URL + screenshot)
[ ] scope confirmed: Console users vs CLI-only installs (they are different)
[ ] every developer asked: did you receive the vendor email?
[ ] auth.json provider list captured (values never copied)
[ ] Anthropic / OpenAI / Google keys used with OpenCode rotated
[ ] old keys confirmed revoked (401 on reuse)
[ ] provider usage reviewed for the incident window, anomalies triaged
[ ] MCP server configs reviewed for credentials of their own
[ ] no-plaintext-key policy applied to the affected machines
[ ] timeline written, owner named, follow-up date set
EOF

# Two things to resist: treating "no email" as proof of safety, and treating
# "keys rotated" as the end of the work.
亮着灯的数据中心走廊

轮换密钥回答不了的问题,用量历史能回答:是不是已经有人花掉了你的预算。

📌 常见问题 FAQ

影响了多少用户?

OpenCode 的公开声明说影响了 1.4% 的 OpenCode 控制台用户。厂商同时表示事件仍在进行、暂不能提供更多细节,所以这个数字应当视为「当前值」而不是「最终值」。

是所有装了 CLI 的人都受影响吗?

按声明的说法,影响范围限于 OpenCode 控制台用户,而不是所有装了 CLI 的人;在这部分用户里,受影响的子集关联了外部厂商。被点名的厂商是 Anthropic、OpenAI 和 Google。

API 密钥泄露了吗?

公开信息尚未确认。StepSecurity 明确表示目前没有看到关于「访问了哪些数据」「厂商 API 密钥是否泄露」的细节。稳妥的假设是:任何关联到 agent 控制台的密钥都应该轮换,因为轮换很便宜,而这种不确定性不便宜。

没收到邮件就安全了吗?

厂商说 10 月 18 日已给每位受影响用户发邮件,没收到即未受影响。这是厂商在事件仍在进行、根因未公布时的断言。请把它当成一个输入,用自己的清单和用量日志去核对,凡是无法明确交代的凭据都轮换。

OpenCode 把厂商凭据存在哪?

存在本地 auth 文件里:macOS 和 Linux 在用户主目录下的 local share 目录,Windows 在用户配置文件下的等价路径(StepSecurity 给出了具体路径)。列出文件里的厂商、逐个轮换,绝不要把文件里的值复制进工单。