Files
Fenris/docs/install/signing-key-ceremony.md
T

7.1 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).

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. XBPS publication is separate and requires its signing key and host acceptance.

Step 3: Verify runner cleanup

The workflow's always() cleanup removes the imported key from the runner's keyring, including after a failed job. Confirm no packaging secret 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