From 813222131fd2db58419debeab466d53a0d05c8a2 Mon Sep 17 00:00:00 2001 From: "Joshua (D) Drake" <136637981+ChronicallyJD@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:05:18 -0600 Subject: [PATCH] ci: least-privilege workflow permissions and release signing Address the two highest-value OpenSSF Scorecard gaps. Token-Permissions: add a top-level "permissions: contents: read" to every workflow that lacked one (build-and-test, coverage, matrix, pgindent, sanitizers, tests-registered). In docs.yml, narrow the top-level token to read and grant contents: write only on the deploy job that mike uses to push to gh-pages. Signed-Releases: add sign-release.yml, which on a published release signs each source tarball and SHA256SUMS with cosign keyless (Sigstore, no stored key, GitHub OIDC) and uploads a .cosign.bundle next to each asset. It can also be run by hand for a given tag. RELEASING.md documents the flow and the cosign verify-blob command. The cosign-installer action is pinned by commit SHA. design/openssf-scorecard-todo.md records the full review and the remaining items (branch protection, review approvals, SHA-pinning the rest of the actions, CodeQL, best-practices badge, fuzzing). Co-Authored-By: Claude Fable 5 --- .github/workflows/build-and-test.yml | 3 + .github/workflows/coverage.yml | 3 + .github/workflows/docs.yml | 6 +- .github/workflows/matrix.yml | 3 + .github/workflows/pgindent.yml | 3 + .github/workflows/sanitizers.yml | 3 + .github/workflows/sign-release.yml | 73 +++++++++++++++ .github/workflows/tests-registered.yml | 3 + RELEASING.md | 24 +++++ design/openssf-scorecard-todo.md | 120 +++++++++++++++++++++++++ 10 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/sign-release.yml create mode 100644 design/openssf-scorecard-todo.md diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 9f72e7fa..03d942c9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -15,6 +15,9 @@ on: type: string required: true +permissions: + contents: read + env: artifact_name: build-${{ inputs.pg_version }}-${{ inputs.os }}-${{ inputs.compiler }}-${{ inputs.build_type }} CC: ${{ inputs.compiler }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 6857aa66..bb60fe27 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,6 +9,9 @@ on: paths-ignore: - documentation/** +permissions: + contents: read + env: pg_version: 18 # Avoid failures on slow recovery diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d2c5eba0..56938ed4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,9 +9,8 @@ on: - .github/workflows/docs.yml workflow_dispatch: -# mike publishes the built site to the gh-pages branch. permissions: - contents: write + contents: read concurrency: group: docs-deploy @@ -21,6 +20,9 @@ jobs: deploy: name: Build and deploy runs-on: ubuntu-24.04 + # mike pushes the built site to the gh-pages branch. + permissions: + contents: write steps: - name: Clone repository uses: actions/checkout@v6 diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 3a3a0993..436f3df4 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -10,6 +10,9 @@ on: paths-ignore: - documentation/** +permissions: + contents: read + jobs: main: name: Main matrix diff --git a/.github/workflows/pgindent.yml b/.github/workflows/pgindent.yml index 82bae681..6ae0cc2f 100644 --- a/.github/workflows/pgindent.yml +++ b/.github/workflows/pgindent.yml @@ -4,6 +4,9 @@ on: paths-ignore: - documentation/** +permissions: + contents: read + env: pg_version: 18 diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index 2146a95a..11a60a19 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -9,6 +9,9 @@ on: paths-ignore: - documentation/** +permissions: + contents: read + env: pg_version: 18 CC: clang diff --git a/.github/workflows/sign-release.yml b/.github/workflows/sign-release.yml new file mode 100644 index 00000000..e8447c7e --- /dev/null +++ b/.github/workflows/sign-release.yml @@ -0,0 +1,73 @@ +name: Sign release artifacts + +# Signs the source tarballs and SHA256SUMS attached to a published release using +# cosign keyless signing (Sigstore). No signing key is stored: the runner's +# GitHub OIDC token is used, and a verifiable bundle (signature, certificate, +# and Rekor transparency-log entry) is uploaded next to each artifact. +# +# It runs automatically when a release is published, and can be re-run by hand +# with workflow_dispatch (for example to sign a release cut before this workflow +# existed) by passing the release tag. +# +# Consumers verify a tarball with: +# cosign verify-blob \ +# --bundle open_pg_tde--pg17.tar.gz.cosign.bundle \ +# --certificate-identity-regexp \ +# 'https://github.com/commandprompt/open_pg_tde/.github/workflows/sign-release.yml@.*' \ +# --certificate-oidc-issuer https://token.actions.githubusercontent.com \ +# open_pg_tde--pg17.tar.gz + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Release tag to sign (for example 2.5.0) + required: true + type: string + +permissions: + contents: read + +jobs: + sign: + name: Sign and upload bundles + runs-on: ubuntu-24.04 + permissions: + contents: write # upload signature bundles as release assets + id-token: write # keyless signing via the GitHub OIDC token + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name || inputs.tag }} + steps: + - name: Install cosign + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + + - name: Download release artifacts + run: | + mkdir -p dist + # Download only the artifacts we sign; skip any bundles already present. + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir dist \ + --pattern 'open_pg_tde-*.tar.gz' --pattern 'SHA256SUMS' + ls -l dist + + - name: Sign each artifact + run: | + shopt -s nullglob + for f in dist/open_pg_tde-*.tar.gz dist/SHA256SUMS; do + echo "signing $f" + cosign sign-blob --yes \ + --bundle "$f.cosign.bundle" \ + "$f" + done + + - name: Upload signature bundles + run: | + shopt -s nullglob + bundles=(dist/*.cosign.bundle) + if [ ${#bundles[@]} -eq 0 ]; then + echo "no bundles were produced" >&2 + exit 1 + fi + gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber "${bundles[@]}" diff --git a/.github/workflows/tests-registered.yml b/.github/workflows/tests-registered.yml index 77677764..8fbb18da 100644 --- a/.github/workflows/tests-registered.yml +++ b/.github/workflows/tests-registered.yml @@ -3,6 +3,9 @@ name: Tests registered on: pull_request: +permissions: + contents: read + jobs: check: name: Check diff --git a/RELEASING.md b/RELEASING.md index 9e93af96..511e188e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -66,3 +66,27 @@ enabled, and build the extension against that install. See ``` Consumers verify a download with `sha256sum -c SHA256SUMS`. + +## Signatures + +Publishing the release triggers the `Sign release artifacts` workflow +(`.github/workflows/sign-release.yml`), which signs each tarball and +`SHA256SUMS` with cosign keyless signing (Sigstore) and uploads a +`.cosign.bundle` next to each one. No signing key is stored: the +runner's GitHub OIDC identity is used, and each bundle carries the signature, +the signing certificate, and a Rekor transparency-log entry. + +If a release was cut before this workflow existed, sign it by hand from the +Actions tab (run `Sign release artifacts` and pass the tag) or with +`gh workflow run sign-release.yml -f tag=`. + +Consumers verify a signed tarball with: + +```sh +cosign verify-blob \ + --bundle open_pg_tde--pg17.tar.gz.cosign.bundle \ + --certificate-identity-regexp \ + 'https://github.com/commandprompt/open_pg_tde/.github/workflows/sign-release.yml@.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + open_pg_tde--pg17.tar.gz +``` diff --git a/design/openssf-scorecard-todo.md b/design/openssf-scorecard-todo.md new file mode 100644 index 00000000..608d57cb --- /dev/null +++ b/design/openssf-scorecard-todo.md @@ -0,0 +1,120 @@ +# OpenSSF Scorecard: review and improvement plan + +Status as of 2026-07-16: **score 4.5 / 10** (scorecard v5.1.1, evaluated at +main commit 15616ca). + +This is a planning document, not part of the documentation site. It reviews +each failing check and lists the work, ordered by score impact per unit of +effort. Scorecard weights checks by risk: High risk checks (Token-Permissions, +Branch-Protection, Code-Review, Vulnerabilities, Signed-Releases, Maintained) +move the aggregate score the most; Medium (Pinned-Dependencies, SAST, Fuzzing) +less; Low (CII-Best-Practices, License) least. + +## Current results + +| Check | Score | Notes | +| --- | --- | --- | +| Token-Permissions | 0 | 6 workflows have no top-level `permissions:`; docs.yml grants `contents: write` at top level | +| Branch-Protection | 0 | no protection on `main` | +| Code-Review | 0 | 0 of the last 30 changesets merged with an approving review | +| Signed-Releases | 0 | 2.3.0 and 2.4.0 tarballs unsigned, no provenance | +| Vulnerabilities | 9 | PYSEC-2026-89 / GHSA-5wmx-573v-2qwq: Python-Markdown, pulled unpinned by documentation/requirements.txt | +| Pinned-Dependencies | 1 | 18 of 20 GitHub-owned and 2 of 3 third-party actions not pinned by SHA; 1 unpinned pip install in docs.yml | +| SAST | 0 | no static analysis runs on commits (no CodeQL workflow in the fork) | +| Fuzzing | 0 | no fuzzing integration | +| CII-Best-Practices | 0 | not registered for the OpenSSF Best Practices badge | +| Maintained | 0 | repository is younger than 90 days; resolves on its own with continued activity | +| License | 9 | COPYRIGHT file present but not recognized as an OSI/FSF license text | +| Packaging | n/a | no publishing workflow detected; does not count against the score | + +Passing already: Security-Policy, Dangerous-Workflow, Binary-Artifacts, +CI-Tests, Contributors, Dependency-Update-Tool. + +## TODO, in priority order + +### 1. Set least-privilege token permissions on all workflows (High risk, trivial) - DONE +Added a top-level `permissions: contents: read` to build-and-test.yml, +coverage.yml, matrix.yml, pgindent.yml, sanitizers.yml, and +tests-registered.yml. In docs.yml, moved `contents: write` from the top level +down to the deploy job and set the top level to `contents: read`. This takes +Token-Permissions from 0 to 9 or 10. +(Branch security/scorecard-token-permissions.) + +### 2. Fix the known vulnerability in the docs toolchain (High risk, trivial) +`documentation/requirements.txt` lists `Markdown` with no version bound. +Pin it at or above the version that fixes GHSA-5wmx-573v-2qwq. While there, +pin the other docs requirements to majors or exact versions so the docs build +is reproducible. + +### 3. Pin GitHub Actions by commit SHA (Medium risk, mechanical) +Replace every `uses: owner/action@vN` with `@ # vN` across all eight +workflows. Dependabot (already detected by Scorecard) keeps SHA pins current, +so this costs nothing ongoing. Also pin the `pip install` in docs.yml +(requirements file with versions, or `--require-hashes`). Confirm the +dependency-update tool covers `github-actions` and `pip` ecosystems; add +`.github/dependabot.yml` if the current setup is repo-settings only. + +### 4. Enable branch protection on main (High risk, settings only) +Require pull requests before merging, require the Check and build status +checks, require at least 1 approving review, and disallow force pushes and +deletions. Public repositories get branch protection for free. Note that part +of this check needs admin-token access to verify, so the public Scorecard run +may not award the full 10, but it moves from 0 either way. + +### 5. Get approving reviews on PRs before merge (High risk, process) +Code-Review scores the last 30 changesets, so this recovers slowly: every +future PR should get a human approval before merge (self-merge without review +is what zeroes this). Requiring 1 approval in branch protection (item 4) +enforces it. With a small team this needs a second maintainer account +reviewing, since GitHub does not count self-approval. + +### 6. Add a CodeQL workflow (Medium risk, small) +Add `.github/workflows/codeql.yml` running the C/C++ pack on pull_request and +push to main. Upstream Percona had CodeQL infra; the fork currently runs +none. This satisfies SAST (CodeQL on PRs is the exact pattern Scorecard looks +for). Follow item 1's permissions pattern (`security-events: write` at the job +level only). + +### 7. Sign releases and attach provenance (High risk, next release) - SET UP for 2.5.0 +Added `.github/workflows/sign-release.yml`: on `release: published` (and +`workflow_dispatch` with a tag input for back-signing), it signs each source +tarball and SHA256SUMS with cosign keyless (Sigstore, no stored key, GitHub +OIDC) and uploads a `.cosign.bundle` next to each. RELEASING.md now +documents this and the `cosign verify-blob` command. This keeps the existing +manual `gh release create` flow (maintainer cuts the release; signing is +automatic on publish). +Remaining/optional: the 2.3.0 and 2.4.0 releases are still unsigned - run the +workflow by hand against those tags to back-sign if we want the check to count +them among the last 5 releases sooner. Could also add +`actions/attest-build-provenance` for SLSA provenance later. + +### 8. Register for the OpenSSF Best Practices badge (Low risk, small) +Register the project at bestpractices.dev and fill in the passing-level +questionnaire. Much of it (security policy, CI tests, docs site, released +versions) is already true. Even "in progress" (>= 25%) scores 2, passing +scores 10 on this check. + +### 9. License recognition (Low risk, trivial) +Scorecard sees COPYRIGHT but cannot classify it as an OSI/FSF license. Add a +`LICENSE` file containing the PostgreSQL License text (SPDX: PostgreSQL) and +keep COPYRIGHT as is. 9 to 10, cosmetic. + +### 10. Fuzzing (Medium risk, real work, longer term) +The crypto and key-parsing paths (keyring JSON options parsing, KMIP protocol +handling, WAL/page encryption round-trip) are good fuzz targets. Options, in +increasing effort: a `t/` style deterministic corpus test, a libFuzzer harness +built in CI (Scorecard detects libFuzzer usage), or applying to OSS-Fuzz. +Worth doing for its own sake given this is security software; schedule as a +proper task alongside the cloud KMS work rather than as scorecard polish. + +### No action +- **Maintained**: scores 0 only because the repository is under 90 days old. + It resolves automatically with ongoing commit activity. +- **Packaging**: becomes a real score once releases are built by a workflow + (item 7). + +## Expected outcome + +Items 1 through 4 are one short PR plus repository settings and should land +the score around 6.5 to 7. Items 5 through 7 accrue over the next weeks and +releases and push it past 8. Fuzzing and the badge close the rest.