From d7f9550089de3e93c7a77b63a49a1d515643f89d Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 29 Oct 2026 16:04:42 +0500 Subject: [PATCH 1/3] Authenticate to the gated pgEdge repositories without leaving credentials in the image --- .github/actions/build-flavor/action.yml | 25 ++++++++++++ .github/workflows/build_images.yaml | 8 ++++ Dockerfile | 52 ++++++++++++++++++++++--- pgedge.docker-bake.hcl | 18 +++++++++ 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index da2271a..c2ff471 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -17,6 +17,12 @@ inputs: Token for the container registry. A composite action has no secrets context, so this has to be passed in by the calling workflow. required: true + repo_client_crt: + description: PEM contents of the client certificate for the gated pgEdge repositories. + required: true + repo_client_key: + description: PEM contents of the matching private key. + required: true dry_run: description: When true, resolve and print the build without pushing required: false @@ -61,6 +67,23 @@ runs: go-version: '1.25' cache-dependency-path: tests/go.sum + # Written to files because bake passes secrets by path. BuildKit mounts + # them into the RUNs that install packages and never writes them to a + # layer, so the published image carries no credentials. + - name: Write the repository client certificate + shell: bash + env: + REPO_CLIENT_CRT_PEM: ${{ inputs.repo_client_crt }} + REPO_CLIENT_KEY_PEM: ${{ inputs.repo_client_key }} + run: | + set -o errexit -o pipefail -o nounset + : "${REPO_CLIENT_CRT_PEM:?is empty or unset}" + : "${REPO_CLIENT_KEY_PEM:?is empty or unset}" + install -d -m 0700 "${RUNNER_TEMP}/pgedge-certs" + install -m 0600 /dev/null "${RUNNER_TEMP}/pgedge-certs/client.key" + printf '%s\n' "$REPO_CLIENT_CRT_PEM" > "${RUNNER_TEMP}/pgedge-certs/client.crt" + printf '%s\n' "$REPO_CLIENT_KEY_PEM" > "${RUNNER_TEMP}/pgedge-certs/client.key" + - name: Build and push by digest id: build shell: bash @@ -99,6 +122,8 @@ runs: "POSTGRES_MAJOR_VERSION=$(jq -r '.postgres_major' <<< "$CELL")" "TARGET=${target}" "TAG=${REPO}" + "REPO_CLIENT_CRT=${RUNNER_TEMP}/pgedge-certs/client.crt" + "REPO_CLIENT_KEY=${RUNNER_TEMP}/pgedge-certs/client.key" ) while IFS= read -r kv; do envs+=("$kv") diff --git a/.github/workflows/build_images.yaml b/.github/workflows/build_images.yaml index eb75275..ce7b9c3 100644 --- a/.github/workflows/build_images.yaml +++ b/.github/workflows/build_images.yaml @@ -132,6 +132,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -190,6 +192,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -246,6 +250,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} @@ -307,6 +313,8 @@ jobs: cell: ${{ toJSON(matrix.cell) }} repo: ${{ inputs.pgedge_image_repo }} registry_token: ${{ secrets.GITHUB_TOKEN }} + repo_client_crt: ${{ secrets.PGEDGE_REPO_CLIENT_CRT }} + repo_client_key: ${{ secrets.PGEDGE_REPO_CLIENT_KEY }} dry_run: ${{ inputs.pgedge_image_dry_run }} no_cache: ${{ inputs.pgedge_image_no_cache }} diff --git a/Dockerfile b/Dockerfile index 056a15f..5b3e432 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,9 @@ useradd -u ${POSTGRES_USER_ID} -m postgres -s /bin/bash dnf install -y epel-release dnf dnf config-manager --set-enabled crb dnf update -y --allowerasing -dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm +# downloads.pgedge.com, not dnf.pgedge.com: the gated repository cannot serve +# the package that configures access to it. +dnf install -y https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm if [[ -n "${PACKAGE_RELEASE_CHANNEL}" ]]; then sed -i "s|release|${PACKAGE_RELEASE_CHANNEL}|g" /etc/yum.repos.d/pgedge.repo fi @@ -51,13 +53,23 @@ ARG POSTGRES_MAJOR_VERSION COPY packagelists/${TARGETARCH}/${POSTGRES_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt # Patch any OS packages (including transitive dependencies pulled in above) # to the latest available errata so the image ships with security fixes. @@ -119,7 +131,9 @@ USER root COPY packagelists/${TARGETARCH}/${PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt dnf update -y dnf clean all @@ -161,7 +183,9 @@ USER root COPY packagelists/${TARGETARCH}/${STANDARD_PACKAGE_LIST_FILE} /usr/share/pgedge/packages.txt -RUN </dev/null 2>&1 || true' EXIT +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y < /usr/share/pgedge/packages.txt # Patch any OS packages (including transitive dependencies pulled in above) # to the latest available errata so the image ships with security fixes. @@ -265,7 +297,9 @@ USER root COPY packagelists/${TARGETARCH}/${COLDFRONT_PACKAGE_LIST_FILE} /usr/share/pgedge/coldfront-packages.txt -RUN </dev/null 2>&1 || true' EXIT +pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force + xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt dnf clean all diff --git a/pgedge.docker-bake.hcl b/pgedge.docker-bake.hcl index 12c81a3..26dadd9 100644 --- a/pgedge.docker-bake.hcl +++ b/pgedge.docker-bake.hcl @@ -63,6 +63,16 @@ variable "TAG" { default = "pgedge" } +variable "REPO_CLIENT_CRT" { + type = string + default = "client.crt" +} + +variable "REPO_CLIENT_KEY" { + type = string + default = "client.key" +} + target "default" { pull = true target = TARGET @@ -78,6 +88,14 @@ target "default" { STANDARD_IMAGE = STANDARD_IMAGE POSTGRES_MAJOR_VERSION = POSTGRES_MAJOR_VERSION } + // The pgEdge repositories authenticate clients with mutual TLS. BuildKit + // mounts these for the RUNs that install packages and never writes them to + // a layer. Point them at your own files with REPO_CLIENT_CRT and + // REPO_CLIENT_KEY. + secret = [ + "id=repo_client_crt,src=${REPO_CLIENT_CRT}", + "id=repo_client_key,src=${REPO_CLIENT_KEY}", + ] platforms = [ "linux/amd64", "linux/arm64", From b0d18aeae851c7d6cd7b9d177322bfa959e4b039 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 29 Oct 2026 16:33:06 +0500 Subject: [PATCH 2/3] Replace an outdated parent's pgedge-release before rewriting the channel --- .github/actions/build-flavor/action.yml | 3 + .gitignore | 5 ++ Dockerfile | 94 +++++++++++++++++++------ 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index c2ff471..8b88711 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -102,6 +102,9 @@ runs: args=( --file pgedge.docker-bake.hcl + # The certificate lives in RUNNER_TEMP, outside the bake file's + # directory, so bake needs the entitlement to read it. + --allow "fs.read=${RUNNER_TEMP}" --set "default.platform=linux/${arch}" --set "default.tags=" --metadata-file metadata.json diff --git a/.gitignore b/.gitignore index 05a3223..1840b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,8 @@ tests/tests # Scratch written by the image build (bake metadata, per-arch digests) digests/ metadata.json + +# Default paths for the gated-repository client certificate in +# pgedge.docker-bake.hcl. Never commit these. +client.crt +client.key diff --git a/Dockerfile b/Dockerfile index 5b3e432..0882d2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,12 +62,24 @@ set -o errexit set -o pipefail set -o nounset -# The pgEdge repositories authenticate clients with mutual TLS. BuildKit -# mounts the secrets for this RUN alone and never writes them to a layer; the -# copies pgedge-subscription-manager installs are removed, and the -# repositories disabled again, before this layer is committed. So the image -# carries no credentials and no repository it cannot reach. +# The pgEdge repositories authenticate clients with mutual TLS. BuildKit mounts +# the secrets for this RUN alone and never writes them to a layer; the copies +# pgedge-subscription-manager installs are removed, and the repositories +# disabled again, before this layer is committed. So the image carries no +# credentials and no repository it cannot reach. trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force xargs dnf install -y < /usr/share/pgedge/packages.txt @@ -140,6 +152,24 @@ set -o errexit set -o pipefail set -o nounset +# The pgEdge repositories authenticate clients with mutual TLS. BuildKit mounts +# the secrets for this RUN alone and never writes them to a layer; the copies +# pgedge-subscription-manager installs are removed, and the repositories +# disabled again, before this layer is committed. So the image carries no +# credentials and no repository it cannot reach. +trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -149,12 +179,6 @@ fi # A delta: re-pinning a package the parent's "dnf update -y" has moved past # would be a downgrade, which dnf refuses. -# The pgEdge repositories authenticate clients with mutual TLS. BuildKit -# mounts the secrets for this RUN alone and never writes them to a layer; the -# copies pgedge-subscription-manager installs are removed, and the -# repositories disabled again, before this layer is committed. So the image -# carries no credentials and no repository it cannot reach. -trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force xargs dnf install -y < /usr/share/pgedge/packages.txt @@ -192,6 +216,24 @@ set -o errexit set -o pipefail set -o nounset +# The pgEdge repositories authenticate clients with mutual TLS. BuildKit mounts +# the secrets for this RUN alone and never writes them to a layer; the copies +# pgedge-subscription-manager installs are removed, and the repositories +# disabled again, before this layer is committed. So the image carries no +# credentials and no repository it cannot reach. +trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -201,12 +243,6 @@ fi # A delta: re-pinning a package the parent's "dnf update -y" has moved past # would be a downgrade, which dnf refuses. -# The pgEdge repositories authenticate clients with mutual TLS. BuildKit -# mounts the secrets for this RUN alone and never writes them to a layer; the -# copies pgedge-subscription-manager installs are removed, and the -# repositories disabled again, before this layer is committed. So the image -# carries no credentials and no repository it cannot reach. -trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force xargs dnf install -y < /usr/share/pgedge/packages.txt @@ -306,6 +342,24 @@ set -o errexit set -o pipefail set -o nounset +# The pgEdge repositories authenticate clients with mutual TLS. BuildKit mounts +# the secrets for this RUN alone and never writes them to a layer; the copies +# pgedge-subscription-manager installs are removed, and the repositories +# disabled again, before this layer is committed. So the image carries no +# credentials and no repository it cannot reach. +trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT + +# A chained stage starts FROM a published parent built before the repositories +# were gated: its pgedge-release predates pgedge-subscription-manager and its +# repository definition is enabled, pointing at a host it can no longer reach. +# Disable those definitions and replace the package first, so neither this +# install nor the channel rewrite below depends on when the parent was built. +dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true +command -v pgedge-subscription-manager >/dev/null 2>&1 \ + || dnf install -y --disablerepo="pgedge*" \ + https://downloads.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + + # The repo config is inherited from the parent image, which may have been built # from a different channel, so re-point it here rather than relying on the base # stage. Idempotent: once rewritten there is no "release" left to match. @@ -315,12 +369,6 @@ fi # Dependencies are dnf's to resolve. No second "dnf update -y": that would move # ColdFront past its pinned NVR. -# The pgEdge repositories authenticate clients with mutual TLS. BuildKit -# mounts the secrets for this RUN alone and never writes them to a layer; the -# copies pgedge-subscription-manager installs are removed, and the -# repositories disabled again, before this layer is committed. So the image -# carries no credentials and no repository it cannot reach. -trap 'rm -f /etc/pki/pgedge/client.crt /etc/pki/pgedge/client.key; dnf config-manager --set-disabled "pgedge*" >/dev/null 2>&1 || true' EXIT pgedge-subscription-manager install /run/secrets/repo_client_crt /run/secrets/repo_client_key --force xargs dnf install -y --setopt=install_weak_deps=False < /usr/share/pgedge/coldfront-packages.txt From f2f1daf3eefe51fad9cb00a602ef3aae3b033b64 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 29 Oct 2026 16:45:15 +0500 Subject: [PATCH 3/3] Scope the bake read entitlement to the certificate directory --- .github/actions/build-flavor/action.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/actions/build-flavor/action.yml b/.github/actions/build-flavor/action.yml index 8b88711..4aacfd0 100644 --- a/.github/actions/build-flavor/action.yml +++ b/.github/actions/build-flavor/action.yml @@ -102,9 +102,10 @@ runs: args=( --file pgedge.docker-bake.hcl - # The certificate lives in RUNNER_TEMP, outside the bake file's - # directory, so bake needs the entitlement to read it. - --allow "fs.read=${RUNNER_TEMP}" + # The certificate lives outside the bake file's directory, so bake + # needs the entitlement to read it. Scoped to the directory the step + # above creates rather than all of RUNNER_TEMP. + --allow "fs.read=${RUNNER_TEMP}/pgedge-certs" --set "default.platform=linux/${arch}" --set "default.tags=" --metadata-file metadata.json