From d6b95d22f05d873bffd8c86633ea434132ea6933 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 9 Jun 2026 14:07:51 +0200 Subject: [PATCH] fix(forge): make Forgejo's signing key actually resolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- nix/modules/hive-forge.nix | 80 +++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index ce420e58..538ae634 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -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 ''; }; };