feat: signed manifests live in the image registry; updaters and first deploys fetch and verify them without the panel [CI]

This commit is contained in:
justme committed 2026-10-08 05:05:13 +08:00
1 parent 4106f831dc
commit 117d34a2af
7 files changed
+267 -28

No files matched your search

+45 -8
View File
@@ -14,7 +14,7 @@
set -eu
usage() {
echo "usage: deploy-node.sh <agent|probe|backend> [--engine podman|docker] [--image-repo R] [--tag T] [--image REF]" >&2
echo "usage: deploy-node.sh <agent|probe|backend> [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2
echo " [--server URL --join TOKEN --name NAME] [--policy auto|manual] [--authfile F]" >&2
echo " backend: [--channel dev|release] [--port N] [--admin-user U] [--admin-password P] [--public-url URL] [--env KEY=VALUE]..." >&2
exit 2
@@ -22,7 +22,7 @@ usage() {
ROLE="${1:-}"; shift || true
case "$ROLE" in agent|probe|backend) ;; *) usage ;; esac
ENGINE=""; REPO=""; TAG=latest; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL=""; PORT=""; ADMIN_USER=""; ADMIN_PASSWORD=""; PUBLIC_URL=""; EXTRA_ENV=""
ENGINE=""; REPO=""; TAG=""; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL=""; PORT=""; ADMIN_USER=""; ADMIN_PASSWORD=""; PUBLIC_URL=""; EXTRA_ENV=""
while [ $# -gt 0 ]; do
case "$1" in
--engine) ENGINE="$2"; shift 2 ;;
@@ -82,13 +82,50 @@ for f in "$PACKAGING/systemd/$UNIT_SRC" "$PACKAGING/update/install-updater.sh" "
[ -r "$f" ] || { echo "missing $f (keep deploy-node.sh next to the packaging/ directory)" >&2; exit 1; }
done
# 1. 镜像:固定到 digest,后面的单元、辅助容器都用同一个引用
# 1. 镜像:固定到 digest,后面的单元、辅助容器都用同一个引用。
# 默认从仓库里的签名清单取(<仓库>-manifest:<通道>,CI 发布的),用随脚本带的发布公钥验签,按清单里的 digest 固定——
# 仓库和网络都不需要可信,也不需要面板在线(Backend 第一次部署时还没有面板)。
# --tag 指定标签就不验签(拉那个标签再固定 digest);--image 直接用你给的引用。
if [ "$ENGINE" = docker ] && [ -n "$AUTHFILE" ]; then export DOCKER_CONFIG="$(dirname "$AUTHFILE")"; fi
pull_quiet() {
if [ "$ENGINE" = podman ] && [ -n "$AUTHFILE" ]; then "$ENGINE" pull -q --authfile "$AUTHFILE" "$1" >/dev/null
else "$ENGINE" pull -q "$1" >/dev/null; fi
}
resolve_verified() {
channel="$CHANNEL"
if [ -z "$channel" ]; then
case "$REPO" in */moshiro-dev/*) channel=dev ;; */moshiro/*) channel=release ;; esac
fi
[ -n "$channel" ] || { echo "cannot tell the release channel from $REPO: pass --channel dev|release, or --tag to skip verification" >&2; return 1; }
command -v openssl >/dev/null 2>&1 || { echo "openssl is required to verify the release signature (or pass --tag to skip verification)" >&2; return 1; }
ref="$REPO-manifest:$channel"
pull_quiet "$ref" || { echo "cannot pull the signed manifest $ref: is the repository readable (--authfile)? Or pass --tag to skip verification" >&2; return 1; }
labels="$("$ENGINE" image inspect --format '{{index .Config.Labels "org.moshiro.manifest"}}|{{index .Config.Labels "org.moshiro.signature"}}' "$ref")" || return 1
vtmp="$(mktemp -d)"
printf '%s' "${labels%%|*}" | base64 -d > "$vtmp/manifest" 2>/dev/null && printf '%s' "${labels#*|}" | base64 -d > "$vtmp/sig" 2>/dev/null \
|| { rm -rf "$vtmp"; echo "the manifest in $ref is not readable" >&2; return 1; }
if ! openssl dgst -sha256 -verify "$PACKAGING/update/release-key.pub.pem" -signature "$vtmp/sig" "$vtmp/manifest" >/dev/null 2>&1; then
rm -rf "$vtmp"; echo "the signature of the manifest in $ref does not match the bundled release key; refusing to install" >&2; return 1
fi
mfield() { sed -n 's/.*"'"$1"'":"\([^"]*\)".*/\1/p' "$vtmp/manifest" | head -n 1; }
vdigest="$(mfield digest)"; vcomponent="$(mfield component)"; vimage="$(mfield image)"; vtag="$(mfield tag)"
rm -rf "$vtmp"
[ "$vcomponent" = "$ROLE" ] && [ "$vimage" = "$REPO" ] || { echo "the manifest is for $vcomponent / $vimage, not $ROLE / $REPO; refusing to install" >&2; return 1; }
case "$vdigest" in sha256:*) ;; *) echo "the manifest has no usable digest" >&2; return 1 ;; esac
[ "${#vdigest}" -eq 71 ] && [ -z "$(printf '%s' "${vdigest#sha256:}" | tr -d '0-9a-f')" ] || { echo "the manifest digest is malformed" >&2; return 1; }
IMAGE="$REPO@$vdigest"
pull_quiet "$IMAGE" || { echo "cannot pull $IMAGE" >&2; return 1; }
echo "verified release $vtag (CI-signed manifest, $ref)"
}
if [ -z "$IMAGE" ]; then
if [ "$ENGINE" = docker ] && [ -n "$AUTHFILE" ]; then export DOCKER_CONFIG="$(dirname "$AUTHFILE")"; fi
if [ "$ENGINE" = podman ] && [ -n "$AUTHFILE" ]; then "$ENGINE" pull -q --authfile "$AUTHFILE" "$REPO:$TAG" >/dev/null
else "$ENGINE" pull -q "$REPO:$TAG" >/dev/null; fi
if [ "$ENGINE" = podman ]; then DIGEST="$("$ENGINE" image inspect --format '{{.Digest}}' "$REPO:$TAG")"; IMAGE="$REPO@$DIGEST"
else IMAGE="$("$ENGINE" image inspect --format '{{index .RepoDigests 0}}' "$REPO:$TAG")"; fi
if [ -z "$TAG" ]; then
resolve_verified || exit 1
else
pull_quiet "$REPO:$TAG"
if [ "$ENGINE" = podman ]; then DIGEST="$("$ENGINE" image inspect --format '{{.Digest}}' "$REPO:$TAG")"; IMAGE="$REPO@$DIGEST"
else IMAGE="$("$ENGINE" image inspect --format '{{index .RepoDigests 0}}' "$REPO:$TAG")"; fi
echo "warning: $REPO:$TAG was not signature-checked (--tag)" >&2
fi
fi
echo "image: $IMAGE"