fix(forge): make Forgejo's signing key actually resolve

Forgejo reported "does not have a signing key" on PRs despite the
forgejo-gpg-init service generating one. Two causes, both fixed:

- SIGNING_KEY = "default" resolves through the forgejo process's git
  config (user.signingkey), not by scanning GNUPGHOME — and the keygen
  never set it, so "default" found no key. forgejo-gpg-init now sets the
  forgejo user's git user.signingkey to the generated key (+ commit and
  tag gpgsign), with HOME pinned to the state dir so the global config
  lands where forgejo reads it.
- The keygen guard was stamp-file based, so a partial state wipe that
  lost the key while keeping the stamp would never regenerate. It's now
  keyed on the actual secret key (regenerate iff gpg shows none) and
  runs idempotently before forgejo on each start.

Also drops the heredoc for the gpg params in favour of printf (avoids
nix-string indentation fragility) and corrects the stale comment that
claimed "default" scans GNUPGHOME.
This commit is contained in:
atlas 2026-06-09 14:07:51 +02:00 committed by mara
commit d6b95d22f0

View file

@ -290,9 +290,12 @@ in
THEMES = "catppuccin-vibec0re,forgejo-auto,forgejo-light,forgejo-dark,gitea-auto,gitea-light,gitea-dark";
};
# Point forgejo at the GPG key generated by the
# forgejo-gpg-init oneshot below. "default" resolves to
# the first secret key found in GNUPGHOME. GNUPGHOME
# must be absolute and writeable by the forgejo user.
# forgejo-gpg-init service below. SIGNING_KEY = "default"
# resolves via the forgejo process's git config
# (`user.signingkey`) — which forgejo-gpg-init sets to the
# generated key — not by scanning GNUPGHOME. GNUPGHOME is
# the keyring forgejo signs from; must be absolute +
# writeable by the forgejo user.
"repository.signing" = {
SIGNING_KEY = "default";
GNUPGHOME = "/var/lib/forgejo/.gnupg";
@ -321,42 +324,67 @@ in
pkgs.gnupg
];
# Generate a GPG signing key for Forgejo on first boot so UI
# merges produce signed commits instead of erroring "no key to
# sign with". The key lives in forgejo's persistent state dir
# (/var/lib/forgejo/.gnupg) and survives container restarts.
# The stamp file prevents re-generation on subsequent boots.
# Service runs as the forgejo user so file ownership is correct.
# Ensure Forgejo has a usable GPG signing key so UI merges / CRUD
# commits are signed instead of erroring "does not have a signing
# key". This service (a) generates a key in forgejo's persistent
# keyring iff one isn't already present — keyed on the actual
# secret key, NOT a stamp file, so a partial state wipe that loses
# the key still regenerates it — and (b) points the forgejo user's
# git config at it (`user.signingkey` + commit/tag gpgsign), which
# is how `SIGNING_KEY = "default"` actually resolves. Runs as the
# forgejo user before forgejo on each start; idempotent (the keygen
# is guarded, the git-config is a cheap re-set).
systemd.services.forgejo-gpg-init = {
description = "generate GPG signing key for Forgejo (once)";
# Start before forgejo so the key is ready when forgejo reads
# repository.signing config on startup.
description = "ensure Forgejo's GPG signing key + git signing config";
# Start before forgejo so the key + signing config are ready when
# forgejo reads repository.signing on startup.
wantedBy = [ "forgejo.service" ];
before = [ "forgejo.service" ];
unitConfig.ConditionPathExists = "!/var/lib/forgejo/.gnupg/hive-key-init.stamp";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "forgejo";
Group = "forgejo";
};
environment.GNUPGHOME = "/var/lib/forgejo/.gnupg";
# GNUPGHOME = the keyring forgejo signs from; HOME so
# `git config --global` lands where the forgejo process reads it.
environment = {
GNUPGHOME = "/var/lib/forgejo/.gnupg";
HOME = "/var/lib/forgejo";
};
path = [
pkgs.gnupg
pkgs.git
pkgs.gnugrep
pkgs.gawk
pkgs.coreutils
];
script = ''
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
gpg --batch --gen-key <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: HyperHive Forge
Name-Email: forgejo@hive
Expire-Date: 0
EOF
touch "$GNUPGHOME/hive-key-init.stamp"
set -euo pipefail
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
# Generate only if no secret key is present (key-based guard,
# not a stamp — a stamp can outlive the key after a state wipe
# and wrongly suppress regeneration).
if ! gpg --list-secret-keys --with-colons 2>/dev/null | grep -q '^sec:'; then
printf '%s\n' \
'%no-protection' \
'Key-Type: RSA' \
'Key-Length: 4096' \
'Name-Real: HyperHive Forge' \
'Name-Email: forgejo@hive' \
'Expire-Date: 0' \
| gpg --batch --gen-key
fi
# Point git (hence Forgejo's SIGNING_KEY="default") at the key.
KEYID=$(gpg --list-secret-keys --keyid-format long --with-colons \
| awk -F: '/^sec:/ { print $5; exit }')
if [ -n "$KEYID" ]; then
git config --global user.signingkey "$KEYID"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
fi
'';
};
};