The optional XBPS publisher signs repository metadata after package signing. Remove the runner key only after publication and release asset upload.
8.0 KiB
Signing key ceremony
The Fenris packaging key signs RPM payloads and clearsigns SHA256SUMS manifests. This document describes the key's lifecycle: creation, per-release use, rotation, and destruction.
Key specification
| Property | Value |
|---|---|
| Algorithm | RSA 3072 |
| UID | Fenris Packaging <packaging@bongbetic.com> |
| Expiry | 2 years from creation |
| Hierarchy | Single key — no master/subkey split (single maintainer, manual builds) |
| Private key storage | Gitea repository Actions secret GPG_PRIVATE_KEY |
| Public key storage | packaging/keys/fenris-packaging.asc in-repo, release notes, docs |
| Keyservers | Never — TOFU-over-TLS via raw URL |
First release: key creation
# Generate the dedicated RSA-3072 packaging key
gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 3072
Name-Real: Fenris Packaging
Name-Email: packaging@bongbetic.com
Expire-Date: 2y
%commit
EOF
# Export the public half — this file is committed to the repo
gpg --armor --export packaging@bongbetic.com > packaging/keys/fenris-packaging.asc
# Print the fingerprint for docs and release notes
gpg --fingerprint packaging@bongbetic.com
Provision the private key as the repository Actions secret GPG_PRIVATE_KEY.
Run the export on the trusted key-generation machine, then enter its output in
the Gitea repository's Actions secret settings. Do not save it in the checkout,
logs, or a runner directory. The release workflow checks its fingerprint
against the committed public key before signing.
gpg --armor --export-secret-keys packaging@bongbetic.com
After provisioning the secret, delete the private key from the key-generation keyring:
gpg --batch --yes --delete-secret-keys packaging@bongbetic.com
gpg --batch --yes --delete-keys packaging@bongbetic.com
The committed fenris-packaging.asc must contain the real public key (replace
the placeholder comments).
XBPS signing key
XBPS uses a separate RSA 3072 key. Its private half is stored as the Gitea
repository Actions secret XBPS_SIGNING_KEY. The corresponding public key is
published at
https://git.bongbetic.com/xavierk/Fenris-xbps/raw/branch/stable/keys/fenris-xbps-signing.pub,
with fingerprint SHA256:AvPMRlKMikPg75u0iKr8AUkxlfU/Ad4k/S4o2M9W4/w.
The secret must match that public key.
The release workflow writes the key to ~/.ssh/id_xbps to sign the XBPS
package. A requested XBPS publication also uses the key to sign repository
metadata. A final always() cleanup removes the runner copy after publication
and release asset upload, including when an earlier step fails.
Per-release signing flow
Each tagged release performs: import → verify → sign → delete on the repository-scoped Gitea Actions runner. The Gitea secret remains configured; the runner's keyring copy is removed after the job.
Step 1: Push the release tag
After updating the version and dated changelog section, push the matching tag:
git push origin v<version>
Step 2: Build, verify, and sign packages
The release workflow imports GPG_PRIVATE_KEY, checks it against
packaging/keys/fenris-packaging.asc, builds packages, signs the RPM and
clearsigned checksum manifest, validates both, and publishes the release. The
workflow imports XBPS_SIGNING_KEY separately and signs the XBPS package.
XBPS publication is optional and also signs repository metadata; it requires
host acceptance and explicit selection during workflow dispatch.
Step 3: Verify runner cleanup
The workflow's always() cleanup removes the GPG key from the runner's keyring
and deletes ~/.ssh/id_xbps, including after a failed job. Confirm no signing
key remains on the runner after the release job.
The Gitea Actions secret remains the approved signing source. Do not copy it to the runner or repository outside the workflow.
Key rotation (outline)
When the key approaches expiry, or if it is compromised:
- Generate a new key using the same procedure as first release.
- Publish the new public key alongside the old one in-repo:
packaging/keys/fenris-packaging.asc # new key (primary) packaging/keys/fenris-packaging-previous.asc # old key (one cycle) - Sign the next RPM with the new key.
- Update
fenris.repoto list bothgpgkeyURLs (dnf accepts multiple):gpgkey=https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/keys/fenris-packaging.asc https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/keys/fenris-packaging-previous.asc - Drop the old key from the repo after one release cycle. Delete
fenris-packaging-previous.ascand revertgpgkeyto the single URL.
Verification
Consumers verify the RPM payload signature via dnf (gpgcheck=1 in
fenris.repo points at the published public key). The SHA256SUMS manifest
verification is manual for downloaded assets:
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS
One-time live probe
Before the first real release, verify the full registry path end-to-end with a throwaway package. This confirms apt/dnf metadata generation, signature verification, and consumer setup work as a real consumer would experience them.
Setup
# Create a throwaway package name to avoid polluting fenris metadata
PROBE_NAME="fenris-regtest"
PROBE_VERSION="0.0.1"
Publish
# Build a throwaway deb and rpm (use the existing nfpm config with a dummy name)
# Or use a pre-built package — the probe tests the registry path, not the build
# Upload deb to all codename pools
for CODENAME in bookworm jammy noble; do
curl --fail -X PUT \
-u "xavierk:${GITEA_TOKEN}" \
-T "dist/${PROBE_NAME}_${PROBE_VERSION}_amd64.deb" \
"https://git.bongbetic.com/api/packages/xavierk/debian/pool/${CODENAME}/main/upload"
done
# Upload rpm
curl --fail -X PUT \
-u "xavierk:${GITEA_TOKEN}" \
-T "dist/${PROBE_NAME}-${PROBE_VERSION}-1.x86_64.rpm" \
"https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/upload"
Verify apt metadata (Debian/Ubuntu consumer perspective)
# On a Debian/Ubuntu machine:
sudo mkdir -p /etc/apt/keyrings
sudo curl -fsSL https://git.bongbetic.com/api/packages/xavierk/debian/repository.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/gitea-xavierk.asc
echo "deb [signed-by=/etc/apt/keyrings/gitea-xavierk.asc] https://git.bongbetic.com/api/packages/xavierk/debian bookworm main" \
| sudo tee /etc/apt/sources.list.d/fenris.list
sudo apt update
apt show ${PROBE_NAME} # metadata present, correct version
apt install --dry-run ${PROBE_NAME} # dependency resolution works
# Verify InRelease signature
apt-key list 2>/dev/null || gpg --no-default-keyring --keyring /etc/apt/keyrings/gitea-xavierk.asc --list-keys
Verify dnf metadata (Fedora consumer perspective)
# On a Fedora machine:
sudo dnf config-manager --add-repo https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/fenris.repo
# Or use Gitea's auto-generated repo for the probe:
sudo dnf config-manager --add-repo https://git.bongbetic.com/api/packages/xavierk/rpm/fenris.repo
dnf info ${PROBE_NAME} # metadata present, correct version
dnf install --assumeno ${PROBE_NAME} # dependency resolution works
# Verify rpm signature
rpm -q --scripts ${PROBE_NAME} # no scripts (throwaway)
Verify checksums and clearsign
# Download from release assets or local build
gpg --verify SHA256SUMS.asc SHA256SUMS
sha256sum -c SHA256SUMS
Cleanup
# Delete the throwaway packages from the registry
for CODENAME in bookworm jammy noble; do
curl --fail -X DELETE \
-u "xavierk:${GITEA_TOKEN}" \
"https://git.bongbetic.com/api/packages/xavierk/debian/pool/${CODENAME}/main/${PROBE_NAME}/${PROBE_VERSION}/amd64"
done
curl --fail -X DELETE \
-u "xavierk:${GITEA_TOKEN}" \
"https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/${PROBE_NAME}/${PROBE_VERSION}/x86_64"
# Remove test source list on consumer machines
sudo rm /etc/apt/sources.list.d/fenris.list
sudo apt update