From 4113d71c8eeaef1057b34faf9692bd3ea73c2afd Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 15:56:20 -0700 Subject: [PATCH 1/6] build(openbao-migrations): refresh kubectl binary Update kubectl from v1.36.4 to v1.37.0 with the official per-architecture checksums, and verify both binaries embed Go 1.26.6 and the expected Kubernetes module before copying them into the image. Kubernetes remains Apache-2.0 licensed; no NOTICE update is required. Signed-off-by: Stephanie Baum --- migrations/openbao/Dockerfile | 23 +++++++++++++++++++---- migrations/openbao/README.md | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/migrations/openbao/Dockerfile b/migrations/openbao/Dockerfile index 274db55d9..6104cb1ed 100644 --- a/migrations/openbao/Dockerfile +++ b/migrations/openbao/Dockerfile @@ -1,5 +1,7 @@ ARG ALPINE_VERSION=3.23 ARG GO_IMAGE=golang:1.27.0-alpine3.23@sha256:3747dcba41c8b0db3211fda4db61638b980e17ac5bb3c94460a975a9cfe19395 +ARG KUBECTL_VERSION=v1.37.0 +ARG KUBECTL_GO_VERSION=go1.26.6 FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS jwker-builder @@ -26,9 +28,9 @@ RUN apk add --no-cache curl tar && \ FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS kubectl-downloader ARG TARGETARCH -ARG KUBECTL_VERSION=v1.36.4 -ARG KUBECTL_LINUX_AMD64_SHA256=8b8f088da2dab964f853b38464033b1be15ede2839eca751482357c45abdd05a -ARG KUBECTL_LINUX_ARM64_SHA256=0ecf44450ee6063bf19dd166a103ee6df4a9034455c2abce626e6eea657d73fb +ARG KUBECTL_VERSION +ARG KUBECTL_LINUX_AMD64_SHA256=6129359f4e1f3848a5572ccb0b26cf28b8ca08cef38c95a765b2f64a2c961a2f +ARG KUBECTL_LINUX_ARM64_SHA256=922df28df248cc00a9e025f947704f1d1482de64ece54cfe57e61f19eaf1eef3 # Verify the official checksum and the reviewed per-architecture checksum. RUN apk add --no-cache curl && \ @@ -45,13 +47,26 @@ RUN apk add --no-cache curl && \ printf '%s %s\n' "${KUBECTL_SHA256}" /out/kubectl | sha256sum -c - && \ chmod +x /out/kubectl +FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS kubectl-verifier + +ARG KUBECTL_VERSION +ARG KUBECTL_GO_VERSION + +COPY --from=kubectl-downloader /out/kubectl /out/kubectl + +# Keep the scanner-relevant toolchain floor explicit and verify the downloaded +# binary is the requested Kubernetes module before it reaches the final image. +RUN go version -m /out/kubectl >/tmp/kubectl.buildinfo && \ + grep -Fq ": ${KUBECTL_GO_VERSION}" /tmp/kubectl.buildinfo && \ + grep -Eq "^[[:space:]]*mod[[:space:]]+k8s.io/kubernetes[[:space:]]+${KUBECTL_VERSION}([[:space:]]|$)" /tmp/kubectl.buildinfo + FROM openbao/openbao:2.5.5 # Install runtime dependencies and copy verified tools from downloader stages. RUN apk add --no-cache openssl uuidgen helm curl bash jq COPY --from=jwker-builder /out/jwker /usr/local/bin/jwker -COPY --from=kubectl-downloader /out/kubectl /usr/local/bin/kubectl +COPY --from=kubectl-verifier /out/kubectl /usr/local/bin/kubectl WORKDIR /app diff --git a/migrations/openbao/README.md b/migrations/openbao/README.md index a1f9261a7..59470713a 100644 --- a/migrations/openbao/README.md +++ b/migrations/openbao/README.md @@ -42,7 +42,7 @@ The shipped `job.yaml` sets a default placeholder value for this variable so the ## Building the container The `Dockerfile` uses the public upstream OpenBao image (`openbao/openbao:2.5.5`) as the base. To use a different base, edit the `FROM` line directly. -It builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.36.4 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum. +It builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.37.0 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum. ```bash docker build -t //openbao-migrations: . From acad3379dec3f5de32b5ea3f452e53e01c7f20bd Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 16:23:39 -0700 Subject: [PATCH 2/6] build(openbao-migrations): rebuild Bao dependencies Build Bao 2.6.2 from the checksum-pinned official distribution source with x/crypto v0.56.0, gRPC v1.83.1, and go-archive v0.3.0. Verify dependency floors and target metadata before copying the binary into the runtime image. OpenBao remains MPL-2.0 and the updated Go modules retain their existing compatible licenses; no NOTICE change is required. Signed-off-by: Stephanie Baum --- migrations/openbao/Dockerfile | 32 +++++- migrations/openbao/README.md | 5 +- migrations/openbao/scripts/build-openbao.sh | 73 ++++++++++++ migrations/openbao/scripts/verify-openbao.sh | 108 ++++++++++++++++++ .../openbao/tests/kv-write-retry-test.sh | 13 ++- 5 files changed, 220 insertions(+), 11 deletions(-) create mode 100755 migrations/openbao/scripts/build-openbao.sh create mode 100755 migrations/openbao/scripts/verify-openbao.sh diff --git a/migrations/openbao/Dockerfile b/migrations/openbao/Dockerfile index 6104cb1ed..99e0642b7 100644 --- a/migrations/openbao/Dockerfile +++ b/migrations/openbao/Dockerfile @@ -1,8 +1,27 @@ ARG ALPINE_VERSION=3.23 ARG GO_IMAGE=golang:1.27.0-alpine3.23@sha256:3747dcba41c8b0db3211fda4db61638b980e17ac5bb3c94460a975a9cfe19395 +ARG BAO_VERSION=2.6.2 +ARG BAO_SOURCE_COMMIT=dd9c19c37a878cf4a81b18efb8d6f0599c7da923 +ARG BAO_SOURCE_SHA256=a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9 ARG KUBECTL_VERSION=v1.37.0 ARG KUBECTL_GO_VERSION=go1.26.6 +FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS bao-builder + +ARG BAO_VERSION +ARG BAO_SOURCE_COMMIT +ARG BAO_SOURCE_SHA256 +ARG TARGETARCH + +RUN apk add --no-cache curl xz +COPY scripts/build-openbao.sh scripts/verify-openbao.sh /usr/local/bin/ +RUN BAO_VERSION="${BAO_VERSION}" \ + BAO_SOURCE_COMMIT="${BAO_SOURCE_COMMIT}" \ + BAO_SOURCE_SHA256="${BAO_SOURCE_SHA256}" \ + TARGETARCH="${TARGETARCH}" \ + OUTPUT_DIR=/out \ + /usr/local/bin/build-openbao.sh + FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS jwker-builder ARG TARGETARCH @@ -60,11 +79,16 @@ RUN go version -m /out/kubectl >/tmp/kubectl.buildinfo && \ grep -Fq ": ${KUBECTL_GO_VERSION}" /tmp/kubectl.buildinfo && \ grep -Eq "^[[:space:]]*mod[[:space:]]+k8s.io/kubernetes[[:space:]]+${KUBECTL_VERSION}([[:space:]]|$)" /tmp/kubectl.buildinfo -FROM openbao/openbao:2.5.5 +FROM openbao/openbao:${BAO_VERSION} +ARG TARGETARCH -# Install runtime dependencies and copy verified tools from downloader stages. -RUN apk add --no-cache openssl uuidgen helm curl bash jq +# Upgrade inherited packages before installing runtime dependencies so the +# image receives fixes published after the upstream OpenBao release. +USER root +RUN apk upgrade --no-cache && \ + apk add --no-cache openssl uuidgen helm curl bash jq +COPY --from=bao-builder --chmod=0555 /out/bao-linux-${TARGETARCH} /usr/bin/bao COPY --from=jwker-builder /out/jwker /usr/local/bin/jwker COPY --from=kubectl-verifier /out/kubectl /usr/local/bin/kubectl @@ -74,4 +98,6 @@ COPY --chmod=775 migrations/ /app/migrations/ COPY --chmod=775 addons/ /app/addons/ COPY --chmod=775 entrypoint.sh /app/ +USER openbao + CMD ["/app/entrypoint.sh"] diff --git a/migrations/openbao/README.md b/migrations/openbao/README.md index 59470713a..0a1d97f7e 100644 --- a/migrations/openbao/README.md +++ b/migrations/openbao/README.md @@ -41,8 +41,9 @@ The shipped `job.yaml` sets a default placeholder value for this variable so the ## Building the container -The `Dockerfile` uses the public upstream OpenBao image (`openbao/openbao:2.5.5`) as the base. To use a different base, edit the `FROM` line directly. -It builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.37.0 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum. +The `Dockerfile` uses the public upstream OpenBao 2.6.2 image as its runtime base. It replaces the upstream `bao` binary with a reproducible build from the matching checksum-pinned source commit. The build pins x/crypto v0.56.0, gRPC v1.83.1, and go-archive v0.3.0, then verifies those dependency floors and the target architecture from the embedded Go build metadata. + +The image also builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.37.0 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum, plus the embedded Kubernetes module and Go 1.26.6 toolchain. ```bash docker build -t //openbao-migrations: . diff --git a/migrations/openbao/scripts/build-openbao.sh b/migrations/openbao/scripts/build-openbao.sh new file mode 100755 index 000000000..0d80f6c84 --- /dev/null +++ b/migrations/openbao/scripts/build-openbao.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd) + +bao_version=${BAO_VERSION:-2.6.2} +bao_source_commit=${BAO_SOURCE_COMMIT:-dd9c19c37a878cf4a81b18efb8d6f0599c7da923} +bao_source_sha256=${BAO_SOURCE_SHA256:-a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9} +bao_commit_date=${BAO_COMMIT_DATE:-2026-08-18T15:43:05Z} +x_crypto_version=${X_CRYPTO_VERSION:-v0.56.0} +grpc_version=${GRPC_VERSION:-v1.83.1} +go_archive_version=${GO_ARCHIVE_VERSION:-v0.3.0} +output_dir=${OUTPUT_DIR:-"$repo_root/files/openbao"} +source_url=${BAO_SOURCE_URL:-"https://github.com/openbao/openbao/releases/download/v${bao_version}/openbao-dist-v${bao_version}.tar.xz"} + +if [ -n "${WORK_DIR:-}" ]; then + work_dir=$WORK_DIR + work_dir_is_ours=0 +else + work_dir=$(mktemp -d "${TMPDIR:-/tmp}/nvcf-openbao-source.XXXXXX") + work_dir_is_ours=1 +fi + +cleanup() { + if [ -z "${KEEP_WORK_DIR:-}" ] && [ "$work_dir_is_ours" = "1" ]; then + rm -rf "$work_dir" + else + echo "Keeping work dir: $work_dir" + fi +} +trap cleanup EXIT INT TERM + +archive="$work_dir/openbao.tar.xz" +source_dir="$work_dir/source" +mkdir -p "$source_dir" "$output_dir" + +curl --fail --location --silent --show-error "$source_url" --output "$archive" +printf '%s %s\n' "$bao_source_sha256" "$archive" | sha256sum -c - +tar -xJf "$archive" --strip-components=1 -C "$source_dir" + +( + cd "$source_dir" + GOFLAGS=-mod=mod go get \ + "golang.org/x/crypto@${x_crypto_version}" \ + "google.golang.org/grpc@${grpc_version}" \ + "github.com/moby/go-archive@${go_archive_version}" + GOFLAGS=-mod=mod go mod tidy + GOFLAGS=-mod=mod go mod verify + + if [ -n "${TARGETARCH:-}" ]; then + arches=$TARGETARCH + else + arches="amd64 arm64" + fi + + for arch in $arches; do + CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build \ + -mod=mod \ + -buildvcs=false \ + -trimpath \ + -tags ui \ + -ldflags "-s -w -X github.com/openbao/openbao/version.fullVersion=${bao_version} -X github.com/openbao/openbao/version.GitCommit=${bao_source_commit} -X github.com/openbao/openbao/version.CommitDate=${bao_commit_date}" \ + -o "$output_dir/bao-linux-${arch}" \ + . + chmod 555 "$output_dir/bao-linux-${arch}" + done +) + +BAO_DIR="$output_dir" ARCHES="${TARGETARCH:-amd64 arm64}" "$script_dir/verify-openbao.sh" diff --git a/migrations/openbao/scripts/verify-openbao.sh b/migrations/openbao/scripts/verify-openbao.sh new file mode 100755 index 000000000..317462212 --- /dev/null +++ b/migrations/openbao/scripts/verify-openbao.sh @@ -0,0 +1,108 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd) + +go_bin=${GO:-go} +bao_dir=${BAO_DIR:-"$repo_root/files/openbao"} +arches=${ARCHES:-"amd64 arm64"} +required_x_crypto_version=${REQUIRED_X_CRYPTO_VERSION:-v0.56.0} +required_grpc_version=${REQUIRED_GRPC_VERSION:-v1.83.1} +required_go_archive_version=${REQUIRED_GO_ARCHIVE_VERSION:-v0.3.0} + +metadata_files= +cleanup_metadata_files() { + # shellcheck disable=SC2086 + rm -f $metadata_files +} +trap cleanup_metadata_files EXIT + +version_ge() { + current=${1#v} + required=${2#v} + awk -v current="$current" -v required="$required" ' + BEGIN { + split(current, a, ".") + split(required, b, ".") + for (i = 1; i <= 3; i++) { + av = a[i] + 0 + bv = b[i] + 0 + if (av > bv) exit 0 + if (av < bv) exit 1 + } + exit 0 + } + ' +} + +dep_version() { + module=$1 + metadata=$2 + awk -v module="$module" '$1 == "dep" && $2 == module { print $3 }' "$metadata" +} + +build_value() { + key=$1 + metadata=$2 + awk -v key="$key" '$1 == "build" && $2 ~ ("^" key "=") { sub("^" key "=", "", $2); print $2 }' "$metadata" +} + +verify_binary() { + arch=$1 + binary="$bao_dir/bao-linux-${arch}" + metadata=$(mktemp) + metadata_files="$metadata_files $metadata" + + if [ ! -x "$binary" ]; then + echo "missing executable OpenBao binary: $binary" >&2 + exit 1 + fi + + "$go_bin" version -m "$binary" > "$metadata" + + path=$(awk '$1 == "path" { print $2 }' "$metadata") + if [ "$path" != "github.com/openbao/openbao" ]; then + echo "$binary has unexpected module path: $path" >&2 + exit 1 + fi + + goos=$(build_value GOOS "$metadata") + goarch=$(build_value GOARCH "$metadata") + cgo_enabled=$(build_value CGO_ENABLED "$metadata") + if [ "$goos" != "linux" ] || [ "$goarch" != "$arch" ] || [ "$cgo_enabled" != "0" ]; then + echo "$binary has unexpected target metadata: GOOS=$goos GOARCH=$goarch CGO_ENABLED=$cgo_enabled" >&2 + exit 1 + fi + + x_crypto_version=$(dep_version golang.org/x/crypto "$metadata") + grpc_version=$(dep_version google.golang.org/grpc "$metadata") + go_archive_version=$(dep_version github.com/moby/go-archive "$metadata") + + if ! version_ge "$x_crypto_version" "$required_x_crypto_version"; then + echo "$binary embeds golang.org/x/crypto $x_crypto_version; need $required_x_crypto_version or newer" >&2 + exit 1 + fi + if ! version_ge "$grpc_version" "$required_grpc_version"; then + echo "$binary embeds google.golang.org/grpc $grpc_version; need $required_grpc_version or newer" >&2 + exit 1 + fi + if ! version_ge "$go_archive_version" "$required_go_archive_version"; then + echo "$binary embeds github.com/moby/go-archive $go_archive_version; need $required_go_archive_version or newer" >&2 + exit 1 + fi + + echo "verified $binary" + echo " x/crypto: $x_crypto_version" + echo " grpc: $grpc_version" + echo " go-archive: $go_archive_version" + + rm -f "$metadata" +} + +for arch in $arches; do + verify_binary "$arch" +done diff --git a/migrations/openbao/tests/kv-write-retry-test.sh b/migrations/openbao/tests/kv-write-retry-test.sh index dbe30f60f..16809f7af 100755 --- a/migrations/openbao/tests/kv-write-retry-test.sh +++ b/migrations/openbao/tests/kv-write-retry-test.sh @@ -21,20 +21,21 @@ # overwriting, and other errors fail fast. # # Requires Docker. The OpenBao version is read from the migrations image -# Dockerfile so the test tracks the shipped server version. Set UTILS_DIR -# to point the test at a different copy of the helper functions (useful to -# demonstrate the failure against an older revision). +# Dockerfile so the test tracks the shipped server version. Set BAO_TEST_IMAGE +# to exercise an already-built migrations image, or UTILS_DIR to point at a +# different copy of the helper functions. set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" utils_dir="${UTILS_DIR:-${script_dir}/../migrations/utils}" -bao_version="$(sed -n 's|^FROM openbao/openbao:||p' "${script_dir}/../Dockerfile" | head -1)" +bao_version="$(sed -n 's|^ARG BAO_VERSION=||p' "${script_dir}/../Dockerfile" | head -1)" if [ -z "${bao_version}" ]; then echo "could not read the OpenBao version from ${script_dir}/../Dockerfile" >&2 exit 1 fi +bao_image="${BAO_TEST_IMAGE:-openbao/openbao:${bao_version}}" container="nvcf-openbao-kv-test-$$" tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/nvcf-openbao-kv-test.XXXXXX")" @@ -66,12 +67,12 @@ cluster_addr = "http://127.0.0.1:8201" api_addr = "http://127.0.0.1:8200" EOF -echo "Starting OpenBao ${bao_version} (container ${container})..." +echo "Starting OpenBao ${bao_version} from ${bao_image} (container ${container})..." docker run -d --name "${container}" -u root --entrypoint /bin/sh \ -v "${tmpdir}/config.hcl":/test/config.hcl:ro \ -v "${script_dir}/inner-kv-write-retry.sh":/test/inner.sh:ro \ -v "${utils_dir}":/test/utils:ro \ - "openbao/openbao:${bao_version}" \ + "${bao_image}" \ -c 'mkdir -p /openbao/data && exec bao server -config=/test/config.hcl' >/dev/null export_addr=(-e BAO_ADDR=http://127.0.0.1:8200) From 843abda631061a1cd190b026de4dd02f43cc26b0 Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 16:31:26 -0700 Subject: [PATCH 3/6] fix(openbao): support non-root migration hooks Make the non-secret initialization ConfigMaps readable and executable by the upstream OpenBao user. This keeps the remediated migrations image non-root while preserving the hook runtime contract. Signed-off-by: Stephanie Baum --- .../openbao/helm/templates/hook-post-01-initcluster.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml b/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml index 925ac10ce..086eb853f 100644 --- a/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml +++ b/deploy/helm/openbao/helm/templates/hook-post-01-initcluster.yaml @@ -67,11 +67,13 @@ spec: - name: init-script configMap: name: {{ $serverFullname }}-init-script - defaultMode: 0500 + # The migrations image runs as the upstream non-root OpenBao user. + # These ConfigMaps contain executable scripts, not secrets. + defaultMode: 0555 - name: utils-script configMap: name: {{ $serverFullname }}-utils-script - defaultMode: 0500 + defaultMode: 0555 --- apiVersion: v1 kind: ConfigMap From b7e50db28a2ae75dc9954a962836e257092128dc Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 16:38:05 -0700 Subject: [PATCH 4/6] fix(openbao-migrations): harden dependency verification Pin the OpenBao 2.6.2 runtime base to its multi-architecture manifest and compare dependency floors with SemVer prerelease ordering. Add focused stable, prerelease, and pseudo-version test cases to the migrations workflow. Signed-off-by: Stephanie Baum --- .github/workflows/openbao-migrations.yml | 3 + migrations/openbao/Dockerfile | 3 +- migrations/openbao/scripts/verify-openbao.sh | 60 +++++++++++++++++-- .../tests/verify-openbao-version-test.sh | 37 ++++++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) create mode 100755 migrations/openbao/tests/verify-openbao-version-test.sh diff --git a/.github/workflows/openbao-migrations.yml b/.github/workflows/openbao-migrations.yml index 584412043..40f3a06ea 100644 --- a/.github/workflows/openbao-migrations.yml +++ b/.github/workflows/openbao-migrations.yml @@ -43,5 +43,8 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Test dependency version comparisons + run: migrations/openbao/tests/verify-openbao-version-test.sh + - name: Run kv write retry test run: migrations/openbao/tests/kv-write-retry-test.sh diff --git a/migrations/openbao/Dockerfile b/migrations/openbao/Dockerfile index 99e0642b7..1aa048221 100644 --- a/migrations/openbao/Dockerfile +++ b/migrations/openbao/Dockerfile @@ -1,6 +1,7 @@ ARG ALPINE_VERSION=3.23 ARG GO_IMAGE=golang:1.27.0-alpine3.23@sha256:3747dcba41c8b0db3211fda4db61638b980e17ac5bb3c94460a975a9cfe19395 ARG BAO_VERSION=2.6.2 +ARG BAO_RUNTIME_SHA256=sha256:11fd73a2102cda9c55d5d881a8c3210303146a7ec1e8ac76f526e175c6d24641 ARG BAO_SOURCE_COMMIT=dd9c19c37a878cf4a81b18efb8d6f0599c7da923 ARG BAO_SOURCE_SHA256=a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9 ARG KUBECTL_VERSION=v1.37.0 @@ -79,7 +80,7 @@ RUN go version -m /out/kubectl >/tmp/kubectl.buildinfo && \ grep -Fq ": ${KUBECTL_GO_VERSION}" /tmp/kubectl.buildinfo && \ grep -Eq "^[[:space:]]*mod[[:space:]]+k8s.io/kubernetes[[:space:]]+${KUBECTL_VERSION}([[:space:]]|$)" /tmp/kubectl.buildinfo -FROM openbao/openbao:${BAO_VERSION} +FROM openbao/openbao:${BAO_VERSION}@${BAO_RUNTIME_SHA256} ARG TARGETARCH # Upgrade inherited packages before installing runtime dependencies so the diff --git a/migrations/openbao/scripts/verify-openbao.sh b/migrations/openbao/scripts/verify-openbao.sh index 317462212..e141bbbbe 100755 --- a/migrations/openbao/scripts/verify-openbao.sh +++ b/migrations/openbao/scripts/verify-openbao.sh @@ -4,8 +4,8 @@ set -eu -script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd) +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH='' cd -- "$script_dir/.." && pwd) go_bin=${GO:-go} bao_dir=${BAO_DIR:-"$repo_root/files/openbao"} @@ -25,20 +25,72 @@ version_ge() { current=${1#v} required=${2#v} awk -v current="$current" -v required="$required" ' + function is_numeric(value) { + return value ~ /^[0-9]+$/ + } + + function compare_identifier(left, right) { + if (is_numeric(left) && is_numeric(right)) { + if ((left + 0) > (right + 0)) return 1 + if ((left + 0) < (right + 0)) return -1 + return 0 + } + if (is_numeric(left)) return -1 + if (is_numeric(right)) return 1 + if (left > right) return 1 + if (left < right) return -1 + return 0 + } + BEGIN { - split(current, a, ".") - split(required, b, ".") + sub(/\+.*/, "", current) + sub(/\+.*/, "", required) + + current_dash = index(current, "-") + required_dash = index(required, "-") + current_core = current_dash ? substr(current, 1, current_dash - 1) : current + required_core = required_dash ? substr(required, 1, required_dash - 1) : required + current_pre = current_dash ? substr(current, current_dash + 1) : "" + required_pre = required_dash ? substr(required, required_dash + 1) : "" + + if (split(current_core, a, ".") != 3 || split(required_core, b, ".") != 3) exit 2 for (i = 1; i <= 3; i++) { + if (!is_numeric(a[i]) || !is_numeric(b[i])) exit 2 av = a[i] + 0 bv = b[i] + 0 if (av > bv) exit 0 if (av < bv) exit 1 } + + # A stable release sorts after every prerelease with the same core. + if (current_pre == "" && required_pre == "") exit 0 + if (current_pre == "") exit 0 + if (required_pre == "") exit 1 + + current_count = split(current_pre, current_ids, ".") + required_count = split(required_pre, required_ids, ".") + count = current_count > required_count ? current_count : required_count + for (i = 1; i <= count; i++) { + if (i > current_count) exit 1 + if (i > required_count) exit 0 + comparison = compare_identifier(current_ids[i], required_ids[i]) + if (comparison > 0) exit 0 + if (comparison < 0) exit 1 + } exit 0 } ' } +if [ "${1:-}" = "--version-ge" ]; then + if [ "$#" -ne 3 ]; then + echo "usage: $0 --version-ge CURRENT REQUIRED" >&2 + exit 2 + fi + version_ge "$2" "$3" + exit +fi + dep_version() { module=$1 metadata=$2 diff --git a/migrations/openbao/tests/verify-openbao-version-test.sh b/migrations/openbao/tests/verify-openbao-version-test.sh new file mode 100755 index 000000000..cb51a8a16 --- /dev/null +++ b/migrations/openbao/tests/verify-openbao-version-test.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +verifier="$script_dir/../scripts/verify-openbao.sh" + +expect_ge() { + current=$1 + required=$2 + if ! "$verifier" --version-ge "$current" "$required"; then + echo "expected $current to satisfy $required" >&2 + exit 1 + fi +} + +expect_lt() { + current=$1 + required=$2 + if "$verifier" --version-ge "$current" "$required"; then + echo "expected $current not to satisfy $required" >&2 + exit 1 + fi +} + +expect_ge v1.83.1 v1.83.1 +expect_ge v1.83.2-0.20260905120000-deadbeef v1.83.1 +expect_ge v0.0.0-20260905120000-deadbeef v0.0.0-20260904120000-feedface +expect_ge v1.83.1-rc.10 v1.83.1-rc.2 + +expect_lt v1.83.1-rc.1 v1.83.1 +expect_lt v1.83.1-0.20260905120000-deadbeef v1.83.1 +expect_lt v1.83.1-rc.2 v1.83.1-rc.10 + +echo "OpenBao dependency version comparisons passed" From c50f50a50f3400d5bd283b8a6b3bbab768b5e57c Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 16:46:20 -0700 Subject: [PATCH 5/6] fix(openbao-migrations): reject malformed versions Validate SemVer core, prerelease, and build identifiers before applying dependency-floor ordering. Cover malformed inputs and require the verifier's invalid-input exit status. Signed-off-by: Stephanie Baum --- migrations/openbao/scripts/verify-openbao.sh | 40 +++++++++++++++++-- .../tests/verify-openbao-version-test.sh | 21 ++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/migrations/openbao/scripts/verify-openbao.sh b/migrations/openbao/scripts/verify-openbao.sh index e141bbbbe..916a4e7d8 100755 --- a/migrations/openbao/scripts/verify-openbao.sh +++ b/migrations/openbao/scripts/verify-openbao.sh @@ -29,6 +29,27 @@ version_ge() { return value ~ /^[0-9]+$/ } + function valid_core(value, parts, count, i) { + count = split(value, parts, ".") + if (count != 3) return 0 + for (i = 1; i <= count; i++) { + if (!is_numeric(parts[i])) return 0 + if (length(parts[i]) > 1 && substr(parts[i], 1, 1) == "0") return 0 + } + return 1 + } + + function valid_identifiers(value, reject_numeric_leading_zero, parts, count, i) { + if (value == "") return 0 + count = split(value, parts, ".") + for (i = 1; i <= count; i++) { + if (parts[i] == "" || parts[i] !~ /^[0-9A-Za-z-]+$/) return 0 + if (reject_numeric_leading_zero && is_numeric(parts[i]) && \ + length(parts[i]) > 1 && substr(parts[i], 1, 1) == "0") return 0 + } + return 1 + } + function compare_identifier(left, right) { if (is_numeric(left) && is_numeric(right)) { if ((left + 0) > (right + 0)) return 1 @@ -43,8 +64,18 @@ version_ge() { } BEGIN { - sub(/\+.*/, "", current) - sub(/\+.*/, "", required) + current_plus = index(current, "+") + required_plus = index(required, "+") + if (current_plus) { + current_build = substr(current, current_plus + 1) + current = substr(current, 1, current_plus - 1) + if (!valid_identifiers(current_build, 0)) exit 2 + } + if (required_plus) { + required_build = substr(required, required_plus + 1) + required = substr(required, 1, required_plus - 1) + if (!valid_identifiers(required_build, 0)) exit 2 + } current_dash = index(current, "-") required_dash = index(required, "-") @@ -53,9 +84,10 @@ version_ge() { current_pre = current_dash ? substr(current, current_dash + 1) : "" required_pre = required_dash ? substr(required, required_dash + 1) : "" - if (split(current_core, a, ".") != 3 || split(required_core, b, ".") != 3) exit 2 + if (!valid_core(current_core, a) || !valid_core(required_core, b)) exit 2 + if (current_dash && !valid_identifiers(current_pre, 1)) exit 2 + if (required_dash && !valid_identifiers(required_pre, 1)) exit 2 for (i = 1; i <= 3; i++) { - if (!is_numeric(a[i]) || !is_numeric(b[i])) exit 2 av = a[i] + 0 bv = b[i] + 0 if (av > bv) exit 0 diff --git a/migrations/openbao/tests/verify-openbao-version-test.sh b/migrations/openbao/tests/verify-openbao-version-test.sh index cb51a8a16..6f906ed9d 100755 --- a/migrations/openbao/tests/verify-openbao-version-test.sh +++ b/migrations/openbao/tests/verify-openbao-version-test.sh @@ -25,6 +25,20 @@ expect_lt() { fi } +expect_invalid() { + current=$1 + required=$2 + if "$verifier" --version-ge "$current" "$required"; then + status=0 + else + status=$? + fi + if [ "$status" -ne 2 ]; then + echo "expected invalid comparison $current against $required to exit 2, got $status" >&2 + exit 1 + fi +} + expect_ge v1.83.1 v1.83.1 expect_ge v1.83.2-0.20260905120000-deadbeef v1.83.1 expect_ge v0.0.0-20260905120000-deadbeef v0.0.0-20260904120000-feedface @@ -34,4 +48,11 @@ expect_lt v1.83.1-rc.1 v1.83.1 expect_lt v1.83.1-0.20260905120000-deadbeef v1.83.1 expect_lt v1.83.1-rc.2 v1.83.1-rc.10 +expect_invalid v1.83.1- v1.83.1 +expect_invalid v1.83.1+ v1.83.1 +expect_invalid v1.83.1-rc..1 v1.83.1 +expect_invalid v1.83.1+build..1 v1.83.1 +expect_invalid v1.83.1-rc.01 v1.83.1 +expect_invalid v01.83.1 v1.83.1 + echo "OpenBao dependency version comparisons passed" From 89d0dd3b147e4137c59ef844b916592400ee17ce Mon Sep 17 00:00:00 2001 From: Stephanie Baum Date: Sat, 5 Sep 2026 17:44:10 -0700 Subject: [PATCH 6/6] fix(openbao-migrations): preserve Kubernetes client skew Rebuild kubectl v1.36.4 from checksum-pinned official source with the digest-pinned Go 1.26.6 toolchain. This retains supported client skew across Kubernetes v1.35-v1.37 while removing the vulnerable Go 1.26.5 build. Build with vendored dependencies only and verify the release identity, toolchain, and target architecture before copying the binary into the runtime image. Kubernetes remains Apache-2.0 licensed; no NOTICE update is required. Signed-off-by: Stephanie Baum --- .github/workflows/openbao-migrations.yml | 3 + migrations/openbao/Dockerfile | 73 ++++++++++++------- migrations/openbao/README.md | 4 +- migrations/openbao/scripts/build-kubectl.sh | 66 +++++++++++++++++ migrations/openbao/scripts/verify-kubectl.sh | 34 +++++++++ .../tests/verify-kubectl-build-test.sh | 43 +++++++++++ 6 files changed, 193 insertions(+), 30 deletions(-) create mode 100755 migrations/openbao/scripts/build-kubectl.sh create mode 100755 migrations/openbao/scripts/verify-kubectl.sh create mode 100755 migrations/openbao/tests/verify-kubectl-build-test.sh diff --git a/.github/workflows/openbao-migrations.yml b/.github/workflows/openbao-migrations.yml index 40f3a06ea..6c23a8d66 100644 --- a/.github/workflows/openbao-migrations.yml +++ b/.github/workflows/openbao-migrations.yml @@ -46,5 +46,8 @@ jobs: - name: Test dependency version comparisons run: migrations/openbao/tests/verify-openbao-version-test.sh + - name: Test kubectl source-build contract + run: migrations/openbao/tests/verify-kubectl-build-test.sh + - name: Run kv write retry test run: migrations/openbao/tests/kv-write-retry-test.sh diff --git a/migrations/openbao/Dockerfile b/migrations/openbao/Dockerfile index 1aa048221..d79e22b4b 100644 --- a/migrations/openbao/Dockerfile +++ b/migrations/openbao/Dockerfile @@ -1,11 +1,16 @@ ARG ALPINE_VERSION=3.23 ARG GO_IMAGE=golang:1.27.0-alpine3.23@sha256:3747dcba41c8b0db3211fda4db61638b980e17ac5bb3c94460a975a9cfe19395 +ARG KUBECTL_GO_IMAGE=golang:1.26.6-alpine3.23@sha256:e57c41c1d5864341031181b0db34b9a537bb5773eb6428e4e5bdaea0f9135406 ARG BAO_VERSION=2.6.2 ARG BAO_RUNTIME_SHA256=sha256:11fd73a2102cda9c55d5d881a8c3210303146a7ec1e8ac76f526e175c6d24641 ARG BAO_SOURCE_COMMIT=dd9c19c37a878cf4a81b18efb8d6f0599c7da923 ARG BAO_SOURCE_SHA256=a7784550a9db16f24e99d65a18c9b12a433707c79ef4c1f34262d3f48171c7a9 -ARG KUBECTL_VERSION=v1.37.0 +ARG KUBECTL_VERSION=v1.36.4 ARG KUBECTL_GO_VERSION=go1.26.6 +ARG KUBECTL_SOURCE_COMMIT=bb826b1d48562f110659e64e8ec444327433db95 +ARG KUBECTL_SOURCE_SHA256=3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821 +ARG KUBECTL_BUILD_DATE=2026-08-20T03:09:25Z +ARG KUBECTL_SOURCE_DATE_EPOCH=1787195365 FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS bao-builder @@ -45,40 +50,52 @@ RUN apk add --no-cache curl tar && \ -ldflags="-s -w -buildid= -X main.version=${JWKER_BUILD_VERSION} -X main.commit=${JWKER_COMMIT} -X main.date=${JWKER_BUILD_DATE}" \ -o /out/jwker ./cmd/jwker -FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS kubectl-downloader +FROM --platform=$BUILDPLATFORM ${KUBECTL_GO_IMAGE} AS kubectl-builder ARG TARGETARCH ARG KUBECTL_VERSION -ARG KUBECTL_LINUX_AMD64_SHA256=6129359f4e1f3848a5572ccb0b26cf28b8ca08cef38c95a765b2f64a2c961a2f -ARG KUBECTL_LINUX_ARM64_SHA256=922df28df248cc00a9e025f947704f1d1482de64ece54cfe57e61f19eaf1eef3 - -# Verify the official checksum and the reviewed per-architecture checksum. -RUN apk add --no-cache curl && \ - case "${TARGETARCH}" in \ - amd64) KUBECTL_SHA256="${KUBECTL_LINUX_AMD64_SHA256}" ;; \ - arm64) KUBECTL_SHA256="${KUBECTL_LINUX_ARM64_SHA256}" ;; \ - *) echo "Unsupported architecture for kubectl: ${TARGETARCH}" >&2; exit 1 ;; \ - esac && \ - KUBECTL_BASE_URL="https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGETARCH}" && \ - mkdir -p /out && \ - curl -fsSLo /out/kubectl "${KUBECTL_BASE_URL}/kubectl" && \ - curl -fsSLo /tmp/kubectl.sha256 "${KUBECTL_BASE_URL}/kubectl.sha256" && \ - test "$(cat /tmp/kubectl.sha256)" = "${KUBECTL_SHA256}" && \ - printf '%s %s\n' "${KUBECTL_SHA256}" /out/kubectl | sha256sum -c - && \ - chmod +x /out/kubectl - -FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS kubectl-verifier +ARG KUBECTL_GO_VERSION +ARG KUBECTL_SOURCE_COMMIT +ARG KUBECTL_SOURCE_SHA256 +ARG KUBECTL_BUILD_DATE +ARG KUBECTL_SOURCE_DATE_EPOCH + +# Rebuild the 1.36 client with the patched toolchain while preserving coverage +# across the repository's latest-and-N-2 Kubernetes support window. +RUN apk add --no-cache curl +COPY scripts/build-kubectl.sh scripts/verify-kubectl.sh /usr/local/bin/ +RUN KUBECTL_VERSION="${KUBECTL_VERSION}" \ + KUBECTL_GO_VERSION="${KUBECTL_GO_VERSION}" \ + KUBECTL_SOURCE_COMMIT="${KUBECTL_SOURCE_COMMIT}" \ + KUBECTL_SOURCE_SHA256="${KUBECTL_SOURCE_SHA256}" \ + KUBECTL_BUILD_DATE="${KUBECTL_BUILD_DATE}" \ + KUBECTL_SOURCE_DATE_EPOCH="${KUBECTL_SOURCE_DATE_EPOCH}" \ + TARGETARCH="${TARGETARCH}" \ + OUTPUT_DIR=/out \ + /usr/local/bin/build-kubectl.sh && \ + KUBECTL_GO_VERSION="${KUBECTL_GO_VERSION}" \ + TARGETARCH="${TARGETARCH}" \ + /usr/local/bin/verify-kubectl.sh + +FROM alpine:${ALPINE_VERSION} AS kubectl-runtime-verifier +ARG TARGETARCH ARG KUBECTL_VERSION ARG KUBECTL_GO_VERSION +ARG KUBECTL_SOURCE_COMMIT +ARG KUBECTL_BUILD_DATE -COPY --from=kubectl-downloader /out/kubectl /out/kubectl +COPY --from=kubectl-builder /out/kubectl /out/kubectl -# Keep the scanner-relevant toolchain floor explicit and verify the downloaded -# binary is the requested Kubernetes module before it reaches the final image. -RUN go version -m /out/kubectl >/tmp/kubectl.buildinfo && \ - grep -Fq ": ${KUBECTL_GO_VERSION}" /tmp/kubectl.buildinfo && \ - grep -Eq "^[[:space:]]*mod[[:space:]]+k8s.io/kubernetes[[:space:]]+${KUBECTL_VERSION}([[:space:]]|$)" /tmp/kubectl.buildinfo +# Execute each target binary and verify the public client identity before it is +# copied into the final image. +RUN KUBECTL_METADATA="$(/out/kubectl version --client -o json)" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"gitVersion\": \"${KUBECTL_VERSION}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"gitCommit\": \"${KUBECTL_SOURCE_COMMIT}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq '"gitTreeState": "clean"' && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"buildDate\": \"${KUBECTL_BUILD_DATE}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"goVersion\": \"${KUBECTL_GO_VERSION}\"" && \ + printf '%s\n' "${KUBECTL_METADATA}" | grep -Fq "\"platform\": \"linux/${TARGETARCH}\"" FROM openbao/openbao:${BAO_VERSION}@${BAO_RUNTIME_SHA256} ARG TARGETARCH @@ -91,7 +108,7 @@ RUN apk upgrade --no-cache && \ COPY --from=bao-builder --chmod=0555 /out/bao-linux-${TARGETARCH} /usr/bin/bao COPY --from=jwker-builder /out/jwker /usr/local/bin/jwker -COPY --from=kubectl-verifier /out/kubectl /usr/local/bin/kubectl +COPY --from=kubectl-runtime-verifier /out/kubectl /usr/local/bin/kubectl WORKDIR /app diff --git a/migrations/openbao/README.md b/migrations/openbao/README.md index 0a1d97f7e..cdece3625 100644 --- a/migrations/openbao/README.md +++ b/migrations/openbao/README.md @@ -11,7 +11,7 @@ This repository ships: - Numbered shell migrations under `migrations/` that run in order against an OpenBao leader - Helper utilities under `migrations/utils/` - The `jwker` CLI used by the install pipeline to convert Kubernetes JWKS material to PEM -- Reproducible `jwker` source build and checksum-verified official `kubectl` binary copied from build stages +- Reproducible `jwker` and `kubectl` source builds copied from verified build stages - Optional addons under `addons/` (e.g., LLS / TURN secret rotation) - An example Kubernetes Job manifest (`job.yaml`) - A Docker-based integration test for the helper functions (`tests/`) @@ -43,7 +43,7 @@ The shipped `job.yaml` sets a default placeholder value for this variable so the The `Dockerfile` uses the public upstream OpenBao 2.6.2 image as its runtime base. It replaces the upstream `bao` binary with a reproducible build from the matching checksum-pinned source commit. The build pins x/crypto v0.56.0, gRPC v1.83.1, and go-archive v0.3.0, then verifies those dependency floors and the target architecture from the embedded Go build metadata. -The image also builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0 and downloads the official Kubernetes v1.37.0 `kubectl` binary for the target architecture. The build verifies both the published Kubernetes checksum and the pinned per-architecture checksum, plus the embedded Kubernetes module and Go 1.26.6 toolchain. +The image also builds `jwker` v0.2.2 from checksum-pinned source with Go 1.27.0. It rebuilds Kubernetes v1.36.4 `kubectl` from the checksum-pinned official source archive with a digest-pinned Go 1.26.6 toolchain and vendored dependencies. Keeping the 1.36 client preserves `kubectl`'s supported one-minor skew across this repository's Kubernetes latest-and-N-2 support window (1.35 through 1.37). The build verifies the source identity, embedded Go and target metadata, and the executable client's version, commit, build date, and platform. ```bash docker build -t //openbao-migrations: . diff --git a/migrations/openbao/scripts/build-kubectl.sh b/migrations/openbao/scripts/build-kubectl.sh new file mode 100755 index 000000000..dba851c79 --- /dev/null +++ b/migrations/openbao/scripts/build-kubectl.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +kubectl_version=${KUBECTL_VERSION:-v1.36.4} +kubectl_source_commit=${KUBECTL_SOURCE_COMMIT:-bb826b1d48562f110659e64e8ec444327433db95} +kubectl_source_sha256=${KUBECTL_SOURCE_SHA256:-3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821} +kubectl_build_date=${KUBECTL_BUILD_DATE:-2026-08-20T03:09:25Z} +kubectl_source_date_epoch=${KUBECTL_SOURCE_DATE_EPOCH:-1787195365} +target_arch=${TARGETARCH:?TARGETARCH must be set} +output_dir=${OUTPUT_DIR:-/out} +source_url=${KUBECTL_SOURCE_URL:-"https://dl.k8s.io/${kubectl_version}/kubernetes-src.tar.gz"} + +case "$target_arch" in + amd64 | arm64) ;; + *) + echo "unsupported kubectl architecture: $target_arch" >&2 + exit 1 + ;; +esac + +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/nvcf-kubectl-source.XXXXXX") +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT INT TERM + +archive="$work_dir/kubernetes-src.tar.gz" +source_dir="$work_dir/source" +mkdir -p "$source_dir" "$output_dir" + +curl --fail --location --silent --show-error "$source_url" --output "$archive" +printf '%s %s\n' "$kubectl_source_sha256" "$archive" | sha256sum -c - +tar -xzf "$archive" -C "$source_dir" + +version_ldflags="" +for package in k8s.io/client-go/pkg/version k8s.io/component-base/version; do + version_ldflags="$version_ldflags -X ${package}.gitVersion=${kubectl_version}" + version_ldflags="$version_ldflags -X ${package}.gitCommit=${kubectl_source_commit}" + version_ldflags="$version_ldflags -X ${package}.gitTreeState=clean" + version_ldflags="$version_ldflags -X ${package}.buildDate=${kubectl_build_date}" + version_ldflags="$version_ldflags -X ${package}.gitMajor=1" + version_ldflags="$version_ldflags -X ${package}.gitMinor=36" +done + +( + cd "$source_dir" + CGO_ENABLED=0 \ + GOOS=linux \ + GOARCH="$target_arch" \ + GOTOOLCHAIN=local \ + GOPROXY=off \ + SOURCE_DATE_EPOCH="$kubectl_source_date_epoch" \ + go build \ + -mod=vendor \ + -trimpath \ + -buildvcs=false \ + -tags=selinux,notest,grpcnotrace \ + -ldflags="-s -w -buildid=${version_ldflags}" \ + -o "$output_dir/kubectl" \ + ./cmd/kubectl +) + +chmod 0555 "$output_dir/kubectl" diff --git a/migrations/openbao/scripts/verify-kubectl.sh b/migrations/openbao/scripts/verify-kubectl.sh new file mode 100755 index 000000000..a6b03db62 --- /dev/null +++ b/migrations/openbao/scripts/verify-kubectl.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +kubectl=${KUBECTL_BINARY:-/out/kubectl} +kubectl_go_version=${KUBECTL_GO_VERSION:-go1.26.6} +target_arch=${TARGETARCH:?TARGETARCH must be set} +metadata=$(mktemp) +trap 'rm -f "$metadata"' EXIT INT TERM + +go version -m "$kubectl" >"$metadata" + +actual_go_version=$(awk 'NR == 1 { print $2 }' "$metadata") +path=$(awk '$1 == "path" { print $2 }' "$metadata") +goos=$(awk '$1 == "build" && $2 ~ /^GOOS=/ { sub(/^GOOS=/, "", $2); print $2 }' "$metadata") +goarch=$(awk '$1 == "build" && $2 ~ /^GOARCH=/ { sub(/^GOARCH=/, "", $2); print $2 }' "$metadata") +cgo_enabled=$(awk '$1 == "build" && $2 ~ /^CGO_ENABLED=/ { sub(/^CGO_ENABLED=/, "", $2); print $2 }' "$metadata") + +if [ "$actual_go_version" != "$kubectl_go_version" ]; then + echo "$kubectl embeds $actual_go_version; expected $kubectl_go_version" >&2 + exit 1 +fi +if [ "$path" != "k8s.io/kubernetes/cmd/kubectl" ]; then + echo "$kubectl has unexpected module path: $path" >&2 + exit 1 +fi +if [ "$goos" != "linux" ] || [ "$goarch" != "$target_arch" ] || [ "$cgo_enabled" != "0" ]; then + echo "$kubectl has unexpected target metadata: GOOS=$goos GOARCH=$goarch CGO_ENABLED=$cgo_enabled" >&2 + exit 1 +fi + +echo "verified $kubectl: $kubectl_go_version linux/$target_arch" diff --git a/migrations/openbao/tests/verify-kubectl-build-test.sh b/migrations/openbao/tests/verify-kubectl-build-test.sh new file mode 100755 index 000000000..58f0919fd --- /dev/null +++ b/migrations/openbao/tests/verify-kubectl-build-test.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +openbao_dir=$(CDPATH='' cd -- "$script_dir/.." && pwd) +dockerfile="$openbao_dir/Dockerfile" +build_script="$openbao_dir/scripts/build-kubectl.sh" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +grep -Fq \ + 'ARG KUBECTL_GO_IMAGE=golang:1.26.6-alpine3.23@sha256:e57c41c1d5864341031181b0db34b9a537bb5773eb6428e4e5bdaea0f9135406' \ + "$dockerfile" || fail "kubectl builder must pin the reviewed Go 1.26.6 multi-architecture manifest" +grep -Fq 'ARG KUBECTL_VERSION=v1.36.4' "$dockerfile" || \ + fail "kubectl must stay on the Kubernetes 1.36 compatibility line" +grep -Fq \ + 'ARG KUBECTL_SOURCE_SHA256=3c28f11492472df48e658551bf268fd92938b127b0f9dcef7090ac800318c821' \ + "$dockerfile" || fail "kubectl source archive checksum is not pinned" +grep -Fq 'GOPROXY=off' "$build_script" || \ + fail "kubectl build must not resolve dependencies outside the source archive" +grep -Fq -- '-mod=vendor' "$build_script" || \ + fail "kubectl build must use the release archive's vendored dependencies" +if TARGETARCH=ppc64le "$build_script" >/dev/null 2>&1; then + fail "kubectl build accepted an unsupported architecture" +fi + +archive=$(mktemp) +trap 'rm -f "$archive"' EXIT INT TERM +printf 'not Kubernetes source\n' >"$archive" +if TARGETARCH=amd64 \ + KUBECTL_SOURCE_URL="file://$archive" \ + KUBECTL_SOURCE_SHA256=0000000000000000000000000000000000000000000000000000000000000000 \ + "$build_script" >/dev/null 2>&1; then + fail "kubectl build accepted a source archive with the wrong checksum" +fi + +echo "kubectl source-build contract verified"