Files
Fenris/docs/install/signing-key-ceremony.md
T
xavierk 3f2dd6a5a1 fix(release): retain XBPS key through publication
The optional XBPS publisher signs repository metadata after package signing. Remove the runner key only after publication and release asset upload.
2026-09-29 04:06:45 +05:30

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:

  1. Generate a new key using the same procedure as first release.
  2. 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)
    
  3. Sign the next RPM with the new key.
  4. Update fenris.repo to list both gpgkey URLs (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
    
  5. Drop the old key from the repo after one release cycle. Delete fenris-packaging-previous.asc and revert gpgkey to 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