From ce302f4afa25798b0ee68d5d6a8d0985ee9a0c91 Mon Sep 17 00:00:00 2001 From: Egor Merkushev Date: Mon, 13 Jul 2026 03:11:48 +0300 Subject: [PATCH] Pin PostgreSQL in hosted image lock --- .github/workflows/publish-hosted-managed-images.yml | 11 +++++++++++ docs/deployment.md | 3 ++- docs/hosted-managed-operations.md | 5 +++-- scripts/validate_hosted_managed_image_lock.py | 6 +++++- tests/test_hosted_managed_image_lock.py | 7 +++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-hosted-managed-images.yml b/.github/workflows/publish-hosted-managed-images.yml index afb36c4..97fa5a0 100644 --- a/.github/workflows/publish-hosted-managed-images.yml +++ b/.github/workflows/publish-hosted-managed-images.yml @@ -36,6 +36,14 @@ jobs: test -n "${ref}" echo "ref=${ref}" >> "${GITHUB_OUTPUT}" + - name: Resolve digest-pinned PostgreSQL runtime + id: postgres + run: | + docker pull postgres:16-alpine + ref="$(docker image inspect --format '{{index .RepoDigests 0}}' postgres:16-alpine)" + test -n "${ref}" + echo "ref=${ref}" >> "${GITHUB_OUTPUT}" + - name: Publish Platform hosted runtime id: platform uses: docker/build-push-action@v6 @@ -70,6 +78,7 @@ jobs: PLATFORM_DIGEST: ${{ steps.platform.outputs.digest }} INGRESS_DIGEST: ${{ steps.ingress.outputs.digest }} CADDY_BASE_REF: ${{ steps.caddy.outputs.ref }} + POSTGRES_IMAGE_REF: ${{ steps.postgres.outputs.ref }} run: | generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" jq -n \ @@ -78,6 +87,7 @@ jobs: --arg platform_ref "ghcr.io/0al-spec/platform-hosted-managed@${PLATFORM_DIGEST}" \ --arg ingress_ref "ghcr.io/0al-spec/platform-hosted-managed-ingress@${INGRESS_DIGEST}" \ --arg caddy_base_ref "${CADDY_BASE_REF}" \ + --arg postgres_ref "${POSTGRES_IMAGE_REF}" \ '{ artifact_kind: "platform_hosted_managed_image_lock", schema_version: 1, @@ -86,6 +96,7 @@ jobs: platforms: ["linux/amd64", "linux/arm64"], images: { platform: {image_ref: $platform_ref}, + postgresql: {image_ref: $postgres_ref}, ingress: { image_ref: $ingress_ref, base_image_ref: $caddy_base_ref, diff --git a/docs/deployment.md b/docs/deployment.md index e7e8756..779c7a8 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -200,7 +200,8 @@ by default, or service/worker network authority expands. Production images are published separately by the manual `publish-hosted-managed-images.yml` workflow. Consume only refs from its validated `platform_hosted_managed_image_lock`; workflow commit tags are build -discovery aids and are not deployment inputs. +discovery aids and are not deployment inputs. The same lock also pins the +third-party PostgreSQL runtime image used by the production Compose profile. ## Local Compose Entry Point diff --git a/docs/hosted-managed-operations.md b/docs/hosted-managed-operations.md index 6ac86d8..11019bb 100644 --- a/docs/hosted-managed-operations.md +++ b/docs/hosted-managed-operations.md @@ -323,9 +323,10 @@ the downloaded lock before using either ref: hosted-managed-image-lock.json ``` -Set `PLATFORM_MANAGED_OPERATION_IMAGE` and +Set `PLATFORM_MANAGED_OPERATION_IMAGE`, +`PLATFORM_MANAGED_OPERATION_POSTGRES_IMAGE`, and `PLATFORM_MANAGED_OPERATION_INGRESS_IMAGE` from the validated `image_ref` -values, not from the workflow's commit tags. The lock is public-safe evidence; +values, not from mutable tags. The lock is public-safe evidence; it does not deploy, change the allowlist, or grant managed-operation authority. `deploy/hosted-managed/production.env.example` contains the complete non-secret diff --git a/scripts/validate_hosted_managed_image_lock.py b/scripts/validate_hosted_managed_image_lock.py index 2bdad48..b10356d 100644 --- a/scripts/validate_hosted_managed_image_lock.py +++ b/scripts/validate_hosted_managed_image_lock.py @@ -13,6 +13,7 @@ EXPECTED_PLATFORMS = ["linux/amd64", "linux/arm64"] EXPECTED_IMAGES = { "platform": "ghcr.io/0al-spec/platform-hosted-managed", + "postgresql": "postgres", "ingress": "ghcr.io/0al-spec/platform-hosted-managed-ingress", } DIGEST_REF = re.compile(r"^(?P[^@]+)@sha256:(?P[0-9a-f]{64})$") @@ -72,7 +73,10 @@ def validate_image_lock(payload: dict[str, Any]) -> list[str]: diagnostics.append(f"{label}_image_invalid") continue match = DIGEST_REF.fullmatch(str(image.get("image_ref") or "")) - if match is None or match.group("name") != expected_name: + names = {expected_name} + if label == "postgresql": + names.add("docker.io/library/postgres") + if match is None or match.group("name") not in names: diagnostics.append(f"{label}_image_ref_invalid") ingress = images.get("ingress") ingress = ingress if isinstance(ingress, dict) else {} diff --git a/tests/test_hosted_managed_image_lock.py b/tests/test_hosted_managed_image_lock.py index 6f92013..f9def3f 100644 --- a/tests/test_hosted_managed_image_lock.py +++ b/tests/test_hosted_managed_image_lock.py @@ -25,6 +25,7 @@ def image_lock() -> dict: "ghcr.io/0al-spec/platform-hosted-managed@sha256:" + digest ) }, + "postgresql": {"image_ref": "postgres@sha256:" + digest}, "ingress": { "image_ref": ( "ghcr.io/0al-spec/platform-hosted-managed-ingress@sha256:" + digest @@ -60,6 +61,12 @@ def test_lock_rejects_mutable_image_and_authority_expansion(self) -> None: self.assertIn("platform_image_ref_invalid", diagnostics) self.assertIn("authority_boundary_expanded", diagnostics) + def test_lock_requires_digest_pinned_postgresql(self) -> None: + payload = image_lock() + payload["images"]["postgresql"]["image_ref"] = "postgres:16-alpine" + diagnostics = validator.validate_image_lock(payload) + self.assertIn("postgresql_image_ref_invalid", diagnostics) + def test_publish_workflow_is_manual_multi_arch_and_attested(self) -> None: workflow_path = ( REPO_ROOT / ".github" / "workflows" / "publish-hosted-managed-images.yml"