fix(packaging/rpm): a release must not publish unsigned RPMs into a gpgcheck=1 repo

Signing was already live and the docs were half right about it. `RPM_GPG_PRIVATE_KEY`
is an ORG-level secret on unom, so it is invisible in this repo's Actions secrets —
which reads exactly like "never set up", and both the rpm.yml step name and
sign-rpms.sh's header still said "dormant". Checked it on the wire instead: a
published punktfunk-web RPM carries an OpenPGP V4 EdDSA header signature from
af245c506f4e4763, the same key committed at packaging/rpm/RPM-GPG-KEY-punktfunk.

The real gap was the failure mode. README.md hands users a repo file with
gpgcheck=1, but sign-rpms.sh exits 0 when the key is missing — so an org secret
that got rotated, renamed, or not inherited would publish an unsigned release into
a repo that rejects unsigned packages, and every user's `dnf upgrade` would break
with us none the wiser. On refs/tags/v* that is now a build failure. Other builds
still fall through unsigned so forks and local builds keep working.

Docs corrected to match: the org-level location (with a wire-level check that
doesn't depend on where the secret lives), the fail-closed rule, and a note that
`rpmkeys --checksig` reporting NOKEY still means signed.

Guard tested locally: exit 1 on refs/tags/v0.21.0, exit 0 on refs/heads/main and
on an unset ref.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 12:12:21 +02:00
co-authored by Claude Opus 5
parent 5db93c1156
commit 7b053a4a39
3 changed files with 33 additions and 7 deletions
+3 -1
View File
@@ -110,7 +110,9 @@ jobs:
# Recommends both). Both need bun (ensured in Prep).
run: PF_VERSION="$PF_VERSION" PF_RELEASE="$PF_RELEASE" PF_WITH_WEB=1 PF_WITH_SCRIPTING=1 bash packaging/rpm/build-rpm.sh
- name: Sign RPMs (dormant until RPM_GPG_PRIVATE_KEY is set — see packaging/rpm/README.md)
# Signs with packages@unom.io (org secret) and self-verifies before publish. On a v* tag a
# missing key FAILS the build rather than publishing unsigned RPMs into a gpgcheck=1 repo.
- name: Sign RPMs
env:
RPM_GPG_PRIVATE_KEY: ${{ secrets.RPM_GPG_PRIVATE_KEY }}
RPM_GPG_PASSPHRASE: ${{ secrets.RPM_GPG_PASSPHRASE }}
+13 -2
View File
@@ -52,6 +52,16 @@ The public key is served from the registry (the `gpgkey=` URL above) and committ
`packaging/rpm/RPM-GPG-KEY-punktfunk`. (This is a GPG/OpenPGP key — a `step-ca`/X.509 cert can't
sign RPMs; step-ca is only for registry/console TLS.)
> `RPM_GPG_PRIVATE_KEY` is an **org-level** secret on `unom`, not a repo secret — it will not show
> up under this repository's Actions secrets. Verify it end to end instead of by its absence there:
> `curl -O <repo-url>/package/punktfunk-web/<ver>/x86_64/…rpm && rpm -qp --qf '%{RSAHEADER:pgpsig}\n'`
> (or `rpmkeys --checksig`, which reports `NOKEY` until you import the public key — `NOKEY` still
> means *signed*, just by a key that box doesn't have yet).
On a `v*` tag build, a missing key **fails** the build: `sign-rpms.sh` will not publish unsigned
RPMs into a repo whose own instructions say `gpgcheck=1`, because every user's `dnf upgrade` would
then break on them. Non-release builds still fall through unsigned so forks and local builds work.
How it was set up (and how to rotate the key):
```sh
@@ -68,8 +78,9 @@ EOF
gpg --armor --export-secret-keys packages@unom.io # -> the RPM_GPG_PRIVATE_KEY CI secret
gpg --armor --export packages@unom.io > packaging/rpm/RPM-GPG-KEY-punktfunk # public half
# 2. Add the armored PRIVATE key as the RPM_GPG_PRIVATE_KEY Gitea Actions secret. Commit the public
# half and publish it to the registry so the gpgkey= URL resolves:
# 2. Add the armored PRIVATE key as the RPM_GPG_PRIVATE_KEY Gitea Actions secret, at the ORG level
# (git.unom.io/org/unom/settings/actions/secrets) so every repo's workflows inherit it. Commit
# the public half and publish it to the registry so the gpgkey= URL resolves:
curl --user "<user>:<write:package-PAT>" --upload-file packaging/rpm/RPM-GPG-KEY-punktfunk \
https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk
```
+17 -4
View File
@@ -1,9 +1,16 @@
#!/usr/bin/env bash
# Detached-GPG-sign the built dist/*.rpm so the Gitea RPM registry can be served with gpgcheck=1.
#
# DORMANT by default: if RPM_GPG_PRIVATE_KEY is unset this exits 0 and leaves the RPMs unsigned —
# exactly today's behaviour — so it is SAFE to ship before a key exists. The signing only activates
# once you add the key as a CI secret (see packaging/rpm/README.md "Enabling per-package signing").
# ACTIVE: RPM_GPG_PRIVATE_KEY is set as an ORG-level secret on `unom` (packages@unom.io,
# AF245C506F4E4763 — the public half is committed at packaging/rpm/RPM-GPG-KEY-punktfunk and served
# from the registry). Every published RPM carries its EdDSA header signature; the repo file in
# README.md tells users gpgcheck=1, which only works because of that.
#
# Without the key this exits 0 and leaves the RPMs unsigned, so a fork or a local build still works
# — but NOT on a release. On refs/tags/v* a missing key is a hard failure: an org secret that got
# rotated, renamed, or not inherited would otherwise publish an unsigned release into a repo whose
# own instructions say gpgcheck=1, and every user's `dnf upgrade` would break on it. Better to fail
# the build than to find out from users. (Same fail-closed rule as the Windows pack scripts.)
#
# Requires a DEDICATED, PASSPHRASE-LESS signing key (the one the runbook generates with
# %no-protection), distinct from the Gitea instance's repo-metadata key — rpm's default signer
@@ -13,7 +20,13 @@
set -euo pipefail
if [ -z "${RPM_GPG_PRIVATE_KEY:-}" ]; then
echo "RPM_GPG_PRIVATE_KEY unset — leaving dist/*.rpm UNSIGNED (registry stays gpgcheck=0)."
case "${GITHUB_REF:-}" in
refs/tags/v*)
echo "RPM_GPG_PRIVATE_KEY unset on a release build (${GITHUB_REF}) — refusing to publish" >&2
echo "unsigned RPMs into a gpgcheck=1 repo. Restore the org secret (packaging/rpm/README.md)." >&2
exit 1 ;;
esac
echo "RPM_GPG_PRIVATE_KEY unset — leaving dist/*.rpm UNSIGNED (non-release build)."
exit 0
fi