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:
1 parent
4106f831dc
commit
117d34a2af
7 files changed
+267
-28
No files matched your search
@@ -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"
|
||||
|
||||
|
||||
Reference in new issue
Block a user