Configuring Gitea/Forgejo to Sign Merge Commits
Gitea supports automatically signing commits it generates (such as when merging pull requests, or editing files through the web editor). Sadly there is no documentation on how to actually configure this, besides vague references that it is left up to the server administrator to achieve.
Secure key management is a topic fraught with complexity and trade off decisions, and the Gitea development team holds a (sensible) position that it is preferable to give no advice than it is to give bad advice.
A position that I, as an internet rando, am absolutely not bound by, so here's what I did!
Edit 2024-08-19: these exact steps also work for Forejo! Just use
/var/lib/forgejo
instead of /var/lib/gitea
for all instructions below
Seriously though, take a close look at your own threat model and security posture before you apply any of the following instructions, which are, frankly, provided for free, as in used mattress. These are educational instructions, and you bear all responsibility for the consequences of following them.
Overview
Under the hood, Gitea basically has its own .gitconfig
which is applied to all
git operations that it performs. At the end of the day, all we need to do is
ensure a GPG signing key is available inside Gitea's "home" directory and make
sure that Gitea can interact with it unattended.
Which is easier said than done as GPG strongly insists on passphrase prompts and is otherwise hostile to unattended interaction. Perhaps it is possible to achieve this by starting and priming a
gpg-agent
instance to be used by Gitea, but I did not bother investigating it as it adds more complexity, and still requires automating entering the passphrase to it. At that rate, a secret of one form or another needs to be available.I also have not (yet) tried using SSH signing. Since git already supports it with some config changes, it might be possible that it "just works", but I don't know if there might be other GPG-assumptions baked into Gitea (like the
/api/v1/signing-key.gpg
endpoint.
Generating a new key
First we want to create a brand new GPG key for this Gitea instance. Note that this key only needs to be configured for signing as we'll only use it to generate a second signing subkey that will be used by Gitea.
gpg --full-generate-key
Follow the wizard and fill in the values as appropriate. Note that GPG will ask for a passphrase. Generate a strong password and save it in your password manager (as we'll need it in a bit).
gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Please select what kind of key you want:
(1) RSA and RSA
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(9) ECC (sign and encrypt) *default*
(10) ECC (sign only)
(14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
(1) Curve 25519 *default*
(4) NIST P-384
(6) Brainpool P-256
Your selection? 1
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: Example Gitea
Email address: gitea@git.example.com
Comment:
You selected this USER-ID:
"Example Gitea <gitea@git.example.com>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: revocation certificate stored as
'/home/ivan/.gnupg/openpgp-revocs.d/F41E36D6735FD9BE1B53518EA1596EF2CE56B350.rev'
public and secret key created and signed.
pub ed25519/0xA1596EF2CE56B350 2024-05-05 [SC]
Key fingerprint = F41E 36D6 735F D9BE 1B53 518E A159 6EF2 CE56 B350
uid Example Gitea <gitea@git.example.com>
Note the line about gpg: revocation certificate stored as '/home/ivan/.gnupg/openpgp-revocs.d/F41E36D6735FD9BE1B53518EA1596EF2CE56B350.rev'
.
This is a revocation certificate for the new key, generated here in case the
key needs to be revoked in the future (but the secret part of the key is somehow
lost or destroyed). Save this somewhere safe (like a password manager) and
destroy this copy when done (and optionally ensure it is scrubbed from your ZFS
snapshots if you care to).
Generating a signing subkey
Now it's time to generate a signing subkey that will be used by Gitea directly. The idea is that this subkey can be rotated at will while the main key remains offline/in cold storage (so it can be trusted for longer periods of time). Note that GPG will ask for the password we set earlier.
gpg --edit-key 0xA1596EF2CE56B350
gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Secret key is available.
sec ed25519/0xA1596EF2CE56B350
created: 2024-05-05 expires: never usage: SC
trust: ultimate validity: ultimate
[ultimate] (1). Example Gitea <gitea@git.example.com>
gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(10) ECC (sign only)
(12) ECC (encrypt only)
(14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
(1) Curve 25519 *default*
(4) NIST P-384
(6) Brainpool P-256
Your selection? 1
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
sec ed25519/0xA1596EF2CE56B350
created: 2024-05-05 expires: never usage: SC
trust: ultimate validity: ultimate
ssb ed25519/0xA36F2B11E12C310D
created: 2024-05-05 expires: never usage: S
[ultimate] (1). Example Gitea <gitea@git.example.com>
gpg> save
Backing up the newly generated keys
Now is a good time to store a secure copy of the key's we've just generated (e.g. storing the output somewhere in your password manager). Once again GPG will ask for the password we created earlier.
gpg --export-secret-keys --armor --export-options export-backup 0xA1596EF2CE56B350
Transferring the signing subkey
Next we'll need to transfer the signing subkey to the Gitea host itself. Running the following will print out the secret portion which we'll copy/paste afterwards. There are other ways to achieve this without leaving traces (either on disk or through the system clipboard) but that is left as an exercise to the (sufficiently paranoid) reader.
gpg --export-secret-subkeys --armor 0xA36F2B11E12C310D
Then, we need to ssh to the Gitea host and temporarily take on the gitea
user.
ssh giteahost.example.com
sudo su gitea
On NixOS, Gitea will use /var/lib/gitea
as its root directory, and data/home
for it's $HOME
equivalent (though both of these are configurable so consult
your distro docs and replace the values as appropriate).
# NB: using --batch here will suppress password prompts, meaning the key will
# be imported without any (password) protection. Make sure Gitea's root
# directory is fully sandboxed away from other users
gpg --homedir /var/lib/gitea/data/home/.gnupg --batch --import
# Paste the output from the previous command here.
# Hit Enter then Ctrl+D to finish
#
# Note: if importing fails, kill all instances of `gpg-agent` and try again.
Now that the (subkey) is imported, we need to update its trust level:
gpg --homedir /var/lib/gitea/data/home/.gnupg --edit-key 0xA36F2B11E12C310D
gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Secret subkeys are available.
gpg: /var/lib/gitea/data/home/.gnupg/trustdb.gpg: trustdb created
pub ed25519/A1596EF2CE56B350
created: 2024-05-05 expires: never usage: SC
trust: unknown validity: unknown
ssb ed25519/A36F2B11E12C310D
created: 2024-05-05 expires: never usage: S
[ unknown] (1). Example Gitea <gitea@git.example.com>
gpg> trust
pub ed25519/A1596EF2CE56B350
created: 2024-05-05 expires: never usage: SC
trust: unknown validity: unknown
ssb ed25519/A36F2B11E12C310D
created: 2024-05-05 expires: never usage: S
[ unknown] (1). Example Gitea <gitea@git.example.com>
Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)
1 = I don't know or won't say
2 = I do NOT trust
3 = I trust marginally
4 = I trust fully
5 = I trust ultimately
m = back to the main menu
Your decision? 5
Do you really want to set this key to ultimate trust? (y/N) y
pub ed25519/A1596EF2CE56B350
created: 2024-05-05 expires: never usage: SC
trust: ultimate validity: unknown
ssb ed25519/A36F2B11E12C310D
created: 2024-05-05 expires: never usage: S
[ unknown] (1). Example Gitea <gitea@git.example.com>
Please note that the shown key validity is not necessarily correct
unless you restart the program.
gpg> quit
Lastly, we need to configure Gitea to not even attempt to open a pinentry prompt
since there isn't going to be anyone around to answer it. We can achieve this by
configuring git to call a wrapper program which invokes GPG with --batch
(which will disable these prompts)
# On NixOS, system-wide packages show up in /run/current-system/sw/bin/
# swap it out with the appropriate location on your system
echo >/var/lib/gitea/data/home/gpg-nopinentry <<'EOF'
#!/usr/bin/env bash
exec /run/current-system/sw/bin/gpg --batch "$@"
EOF
chmod +x /var/lib/gitea/data/home/gpg-nopinentry
echo >>/var/lib/gitea/data/home/.gitconfig <<EOF
[gpg]
program = "/var/lib/gitea/data/home/gpg-nopinentry"
EOF
Telling Gitea about the new key
Gitea still needs to be instructed to use the key. There are a number of configuration options to consider but here are the most important ones to set. Make sure they match the values that were originally set when generating the root key.
[repository.signing]
SIGNING_KEY=0xA36F2B11E12C310D
SIGNING_NAME=Example Gitea
SIGNING_EMAIL=gitea@git.example.com
Clean up
Back on the original host we can now purge the secret keys (assuming they have been backed up securely) to minimize them getting compromised:
gpg --delete-secret-keys 0xA1596EF2CE56B350
Happy self-hosting!