refactor: remove the runtime binary binding; every release registration path verifies the CI signature [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

The hash a node reports about itself cannot be trusted (a swapped binary can
copy the old hash), so the runtime binding only caught files that were replaced
by something that did not lie. Provenance stays where it can be verified: the
signed release manifest (digest) and the signed SHA256SUMS.

- Remove BinaryBindingService, the node_bindings table (schema v15 drops it),
  BinaryPolicy, the handshake hash/manifest fields and the admin binary column.
- Drop the per-platform "binaries" list from the release manifest, the hash
  scripts and the updater's installed.manifest copy.
- ReleaseService.PublishAsync now verifies the release signature, so the admin
  POST /releases route is no longer an unsigned path; tests sign with a test key.
- Docs: section 16 explains why the runtime check is gone.
This commit is contained in:
justme committed 2026-10-08 18:30:05 +08:00
1 parent a4f3d1892a
commit 904ea2b0ae
49 files changed
+115 -1143

No files matched your search

+1 -25
View File
@@ -1,7 +1,7 @@
import { Download, KeyRound, Plus, Trash2 } from 'lucide-react'
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import { api } from '@shared/api/client'
import type { AgentDto, ApiTokenDto, BuildInfoDto, NodeBinaryDto, NodeOwnerDto, CapabilityCatalogDto, MeDto, PasskeyDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, AuditDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
import type { AgentDto, ApiTokenDto, BuildInfoDto, NodeOwnerDto, CapabilityCatalogDto, MeDto, PasskeyDto, NodeUpdateDto, ReleaseDto, CreatedApiTokenDto, AuditDto, EventDto, Page, ProbeDto, RawChunkMetaDto, RecomputeDto, SettingDto, UserDto } from '@shared/api/types'
import { fmtBuiltAt, frontendBuild } from '@shared/buildInfo'
import { fmtNum, fmtTime } from '@shared/format'
import { translateSetting } from '@shared/i18n'
@@ -207,10 +207,6 @@ const UPDATE_STATE: Record<string, { text: string; tone: '' | 'ok' | 'warn' | 'b
invalid: { text: '清单无效', tone: 'bad' },
}
/**
* 节点二进制的绑定:第一次握手成功时绑定它正在运行的二进制的哈希,之后对不上又不在 CI 签名的更新清单里就拒绝连接。
* 「已签名」表示哈希出现在一份 CI 签名的清单里;否则只是首次握手时记下的。不符时可以确认当前的二进制是自己要的(比如手工升级了)。
*/
/**
* 节点的属主:节点自己要求有属主(passkey),安装时固定在本地;面板顺着它报上来的凭证 id 反查出绑定在哪个账户的哪把 passkey 上。
* 以后给节点更多接口和操作(web ssh、加密内容)时,只认这些 passkey。
@@ -228,22 +224,6 @@ function OwnerCell({ owners }: { owners?: NodeOwnerDto[] | null }) {
)
}
function BinaryCell({ binary, busy, onAccept }: { binary?: NodeBinaryDto | null; busy: boolean; onAccept: () => void }) {
if (!binary) return <span className="faint">未绑定</span>
const short = binary.sha256.slice(0, 10)
return (
<span className="row" style={{ gap: 8 }} title={binary.violation ?? binary.sha256}>
{binary.violation ? <Chip tone="bad">不符</Chip> : <Chip tone={binary.signed ? 'ok' : ''}>{binary.signed ? '已签名' : '首次绑定'}</Chip>}
<span className="mono muted">{short}</span>
{binary.violation ? (
<button type="button" className="btn small" disabled={busy} onClick={onAccept} title="清除绑定,节点下一次连接时重新绑定当前的二进制">
接受当前二进制
</button>
) : null}
</span>
)
}
function UpdateCell({ update, busy, onUpdate }: { update?: NodeUpdateDto | null; busy: boolean; onUpdate: () => void }) {
if (!update) return <span className="faint">—</span>
const s = UPDATE_STATE[update.state] ?? { text: update.state, tone: '' as const }
@@ -310,7 +290,6 @@ function Versions() {
<th>构建时间</th>
<th>系统</th>
<th>更新</th>
<th>二进制</th>
<th>属主</th>
</tr>
</thead>
@@ -333,9 +312,6 @@ function Versions() {
<td>
<UpdateCell update={n.update} busy={busy} onUpdate={() => requestUpdate([n.id])} />
</td>
<td>
<BinaryCell binary={n.binary} busy={busy} onAccept={() => void run(async () => { await api.del(`/api/admin/nodes/${n.id}/binary`); info.reload() }, '已清除绑定,节点下一次连接时重新绑定')} />
</td>
<td>
<OwnerCell owners={n.owners} />
</td>
+1 -70
View File
@@ -389,7 +389,7 @@ export interface paths {
/** The latest signed release manifest of each component */
get: operations["releases_list"];
put?: never;
/** Register a CI-signed release manifest; online nodes get it right away (their updaters verify the signature). Admin or release tokens */
/** Register a CI-signed release manifest (the signature is checked against the release key); online nodes get it right away. Admin or release tokens */
post: operations["releases_publish"];
delete?: never;
options?: never;
@@ -1006,23 +1006,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/nodes/{id}/binary": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Accept the node's current binary: clear its binding so the next handshake binds it again */
delete: operations["nodes_binary_clear"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/backend/update": {
parameters: {
query?: never;
@@ -1715,17 +1698,6 @@ export interface components {
version: number | string;
needs: string[];
};
NodeBinaryDto: {
sha256: string;
signed: boolean;
/** Format: int64 */
seq: number | string;
/** Format: date-time */
bound_at: unknown;
violation: null | string;
/** Format: date-time */
violation_at: unknown;
};
NodeBuildDto: {
id: string;
kind: string;
@@ -1737,7 +1709,6 @@ export interface components {
commit: null | string;
built_at: null | string;
update?: null | components["schemas"]["NodeUpdateDto"];
binary?: null | components["schemas"]["NodeBinaryDto"];
owners?: null | components["schemas"]["NodeOwnerDto"][];
};
NodeInfoDto: {
@@ -5736,46 +5707,6 @@ export interface operations {
};
};
};
nodes_binary_clear: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description No Content */
204: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["ProblemDto"];
};
};
};
};
backend_update_get: {
parameters: {
query?: never;
-1
View File
@@ -19,7 +19,6 @@ export type BuildInfoDto = Narrow<S['BuildInfoDto']>
export type BackendUpdateDto = Narrow<S['BackendUpdateDto']>
export type NodeBuildDto = Narrow<S['NodeBuildDto']>
export type NodeUpdateDto = Narrow<S['NodeUpdateDto']>
export type NodeBinaryDto = Narrow<S['NodeBinaryDto']>
export type NodeOwnerDto = Narrow<S['NodeOwnerDto']>
export type ReleaseDto = Narrow<S['ReleaseDto']>
export type LinkDto = Narrow<S['LinkDto']>