Files
Fenris/docs/install/signing-key-ceremony.md
T
xavierkandCommandCodeBot 1e2ddfb928 fix(packaging): address review findings for #44
- nfpm.yaml: type:config → config_noreplace (RPM noreplace semantics)
- nfpm.yaml: type:ghost → type:dir for /var/lib/fenris (deb compatibility)
- postinst.sh/rpm/post.sh: fix timer restart — capture running unit before
  daemon-reload so the diff actually detects changes
- README: fix Python floor to ≥3.10 (was ≥3.9, inconsistent with Makefile)
- signing-key-ceremony.md: fix stale claim about nfpm signing RPMs
  (actual path is post-build rpmsign)
- tests/conftest.py: extract shared _get_version() and _read() helpers
- tests: wire up to shared conftest helpers
- release.yml: extract VERSION once via GITHUB_OUTPUT step

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-09-03 15:25:35 +05:30

7.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 Password manager only
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

Save the private key to the password manager immediately:

gpg --armor --export-secret-keys packaging@bongbetic.com

Then delete the private key from the local keyring — it must never persist on any build host:

gpg --delete-secret-keys packaging@bongbetic.com
gpg --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 release performs: import → sign → delete. The private key is never stored on disk longer than the release takes.

Step 1: Import the private key

Retrieve the private key from the password manager and import it:

gpg --import /tmp/packaging-key-private.asc
rm /f /tmp/packaging-key-private.asc   # Shred if possible

Step 2: Build and sign packages

The Makefile target make release handles signing automatically when the key is in the keyring:

make release    # builds, signs RPM, clearsigns SHA256SUMS, prints upload steps

Under the hood:

  1. rpmsign --addsign signs the RPM payload with the packaging key (invoked by make sign-rpm).
  2. sha256sum generates the checksum manifest.
  3. gpg --clearsign produces SHA256SUMS.asc with the packaging key.

Step 3: Delete the private key

Immediately after signing:

gpg --delete-secret-keys packaging@bongbetic.com
gpg --delete-keys packaging@bongbetic.com

Verify the key is gone:

gpg --list-keys packaging@bongbetic.com
# Should produce: gpg: keyblock resource ...: No such file or directory

The entire import → sign → delete cycle should take minutes. The private key must never be left in any keyring between releases.

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