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 -29
View File
@@ -122,8 +122,7 @@ fetch_offer() {
cp "$OFFER_MANIFEST" "$WORK/feed.manifest"; cp "$OFFER_SIG" "$WORK/feed.sig"
fi
fetch_mailbox
# 信箱里没有东西,而仓库里的清单没通过(验证失败,或者就是已经装的这一份):留下仓库的那份,让后面照常报出原因,
# 而不是悄悄当作没有更新;「已经装的这一份」也要留下,因为要把它交给节点握手时带给 Backend
# 信箱里没有东西,而仓库里的清单没通过(验证失败,或者就是已经装的这一份):留下仓库的那份,让后面照常报出原因,而不是悄悄当作没有更新
if [ -n "$feed_error" ] && [ ! -s "$OFFER_MANIFEST" ]; then
cp "$WORK/feed.manifest" "$OFFER_MANIFEST"; cp "$WORK/feed.sig" "$OFFER_SIG"
fi
@@ -165,29 +164,6 @@ write_status() { # state available_json error
fi
}
# 节点握手时要带给 Backend 的「我是从哪份 CI 签名清单安装的」:Backend 用 CI 公钥验过,才相信清单里列的官方二进制哈希。
# 写在节点只读挂载的状态目录里(节点自己改不了),清单在前、签名在后;内容没变就不写(保护路由器的闪存)。
write_installed() {
if cmp -s "$OFFER_MANIFEST" "$STATE_DIR/$COMPONENT.installed.manifest" 2>/dev/null && cmp -s "$OFFER_SIG" "$STATE_DIR/$COMPONENT.installed.sig" 2>/dev/null; then
return 0
fi
for pair in "installed.manifest:$OFFER_MANIFEST" "installed.sig:$OFFER_SIG"; do
name="${pair%%:*}"; src="${pair#*:}"
if [ -n "$DATA_VOLUME" ]; then
"$ENGINE" run --pull=never --rm -i --user 0 --network none --read-only --cap-drop ALL --cap-add DAC_OVERRIDE --security-opt no-new-privileges \
-v "$STATUS_VOLUME:/status" --entrypoint /bin/sh "$HELPER_IMAGE" \
-c 'tmp=$(mktemp /status/.installed.XXXXXX); cat > "$tmp"; chmod 644 "$tmp"; mv -f "$tmp" "/status/$1"' sh "$name" < "$src" || return 0
else
destination="$(mktemp "$STATUS_DIR/.installed.XXXXXX")"
cp "$src" "$destination"
chmod 644 "$destination"
mv -f "$destination" "$STATUS_DIR/$name"
fi
done
cp "$OFFER_MANIFEST" "$STATE_DIR/$COMPONENT.installed.manifest"
cp "$OFFER_SIG" "$STATE_DIR/$COMPONENT.installed.sig"
}
# 第一次部署(deploy-node.sh 已经验过签并按 digest 固定)时还没有安装记录:清单的 digest 就是正在跑的这个镜像,就记下来,不当作「有更新」
seed_installed() {
[ ! -r "$INSTALLED" ] || return 0
@@ -283,8 +259,6 @@ do_check() {
case "$ERR" in
"no offer") write_status up_to_date null "" ;;
"stale manifest"*)
# 就是已经装的这一份(同一个 seq、同一个 digest):交给节点握手时带给 Backend
if [ "$M_SEQ" = "$(installed_seq)" ] && [ "$M_DIGEST" = "$(current_digest)" ]; then write_installed; fi
write_status up_to_date null "" ;;
*) write_status invalid null "$ERR"; echo "offer rejected: $ERR" >&2 ;;
esac
@@ -293,7 +267,6 @@ do_check() {
adopt_next_key
seed_installed
if [ "$M_DIGEST" = "$(current_digest)" ]; then
write_installed
write_status up_to_date null ""
return 0
fi
@@ -339,7 +312,6 @@ do_apply() {
fi
if [ "$healthy" = true ] && service_ctl is-active --quiet "$UNIT" && [ "$before" = "$after" ]; then
printf '%s\n%s\n' "$M_SEQ" "$M_DIGEST" > "$INSTALLED"
write_installed
write_status up_to_date null ""
echo "updated to $M_TAG"
return 0
+2 -11
View File
@@ -111,22 +111,13 @@ expect "first install: wrong signing key is refused" "REFUSED" "$(resolve "$T/c.
expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" probe "$REPO")"
expect "first install: manifest naming another repository is refused" "REFUSED" "$(resolve "$T/b.pem" agent "registry.evil/moshiro-dev/moshiro-agent")"
# --- 第一次部署:正在跑的镜像就是清单里的这个 digest → 记成已安装(不当作有更新),并把验过的清单交给节点握手时带上
# --- 第一次部署:正在跑的镜像就是清单里的这个 digest → 记成已安装(不当作有更新)
sed -i '/^FEED=off/d' "$T/conf/agent.conf"
rm -f "$T/state/agent.installed" "$T/state/agent.installed.manifest" "$T/state/agent.installed.sig" "$T/data/update-status/installed."* "$T/data/update/offer."*
rm -f "$T/state/agent.installed" "$T/data/update/offer."*
printf 'IMAGE=%s@%s\n' "$REPO" "$DIGEST" > "$T/env"
publish "$T/b.pem" 50
expect "first deploy: the running image is already up to date" "up_to_date|" "$(check)"
if [ "$(sed -n 2p "$T/state/agent.installed" 2>/dev/null)" = "$DIGEST" ]; then echo "ok first deploy: install record seeded from the running image"; else echo "FAIL first deploy: no install record"; fail=1; fi
label="$(cat "$T/feed/$REF_KEY")"
if [ "$(cat "$T/data/update-status/installed.manifest" 2>/dev/null | base64 -w0)" = "${label%%|*}" ] && [ "$(cat "$T/data/update-status/installed.sig" 2>/dev/null)" = "${label#*|}" ]; then
echo "ok the verified manifest is left for the node to present at its handshake"
else
echo "FAIL installed.manifest / installed.sig missing or different"; fail=1
fi
# 老版本的更新器没写过:同一份(seq 相同)下次检查补上
rm -f "$T/data/update-status/installed."* "$T/state/agent.installed.manifest" "$T/state/agent.installed.sig"
expect "already installed: still up to date" "up_to_date|" "$(check)"
if [ -s "$T/data/update-status/installed.manifest" ] && [ -s "$T/data/update-status/installed.sig" ]; then echo "ok a missing copy is written again for the node"; else echo "FAIL the copy was not written again"; fail=1; fi
exit "$fail"