保护一个持有密钥的 Solana 交易机器人

密钥就是全部攻击面。它从哪里泄漏、泄漏时什么能限定损失,以及没人审计的那份依赖风险。

BoltTx Team··15 min read
solana安全密钥管理交易机器人运维交易上链

交易机器人是一个自动签名交易的程序。★这就是它的全部安全模型 —— 而它意味着:一个被攻破的进程,就是一个被攻破的钱包。★

有用的问题不是"怎么让攻破不可能发生"。而是"它发生时,攻击者拿到什么"。

值得作为起点的那个假设

★按"这个进程会被攻破"来设计,因为另一种选择,是一个对损失没有任何上界的设计。★

热钱包余额 = ★一个被攻破的机器人能造成的最大损失★

这一行就能定下大部分关于规模的争论。 一个装着策略运转一天所需资金的钱包,损失的是一天的运营资金。一个装着全部的钱包,损失全部 —— 而再多的代码加固,都改变不了你选的是哪一个。

按信任等级拆,不是按方便拆:

钱包 装什么 被攻破时
★热钱包 / 交易★ ★只装运营资金★ ★有界,可恢复★
手续费支付方 付手续费的 SOL 只会浪费手续费
金库 ★其余全部★ ★永不上线★

密钥实际是从哪里泄漏的

通常不是通过密码学。而是通过普通的运维失误:

// ★下面每一种都真实发生过。★
console.log("signing with", keypair.secretKey);     // 日志
throw new Error(`failed for ${JSON.stringify(cfg)}`); // ★配置进了异常栈★
await axios.post(url, { ...config });                // ★发给了第三方★
git add .env                                          // 提交进了仓库

★异常栈是最容易被忽略的那个。★ 一个捕获了配置的错误对象,被发给错误上报服务 —— 你刚刚把密钥导出给了一个供应商。

不花成本的具体防御:

把密钥放进一个专用对象,它没有 toJSON,toString 返回一个占位符。

★永远不要把密钥和"会被记录的配置"放进同一个结构里。★

从环境变量或密钥管理服务加载,绝不从仓库里的文件加载。

在密钥存在之前就把它的路径加进 .gitignore,而不是之后。

如果你的密钥处理很扎实、交易还是会错过,免费领个 BoltTx key 改一行就能测测提交路径。

依赖:最容易被跳过的那块攻击面

你的机器人 import 了几十个包。★其中任何一个,都以和你自己的代码同等的权限接触你的密钥。★

npm ci                    # ★用 lockfile,不是 npm install★
npm audit --production

真正能降低这份风险的:

★把版本钉死,并提交 lockfile。★ 一个 caret 范围意味着一个被投毒的补丁版本会自动进入你的构建。

延迟升级。 供应链投毒通常在几天内被发现。晚一周不用最新版,是很便宜的保险。

审视一个依赖需要什么。 一个需要网络访问的图表库,值得再看一眼。

★价值最高的习惯,是把"持有密钥的那个进程"里的依赖数量降到最低。★ 任何不需要挨着签名密钥运行的东西,都该跑在别处。

限定这把密钥能做什么

应用层的限制防的是一个糊涂的机器人。★它防不了一个被攻破的,因为持有密钥的代码可以跳过它自己的检查。★

只有链上约束能扛过这个:

委托的代币授权。 批准一个特定额度,让密钥只能花这么多。

由程序强制的限制。 一个拒绝任何超出参数的小型链上程序。★这是唯一一种"握着密钥也不足以拿走资金"的选项。★

带时间锁的提现。 从金库转出需要延迟,给你一个发现的窗口。

进程内的限制仍然值得有 —— 它们能抓住 bug 和失控循环,而那比被攻破常见得多。只是不要把它们误当成安全边界。

检测,是把"入侵"变成"事故"的东西

★你没法阻止每一次攻破,但在几分钟内而不是几天内察觉,会彻底改变结果。★

// ★链上有、而你的日志不知道的活动。★
const onChain = await connection.getSignaturesForAddress(wallet, { limit: 100 });
const unlogged = onChain.filter((s) => !logged.has(s.signature));
if (unlogged.length) alert(`${unlogged.length} 笔无法解释的交易`);

★这是一个交易机器人能拥有的最有价值的告警。★ 一笔由你的密钥签名、而你的机器人没有发起的交易,只有一种解释 —— 这种告警值得半夜把人叫醒。

同样值得告警的:

余额下降得比交易能解释的更快。

转账到你已知集合之外的地址。

★机器人已经停机时还有活动★ —— 毫无歧义,而且是即时信号。

在需要之前就准备好响应预案

★在事故进行中才琢磨该做什么,正是资金在讨论中流失的方式。★

1. 停掉机器人。 在做任何别的事之前先杀掉进程。

2. 转走剩下的。 预先写好并测试过这个脚本。 ★一个你在事故中才开始写的归集脚本,是一个你在事故中才开始调试的脚本。★

3. 撤销委托授权。 任何已批准的代币授权,在钱包被清空之后依然存在。

4. 轮换一切。 新密钥、新 API 凭据,如果主机可疑就换新服务器。

5. 在重新部署之前找到入口。 ★从一个含有那个入侵点的备份恢复,是把它重演一遍。★

最要紧的那几条实践

★按"每单位投入能防住多少损失"排序:★

小额热钱包。 限定了其它每一种失败。 一个决策,永久有效。

金库分离,离线。 机器人够得到的任何东西都抽不干它。

密钥绝不进日志或异常栈。 防住现实中最常见的那种泄漏。

"无法解释的交易"告警。 把一次静默的抽干变成一次五分钟的事故。

锁死依赖。 关掉那份没人审计的攻击面。

其余的都是精修。 ★做到这五条的机器人,比那些内部控制做得再精密、热钱包里却装满钱的,安全得多。★

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

★安全和上链是两个独立的问题。★ 一个安全做得好的机器人仍然要和其它交易一样争夺收录;而一个很快、但密钥泄漏了的机器人,不管 slot 分布多好都会输光。

BoltTx 在这里的位置

我们负责提交。密钥保管完全在你这边。

★你在本地签名,我们永远看不到你的私钥 —— 没有任何密钥材料需要发给我们,我们的 API 也没有任何一个版本接受它。★ 我们不托管资金、不代签、不修改交易内容。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。API key 泄漏损失的是配额,不是资金 —— 两者是不同的凭据,后果也不同。

tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

怎么保护一个 Solana 交易机器人? 假设进程会被攻破,并限定损失。 小额热钱包、离线金库、密钥不进日志,以及一个"无法解释的交易"告警,覆盖了大部分风险。

热钱包该放多少 SOL? 策略运转所需的量,不多放。 那个余额就是被攻破时的最大损失 —— 这让它是一个安全决策,而不是一个方便与否的决策。

私钥通常从哪里泄漏? 日志、异常栈、错误上报服务,以及被提交进仓库的配置文件。 密码学失败很罕见,普通运维失误不罕见。

异常栈为什么危险? 一个捕获了配置、并被发给上报服务的错误,会把那份配置里的一切导出去。 别把密钥放进任何可能被序列化的结构里。

应用层的限制能保护我吗? 对 bug 和失控循环能,对被攻破不能 —— 因为持有密钥的代码可以跳过它自己的检查。只有链上约束能扛住。

有哪些链上保护手段? 限额的代币委托授权、强制参数的链上程序,以及带时间锁的金库提现。 只有这些能在攻击者握着密钥时依然成立。

怎么降低依赖风险? 钉死版本、提交 lockfile、把升级推迟几天,并把"持有密钥的那个进程"里的包数量降到最低。

为什么要延迟升级依赖? 供应链投毒通常在几天内被发现。 晚一周不用最新版代价很小,却能避免成为早期受害者。

最有价值的安全告警是什么? 由你的密钥签名、而你的日志不知道的链上交易。 那只有一种解释,而且需要立即行动。

怎么快速发现被攻破? 频繁地拿链上活动和自己的日志对账,并对"交易无法解释的余额变动"或"机器人停机时的活动"告警。

事故响应该怎么做? 停进程、用预先测试过的脚本转走剩余资金、撤销委托、轮换每一个凭据,并在重新部署之前找到入口。

为什么要预先写好归集脚本? 因为在事故中才写它,意味着在事故中才调试它。它应该提前测试好,拿来就能跑、不用改。

钱包已经被清空了,撤销委托还有意义吗? 有。已批准的代币授权独立于余额存在,所以一个留着没撤的委托,能把你之后再充进去的资金也拿走。

金库该上线吗? 不该。它该装机器人不需要的一切,而补款是它和热钱包之间唯一的通路。

BoltTx 会看到我的私钥吗? 不会。你在本地签名,而且没有任何 API 接受密钥材料。 一个泄漏的 API key 损失的是配额而不是资金 —— 两者是不同的凭据。

单项价值最高的安全决策是什么? 热钱包放多少。 它限定了其它每一种失败模式,它是一个决策,而且再多的代码加固都替代不了它。

延伸阅读

← 返回博客列表