From bd1f07b6763355fcb116d9bd3de68d38f8583301 Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:10:17 -0500 Subject: [PATCH 1/6] feat: add PostgreSQL UBI development image --- .dockerignore | 14 ++ .editorconfig | 17 +++ .gitattributes | 11 ++ .github/CODEOWNERS | 6 + .github/ISSUE_TEMPLATE/bug.yml | 66 +++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/dependabot.yml | 34 +++++ .github/pull_request_template.md | 21 +++ .github/requirements/pre-commit.txt | 20 +++ .github/workflows/ci.yml | 206 ++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 47 +++++++ .github/workflows/scorecard.yml | 51 +++++++ .gitignore | 5 + .pre-commit-config.yaml | 55 ++++++++ AGENTS.md | 11 ++ CHANGELOG.md | 9 +- CLAUDE.md | 39 ++++++ CONTRIBUTING.md | 22 +++ Containerfile | 76 ++++++++++ LICENSE | 1 - README.md | 67 ++++++++- SECURITY.md | 30 ++++ THIRD_PARTY_NOTICES.md | 13 ++ compose.yaml | 30 ++++ container/entrypoint.sh | 94 +++++++++++++ docs/ARTIFACT-ACQUISITION.md | 73 ++++++++++ docs/CI.md | 34 +++++ docs/PACKAGE-SOURCE.md | 42 ++++++ docs/REPOSITORY-GOVERNANCE.md | 32 +++++ docs/ROADMAP.md | 35 +++++ docs/SUPPORT.md | 16 +++ tests/smoke.sh | 168 +++++++++++++++++++++++ 32 files changed, 1350 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/requirements/pre-commit.txt create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/scorecard.yml create mode 100644 .pre-commit-config.yaml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 Containerfile create mode 100644 SECURITY.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 compose.yaml create mode 100644 container/entrypoint.sh create mode 100644 docs/ARTIFACT-ACQUISITION.md create mode 100644 docs/CI.md create mode 100644 docs/PACKAGE-SOURCE.md create mode 100644 docs/REPOSITORY-GOVERNANCE.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/SUPPORT.md create mode 100644 tests/smoke.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..db54a33 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.pre-commit-cache +.venv +build +dist +docs +security-results +tests +*.key +*.pem +*.sarif +*.spdx.json +grype*.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..17b9f12 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab + +[*.{json,md,yaml,yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2a72d17 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf + +*.gif binary +*.ico binary +*.jpg binary +*.jpeg binary +*.png binary +*.pdf binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cd0028b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +* @joey-huckabee + +/.github/ @joey-huckabee +/Containerfile @joey-huckabee +/container/ @joey-huckabee +/tests/ @joey-huckabee diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..6a5a0ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,66 @@ +name: Bug report +description: Report reproducible image, runtime, or documentation behavior +title: "bug: " +labels: + - bug +body: + - type: markdown + attributes: + value: >- + Do not disclose vulnerabilities here. Use the private security link + shown before opening an issue. + - type: input + id: image + attributes: + label: Image tag and digest + description: Include both when available; do not report only a mutable tag. + placeholder: ghcr.io/datopsis/postgresql-ubi:v18.6-ubi9-r20260910.1@sha256:... + validations: + required: true + - type: input + id: runtime + attributes: + label: Runtime, host, and versions + placeholder: RHEL and Podman, Docker, or OpenShift versions + validations: + required: true + - type: dropdown + id: architecture + attributes: + label: Architecture + options: + - linux/amd64 + - linux/arm64 + - Other or unknown + validations: + required: true + - type: textarea + id: configuration + attributes: + label: Minimal PostgreSQL and deployment configuration + description: Remove credentials, keys, internal names, and other secrets. + render: text + validations: + required: true + - type: textarea + id: behavior + attributes: + label: Reproduction and observed behavior + description: List exact steps, the result, and the expected result. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant logs + description: Redact secrets and personal or internal information. + render: text + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched existing issues and tested the current development revision or a supported release. + required: true + - label: I removed secrets and sensitive data from this report. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e46a152 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Report a vulnerability privately + url: https://github.com/datopsis/postgresql-ubi/security/advisories/new + about: Never disclose a suspected vulnerability in a public issue. + - name: PostgreSQL upstream + url: https://www.postgresql.org/support/ + about: Use upstream channels for behavior that reproduces outside this image. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9192c6e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,34 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: pre-commit + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + pre-commit-hooks: + patterns: + - "*" + + - package-ecosystem: pip + directory: /.github/requirements + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + ci-python-tools: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..cd254d7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## Summary + +Describe the user-facing or operational outcome and why the change is needed. + +## Validation + +- [ ] I ran the relevant local checks from `docs/CI.md`. +- [ ] I reviewed logs, warnings, annotations, skipped steps, and retained + evidence rather than relying only on green status checks. +- [ ] I added or updated tests for behavior changes. +- [ ] I updated user and operator documentation where needed. +- [ ] I recorded notable completed work in `CHANGELOG.md` and removed it from + the forward-looking roadmap where applicable. +- [ ] I did not weaken a security or release control without documenting the + threat, rationale, compensating control, owner, and expiry. + +## Security and release impact + +State whether this changes image contents, runtime behavior, supported scope, +or release evidence. If it changes a published artifact, identify the required +version action under `docs/VERSION.md`. List accepted findings or write `None`. diff --git a/.github/requirements/pre-commit.txt b/.github/requirements/pre-commit.txt new file mode 100644 index 0000000..a0292e8 --- /dev/null +++ b/.github/requirements/pre-commit.txt @@ -0,0 +1,20 @@ +cfgv==3.5.0 \ + --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 +distlib==0.4.3 \ + --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b +filelock==3.32.5 \ + --hash=sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2 +identify==2.6.19 \ + --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a +nodeenv==1.10.0 \ + --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 +platformdirs==4.11.7 \ + --hash=sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6 +pre-commit==4.6.2 \ + --hash=sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e +python-discovery==1.6.0 \ + --hash=sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a +PyYAML==6.0.3 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 +virtualenv==21.7.8 \ + --hash=sha256:3040eb3cbf5d32b10ffd57d167e6a162237ad82ba7d8cf1400a1efed593d85ac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1115f7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,206 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + schedule: + - cron: "23 7 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: lint + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + - name: Install pre-commit + run: >- + python -m pip install --require-hashes --only-binary=:all: + --disable-pip-version-check + --requirement .github/requirements/pre-commit.txt + + - name: Run repository checks + run: pre-commit run --all-files --show-diff-on-failure + + - name: Audit GitHub Actions security + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + with: + advanced-security: false + annotations: true + min-severity: medium + + configuration-security: + name: configuration security + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Scan repository configuration + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: config + scan-ref: . + format: table + exit-code: "1" + severity: CRITICAL,HIGH + + image: + name: image (${{ matrix.architecture }}) + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + platform: linux/amd64 + machine: x86_64 + - architecture: arm64 + runner: ubuntu-24.04-arm + platform: linux/arm64 + machine: aarch64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + permissions: + contents: read + security-events: write + env: + TEST_IMAGE: localhost/postgresql-ubi9:test-${{ matrix.architecture }} + SBOM_FILE: postgresql-ubi9-${{ matrix.architecture }}.spdx.json + GRYPE_SARIF: grype-${{ matrix.architecture }}.sarif + GRYPE_ALL: grype-all-${{ matrix.architecture }}.json + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - 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: Confirm loaded image architecture + env: + EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} + run: >- + test "$(docker image inspect --format '{{.Architecture}}' "${TEST_IMAGE}")" + = "${EXPECTED_ARCHITECTURE}" + + - name: Run restricted-runtime smoke tests + env: + CONTAINER_RUNTIME: docker + IMAGE: ${{ env.TEST_IMAGE }} + run: bash tests/smoke.sh + + - name: Scan image with Trivy + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.TEST_IMAGE }} + format: table + exit-code: "1" + ignore-unfixed: true + severity: CRITICAL,HIGH + + - name: Generate SPDX SBOM with Syft + uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + image: ${{ env.TEST_IMAGE }} + format: spdx-json + output-file: ${{ env.SBOM_FILE }} + syft-version: v1.51.1 + upload-artifact: false + upload-release-assets: false + + - name: Scan Syft SBOM with Grype + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: ${{ env.SBOM_FILE }} + output-format: sarif + output-file: ${{ env.GRYPE_SARIF }} + severity-cutoff: high + only-fixed: true + fail-build: true + cache-db: true + grype-version: v0.118.0 + + - name: Record all Grype findings + if: ${{ always() && hashFiles(env.SBOM_FILE) != '' }} + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: ${{ env.SBOM_FILE }} + output-format: json + output-file: ${{ env.GRYPE_ALL }} + severity-cutoff: negligible + only-fixed: false + fail-build: false + cache-db: true + grype-version: v0.118.0 + + - name: Retain image security artifacts + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: image-security-${{ github.sha }}-${{ matrix.architecture }} + path: | + ${{ env.SBOM_FILE }} + ${{ env.GRYPE_SARIF }} + ${{ env.GRYPE_ALL }} + if-no-files-found: warn + retention-days: 14 + + - name: Publish Grype findings to code scanning + if: ${{ always() && github.event_name != 'pull_request' && hashFiles(env.GRYPE_SARIF) != '' }} + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + sarif_file: ${{ env.GRYPE_SARIF }} + category: grype-image-${{ matrix.architecture }} + + image-result: + name: image + if: ${{ always() }} + needs: image + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every native image job + env: + MATRIX_RESULT: ${{ needs.image.result }} + run: test "${MATRIX_RESULT}" = success diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3ba1e4f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: CodeQL + +on: + pull_request: + paths: + - ".github/workflows/**" + push: + branches: + - main + paths: + - ".github/workflows/**" + schedule: + - cron: "11 6 * * 3" + workflow_dispatch: + +permissions: read-all + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze-actions: + name: Analyze GitHub Actions + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: actions + build-mode: none + queries: security-extended + + - name: Analyze GitHub Actions + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + category: /language:actions diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..3dec7e0 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,51 @@ +name: OpenSSF Scorecard + +on: + branch_protection_rule: + push: + branches: + - main + schedule: + - cron: "37 8 * * 6" + workflow_dispatch: + +permissions: read-all + +concurrency: + group: scorecard-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + security-events: write + id-token: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run Scorecard analysis + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + file_mode: git + + - name: Upload SARIF artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openssf-scorecard-sarif + path: results.sarif + retention-days: 5 + + - name: Upload results to code scanning + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 9fbbc6e..1c271fd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ cosign.bundle __pycache__/ .pytest_cache/ +# Local database and secret material +data/ +secrets/ +*.key +*.pem diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0062f64 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +minimum_pre_commit_version: "4.0.0" +default_install_hook_types: + - pre-commit + - commit-msg + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 + hooks: + - id: check-added-large-files + args: + - --maxkb=1024 + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: destroyed-symlinks + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + args: + - --fix=lf + - id: trailing-whitespace + args: + - --markdown-linebreak-ext=md + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: 745eface02aef23e168a8afb6b5737818efbea95 # v0.11.0.1 + hooks: + - id: shellcheck + + - repo: https://github.com/shenxianpeng/hadolint-pre-commit + rev: 6b998985ab8b766ed8104e4fcfeb0a4765f4c364 # v2.15.1.2 + hooks: + - id: hadolint + files: (^|/)(Containerfile|Dockerfile[^/]*)$ + args: + - --failure-threshold + - warning + + - repo: https://github.com/rhysd/actionlint + rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12 + hooks: + - id: actionlint + + - repo: local + hooks: + - id: forbid-coauthor-trailers + name: forbid co-author attribution trailers + language: pygrep + entry: "(?im)^Co-Authored-By:" + stages: + - commit-msg diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..83e9498 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# Agent guidance + +Follow the repository guidance in `CLAUDE.md`, including its security, +verification, documentation, and Git conventions. + +Do not weaken authentication, durable-storage safeguards, the rootless runtime, +read-only-root compatibility, pinned-input policy, vulnerability gates, or +signed-release process merely to make a test or release pass. + +Never add `Co-Authored-By`, AI, assistant, or tool-attribution trailers to +commits. Tool attribution belongs in tool logs, not Git history. diff --git a/CHANGELOG.md b/CHANGELOG.md index b9edd11..d277a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,4 +14,11 @@ but container releases use the upstream-derived format documented in - Selected PostgreSQL 18 as the maintained major line and 18.6 as the initial build target on UBI 9. - Adapted the `nginx-ubi` immutable container versioning policy for PostgreSQL. - +- Added the repository governance, contribution, security, issue, dependency, + and pinned local-check foundations used by the `nginx-ubi` project. +- Added least-privilege CI, CodeQL, OpenSSF Scorecard, native AMD64 and ARM64 + image tests, vulnerability scanning, and SPDX SBOM generation. +- Documented the PGDG package-source decision after confirming that public UBI + 9.8 repositories do not expose PostgreSQL 18. +- Added a UBI Micro development image, secure initialization entrypoint, + restricted Compose service, and stateful rootless smoke tests. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e926d41 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# Repository guidance + +## Project overview + +This repository builds a security-oriented, rootless PostgreSQL 18 container +on Red Hat UBI 9 Micro. Preserve these non-negotiable properties: + +- digest-pinned UBI base images and exact PostgreSQL build inputs; +- a package-manager-free final runtime; +- non-root PostgreSQL execution with no privilege transition; +- authenticated network access using SCRAM by default; +- no generated, default, or logged database passwords; +- a persistent, explicitly writable data volume with a read-only root + filesystem; +- no Linux capabilities and `no-new-privileges` in documented deployments; +- graceful shutdown and persistent-data restart tests; +- native AMD64 and ARM64 CI, SBOMs, vulnerability scans, and signed immutable + releases; and +- no support, FIPS, STIG, or platform claims without matching evidence. + +The release rules are in `docs/VERSION.md`. Package-source and forward-looking +work are defined in `docs/PACKAGE-SOURCE.md` and `docs/ROADMAP.md`. + +## Development and verification + +Use Podman for the primary local workflow. For image-affecting work, verify at +least initialization, authenticated SQL, data persistence across replacement, +clean shutdown, non-root execution, read-only-root behavior, zero capabilities, +`no-new-privileges`, and failure without initialization credentials. + +Never commit generated databases, passwords, private keys, SBOMs, SARIF, or +scanner caches. + +## Git conventions + +Keep changes small and reviewable. Use protected `main`, required checks, and +pull requests. Do not force-push or move release tags. Use concise Conventional +Commit subjects. Do not add AI, assistant, tool-attribution, or +`Co-Authored-By` trailers. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b96415c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Contributions are welcome through GitHub pull requests. Report suspected +vulnerabilities through the private process in `SECURITY.md`. + +Before changing the repository, read `CLAUDE.md`, `docs/ROADMAP.md`, +`docs/SUPPORT.md`, and `docs/VERSION.md`. Keep runtime additions minimal, pin +external inputs, and never commit credentials or database contents. Behavior +changes require matching tests, operator guidance, security review, and a +changelog entry. + +Run repository checks with: + +```console +python -m pip install --require-hashes --only-binary=:all: \ + --requirement .github/requirements/pre-commit.txt +pre-commit run --all-files --show-diff-on-failure +``` + +Image changes must also pass the build and smoke commands in `README.md`. +Use concise Conventional Commit subjects and never add AI, assistant, tool, +or `Co-Authored-By` attribution trailers. diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..610ec9c --- /dev/null +++ b/Containerfile @@ -0,0 +1,76 @@ +# syntax=docker/dockerfile:1.7 + +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 ${UBI_MINIMAL_IMAGE} AS builder + +ARG PGDG_REPO_RPM="https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm" +ARG POSTGRESQL_RPM_VERSION="18.6-1PGDG.rhel9.8" + +# This development build resolves an exact PostgreSQL RPM version and its +# dependency closure during the builder stage. The first release must replace +# this with the locked, externally verified, network-disabled assembly defined +# in docs/ROADMAP.md. +# hadolint ignore=DL3041 +RUN microdnf install -y dnf \ + && dnf install -y "${PGDG_REPO_RPM}" \ + && mkdir -p /runtime \ + && dnf install -y \ + --installroot=/runtime \ + --releasever=9 \ + --setopt=install_weak_deps=0 \ + --setopt=keepcache=0 \ + "postgresql18-${POSTGRESQL_RPM_VERSION}" \ + "postgresql18-libs-${POSTGRESQL_RPM_VERSION}" \ + "postgresql18-server-${POSTGRESQL_RPM_VERSION}" \ + ca-certificates nss_wrapper tzdata \ + && dnf clean all \ + && microdnf clean all \ + && rm -rf \ + /runtime/run/* \ + /runtime/tmp/* \ + /runtime/var/cache/dnf \ + /runtime/var/log/* \ + /runtime/var/tmp/* \ + && mkdir -p /runtime/var/lib/pgsql /runtime/run/postgresql \ + && chown -R 26:0 /runtime/var/lib/pgsql /runtime/run/postgresql \ + && chmod 2775 /runtime/var/lib/pgsql /runtime/run/postgresql + +FROM ${UBI_MICRO_IMAGE} + +ARG POSTGRESQL_VERSION="18.6" +ARG POSTGRESQL_RPM_VERSION="18.6-1PGDG.rhel9.8" + +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}" + +COPY --from=builder /runtime/ / +COPY --chown=0:0 --chmod=0755 container/entrypoint.sh /usr/local/bin/postgresql-entrypoint + +ENV LANG="C.UTF-8" \ + TZ="UTC" \ + PATH="/usr/pgsql-18/bin:${PATH}" \ + PGDATA="/var/lib/pgsql/data" \ + POSTGRES_USER="postgres" + +VOLUME ["/var/lib/pgsql"] + +USER 26:0 +WORKDIR /var/lib/pgsql + +EXPOSE 5432 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ + CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=/run/postgresql", "--port=5432"] + +STOPSIGNAL SIGINT + +ENTRYPOINT ["postgresql-entrypoint"] +CMD ["postgres"] diff --git a/LICENSE b/LICENSE index fb6d1d1..db64ec4 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index c2ab200..8f97a3a 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,66 @@ track current PostgreSQL 18 minor releases rather than remain on 18.6. A move to another PostgreSQL major version is a deliberate compatibility and data upgrade decision, not an automatic dependency update. +## Development image + +The current development image provides: + +- a digest-pinned UBI 9 Minimal builder and UBI 9 Micro runtime; +- exact PGDG PostgreSQL 18.6 RPM selection; +- a package-manager-free final image; +- fixed non-root (`26:0`) and OpenShift-style arbitrary-UID execution; +- mandatory initialization credentials and SCRAM-SHA-256 network + authentication; +- a persistent data volume with read-only-root compatibility; +- dropped-capability and `no-new-privileges` operation; and +- 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). + +## Build and test + +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 \ + bash tests/smoke.sh +``` + +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 +``` + +The service listens only on `127.0.0.1:5432`. Remove the development volume +deliberately with `podman compose down --volumes` when its database is no +longer needed. + +## Initialization interface + +The first start of an empty `/var/lib/pgsql` volume requires exactly one of: + +- `POSTGRES_PASSWORD`; or +- `POSTGRES_PASSWORD_FILE`, which is preferred for orchestrator-mounted + secrets. + +`POSTGRES_USER` defaults to `postgres`. `POSTGRES_DB` optionally creates a +database with that name and otherwise defaults to `POSTGRES_USER`. Credentials +are used only for initial database creation; an existing PostgreSQL 18 data +directory starts without them. A data directory from another PostgreSQL major +version is rejected with an actionable error. + +Host network authentication is SCRAM-SHA-256. The development image does not +provide a `trust` escape hatch for remote clients. + ## Images and releases Container releases use immutable annotated tags in this form: @@ -37,10 +97,15 @@ The repository does not initially publish mutable tags such as `latest`, `18`, or `18.6`. Production deployments should pin an immutable OCI digest. See the [complete versioning and release policy](docs/VERSION.md). +## Support status + +No supported image has been released. The current implementation is for +development and evaluation; consult the [support matrix](docs/SUPPORT.md) +before relying on any platform or operational claim. + ## References - [PostgreSQL versioning policy](https://www.postgresql.org/support/versioning/) - [PostgreSQL 18.6 release notes](https://www.postgresql.org/docs/release/18.6/) - [Red Hat Application Streams life cycle](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) - [Red Hat Universal Base Images](https://developers.redhat.com/products/rhel/ubi) - diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1f7da5e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security policy + +## Supported versions + +No supported image has been published. Repository revisions and development +images are available for evaluation but receive no security-support commitment. + +## Reporting a vulnerability + +Do not open a public issue for a suspected vulnerability. Use the repository's +**Security** tab and select **Report a vulnerability**: + + + +Include the image tag and digest when available, architecture, runtime and host +versions, PostgreSQL configuration, reproduction steps, and whether the issue +appears to originate in this packaging, PostgreSQL, PGDG, or UBI. Remove +credentials, private keys, internal hostnames, and database contents. + +Upstream vulnerabilities should follow the applicable upstream process: + +- PostgreSQL: +- Red Hat: + +## Handling and disclosure + +Maintainers will validate scope and coordinate disclosure with suppliers when +appropriate. No response or remediation SLA is promised until the first +supported release defines one. Scanner matches require both PGDG and Red Hat +advisory context; version strings alone are not sufficient triage evidence. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..7c31e11 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,13 @@ +# Third-party software and terms + +This repository's Apache-2.0 license covers Datopsis-authored packaging and +documentation only. The image also contains separately licensed software. + +| Component | Supplier | License and terms | +| --- | --- | --- | +| PostgreSQL | PostgreSQL Global Development Group, packaged by PGDG | [PostgreSQL License](https://www.postgresql.org/about/licence/) and PGDG package metadata | +| Red Hat Universal Base Image | Red Hat | [UBI terms](https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI) | + +Release SBOMs must identify the exact installed package inventory. Before a +release, review bundled license material, source availability, redistribution +terms, and notices for PostgreSQL and every runtime dependency. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..be11f0b --- /dev/null +++ b/compose.yaml @@ -0,0 +1,30 @@ +services: + postgresql: + image: localhost/postgresql-ubi9:development + build: + context: . + dockerfile: Containerfile + environment: + POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password + secrets: + - postgres-password + ports: + - "127.0.0.1:5432:5432" + read_only: true + tmpfs: + - /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 + - /run/postgresql:rw,noexec,nosuid,nodev,size=16m,mode=0775 + volumes: + - postgres-data:/var/lib/pgsql + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + stop_signal: SIGINT + +secrets: + postgres-password: + environment: POSTGRES_PASSWORD + +volumes: + postgres-data: diff --git a/container/entrypoint.sh b/container/entrypoint.sh new file mode 100644 index 0000000..2a0dc08 --- /dev/null +++ b/container/entrypoint.sh @@ -0,0 +1,94 @@ +#!/bin/sh +set -eu + +postgres_major=18 + +fatal() { + printf 'postgresql-entrypoint: %s\n' "$*" >&2 + exit 1 +} + +configure_arbitrary_uid() { + if id -un >/dev/null 2>&1; then + return + fi + + wrapper=/usr/lib64/libnss_wrapper.so + test -r "${wrapper}" || fatal "current UID is unknown and nss_wrapper is unavailable" + + passwd_file=/tmp/postgresql-passwd + cp /etc/passwd "${passwd_file}" + printf 'postgres:x:%s:0:PostgreSQL Server:%s:/sbin/nologin\n' \ + "$(id -u)" "${PGDATA}" >>"${passwd_file}" + export NSS_WRAPPER_PASSWD="${passwd_file}" + export NSS_WRAPPER_GROUP=/etc/group + export LD_PRELOAD="${wrapper}${LD_PRELOAD:+:${LD_PRELOAD}}" +} + +read_initial_password() { + if test -n "${POSTGRES_PASSWORD:-}" && test -n "${POSTGRES_PASSWORD_FILE:-}"; then + fatal "set only one of POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE" + fi + + if test -n "${POSTGRES_PASSWORD_FILE:-}"; then + test -r "${POSTGRES_PASSWORD_FILE}" || \ + fatal "POSTGRES_PASSWORD_FILE is not readable" + POSTGRES_PASSWORD=$(cat "${POSTGRES_PASSWORD_FILE}") + fi + + test -n "${POSTGRES_PASSWORD:-}" || \ + fatal "initialization requires POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE" +} + +initialize_database() { + read_initial_password + umask 077 + password_file="/tmp/postgresql-password.$$" + printf '%s' "${POSTGRES_PASSWORD}" >"${password_file}" + trap 'rm -f "${password_file}"' EXIT HUP INT TERM + + initdb \ + --pgdata="${PGDATA}" \ + --username="${POSTGRES_USER}" \ + --pwfile="${password_file}" \ + --auth-host=scram-sha-256 \ + --auth-local=trust \ + --encoding=UTF8 + + printf "\nlisten_addresses = '*'\nunix_socket_directories = '/run/postgresql'\npassword_encryption = 'scram-sha-256'\n" \ + >>"${PGDATA}/postgresql.conf" + printf '\nhost all all all scram-sha-256\n' >>"${PGDATA}/pg_hba.conf" + + pg_ctl --pgdata="${PGDATA}" \ + --options="-c listen_addresses='' -c unix_socket_directories=/tmp" \ + --wait start + + database=${POSTGRES_DB:-${POSTGRES_USER}} + if test "${database}" != "${POSTGRES_USER}"; then + createdb --host=/tmp --username="${POSTGRES_USER}" -- "${database}" + fi + + pg_ctl --pgdata="${PGDATA}" --mode=fast --wait stop + rm -f "${password_file}" + trap - EXIT HUP INT TERM + unset POSTGRES_PASSWORD +} + +if test "${1:-}" = "postgres"; then + configure_arbitrary_uid + mkdir -p "${PGDATA}" + test -w "${PGDATA}" || fatal "PGDATA is not writable: ${PGDATA}" + + if test ! -s "${PGDATA}/PG_VERSION"; then + if test -n "$(find "${PGDATA}" -mindepth 1 -maxdepth 1 -print -quit)"; then + fatal "PGDATA is non-empty but has no PG_VERSION file" + fi + initialize_database + else + installed_major=$(cat "${PGDATA}/PG_VERSION") + test "${installed_major}" = "${postgres_major}" || \ + fatal "PGDATA major ${installed_major} is incompatible with PostgreSQL ${postgres_major}" + fi +fi + +exec "$@" diff --git a/docs/ARTIFACT-ACQUISITION.md b/docs/ARTIFACT-ACQUISITION.md new file mode 100644 index 0000000..e21d5c6 --- /dev/null +++ b/docs/ARTIFACT-ACQUISITION.md @@ -0,0 +1,73 @@ +# External artifact acquisition + +Status: required for the first release. The development `Containerfile` +currently resolves an exact RPM version during its builder stage and must be +migrated before release. + +## Build contract + +Ordinary pull-request, `main`, and release assembly must not resolve mutable +package metadata or download packages, keys, or repository configuration from +inside the container build. A separate preparation phase acquires and verifies +every input. Assembly then runs with network access and image pulling disabled. + +The pipeline separates: + +1. **Resolution**, performed only by an explicit lock-update operation. +2. **Acquisition**, which downloads only artifacts already named by the lock. +3. **Verification**, which checks publisher identity and exact bytes. +4. **Assembly**, which consumes only the verified local bundle. + +## Required lock contents + +The architecture-specific lock records: + +- each UBI base reference and expected manifest digest; +- the PGDG repository-package identity and checksum; +- every PostgreSQL and UBI RPM NEVRA, byte size, and SHA-256 digest; +- expected architecture and RPM signing fingerprint; +- source-RPM locations and digests; +- artifact-source identifier; and +- lock schema and artifact-bundle version. + +The lock is reviewable repository content. Credentials, tokens, private +endpoints, and private trust material are not. + +## Verification requirements + +Before assembly, CI must: + +- reject missing, additional, duplicate, wrong-architecture, or wrong-version + files; +- verify every artifact's size and SHA-256 digest; +- verify RPM signatures against approved full fingerprints; +- verify package NEVRA and source-RPM correspondence; +- verify base-image platform and manifest digest; +- sanitize acquisition logs; and +- retain the lock digest and verification result with release evidence. + +Checksums prove exact bytes; signatures prove publisher authorization under the +accepted key policy. TLS transport does not replace either check. + +## Network-disabled assembly + +The verified RPM bundle is supplied as an ephemeral build context excluded from +Git. The Containerfile installs it into a temporary root and copies only the +required runtime filesystem into UBI Micro. Assembly must prove: + +- no network access and no image pulling; +- no dependency resolution; +- no repository credentials, configuration, keys, or caches in image layers, + history, labels, SBOM, or provenance; and +- failure for any incomplete or unverified bundle. + +Changing any locked package, base digest, acquisition tool, or verification +policy invalidates earlier release-candidate evidence. + +## Current development limitation + +The current Containerfile downloads the PGDG repository package and resolves +the exact PostgreSQL 18.6 RPM version during the builder stage. This supports +early runtime development but is not release-qualified or reproducible if the +repository removes or changes metadata. No image produced by that path may be +announced as supported. diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..37140ca --- /dev/null +++ b/docs/CI.md @@ -0,0 +1,34 @@ +# Continuous integration + +CI runs repository validation, configuration scanning, native AMD64 and ARM64 +image builds, restricted-runtime smoke tests, Trivy and Grype vulnerability +gates, and Syft SPDX SBOM generation. The aggregate `image` job fails unless +every native image job succeeds. + +## Local repository checks + +```console +python -m pip install --require-hashes --only-binary=:all: \ + --requirement .github/requirements/pre-commit.txt +pre-commit run --all-files --show-diff-on-failure +``` + +## Local image checks + +```console +podman build --format docker --file Containerfile \ + --tag localhost/postgresql-ubi9:development . +CONTAINER_RUNTIME=podman IMAGE=localhost/postgresql-ubi9:development \ + bash tests/smoke.sh +``` + +Local success is development evidence, not native multi-architecture or +release evidence. Review workflow logs, warnings, skipped steps, scanner +results, and retained artifacts rather than relying only on a green aggregate +status. + +## Evidence boundary + +CI artifacts contain exact-commit SBOM and scan results and currently expire +after 14 days. Release evidence requirements, retention, signing, provenance, +and publication remain first-release roadmap work. diff --git a/docs/PACKAGE-SOURCE.md b/docs/PACKAGE-SOURCE.md new file mode 100644 index 0000000..f2fe0f6 --- /dev/null +++ b/docs/PACKAGE-SOURCE.md @@ -0,0 +1,42 @@ +# PostgreSQL package-source decision + +## Decision + +Use the PostgreSQL Global Development Group's official PGDG RPM repository for +PostgreSQL 18 while retaining digest-pinned Red Hat UBI 9 Minimal and Micro base +images. Pin the exact RPM epoch, version, release, architecture, dependency +closure, repository package, checksums, and signing identity. + +An inspection on 2026-09-11 found that the public UBI 9.8 repositories expose +PostgreSQL 13.23 but not the PostgreSQL 18 stream. The PGDG RHEL 9 repository +exposes these PostgreSQL 18.6 packages for x86_64: + +```text +postgresql18-0:18.6-1PGDG.rhel9.8.x86_64 +postgresql18-libs-0:18.6-1PGDG.rhel9.8.x86_64 +postgresql18-server-0:18.6-1PGDG.rhel9.8.x86_64 +``` + +The observed PGDG repository key fingerprint was: + +```text +D4BF 08AE 67A0 B4C7 A1DB CCD2 40BC A2B4 08B4 0D20 +``` + +These are observed development inputs, not a permanent lock or release claim. +The first release requires equivalent availability and native tests on AMD64 +and ARM64 plus an external artifact lock and network-disabled assembly. + +## Supplier boundary + +PostgreSQL and PGDG are the application publisher and packager; Red Hat remains +the base-image and UBI dependency supplier. This combination is not a Red Hat +PostgreSQL support claim. Security review must consider advisories and package +metadata from both suppliers. + +## References + +- +- +- +- diff --git a/docs/REPOSITORY-GOVERNANCE.md b/docs/REPOSITORY-GOVERNANCE.md new file mode 100644 index 0000000..14e94e6 --- /dev/null +++ b/docs/REPOSITORY-GOVERNANCE.md @@ -0,0 +1,32 @@ +# Repository governance + +## Protected development + +The default branch is `main`. Image, workflow, security, and release-sensitive +changes use pull requests, review, and required checks. Workflows default to +read-only permissions and receive narrower write permissions only in the job +that requires them. Third-party Actions are pinned by full commit SHA. + +Before the first release, configure rules that require the lint, +configuration-security, native AMD64/ARM64 image, aggregate image, and CodeQL +checks. Require conversation resolution and block force pushes and deletion. + +## Release tags + +Add a tag ruleset matching `docs/VERSION.md` only after the release workflow +and authorized actors are defined. It must prevent release-tag updates and +deletion. A GitHub Release represents a published container image, never a +source-only documentation revision. + +## Security features + +Keep dependency-graph alerts, Dependabot security updates, secret scanning, +push protection, CodeQL, OpenSSF Scorecard, and private vulnerability reporting +enabled where the organization plan supports them. Security reports use the +private advisory route, not public issues. + +## Ownership + +CODEOWNERS identifies review responsibility but does not replace branch rules. +Runtime, authentication, storage, entrypoint, package-source, workflow, and +release changes require explicit security and operational review. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..a8d76c3 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,35 @@ +# First-release roadmap + +This is the release gate for the first supported `postgresql-ubi` image. + +## Package 1: project contract + +- [x] Define repository, image, versioning, support, contribution, and security + contracts. +- [x] Add pinned repository checks and baseline GitHub automation. +- [x] Select PGDG PostgreSQL 18 RPMs after verifying the public UBI limitation. + +## Package 2: development image + +- [x] Add a digest-pinned UBI Minimal builder and UBI Micro runtime. +- [x] Add a non-root entrypoint with mandatory initialization credentials and + SCRAM network authentication. +- [x] Add a restricted Compose service and stateful smoke tests. +- [ ] Replace build-time repository resolution with an architecture-specific + artifact lock, verified acquisition, and network-disabled assembly. + +## Package 3: release assurance + +- [ ] Qualify native AMD64 and ARM64 builds and runtime tests. +- [ ] Review complete SBOM and Trivy/Grype findings with PGDG and Red Hat + advisory context. +- [ ] Add release-tag validation, keyless signing, provenance, attestations, + immutable GHCR publication, and release verification. +- [ ] Define supported update cadence, supersession, and vulnerability SLAs. + +## Package 4: platform qualification + +- [ ] Qualify a documented rootless Podman and Linux host baseline. +- [ ] Qualify Docker compatibility independently. +- [ ] Test OpenShift restricted-SCC arbitrary-UID operation. +- [ ] Add backup/restore, TLS, logging, storage, and major-upgrade guidance. diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md new file mode 100644 index 0000000..5f57530 --- /dev/null +++ b/docs/SUPPORT.md @@ -0,0 +1,16 @@ +# Support status + +No supported container image has been released. Everything currently in this +repository is development material unless an immutable release and its evidence +are explicitly named by a published support statement. + +| Area | Current status | +| --- | --- | +| PostgreSQL 18 on UBI 9 | Development target | +| Linux AMD64 | Local development evidence only | +| Linux ARM64 | Planned native CI evidence | +| Rootless Podman | Primary planned runtime | +| Docker-compatible runtimes | Planned compatibility testing | +| OpenShift arbitrary UID | Planned; not yet qualified | +| Major-version upgrades | Not implemented or supported | +| Replication and high availability | Outside the first image release boundary | diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100644 index 0000000..7bc8475 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +runtime="${CONTAINER_RUNTIME:-podman}" +image="${IMAGE:-localhost/postgresql-ubi9:development}" +password="smoke-$RANDOM-$$-OnlyForTesting" +prefix="postgresql-ubi9-smoke-${RANDOM}-$$" +primary="${prefix}-primary" +restart="${prefix}-restart" +arbitrary="${prefix}-arbitrary" +missing_password="${prefix}-missing-password" +wrong_major="${prefix}-wrong-major" +primary_volume="${prefix}-primary-data" +arbitrary_volume="${prefix}-arbitrary-data" +missing_volume="${prefix}-missing-data" +no_new_privileges="no-new-privileges:true" + +if grep -qi podman <<<"$("${runtime}" --version 2>&1)"; then + no_new_privileges="no-new-privileges" +fi + +cleanup() { + "${runtime}" rm --force \ + "${primary}" "${restart}" "${arbitrary}" \ + "${missing_password}" "${wrong_major}" >/dev/null 2>&1 || true + "${runtime}" volume rm --force \ + "${primary_volume}" "${arbitrary_volume}" "${missing_volume}" \ + >/dev/null 2>&1 || true +} +trap cleanup EXIT + +run_restricted() { + local name="$1" + local volume="$2" + shift 2 + "${runtime}" run --detach --name "${name}" \ + --read-only \ + --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 \ + --tmpfs /run/postgresql:rw,noexec,nosuid,nodev,size=16m,mode=0775 \ + --mount "type=volume,src=${volume},dst=/var/lib/pgsql" \ + --cap-drop ALL \ + --security-opt "${no_new_privileges}" \ + "$@" "${image}" >/dev/null +} + +wait_for_postgresql() { + local name="$1" + local _ + for _ in {1..60}; do + if "${runtime}" exec "${name}" \ + pg_isready --quiet --host=/run/postgresql --port=5432; then + return + fi + sleep 1 + done + "${runtime}" logs "${name}" >&2 + return 1 +} + +wait_for_failure() { + local name="$1" + local state + local code + local _ + for _ in {1..20}; do + state="$("${runtime}" inspect --format '{{.State.Status}}' "${name}")" + if test "${state}" != running; then + code="$("${runtime}" inspect --format '{{.State.ExitCode}}' "${name}")" + test "${code}" != 0 + return + fi + sleep 1 + done + "${runtime}" logs "${name}" >&2 + return 1 +} + +assert_process_security() { + local name="$1" + # Variables expand inside the container. + # shellcheck disable=SC2016 + "${runtime}" exec "${name}" sh -eu -c ' + for status in /proc/[0-9]*/status; do + uid=$(sed -n "s/^Uid:[[:space:]]*\([0-9]*\).*/\1/p" "${status}") + cap=$(sed -n "s/^CapEff:[[:space:]]*//p" "${status}") + nnp=$(sed -n "s/^NoNewPrivs:[[:space:]]*//p" "${status}") + test "${uid}" -ne 0 + test "${cap}" = 0000000000000000 + test "${nnp}" = 1 + done + ' +} + +sql() { + local name="$1" + local statement="$2" + "${runtime}" exec --env "PGPASSWORD=${password}" "${name}" \ + psql --host=127.0.0.1 --username=postgres \ + --dbname=postgres --tuples-only --no-align --command="${statement}" +} + +test "$("${runtime}" image inspect --format '{{.Config.User}}' "${image}")" = "26:0" + +"${runtime}" volume create "${primary_volume}" >/dev/null +run_restricted "${primary}" "${primary_volume}" \ + --env "POSTGRES_PASSWORD=${password}" +wait_for_postgresql "${primary}" +test "$(sql "${primary}" 'SHOW server_version;')" = "18.6" +test "$(sql "${primary}" 'SHOW password_encryption;')" = "scram-sha-256" +sql "${primary}" \ + 'CREATE TABLE persistence_probe (value text NOT NULL); INSERT INTO persistence_probe VALUES ('"'"'survives'"'"');' \ + >/dev/null +test "$("${runtime}" exec "${primary}" id -u)" = 26 +test "$("${runtime}" exec "${primary}" id -g)" = 0 +assert_process_security "${primary}" +"${runtime}" exec "${primary}" sh -ceu \ + '! command -v dnf; ! command -v microdnf; ! command -v rpm; ! command -v yum' +"${runtime}" exec "${primary}" sh -ceu \ + '! (printf probe > /root-filesystem-probe) 2>/dev/null' +if grep -Fq "${password}" <<<"$("${runtime}" logs "${primary}" 2>&1)"; then + echo "Initialization password was exposed in container logs" >&2 + exit 1 +fi + +"${runtime}" stop --time 30 "${primary}" >/dev/null +test "$("${runtime}" inspect --format '{{.State.ExitCode}}' "${primary}")" = 0 +"${runtime}" rm "${primary}" >/dev/null + +run_restricted "${restart}" "${primary_volume}" +wait_for_postgresql "${restart}" +test "$(sql "${restart}" 'SELECT value FROM persistence_probe;')" = survives +"${runtime}" stop --time 30 "${restart}" >/dev/null +"${runtime}" rm "${restart}" >/dev/null + +"${runtime}" volume create "${arbitrary_volume}" >/dev/null +run_restricted "${arbitrary}" "${arbitrary_volume}" \ + --user 10001:0 --env "POSTGRES_PASSWORD=${password}" +wait_for_postgresql "${arbitrary}" +test "$("${runtime}" exec "${arbitrary}" id -u)" = 10001 +assert_process_security "${arbitrary}" +test "$(sql "${arbitrary}" 'SELECT current_user;')" = postgres +"${runtime}" stop --time 30 "${arbitrary}" >/dev/null +"${runtime}" rm "${arbitrary}" >/dev/null + +"${runtime}" volume create "${missing_volume}" >/dev/null +run_restricted "${missing_password}" "${missing_volume}" +wait_for_failure "${missing_password}" +grep -Fq 'initialization requires POSTGRES_PASSWORD' \ + <<<"$("${runtime}" logs "${missing_password}" 2>&1)" +"${runtime}" rm "${missing_password}" >/dev/null + +"${runtime}" run --detach --name "${wrong_major}" \ + --read-only \ + --mount "type=volume,src=${primary_volume},dst=/var/lib/pgsql" \ + --cap-drop ALL \ + --security-opt "${no_new_privileges}" \ + --entrypoint sh "${image}" \ + -c 'printf 17 > /var/lib/pgsql/data/PG_VERSION' >/dev/null +while test "$("${runtime}" inspect --format '{{.State.Status}}' "${wrong_major}")" = running; do + sleep 1 +done +"${runtime}" rm "${wrong_major}" >/dev/null +run_restricted "${wrong_major}" "${primary_volume}" +wait_for_failure "${wrong_major}" +grep -Fq 'PGDATA major 17 is incompatible with PostgreSQL 18' \ + <<<"$("${runtime}" logs "${wrong_major}" 2>&1)" + +echo "PostgreSQL restricted-runtime scenario tests passed for ${image}" From 7794a6c158184f9017b921b817299722055c7420 Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:18:48 -0500 Subject: [PATCH 2/6] build: restrict PGDG repository selection --- CHANGELOG.md | 2 ++ Containerfile | 1 + docs/PACKAGE-SOURCE.md | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d277a94..b6ba5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,3 +22,5 @@ but container releases use the upstream-derived format documented in 9.8 repositories do not expose PostgreSQL 18. - Added a UBI Micro development image, secure initialization entrypoint, restricted Compose service, and stateful rootless smoke tests. +- Restricted PGDG dependency resolution to the PostgreSQL 18 repository after + native ARM64 exposed inconsistent unused `pgdg-common` metadata. diff --git a/Containerfile b/Containerfile index 610ec9c..4ec8169 100644 --- a/Containerfile +++ b/Containerfile @@ -19,6 +19,7 @@ RUN microdnf install -y dnf \ && dnf install -y \ --installroot=/runtime \ --releasever=9 \ + --disablerepo=pgdg-common,pgdg17,pgdg16,pgdg15,pgdg14 \ --setopt=install_weak_deps=0 \ --setopt=keepcache=0 \ "postgresql18-${POSTGRESQL_RPM_VERSION}" \ diff --git a/docs/PACKAGE-SOURCE.md b/docs/PACKAGE-SOURCE.md index f2fe0f6..429afcb 100644 --- a/docs/PACKAGE-SOURCE.md +++ b/docs/PACKAGE-SOURCE.md @@ -27,6 +27,11 @@ These are observed development inputs, not a permanent lock or release claim. The first release requires equivalent availability and native tests on AMD64 and ARM64 plus an external artifact lock and network-disabled assembly. +The development build enables only the PGDG 18 application repository from the +PGDG set. Older major-version and PGDG common repositories are disabled so they +cannot influence the dependency closure; RPM and repository-metadata signature +checks remain enabled for the selected source. + ## Supplier boundary PostgreSQL and PGDG are the application publisher and packager; Red Hat remains From 9cc328b3bd71dd584314f05d9bc53ea9ac2140f8 Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:32:05 -0500 Subject: [PATCH 3/6] build: pin PostgreSQL artifacts by architecture --- CHANGELOG.md | 6 ++-- Containerfile | 61 +++++++++++++++++++++++++++--------- compose.yaml | 1 - container/entrypoint.sh | 2 +- docs/ARTIFACT-ACQUISITION.md | 11 ++++--- docs/PACKAGE-SOURCE.md | 10 +++--- tests/smoke.sh | 3 +- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ba5ef..fcc2016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,5 +22,7 @@ but container releases use the upstream-derived format documented in 9.8 repositories do not expose PostgreSQL 18. - Added a UBI Micro development image, secure initialization entrypoint, restricted Compose service, and stateful rootless smoke tests. -- Restricted PGDG dependency resolution to the PostgreSQL 18 repository after - native ARM64 exposed inconsistent unused `pgdg-common` metadata. +- Replaced PGDG repository resolution with checksum-pinned, signature-checked + PostgreSQL RPM artifacts after native ARM64 exposed invalid signed metadata. +- Moved the local PostgreSQL socket to the restricted `/tmp` tmpfs so arbitrary + non-root users behave consistently under Docker and Podman. diff --git a/Containerfile b/Containerfile index 4ec8169..fac1fb0 100644 --- a/Containerfile +++ b/Containerfile @@ -3,28 +3,59 @@ 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 + FROM ${UBI_MINIMAL_IMAGE} AS builder -ARG PGDG_REPO_RPM="https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm" -ARG POSTGRESQL_RPM_VERSION="18.6-1PGDG.rhel9.8" +COPY --from=pgdg-artifacts / /tmp/pgdg/ +COPY --from=pgdg-key /PGDG-RPM-GPG-KEY-RHEL /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL -# This development build resolves an exact PostgreSQL RPM version and its -# dependency closure during the builder stage. The first release must replace -# this with the locked, externally verified, network-disabled assembly defined -# in docs/ROADMAP.md. +# 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 \ - && dnf install -y "${PGDG_REPO_RPM}" \ + && rpm --import /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \ + && rpm --checksig /tmp/pgdg/*.rpm \ && mkdir -p /runtime \ + && rpm --root /runtime --initdb \ + && rpm --root /runtime --import /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \ && dnf install -y \ --installroot=/runtime \ --releasever=9 \ - --disablerepo=pgdg-common,pgdg17,pgdg16,pgdg15,pgdg14 \ + --setopt=localpkg_gpgcheck=1 \ --setopt=install_weak_deps=0 \ --setopt=keepcache=0 \ - "postgresql18-${POSTGRESQL_RPM_VERSION}" \ - "postgresql18-libs-${POSTGRESQL_RPM_VERSION}" \ - "postgresql18-server-${POSTGRESQL_RPM_VERSION}" \ + /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 \ @@ -34,9 +65,9 @@ RUN microdnf install -y dnf \ /runtime/var/cache/dnf \ /runtime/var/log/* \ /runtime/var/tmp/* \ - && mkdir -p /runtime/var/lib/pgsql /runtime/run/postgresql \ - && chown -R 26:0 /runtime/var/lib/pgsql /runtime/run/postgresql \ - && chmod 2775 /runtime/var/lib/pgsql /runtime/run/postgresql + && mkdir -p /runtime/var/lib/pgsql \ + && chown -R 26:0 /runtime/var/lib/pgsql \ + && chmod 2775 /runtime/var/lib/pgsql FROM ${UBI_MICRO_IMAGE} @@ -69,7 +100,7 @@ WORKDIR /var/lib/pgsql EXPOSE 5432 HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ - CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=/run/postgresql", "--port=5432"] + CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=/tmp", "--port=5432"] STOPSIGNAL SIGINT diff --git a/compose.yaml b/compose.yaml index be11f0b..ac2d353 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,7 +13,6 @@ services: read_only: true tmpfs: - /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 - - /run/postgresql:rw,noexec,nosuid,nodev,size=16m,mode=0775 volumes: - postgres-data:/var/lib/pgsql cap_drop: diff --git a/container/entrypoint.sh b/container/entrypoint.sh index 2a0dc08..8432de1 100644 --- a/container/entrypoint.sh +++ b/container/entrypoint.sh @@ -55,7 +55,7 @@ initialize_database() { --auth-local=trust \ --encoding=UTF8 - printf "\nlisten_addresses = '*'\nunix_socket_directories = '/run/postgresql'\npassword_encryption = 'scram-sha-256'\n" \ + printf "\nlisten_addresses = '*'\nunix_socket_directories = '/tmp'\npassword_encryption = 'scram-sha-256'\n" \ >>"${PGDATA}/postgresql.conf" printf '\nhost all all all scram-sha-256\n' >>"${PGDATA}/pg_hba.conf" diff --git a/docs/ARTIFACT-ACQUISITION.md b/docs/ARTIFACT-ACQUISITION.md index e21d5c6..0416ba9 100644 --- a/docs/ARTIFACT-ACQUISITION.md +++ b/docs/ARTIFACT-ACQUISITION.md @@ -66,8 +66,9 @@ policy invalidates earlier release-candidate evidence. ## Current development limitation -The current Containerfile downloads the PGDG repository package and resolves -the exact PostgreSQL 18.6 RPM version during the builder stage. This supports -early runtime development but is not release-qualified or reproducible if the -repository removes or changes metadata. No image produced by that path may be -announced as supported. +The current Containerfile checksum-pins the three PostgreSQL 18.6 RPMs and the +PGDG signing key, then verifies the RPM signatures. The builder still resolves +the UBI dependency closure over the network. This supports early runtime +development but is not release-qualified or fully reproducible if UBI metadata +or packages change. No image produced by that path may be announced as +supported. diff --git a/docs/PACKAGE-SOURCE.md b/docs/PACKAGE-SOURCE.md index 429afcb..70c3c45 100644 --- a/docs/PACKAGE-SOURCE.md +++ b/docs/PACKAGE-SOURCE.md @@ -27,10 +27,12 @@ These are observed development inputs, not a permanent lock or release claim. The first release requires equivalent availability and native tests on AMD64 and ARM64 plus an external artifact lock and network-disabled assembly. -The development build enables only the PGDG 18 application repository from the -PGDG set. Older major-version and PGDG common repositories are disabled so they -cannot influence the dependency closure; RPM and repository-metadata signature -checks remain enabled for the selected source. +The development build downloads the three architecture-specific PostgreSQL RPMs +directly from the PGDG 18 directory. Each URL and SHA-256 digest is explicit in +the Containerfile. The PGDG signing key is also checksum-pinned, its fingerprint +is documented above, and every RPM signature is checked before installation. +This avoids mutable PGDG repository metadata influencing the package selection; +DNF resolves only the remaining dependencies from the UBI repositories. ## Supplier boundary diff --git a/tests/smoke.sh b/tests/smoke.sh index 7bc8475..e411a8e 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -36,7 +36,6 @@ run_restricted() { "${runtime}" run --detach --name "${name}" \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 \ - --tmpfs /run/postgresql:rw,noexec,nosuid,nodev,size=16m,mode=0775 \ --mount "type=volume,src=${volume},dst=/var/lib/pgsql" \ --cap-drop ALL \ --security-opt "${no_new_privileges}" \ @@ -48,7 +47,7 @@ wait_for_postgresql() { local _ for _ in {1..60}; do if "${runtime}" exec "${name}" \ - pg_isready --quiet --host=/run/postgresql --port=5432; then + pg_isready --quiet --host=/tmp --port=5432; then return fi sleep 1 From 871e0e6203e9cf14db33e22c269cea94b3dd8d2b Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:40:50 -0500 Subject: [PATCH 4/6] fix: verify architecture-specific PGDG keys --- CHANGELOG.md | 1 + Containerfile | 14 ++++++++++---- docs/PACKAGE-SOURCE.md | 10 ++++++---- tests/smoke.sh | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc2016..696ae04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,5 +24,6 @@ but container releases use the upstream-derived format documented in restricted Compose service, and stateful rootless smoke tests. - Replaced PGDG repository resolution with checksum-pinned, signature-checked PostgreSQL RPM artifacts after native ARM64 exposed invalid signed metadata. +- Pinned the distinct PGDG ARM64 signing key used by the AArch64 RPM artifacts. - Moved the local PostgreSQL socket to the restricted `/tmp` tmpfs so arbitrary non-root users behave consistently under Docker and Podman. diff --git a/Containerfile b/Containerfile index fac1fb0..a13e460 100644 --- a/Containerfile +++ b/Containerfile @@ -30,11 +30,13 @@ 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_MINIMAL_IMAGE} AS builder COPY --from=pgdg-artifacts / /tmp/pgdg/ -COPY --from=pgdg-key /PGDG-RPM-GPG-KEY-RHEL /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL +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. @@ -42,11 +44,15 @@ COPY --from=pgdg-key /PGDG-RPM-GPG-KEY-RHEL /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL # 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 \ + && rpm --import \ + /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \ + /tmp/pgdg/PGDG-RPM-GPG-KEY-AARCH64-RHEL \ && rpm --checksig /tmp/pgdg/*.rpm \ && mkdir -p /runtime \ && rpm --root /runtime --initdb \ - && rpm --root /runtime --import /tmp/pgdg/PGDG-RPM-GPG-KEY-RHEL \ + && 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 \ @@ -100,7 +106,7 @@ WORKDIR /var/lib/pgsql EXPOSE 5432 HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ - CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=/tmp", "--port=5432"] + CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=127.0.0.1", "--port=5432"] STOPSIGNAL SIGINT diff --git a/docs/PACKAGE-SOURCE.md b/docs/PACKAGE-SOURCE.md index 70c3c45..d8ba2b5 100644 --- a/docs/PACKAGE-SOURCE.md +++ b/docs/PACKAGE-SOURCE.md @@ -17,10 +17,11 @@ postgresql18-libs-0:18.6-1PGDG.rhel9.8.x86_64 postgresql18-server-0:18.6-1PGDG.rhel9.8.x86_64 ``` -The observed PGDG repository key fingerprint was: +The observed PGDG repository key fingerprints were: ```text -D4BF 08AE 67A0 B4C7 A1DB CCD2 40BC A2B4 08B4 0D20 +x86_64: D4BF 08AE 67A0 B4C7 A1DB CCD2 40BC A2B4 08B4 0D20 +aarch64: B031 F89F C983 E982 6290 6B6E 177B 343B B973 8825 ``` These are observed development inputs, not a permanent lock or release claim. @@ -29,8 +30,9 @@ and ARM64 plus an external artifact lock and network-disabled assembly. The development build downloads the three architecture-specific PostgreSQL RPMs directly from the PGDG 18 directory. Each URL and SHA-256 digest is explicit in -the Containerfile. The PGDG signing key is also checksum-pinned, its fingerprint -is documented above, and every RPM signature is checked before installation. +the Containerfile. The architecture-specific PGDG signing keys are also +checksum-pinned, their fingerprints are documented above, and every RPM +signature is checked before installation. This avoids mutable PGDG repository metadata influencing the package selection; DNF resolves only the remaining dependencies from the UBI repositories. diff --git a/tests/smoke.sh b/tests/smoke.sh index e411a8e..988be46 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -47,7 +47,7 @@ wait_for_postgresql() { local _ for _ in {1..60}; do if "${runtime}" exec "${name}" \ - pg_isready --quiet --host=/tmp --port=5432; then + pg_isready --quiet --host=127.0.0.1 --port=5432; then return fi sleep 1 From 8499da2f4a428b6c9ecb82d6e92af05916e80162 Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:47:07 -0500 Subject: [PATCH 5/6] fix: expose startup logs in container runtime --- CHANGELOG.md | 2 ++ container/entrypoint.sh | 2 +- tests/smoke.sh | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 696ae04..fb1368d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,3 +27,5 @@ but container releases use the upstream-derived format documented in - Pinned the distinct PGDG ARM64 signing key used by the AArch64 RPM artifacts. - Moved the local PostgreSQL socket to the restricted `/tmp` tmpfs so arbitrary non-root users behave consistently under Docker and Podman. +- Directed PostgreSQL logs to container output and made readiness probes use the + absolute binary path and loopback TCP for consistent engine behavior. diff --git a/container/entrypoint.sh b/container/entrypoint.sh index 8432de1..29932e4 100644 --- a/container/entrypoint.sh +++ b/container/entrypoint.sh @@ -55,7 +55,7 @@ initialize_database() { --auth-local=trust \ --encoding=UTF8 - printf "\nlisten_addresses = '*'\nunix_socket_directories = '/tmp'\npassword_encryption = 'scram-sha-256'\n" \ + printf "\nlisten_addresses = '*'\nunix_socket_directories = '/tmp'\nlogging_collector = off\npassword_encryption = 'scram-sha-256'\n" \ >>"${PGDATA}/postgresql.conf" printf '\nhost all all all scram-sha-256\n' >>"${PGDATA}/pg_hba.conf" diff --git a/tests/smoke.sh b/tests/smoke.sh index 988be46..5752793 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -47,11 +47,13 @@ wait_for_postgresql() { local _ for _ in {1..60}; do if "${runtime}" exec "${name}" \ - pg_isready --quiet --host=127.0.0.1 --port=5432; then + /usr/pgsql-18/bin/pg_isready \ + --quiet --host=127.0.0.1 --port=5432; then return fi sleep 1 done + "${runtime}" inspect --format '{{json .State}}' "${name}" >&2 "${runtime}" logs "${name}" >&2 return 1 } From 7049ce65b5d6437a590bb18c303e55c479e3c43a Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:52:21 -0500 Subject: [PATCH 6/6] test: use SQL for container readiness --- CHANGELOG.md | 2 +- Containerfile | 2 +- tests/smoke.sh | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1368d..62f81bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,4 +28,4 @@ but container releases use the upstream-derived format documented in - Moved the local PostgreSQL socket to the restricted `/tmp` tmpfs so arbitrary non-root users behave consistently under Docker and Podman. - Directed PostgreSQL logs to container output and made readiness probes use the - absolute binary path and loopback TCP for consistent engine behavior. + absolute client path and a real SQL query for consistent engine behavior. diff --git a/Containerfile b/Containerfile index a13e460..b7166e3 100644 --- a/Containerfile +++ b/Containerfile @@ -106,7 +106,7 @@ WORKDIR /var/lib/pgsql EXPOSE 5432 HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ - CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=127.0.0.1", "--port=5432"] + CMD ["/usr/pgsql-18/bin/psql", "--quiet", "--host=/tmp", "--username=postgres", "--dbname=postgres", "--command=SELECT 1"] STOPSIGNAL SIGINT diff --git a/tests/smoke.sh b/tests/smoke.sh index 5752793..2211a7c 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -46,9 +46,10 @@ wait_for_postgresql() { local name="$1" local _ for _ in {1..60}; do - if "${runtime}" exec "${name}" \ - /usr/pgsql-18/bin/pg_isready \ - --quiet --host=127.0.0.1 --port=5432; then + if "${runtime}" exec --env "PGPASSWORD=${password}" "${name}" \ + /usr/pgsql-18/bin/psql --quiet --host=127.0.0.1 \ + --username=postgres --dbname=postgres --command='SELECT 1' \ + >/dev/null 2>&1; then return fi sleep 1