Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db29384
ci: inspect artifact repositories
joey-huckabee Sep 11, 2026
fae46b4
ci: inspect source package metadata
joey-huckabee Sep 11, 2026
f2578cf
fix: use supported repository metadata fields
joey-huckabee Sep 11, 2026
c84bf5f
ci: inspect approved signing keys
joey-huckabee Sep 11, 2026
cce6e69
feat: resolve complete artifact closure
joey-huckabee Sep 11, 2026
9b13681
fix: trust approved keys in resolution root
joey-huckabee Sep 11, 2026
d7e6603
chore: expose resolver diagnostics
joey-huckabee Sep 11, 2026
608356d
chore: diagnose repository mappings
joey-huckabee Sep 11, 2026
08c4dda
fix: identify modern RPM signatures
joey-huckabee Sep 11, 2026
4f8bbd1
fix: write lock outside resolver output
joey-huckabee Sep 11, 2026
aa97c45
feat: enforce locked offline assembly
joey-huckabee Sep 11, 2026
c56e3a5
docs: define locked artifact operations
joey-huckabee Sep 11, 2026
998ec67
fix: finalize verified runtime root
joey-huckabee Sep 11, 2026
aa4efe4
fix: enforce approved artifact origins
joey-huckabee Sep 11, 2026
5c4cbb6
test: diagnose final image hygiene
joey-huckabee Sep 11, 2026
18f55ac
fix: remove runtime repository metadata
joey-huckabee Sep 11, 2026
0fb9e04
test: inspect build provenance hygiene
joey-huckabee Sep 11, 2026
fb3e06f
fix: export provenance as OCI evidence
joey-huckabee Sep 11, 2026
e76183b
fix: share locked bases with provenance builder
joey-huckabee Sep 11, 2026
9a787e7
fix: capture offline build provenance
joey-huckabee Sep 11, 2026
3114702
fix: minimize retained build metadata
joey-huckabee Sep 11, 2026
c9abf51
docs: record package 2 completion
joey-huckabee Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

/.github/ @joey-huckabee
/Containerfile @joey-huckabee
/artifacts/ @joey-huckabee
/container/ @joey-huckabee
/scripts/ @joey-huckabee
/SECURITY.md @joey-huckabee
/docs/ARTIFACT-ACQUISITION.md @joey-huckabee
/docs/MAINTENANCE.md @joey-huckabee
Expand Down
62 changes: 51 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ jobs:
- name: Run repository checks
run: pre-commit run --all-files --show-diff-on-failure

- name: Run artifact lock negative tests
run: python -m unittest discover -s tests -p 'test_*.py' -v

- name: Validate committed artifact locks
run: |
python scripts/artifacts.py validate-inputs artifacts/lock-inputs.json
python scripts/artifacts.py validate-lock artifacts/locks/amd64.json
python scripts/artifacts.py validate-lock artifacts/locks/arm64.json

- name: Audit GitHub Actions security
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
with:
Expand Down Expand Up @@ -89,6 +98,7 @@ jobs:
env:
TEST_IMAGE: localhost/postgresql-ubi9:test-${{ matrix.architecture }}
SBOM_FILE: postgresql-ubi9-${{ matrix.architecture }}.spdx.json
PROVENANCE_FILE: provenance-${{ matrix.architecture }}.json
GRYPE_SARIF: grype-${{ matrix.architecture }}.sarif
GRYPE_ALL: grype-all-${{ matrix.architecture }}.json
steps:
Expand All @@ -99,23 +109,46 @@ jobs:

- name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
with:
driver: docker

- name: Confirm native runner architecture
env:
EXPECTED_MACHINE: ${{ matrix.machine }}
run: test "$(uname -m)" = "${EXPECTED_MACHINE}"

- name: Build test image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: Containerfile
platforms: ${{ matrix.platform }}
load: true
push: false
tags: ${{ env.TEST_IMAGE }}
cache-from: type=gha,scope=image-${{ matrix.architecture }}
cache-to: type=gha,mode=max,scope=image-${{ matrix.architecture }}
- name: Acquire, verify, and build from the locked bundle
env:
BUILD_METADATA_FILE: ${{ env.PROVENANCE_FILE }}
BUILDX_METADATA_PROVENANCE: min
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash scripts/build-offline.sh

- name: Prove acquisition material is absent from the final image
run: |
if docker history --no-trunc --format '{{.CreatedBy}}' "${TEST_IMAGE}" | grep -E 'https?://|RPM-GPG-KEY|/tmp/artifacts'; then
echo 'forbidden acquisition material found in final image history' >&2
exit 1
fi
test -s "${PROVENANCE_FILE}"
if grep -E 'download[.]postgresql[.]org|dnf-srpms[.]postgresql[.]org|cdn-ubi[.]redhat[.]com|security[.]access[.]redhat[.]com|RPM-GPG-KEY|[.]artifact-bundle|/tmp/artifacts' "${PROVENANCE_FILE}"; then
echo 'forbidden acquisition material found in build provenance' >&2
exit 1
fi
docker run --rm --user 0 --entrypoint /bin/sh "${TEST_IMAGE}" -ec '
for path in /etc/dnf /etc/pki/entitlement /etc/pki/rpm-gpg /etc/rhsm /etc/yum.repos.d /var/cache/dnf /var/cache/yum; do
if test -e "${path}"; then
echo "forbidden final-image path: ${path}" >&2
exit 1
fi
done
if find / -xdev -type f \( -name "*RPM-GPG-KEY*" -o -name "*.repo" -o -perm /6000 -o -perm -0002 \) -print -quit | grep -q .; then
echo "forbidden key, repository, privilege, or writable file found" >&2
find / -xdev -type f \( -name "*RPM-GPG-KEY*" -o -name "*.repo" -o -perm /6000 -o -perm -0002 \) -print >&2
exit 1
fi
'

- name: Confirm loaded image architecture
env:
Expand Down Expand Up @@ -149,6 +182,12 @@ jobs:
upload-artifact: false
upload-release-assets: false

- name: Prove acquisition material is absent from the SBOM
run: >-
test -z "$(grep -E
'download[.]postgresql[.]org|dnf-srpms[.]postgresql[.]org|cdn-ubi[.]redhat[.]com|security[.]access[.]redhat[.]com|RPM-GPG-KEY|[.]artifact-bundle|/tmp/artifacts'
"${SBOM_FILE}" || true)"

- name: Scan Syft SBOM with Grype
uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2
with:
Expand Down Expand Up @@ -181,6 +220,7 @@ jobs:
name: image-security-${{ github.sha }}-${{ matrix.architecture }}
path: |
${{ env.SBOM_FILE }}
${{ env.PROVENANCE_FILE }}
${{ env.GRYPE_SARIF }}
${{ env.GRYPE_ALL }}
if-no-files-found: warn
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/update-locks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Propose artifact lock update

on:
workflow_dispatch:

permissions:
contents: read

jobs:
resolve:
name: resolve candidate (${{ matrix.architecture }})
strategy:
fail-fast: false
matrix:
include:
- architecture: amd64
runner: ubuntu-24.04
platform: linux/amd64
- architecture: arm64
runner: ubuntu-24.04-arm
platform: linux/arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Validate reviewed lock inputs
run: python scripts/artifacts.py validate-inputs artifacts/lock-inputs.json

- name: Acquire PostgreSQL seeds and approved keys
env:
ARCHITECTURE: ${{ matrix.architecture }}
run: >-
python scripts/artifacts.py acquire-inputs
--inputs artifacts/lock-inputs.json
--architecture "${ARCHITECTURE}"
--output resolution-inputs

- name: Resolve and inspect the complete RPM closure
env:
ARCHITECTURE: ${{ matrix.architecture }}
PLATFORM: ${{ matrix.platform }}
UBI_MINIMAL: registry.access.redhat.com/ubi9/ubi-minimal:9.8@sha256:7fbeae18dc9476399f565e68255f602a3374ea8614ba3d14843565131a13ff93
run: >-
docker run --rm --platform "${PLATFORM}"
--volume "${PWD}:/work" --workdir /work
"${UBI_MINIMAL}" bash scripts/resolve-lock.sh
"${ARCHITECTURE}" /work/resolution-inputs /work/resolution-output

- name: Render and validate candidate lock
env:
ARCHITECTURE: ${{ matrix.architecture }}
run: |
python scripts/render-lock.py \
--inputs artifacts/lock-inputs.json \
--architecture "${ARCHITECTURE}" \
--binary-inventory resolution-output/binary-inventory.tsv \
--source-inventory resolution-output/source-inventory.tsv \
--output "${ARCHITECTURE}.lock.json"
python scripts/artifacts.py validate-lock "${ARCHITECTURE}.lock.json"

- name: Retain candidate for maintainer review
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: artifact-lock-candidate-${{ matrix.architecture }}
path: |
${{ matrix.architecture }}.lock.json
resolution-output/binary-inventory.tsv
resolution-output/source-inventory.tsv
if-no-files-found: error
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
*.sarif
*.spdx.json
*.sbom.json
provenance-*.json
*.oci.tar
cosign.bundle

# Local tooling
.venv/
__pycache__/
.pytest_cache/
.artifact-bundle/

# Local database and secret material
data/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ but container releases use the upstream-derived format documented in
withdrawal handling, and accountable ownership.
- Added the release qualification evidence schema and a public-badge policy
that prohibits unsupported security, platform, or certification claims.
- Added schema-defined AMD64 and ARM64 artifact locks for the complete runtime
RPM and source closure, fail-closed acquisition and publisher verification,
a manual lock-update workflow, and network-disabled offline assembly.
117 changes: 46 additions & 71 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,68 @@
ARG UBI_MINIMAL_IMAGE="registry.access.redhat.com/ubi9/ubi-minimal:9.8@sha256:7fbeae18dc9476399f565e68255f602a3374ea8614ba3d14843565131a13ff93"
ARG UBI_MICRO_IMAGE="registry.access.redhat.com/ubi9/ubi-micro:9.8@sha256:f332c99eb8f798a8486821c91937f10ad64ee83d7e739303be2df051040918f6"

FROM scratch AS pgdg-artifacts-amd64

ADD --checksum=sha256:ae57ba32d87fa3c311da9545bc85682ae04dc41cfbabf60d7f6c185099604f8a \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-x86_64/postgresql18-18.6-1PGDG.rhel9.8.x86_64.rpm /postgresql18.rpm
ADD --checksum=sha256:7a4d55c02b8bab1b359aa25ce53d81db77cd39026a980dd8f0210031a5c31654 \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-x86_64/postgresql18-libs-18.6-1PGDG.rhel9.8.x86_64.rpm /postgresql18-libs.rpm
ADD --checksum=sha256:f7f1915d63756f6a37f3f2e5cc84d03893d0d7a55dab8ac350871bd1c48a0754 \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-x86_64/postgresql18-server-18.6-1PGDG.rhel9.8.x86_64.rpm /postgresql18-server.rpm

FROM scratch AS pgdg-artifacts-arm64

ADD --checksum=sha256:3ec399a4d57b43cbba03f610adc4a4c6daaea0dc8b4f3d73175b1dedf7e2bddf \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-aarch64/postgresql18-18.6-1PGDG.rhel9.8.aarch64.rpm /postgresql18.rpm
ADD --checksum=sha256:662bac810d50ece9d32f7ab6960f01e8e1416cc0a0634a4ea7f7ec8ea744146b \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-aarch64/postgresql18-libs-18.6-1PGDG.rhel9.8.aarch64.rpm /postgresql18-libs.rpm
ADD --checksum=sha256:761001b6e560041f2f6f0d7abe9e57b30a98a2c039b81682d77969928a366add \
https://download.postgresql.org/pub/repos/yum/18/redhat/rhel-9-aarch64/postgresql18-server-18.6-1PGDG.rhel9.8.aarch64.rpm /postgresql18-server.rpm

ARG TARGETARCH
# The selected scratch stage contains checksum-pinned artifacts only.
# hadolint ignore=DL3006
FROM pgdg-artifacts-${TARGETARCH} AS pgdg-artifacts

FROM scratch AS pgdg-key

ADD --checksum=sha256:a70c9527426017d00fa4e6f9d2941d515357a27a7be82e155248ece53bbe5453 \
https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-RHEL /PGDG-RPM-GPG-KEY-RHEL
ADD --checksum=sha256:cc506fa92aa97e8e58f88551a2ec99a61d9d603f7f2c1ae0c06191f58c29979f \
https://download.postgresql.org/pub/repos/yum/keys/PGDG-RPM-GPG-KEY-AARCH64-RHEL /PGDG-RPM-GPG-KEY-AARCH64-RHEL
FROM ${UBI_MICRO_IMAGE} AS runtime-base

FROM ${UBI_MINIMAL_IMAGE} AS builder

COPY --from=pgdg-artifacts / /tmp/pgdg/
COPY --from=pgdg-key /PGDG-RPM-GPG-KEY-RHEL /PGDG-RPM-GPG-KEY-AARCH64-RHEL /tmp/pgdg/

# PostgreSQL artifacts and their signing key are checksum-pinned above. RPM
# signatures are then verified before DNF resolves only their UBI dependencies.
# The first release must also lock that dependency closure and assemble without
# network access as defined in docs/ROADMAP.md.
# hadolint ignore=DL3041
RUN microdnf install -y dnf \
&& rpm --import \
/tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \
/tmp/pgdg/PGDG-RPM-GPG-KEY-AARCH64-RHEL \
&& rpm --checksig /tmp/pgdg/*.rpm \
ARG TARGETARCH
ARG ARTIFACT_LOCK_SHA256
COPY .artifact-bundle/${TARGETARCH}/ /tmp/artifacts/
COPY --chmod=0755 scripts/verify-rpm-bundle.sh /usr/local/bin/verify-rpm-bundle
COPY --from=runtime-base / /final/
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# The bundle is acquired and hash/fingerprint verified before this build. The
# build rechecks its selected lock, exact RPM metadata, and signatures without
# contacting repositories or resolving dependencies.
RUN test -n "${ARTIFACT_LOCK_SHA256}" \
&& test "$(cat /tmp/artifacts/LOCK-SHA256)" = "${ARTIFACT_LOCK_SHA256}" \
&& verify-rpm-bundle /tmp/artifacts \
&& mkdir -p /runtime \
&& rpm --root /runtime --initdb \
&& rpm --root /runtime --import \
/tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \
/tmp/pgdg/PGDG-RPM-GPG-KEY-AARCH64-RHEL \
&& dnf install -y \
--installroot=/runtime \
--releasever=9 \
--setopt=localpkg_gpgcheck=1 \
--setopt=install_weak_deps=0 \
--setopt=keepcache=0 \
/tmp/pgdg/postgresql18.rpm \
/tmp/pgdg/postgresql18-libs.rpm \
/tmp/pgdg/postgresql18-server.rpm \
ca-certificates nss_wrapper tzdata \
&& dnf clean all \
&& microdnf clean all \
&& rpm --root /runtime --import /tmp/artifacts/keys/* \
&& rpm --root /runtime --install /tmp/artifacts/rpms/*.rpm \
&& rpm --root /runtime --query --all \
--qf '%{NAME}-%{VERSION}-%{RELEASE}\n' \
| sed -n '/^gpg-pubkey-/p' > /tmp/imported-keys \
&& while IFS= read -r key; do \
rpm --root /runtime --erase "${key}"; \
done < /tmp/imported-keys \
&& cp -a /runtime/. /final/ \
&& rm -rf \
/runtime/run/* \
/runtime/tmp/* \
/runtime/var/cache/dnf \
/runtime/var/log/* \
/runtime/var/tmp/* \
&& mkdir -p /runtime/var/lib/pgsql \
&& chown -R 26:0 /runtime/var/lib/pgsql \
&& chmod 2775 /runtime/var/lib/pgsql

FROM ${UBI_MICRO_IMAGE}
/final/etc/dnf \
/final/etc/pki/entitlement \
/final/etc/pki/rpm-gpg \
/final/etc/rhsm \
/final/etc/yum.repos.d \
/final/run/* \
/final/tmp/* \
/final/var/cache/dnf \
/final/var/cache/yum \
/final/var/lib/rhsm \
/final/var/log/* \
/final/var/tmp/* \
&& find /final -xdev -type f -perm /6000 -exec chmod a-s {} + \
&& ! find /final -xdev -type f -perm -0002 -print -quit | grep -q . \
&& mkdir -p /final/var/lib/pgsql \
&& chown -R 26:0 /final/var/lib/pgsql \
&& chmod 2775 /final/var/lib/pgsql

FROM scratch

ARG POSTGRESQL_VERSION="18.6"
ARG POSTGRESQL_RPM_VERSION="18.6-1PGDG.rhel9.8"
ARG ARTIFACT_LOCK_SHA256

LABEL org.opencontainers.image.title="PostgreSQL on Red Hat UBI 9" \
org.opencontainers.image.description="A security-oriented, rootless PostgreSQL image built on Red Hat UBI 9 Micro" \
org.opencontainers.image.source="https://github.com/datopsis/postgresql-ubi" \
org.opencontainers.image.documentation="https://github.com/datopsis/postgresql-ubi#readme" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.vendor="Datopsis" \
org.opencontainers.image.version="${POSTGRESQL_VERSION}" \
io.datopsis.postgresql.rpm-version="${POSTGRESQL_RPM_VERSION}"
io.datopsis.postgresql.rpm-version="${POSTGRESQL_RPM_VERSION}" \
io.datopsis.artifact-lock.sha256="${ARTIFACT_LOCK_SHA256}"

COPY --from=builder /runtime/ /
COPY --from=builder /final/ /
COPY --chown=0:0 --chmod=0755 container/entrypoint.sh /usr/local/bin/postgresql-entrypoint

ENV LANG="C.UTF-8" \
Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ The current development image provides:
- stateful smoke tests for initialization, authentication, persistence,
shutdown, arbitrary UIDs, and incompatible data directories.

The current builder still resolves the locked package version from public
repositories. Network-disabled assembly from a checked artifact lock remains a
first-release gate; see [the roadmap](docs/ROADMAP.md) and
[package-source decision](docs/PACKAGE-SOURCE.md).
Architecture-specific locks now cover the complete binary dependency closure,
publisher keys, source RPMs, and digest-pinned UBI inputs. Artifact acquisition
is separate from a network-disabled, pull-disabled container build; see the
[artifact acquisition contract](docs/ARTIFACT-ACQUISITION.md).

## Approved first-release boundary

Expand All @@ -63,18 +63,24 @@ the roadmap explicitly records later qualification. See the complete
Build and run the restricted-runtime test suite with rootless Podman:

```console
podman build --format docker --file Containerfile \
--tag localhost/postgresql-ubi9:development .
CONTAINER_RUNTIME=podman IMAGE=localhost/postgresql-ubi9:development \
CONTAINER_RUNTIME=podman IMAGE=localhost/postgresql-ubi:development \
bash scripts/build-offline.sh
CONTAINER_RUNTIME=podman IMAGE=localhost/postgresql-ubi:development \
bash tests/smoke.sh
```

The preparation command requires Python 3, GnuPG, and rootless Podman. It
downloads only the current native architecture's committed artifacts, verifies
their exact bytes and publisher identities, pre-pulls the locked bases, and
then assembles with networking and further image pulls disabled. Remove the
ignored `.artifact-bundle/` directory before intentionally reacquiring it.

Start the Compose development service by supplying its secret from the host
environment:

```console
export POSTGRES_PASSWORD='replace-with-a-development-secret'
podman compose up --build
podman compose up
```

The service listens only on `127.0.0.1:5432`. Remove the development volume
Expand Down
Loading
Loading