mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Harden release Docker key embedding cache
This commit is contained in:
parent
2bacf4ec9a
commit
fb6b53268a
6 changed files with 62 additions and 1 deletions
18
.github/workflows/create-release.yml
vendored
18
.github/workflows/create-release.yml
vendored
|
|
@ -288,6 +288,20 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Derive license public key Docker cache key
|
||||
id: license_key_cache
|
||||
env:
|
||||
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
decoded_len="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')"
|
||||
if [ "${decoded_len}" != "32" ]; then
|
||||
echo "PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes." >&2
|
||||
exit 1
|
||||
fi
|
||||
key_sha256="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"
|
||||
echo "sha256=${key_sha256}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build Docker image (verify only)
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
|
|
@ -302,6 +316,7 @@ jobs:
|
|||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ needs.prepare.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
|
|
@ -321,6 +336,7 @@ jobs:
|
|||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache,mode=max
|
||||
build-args: |
|
||||
VERSION=${{ needs.prepare.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
|
|
@ -347,11 +363,13 @@ jobs:
|
|||
PULSE_UPDATE_SIGNING_KEY: ${{ secrets.PULSE_UPDATE_SIGNING_KEY }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
run: |
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"
|
||||
docker build \
|
||||
--target runtime \
|
||||
--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY \
|
||||
--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY \
|
||||
--build-arg VERSION="${{ needs.prepare.outputs.tag }}" \
|
||||
--build-arg PULSE_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256}" \
|
||||
--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}" \
|
||||
-t pulse-helm-smoke:${{ needs.prepare.outputs.version }} \
|
||||
.
|
||||
|
|
|
|||
16
.github/workflows/publish-docker.yml
vendored
16
.github/workflows/publish-docker.yml
vendored
|
|
@ -127,6 +127,20 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Derive license public key Docker cache key
|
||||
id: license_key_cache
|
||||
env:
|
||||
PULSE_LICENSE_PUBLIC_KEY: ${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
decoded_len="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')"
|
||||
if [ "${decoded_len}" != "32" ]; then
|
||||
echo "PULSE_LICENSE_PUBLIC_KEY must decode to 32 bytes." >&2
|
||||
exit 1
|
||||
fi
|
||||
key_sha256="$(printf '%s' "${PULSE_LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')"
|
||||
echo "sha256=${key_sha256}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build and push Pulse server image (multi-arch)
|
||||
id: build_server_image
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
|
|
@ -140,6 +154,7 @@ jobs:
|
|||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
|
|
@ -182,6 +197,7 @@ jobs:
|
|||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/pulse-agent:buildcache
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}
|
||||
PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
|
||||
secrets: |
|
||||
pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}
|
||||
|
|
|
|||
11
Dockerfile
11
Dockerfile
|
|
@ -29,6 +29,7 @@ FROM --platform=linux/amd64 golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc
|
|||
|
||||
ARG BUILD_AGENT
|
||||
ARG VERSION
|
||||
ARG PULSE_LICENSE_PUBLIC_KEY_SHA256
|
||||
ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY
|
||||
WORKDIR /app
|
||||
|
||||
|
|
@ -64,9 +65,19 @@ RUN --mount=type=cache,id=pulse-go-mod,target=/go/pkg/mod \
|
|||
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && \
|
||||
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") && \
|
||||
LICENSE_PUBLIC_KEY="" && \
|
||||
EXPECTED_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256#SHA256:}" && \
|
||||
UPDATE_SIGNING_KEY="" && \
|
||||
UPDATE_PUBLIC_KEYS="" && \
|
||||
if [ -f /run/secrets/pulse_license_public_key ]; then LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"; fi && \
|
||||
if [ -n "${LICENSE_PUBLIC_KEY}" ]; then \
|
||||
LICENSE_PUBLIC_KEY_BYTES="$(printf '%s' "${LICENSE_PUBLIC_KEY}" | base64 -d | wc -c | tr -d ' ')" && \
|
||||
if [ "${LICENSE_PUBLIC_KEY_BYTES}" != "32" ]; then echo "Error: mounted license public key must decode to 32 bytes." >&2; exit 1; fi; \
|
||||
fi && \
|
||||
if [ -n "${EXPECTED_LICENSE_PUBLIC_KEY_SHA256}" ]; then \
|
||||
if [ -z "${LICENSE_PUBLIC_KEY}" ]; then echo "Error: PULSE_LICENSE_PUBLIC_KEY_SHA256 was provided but no license public key was mounted." >&2; exit 1; fi && \
|
||||
ACTUAL_LICENSE_PUBLIC_KEY_SHA256="$(printf '%s' "${LICENSE_PUBLIC_KEY}" | base64 -d | sha256sum | awk '{print $1}')" && \
|
||||
if [ "${ACTUAL_LICENSE_PUBLIC_KEY_SHA256}" != "${EXPECTED_LICENSE_PUBLIC_KEY_SHA256}" ]; then echo "Error: mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256." >&2; exit 1; fi; \
|
||||
fi && \
|
||||
if [ -f /run/secrets/pulse_update_signing_key ]; then UPDATE_SIGNING_KEY="$(tr -d '\r\n' < /run/secrets/pulse_update_signing_key)"; fi && \
|
||||
if [ -n "${UPDATE_SIGNING_KEY}" ]; then UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"; fi && \
|
||||
if [ -n "${PULSE_UPDATE_SIGNING_PUBLIC_KEY:-}" ] && [ -z "${UPDATE_PUBLIC_KEYS}" ]; then echo "Error: PULSE_UPDATE_SIGNING_PUBLIC_KEY was provided but no update signing key was mounted." >&2; exit 1; fi && \
|
||||
|
|
|
|||
|
|
@ -341,7 +341,10 @@ the mounted license public key through `PULSE_LICENSE_PUBLIC_KEY_SHA256` and
|
|||
the `Dockerfile` must verify that fingerprint before embedding the key. A
|
||||
release image build must fail closed if the fingerprint is present but the
|
||||
secret is missing, malformed, or mismatched, so cached no-key binaries cannot
|
||||
be reused for release-grade hosted or self-hosted runtime images.
|
||||
be reused for release-grade hosted or self-hosted runtime images. The matching
|
||||
installability proof lives in `scripts/installtests/build_release_assets_test.go`
|
||||
and `scripts/release_control/release_promotion_policy_test.py`, and both must
|
||||
assert the secret mount and non-secret fingerprint argument together.
|
||||
That same supply-chain boundary also owns the checked-in build roots
|
||||
themselves. `Dockerfile` must pin its Node, Go, and Alpine bases by immutable
|
||||
manifest-list digest so multi-arch release builds do not silently drift onto a
|
||||
|
|
|
|||
|
|
@ -303,10 +303,13 @@ func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) {
|
|||
`COPY scripts/release_ldflags.sh ./scripts/release_ldflags.sh`,
|
||||
`COPY scripts/release_update_key.go ./scripts/release_update_key.go`,
|
||||
`COPY scripts/render_installers.go ./scripts/render_installers.go`,
|
||||
`ARG PULSE_LICENSE_PUBLIC_KEY_SHA256`,
|
||||
`--mount=type=secret,id=pulse_license_public_key,required=false`,
|
||||
`--mount=type=secret,id=pulse_update_signing_key,required=false`,
|
||||
`ARG PULSE_UPDATE_SIGNING_PUBLIC_KEY`,
|
||||
`LICENSE_PUBLIC_KEY="$(tr -d '\r\n' < /run/secrets/pulse_license_public_key)"`,
|
||||
`EXPECTED_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256#SHA256:}"`,
|
||||
`mounted license public key does not match PULSE_LICENSE_PUBLIC_KEY_SHA256.`,
|
||||
`UPDATE_PUBLIC_KEYS="$(go run ./scripts/release_update_key.go public-key --private-key "${UPDATE_SIGNING_KEY}")"`,
|
||||
`mounted update signing key does not match PULSE_UPDATE_SIGNING_PUBLIC_KEY.`,
|
||||
`./scripts/release_ldflags.sh server --version "${VERSION}" --build-time "${BUILD_TIME}" --git-commit "${GIT_COMMIT}"`,
|
||||
|
|
@ -367,6 +370,8 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`provenance: mode=max`,
|
||||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`id: license_key_cache`,
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
|
||||
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
|
||||
|
|
@ -374,6 +379,7 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`DOCKER_BUILDKIT: 1`,
|
||||
`--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY`,
|
||||
`--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY`,
|
||||
`--build-arg PULSE_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256}"`,
|
||||
`--build-arg PULSE_UPDATE_SIGNING_PUBLIC_KEY="${PULSE_UPDATE_SIGNING_PUBLIC_KEY}"`,
|
||||
`id-token: write`,
|
||||
`attestations: write`,
|
||||
|
|
@ -397,6 +403,8 @@ func TestReleaseWorkflowsUseSecretSafeAttestedImageBuilds(t *testing.T) {
|
|||
`provenance: mode=max`,
|
||||
`sbom: true`,
|
||||
`secrets: |`,
|
||||
`id: license_key_cache`,
|
||||
`PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}`,
|
||||
`PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}`,
|
||||
`pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}`,
|
||||
`pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}`,
|
||||
|
|
|
|||
|
|
@ -346,10 +346,13 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn('./scripts/backfill-release-assets.sh --tag "${{ inputs.tag }}" --repo "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn('./scripts/validate-published-release.sh "${{ inputs.tag }}" "${{ github.repository }}"', backfill_workflow)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", backfill_workflow)
|
||||
self.assertIn("id: license_key_cache", content)
|
||||
self.assertIn("PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}", content)
|
||||
self.assertIn("pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}", content)
|
||||
self.assertIn("pulse_update_signing_key=${{ secrets.PULSE_UPDATE_SIGNING_KEY }}", content)
|
||||
self.assertIn("--secret id=pulse_license_public_key,env=PULSE_LICENSE_PUBLIC_KEY", content)
|
||||
self.assertIn("--secret id=pulse_update_signing_key,env=PULSE_UPDATE_SIGNING_KEY", content)
|
||||
self.assertIn('--build-arg PULSE_LICENSE_PUBLIC_KEY_SHA256="${PULSE_LICENSE_PUBLIC_KEY_SHA256}"', content)
|
||||
self.assertNotIn("provenance: false", content)
|
||||
self.assertIn("Derived rollback command:", helper)
|
||||
self.assertIn("./scripts/install.sh --version", helper)
|
||||
|
|
@ -424,6 +427,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
|
|||
self.assertIn("subject-name: docker.io/rcourtman/pulse-agent", publish)
|
||||
self.assertIn("subject-name: ghcr.io/${{ github.repository_owner }}/pulse-agent", publish)
|
||||
self.assertIn("create-storage-record: false", publish)
|
||||
self.assertIn("id: license_key_cache", publish)
|
||||
self.assertIn("PULSE_LICENSE_PUBLIC_KEY_SHA256=${{ steps.license_key_cache.outputs.sha256 }}", publish)
|
||||
self.assertIn("pulse_license_public_key=${{ secrets.PULSE_LICENSE_PUBLIC_KEY }}", publish)
|
||||
self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", publish)
|
||||
self.assertNotIn("provenance: false", publish)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue