From 1fcdd2999fe1e04308ec913651b645a6f6882eda Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 01:08:00 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=90=8B=F0=9F=94=A7=EF=BC=9Are-add=20p?= =?UTF-8?q?ostStartCommand=20so=20ssh-format=20signing=20survives=20an=20a?= =?UTF-8?q?ttach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1775 replaced postCreateCommand+postStartCommand with postCreateCommand alone. postCreateCommand only ever runs once, at container build time, but the SSH agent VS Code forwards is a fresh, per-session socket -- so nothing was left to re-materialize gpg.format=ssh's signing-key file on later attaches. VS Code forwards the running agent automatically but never copies key files in, so a user.signingkey path copied verbatim from the host's gitconfig (e.g. ~/.ssh/id_ed25519.pub) pointed at a file that had never existed in the container, and `git commit -S` failed with "Couldn't load public key ...: No such file or directory". post-start.sh now runs on every attach and writes the forwarded agent's public key to user.signingkey's path whenever the agent is holding exactly one identity. It can't sign with the wrong key even if the agent's identity turns out to be unrelated: the actual signature still goes through the agent by fingerprint, so a mismatched file just makes ssh-keygen report no matching identity instead of mis-signing silently. Also drops the "To sign commits: git config --global commit.gpgsign true" hint from post-create.sh's closing banner -- it was often already true (git config copied from the host) and was never the actual blocker, so it just gave false reassurance. post-start.sh's own output now reports accurate, per-attach signing status instead. Co-Authored-By: Claude Sonnet 5 --- .devcontainer/devcontainer.json | 4 +++ .devcontainer/post-create.sh | 2 -- .devcontainer/post-start.sh | 44 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100755 .devcontainer/post-start.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f5634d058..c9c8556bd 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -45,6 +45,10 @@ }, "postCreateCommand": "bash .devcontainer/post-create.sh", + // Unlike postCreateCommand, this runs on every attach, not just the first. + // Forwarded agent sockets are per-session, so anything that depends on one + // (see post-start.sh) has to be re-checked here. + "postStartCommand": "bash .devcontainer/post-start.sh", "customizations": { "vscode": { diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index cc3412c05..950a0c527 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -135,6 +135,4 @@ Ready, on Node $(node -v). In fish, \`nvm use\` reads .nvmrc. Switching there affects that session only; the container baseline stays on the version package.json pins. - -To sign commits: git config --global commit.gpgsign true EOF diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh new file mode 100755 index 000000000..ed96f354e --- /dev/null +++ b/.devcontainer/post-start.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# ------------------------------------------------------------------------------ +# Copyright (c) The OpenINF Authors & Friends. All rights reserved. +# License: MIT OR Apache-2.0 OR BlueOak-1.0.0 +# ------------------------------------------------------------------------------ +# +# Runs on every container start/attach, not just the first. Agent forwarding +# is per-session -- VS Code opens a fresh SSH_AUTH_SOCK each time it attaches +# -- so anything that depends on it belongs here rather than in +# post-create.sh, which never runs again after the container is built. + +set -uo pipefail + +# Under gpg.format=ssh, `git commit -S` shells out to +# `ssh-keygen -Y sign -f ...`, which reads that file for the +# public key and then asks the running agent (SSH_AUTH_SOCK) to sign with the +# matching private key. VS Code forwards the agent itself but, per +# https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials, +# never copies key files into the container -- so a signingkey path copied +# verbatim from the host's gitconfig points at a file that has never existed +# here, and signing fails with "Couldn't load public key ...: No such file or +# directory" no matter how many times post-create.sh's `commit.gpgsign true` +# hint is followed. +# +# If the forwarded agent is holding exactly one identity, write it out so that +# path resolves. This can't sign with the wrong key even if it guessed wrong: +# the actual signature still goes through the agent, keyed by fingerprint, so +# a mismatched file just makes ssh-keygen report no matching identity instead +# of silently mis-signing. +if [ "$(git config --global gpg.format 2>/dev/null || true)" = "ssh" ]; then + signingkey="$(git config --global user.signingkey 2>/dev/null || true)" + if [ -n "${signingkey}" ] && [ ! -f "${signingkey}" ]; then + identities="$(ssh-add -L 2>/dev/null || true)" + count="$(printf '%s\n' "${identities}" | grep -c '^ssh-' || true)" + if [ "${count}" -eq 1 ]; then + mkdir -p -m 700 "$(dirname "${signingkey}")" + printf '%s\n' "${identities}" >"${signingkey}" + chmod 644 "${signingkey}" + echo "==> Commit signing ready (${signingkey}, from forwarded SSH agent)" + else + echo "==> Commit signing NOT ready: forwarded SSH agent has ${count} identities, need exactly 1 to write ${signingkey}" >&2 + fi + fi +fi From fb0f12105eb262f93fcce836b2c7835a221c6adb Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 01:08:13 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Arewrite=20?= =?UTF-8?q?agent-forwarding=20docs=20for=20the=20stock=20devcontainer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The doc still described the container architecture #1775 removed: a custom base image with its own sshd on a forwarded port 2222, a `vscode` user, and a manual `RemoteForward` SSH tunnel (`host gpgtunnel`) to reach a GPG agent extra socket -- mostly written for Git Bash/Gpg4Win on Windows. None of that exists anymore; following it now would send someone looking for a port and a user that are no longer there. Replaced it with what's actually true of the current devcontainer: VS Code forwards a running SSH or GPG agent automatically with no devcontainer.json config needed, but never copies key material in, which is why gpg.format=ssh needs post-start.sh's help (see the previous commit) and gpg.format=openpgp doesn't. Covers setup for both signing formats, notes that GitHub Desktop's "Automatically sign commits" preference is the gpg.format=ssh path, and adds a troubleshooting section keyed to post-start.sh's own output. Also updates project-terms.txt: adds the git-config tokens the rewrite now uses (gpgsign, openpgp, signingkey) and drops updatestartuptty and psusan, whose only uses were in the content just removed. Co-Authored-By: Claude Sonnet 5 --- collections/_docs/agent-forwarding.md | 413 ++++++++------------------ project-terms.txt | 5 +- 2 files changed, 131 insertions(+), 287 deletions(-) diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 4cd9253b6..702894008 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -3,299 +3,142 @@ title: OpenINF Next-Gen Guidance on Agent Forwarding category: contributing permalink: /docs/dev/internals/contributing/agent-forwarding/ relevant_urls: - - https://github.com/OpenINF/docker-fisher/issues/5 # psusan + auth awkwardness - - https://dev.gnupg.org/T3883 # Win32-OpenSSH support for gpg-agent's ssh-agent + - https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials + - https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification toc: true draft: true --- -Additional setup procedures may be necessary for the chosen few who hold core -OpenINF membership to sign any potential Git commits or tags. This guide will be -especially relevant for those developing inside the container provided as there -will likely be snafus to overcome before connecting to the devcontainer. - -## Connecting to GitHub with SSH - -You can connect to GitHub using the Secure Shell Protocol (SSH), which provides -a secure channel over an unsecured network. - -Using the SSH protocol, you can connect and authenticate to remote servers and -services. With SSH keys, you can connect to GitHub without supplying your -username and personal access token at each visit. - -When one sets up SSH, it's necessary to first generate a new SSH key and then -add it to the **[`ssh-agent`][]**. One must add the SSH key to their account on -GitHub before any usage of the key to authenticate may occur. - -### 1.1.1     Generating a new SSH key and adding it to the ssh-agent - -If you don't already have an SSH key, you must generate a new one for -authentication. You can check for existing keys if you are unsure whether you -already have an SSH key. For more information, see "[Checking for existing SSH -keys][]". - -If you don't want to reenter your passphrase every time you use your SSH key, -you can add your key to the SSH agent, which will manage your SSH keys and -remember your passphrases. - -#### 1.1.1.1    Working with SSH key passphrases - -You can secure your SSH keys and configure an authentication agent so that you -won't have to reenter your passphrase every time you use your SSH keys. - -With SSH keys, if someone gains access to your computer, they also gain access -to every system that uses that key. To add an extra layer of security, you can -add a passphrase to your SSH key. You can use `ssh``-agent` to save your -passphrase securely, so you don't have to reenter it. - -## 1.2    Adding or changing a passphrase - -### 1.2.1     Adding a new SSH key to your GitHub account - -You can further secure your SSH key by using a hardware security key, which -requires the physical hardware security key to be attached to your computer when -the key pair is used to authenticate with SSH. You can also secure your SSH key -by adding your key to the ssh-agent and using a passphrase. For more -information, see "[Working with SSH key passphrases][]". - -## 1.3    Auto-start of the gpg-agent - -The _gpg-agent_ is the central part of the GnuPG system. It takes care of all -private (secret) keys and, if required, diverts operations to a smartcard or -other token. It also supports Secure Shell (SSH) by implementing the ssh-agent -protocol. - -The traditional way to run _gpg-agent_ on \*nix systems is by launching it at -login time and using an environment variable (GPG_AGENT_INFO) to tell the other -GnuPG modules how to connect to the agent. However, correctly managing the -startup and this environment variable is cumbersome, so a more straightforward -method is required. Since GnuPG 2.0.16, the --use-standard-socket option already -allowed starting the agent on the fly; however, the environment variable was -still needed. - -With GnuPG 2.1, the need for GPG_AGENT_INFO has been completely removed, and the -variable is ignored. - -Instead, a fixed _Unix domain socket_ named S.gpg-agent in the GnuPG home -directory (by default ~/.gnupg) is used. The agent is also started on-demand by -all tools requiring services from the agent. - -If the option `--enable-ssh-support` is used, the auto-start mechanism does not -work because _ssh_ does not know about this mechanism. Instead, the environment -variable `SSH_AUTH_SOCK` must be set to the `S.gpg-agent.ssh` socket in the -GnuPG home directory. Further, `gpg-agent` must be started by either using a -GnuPG command that implicitly starts `gpg-agent` or by using -`gpgconf --launch gpg-agent` to explicitly start it without first having to use -a GnuPG command. - -`gpg-agent` is a daemon to manage secret (private) keys independently from any -protocol. It is a backend for gpg, gpgsm, and other utilities. - -GPG Agent Configuration - -There are a few configuration files needed for the operation of the agent. They -may all be found in the current GnuPG home directory, which defaults -to ~/.gnupg. - -:::windows - -However, there may be problems on Windows systems with Gpg4Win installed; -**Gpg4Win may have changed this default GnuPG home directory location** to an -AppData subdirectory (i.e., `C:\Users\\AppData\Roaming\gnupg`). This -location, however, is not the location our GPG agent will be using, so to -reaffirm our preference for the default location, we will set -the GNUPGHOME environment variable to ~/.gnupg in the Git Bash startup script by -running the following. - -echo 'export GNUPGHOME="~/.gnupg"' >> .bashrc - -::: - -Setting environment variables - -Add the following lines to your .bashrc or whatever initialization file is used -for all shell invocations: - -GPG_TTY=$(tty) - -export GPG_TTY - -This variable may only be helpful if you use `pinentry-curses` (the -terminal-based pin entry program). - -:::windows - -On Windows systems, you should add the above lines to the ~/.bashrc file for use -by Git Bash. - -::: - -GnuPG configuration - -It's important to note that GnuPG on the remote system still needs your public -GPG keys to work correctly. So you have to ensure they are available on the -remote system even if your secret keys are not. - -:::note{.note} - -If you use VSCode with the remote extension pack, you may skip this step and -move on to the next section. This step of copying over your local public GPG -keyring into the remote container gets done automatically by the extension. - -::: - -:::excerpt{.quote} - -[SCP]{#scp .dfn} is a means of -securely transferring [computer files][] between a local [host][] and a remote -host or between two remote hosts. It is based on the [Secure Shell][] (SSH) -protocol.[^1] "SCP" commonly refers to both the Secure Copy Protocol and the -program itself.[^2] - - - -::: - -During _**[`ssh-agent`][]** initialization_, the extra socket (named -**`S.gpg-agent.extra`** by default) gets created in the GnuPG home directory. - -The intended use for this extra socket is to set up a _Unix domain socket_ -forwarding from a remote machine to this socket on the local device. -A gpg process running on the remote box (or, in our case, in the devcontainer) -may connect to the local gpg-agent and use its private keys. This activity -enables decrypting or signing data on a remote machine without exposing the -private keys to the remote box. Although this technique is usually taken as a -precaution when the connection between two systems goes over a hostile network, -it is convenient to avoid transferring private keys to the devcontainer. - -### 1.3.1.1    Manually specify GPG agent configuration - -To guarantee that the connection (going over a kernel IPC channel) between the -two systems goes to the right place, it is advisable to explicitly specify the -local filename (in full) of the extra socket in the GPG agent config file. Do so -by adding the following line to the `gpg-agent.conf` file in the GnuPG home -directory. - -```text -extra-socket /c/Users//.gnupg/S.gpg-agent.extra -``` - -This extra socket is the one our local `gpg-agent` will be using rather than -S.gpg-agent because its limitations theoretically make it more secure. - -Enable GPG agent support of SSH - -Add the following line to your GPG-agent config file. - -```text -enable-ssh-support -``` - -The OpenSSH Agent protocol is always enabled, but `gpg-agent` will only set -the `SSH_AUTH_SOCK` environment variable with this option specified. - -In this mode of operation, the agent implements both the `gpg-agent` protocol -and the agent protocol used by OpenSSH (through a separate socket). -Consequently, using the `gpg-agent` as a drop-in replacement for the -well-known `ssh-agent` should be possible. - -SSH keys, intended for use through the agent, need to be added to the -gpg-agent initially through the ssh-add utility. Upon adding a key, ssh-add will -ask for the password of the provided key file and send the unprotected key -material to the agent. This routine will cause the gpg-agent to ask for a -passphrase, which it will use to encrypt the newly-received key and store it in -a gpg-agent-specific directory for later use. Once an SSH key has been added to -the gpg-agent in this manner, the gpg-agent will be ready to use the newly-added -key. - -:::note{.note} - -If the `gpg-agent` receives a signature request, the user may need prompting for -a passphrase, which is necessary to decrypt any SSH keys stored. Since -the ssh-agent protocol does not contain a mechanism for telling the agent on -which display/terminal it's running, gpg-agent's ssh-support will use the TTY or -X display where gpg-agent started. To switch this display to the current one, -you may use the following command. - -```console -gpg-connect-agent updatestartuptty /bye -``` - -::: - -Although all GnuPG components try to start the **[`gpg-agent`][]** as needed, -this is not possible for _the **[`ssh`][]** support_ because **[`ssh`][]** does -not know about it. Thus, if no GnuPG tool, that usually accesses the -**[`gpg-agent`][]** (causing the initial start of it) ever ran, there is no -guarantee that **[`ssh`][]** can use **[`gpg-agent`][]** for authentication. To -fix this, one may start **[`gpg-agent`][]** , if needed, by using this simple -command: - -```console -gpg-connect-agent /bye -``` - -:::note{.tip} - -Adding the `--verbose` flag shows the progress of starting the agent. - -::: - -### Correctly managing the startup of the GPG agent - -The traditional way to run _gpg-agent_ on \*nix systems is by launching it at -login time. - -To be sure, we will add the following line to… - -:::windows - -The `--enable-putty-support` flag is only available under Windows and allows the -use of gpg-agent with the PuTTY implementation of SSH. This usage is similar to -the regular ssh-agent, which supports OpenSSH implementations of SSH on \*nix -systems, but differs in its use of Windows Message Queues as PuTTY requires. - -::: - -to load configuration details - -#### 1.3.1.2    specify SSH configuration - -SSH configuration - -Add the following to the file located at ~/.ssh/config. If it does not yet -exist, create it. - -```text -host gpgtunnel - -hostname localhost - -port 2222 - -User vscode - -RemoteForward /home/vscode/.gnupg/S.gpg-agent -/c/Users//.gnupg/S.gpg-agent.extra -``` +Core OpenINF members who sign their commits or tags need a couple of things +forwarded from the host into the devcontainer: a running SSH or GPG agent, and, +for SSH-format signing, a public key file. This guide covers how that forwarding +works today and what to do when it doesn't. + +The devcontainer changed substantially in [#1775][]: it no longer runs its own +`sshd` on a forwarded port, and there is no more `vscode` user or manual +`RemoteForward` tunnel to configure. Everything below reflects that container. +If you find instructions elsewhere -- including an old revision of this file -- +mentioning port 2222 or a `gpgtunnel` SSH host, they predate that change and no +longer apply. + +## How forwarding works here + +VS Code (and compatible tools like the Dev Containers CLI) forwards your +_running_ SSH agent into the container automatically; no devcontainer.json +configuration is required for it. What it does **not** do is copy any key +material in -- not the private key, and, for SSH-format signing, not even the +public key file. See VS Code's own docs on [sharing Git credentials][] for the +authoritative description. + +That gap matters because of how the two signing formats differ: + +- **`gpg.format=openpgp`** (classic GPG): the forwarded agent alone is enough. + `gpg` talks to the agent socket and never needs a local copy of anything. + `post-create.sh` runs `gpg --list-keys` once so the container's keyring files + exist; beyond that, there is nothing this repo needs to do. +- **`gpg.format=ssh`**: Git shells out to + `ssh-keygen -Y sign -f ...`. That `-f` argument is a **file + path**, and `user.signingkey` in a gitconfig copied from your host points at a + host path (typically `~/.ssh/id_ed25519.pub`) that has never existed in the + container. The forwarded agent doesn't help until that file exists. + +`.devcontainer/post-start.sh` closes that second gap. It runs on every container +**start/attach**, unlike `post-create.sh`, which only ever runs once, when the +container is first built -- too early, since the forwarded agent socket is a +fresh, per-session thing set up on each attach. If `gpg.format` is `ssh` and the +forwarded agent is holding exactly one identity, `post-start.sh` writes that +public key out to `user.signingkey`'s path. It deliberately does nothing if the +agent has zero identities (nothing to write) or more than one (no reliable way +to know which one you mean) -- watch its output on attach to see which case +you're in. + +Either way, the actual cryptographic signing still happens on the host, via the +forwarded agent. No private key material is ever copied into the container. + +## Setting up SSH-format signing (recommended) + +This is what GitHub Desktop's own "Automatically sign commits" preference +configures, so if you sign from Desktop, use this format. + +1. Make sure the key you want to sign with is loaded into your platform's SSH + agent: + + ```console + ssh-add -l + ``` + + If it isn't listed, add it. On macOS, add `--apple-use-keychain` so it + survives a reboot instead of needing `ssh-add` again every session: + + ```console + ssh-add --apple-use-keychain ~/.ssh/id_ed25519 + ``` + +2. Point Git at it. GitHub Desktop's signing preference does this for you; run + it by hand if you sign from the CLI instead: + + ```console + git config --global gpg.format ssh + git config --global user.signingkey ~/.ssh/id_ed25519.pub + git config --global commit.gpgsign true + ``` + +3. Reopen or rebuild the devcontainer and watch `post-start.sh`'s output on + attach. `Commit signing ready` means the public key was found in the + forwarded agent and written into the container. Anything else means the agent + forwarded into _this_ session doesn't have exactly one identity -- check + `ssh-add -l` on the host first. + +GitHub Desktop needs the key to exist as a real file on disk; it does not work +with agent-only or Secure Enclave-backed keys. A plain `ssh-keygen`-generated +key pair works better here than a hardware-backed one. + +Signature _verification_ (`git log --show-signature`, the "Verified" badge on +GitHub) is a separate concern from signing and isn't covered above -- see +GitHub's docs on [commit signature verification][] if you need that working +locally too. + +## Setting up GPG-format signing + +If you use an actual OpenPGP key instead: + +1. Make sure `gpg-agent` on the host has your key and is reachable the normal + way (`gpg --list-secret-keys` should show it). +2. Leave `gpg.format` unset (or set it to `openpgp`), and set: + + ```console + git config --global user.signingkey + git config --global commit.gpgsign true + ``` + +3. Nothing in this repo's devcontainer needs configuring beyond what's already + there: `gnupg` ships in the base image, and the forwarded agent socket is all + `gpg` needs. + +## Troubleshooting + +- **`ssh-add -l` says "The agent has no identities"** -- this is a host-side + fact, not a container problem. Add the key on the host and reattach. +- **`post-start.sh` reports more than one identity** -- it won't guess. Either + unload the extra identities from the agent for this session, or, inside the + container, write the file yourself from the one you mean (path from + `git config --global user.signingkey`): + + ```console + ssh-add -L | grep > + ``` + +- **It worked before, stopped working after a container rebuild** -- the file + `post-start.sh` writes lives in the container's filesystem, not a volume, so a + rebuild removes it. It gets rewritten on the next attach as long as the agent + still has exactly one identity at that point. -[^1]: https://en.wikipedia.org/wiki/Secure_copy_protocol#cite_note-1 -[^2]: https://en.wikipedia.org/wiki/Secure_copy_protocol#cite_note-Pechanec-2 - -[`ssh`]: https://en.wikipedia.org/wiki/Secure_Shell -[`ssh-agent`]: https://en.wikipedia.org/wiki/Ssh-agent -[`gpg-agent`]: https://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html -[Checking for existing SSH keys]: https://docs.github.com/en/github/authenticating-to-github/checking-for-existing-ssh-keys -[computer files]: https://en.wikipedia.org/wiki/Computer_file -[host]: https://en.wikipedia.org/wiki/Server_(computing) -[Secure Shell]: https://en.wikipedia.org/wiki/Secure_Shell -[Working with SSH key passphrases]: https://docs.github.com/en/github/authenticating-to-github/working-with-ssh-key-passphrases +[#1775]: https://github.com/OpenINF/openinf.github.io/pull/1775 +[sharing Git credentials]: https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials +[commit signature verification]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification diff --git a/project-terms.txt b/project-terms.txt index 3eff81031..1e547f924 100755 --- a/project-terms.txt +++ b/project-terms.txt @@ -31,6 +31,7 @@ gpg gpg4win Gpg4win gpgconf +gpgsign gpgsm gpgtunnel Grault @@ -51,11 +52,11 @@ OpenINF openinfbot OpenINFbot OpenINFBot +openpgp outro pinentry Potenti PowerShell -psusan Quux Renovatebot rubocop @@ -64,10 +65,10 @@ scssify sdcard SDK Servagility +signingkey siteify skipcq SLOCs smartcard soonish UI -updatestartuptty From 9954beb67af1a7ac5e213f7f16b8a86a6a47cb97 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 01:26:01 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Acorrect=20?= =?UTF-8?q?the=20GitHub=20Desktop=20signing=20claims=20in=20agent-forwardi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit's rewrite asserted that GitHub Desktop has an "Automatically sign commits" preference which configures gpg.format=ssh for you. It has no such setting. Desktop has no commit-signing UI and no key of its own; it shells out to Git and inherits whatever git config already says, so configuring Git is the whole job whether you commit from Desktop or a terminal. Says so explicitly now, and adds the thing that actually misleads people here: a "Verified" badge on GitHub proves nothing about local signing. Commits made or squash-merged through github.com are signed server-side with GitHub's web-flow key (B5690EEEBB952194) and render identically to locally signed ones. Every signature in this repository's history is that key -- there is not one locally signed commit -- which is easy to mistake for working local signing. Adds a `git cat-file commit HEAD` check so the question can be settled by looking rather than assuming. Also notes that step 2 has to run on the host: the container gets its own copy of ~/.gitconfig at build time, so running those commands inside the container configures only the container and leaves the Mac untouched. Co-Authored-By: Claude Sonnet 5 --- collections/_docs/agent-forwarding.md | 35 +++++++++++++++++++++------ project-terms.txt | 1 + 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 702894008..e4b41599c 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -57,8 +57,16 @@ forwarded agent. No private key material is ever copied into the container. ## Setting up SSH-format signing (recommended) -This is what GitHub Desktop's own "Automatically sign commits" preference -configures, so if you sign from Desktop, use this format. +Recommended because it's the less fiddly of the two to get working across host +and container, not because any tool requires it. + +Note that GitHub Desktop has no commit-signing setting of its own and no key of +its own. It shells out to Git and inherits whatever `git config` says, so the +steps below are the entire setup whether you commit from Desktop or from a +terminal. A "Verified" badge on GitHub is _not_ evidence that local signing is +configured: commits made or squash-merged through github.com are signed +server-side with GitHub's own web-flow key, which looks identical on the site +and involves nothing on your machine. 1. Make sure the key you want to sign with is loaded into your platform's SSH agent: @@ -74,8 +82,10 @@ configures, so if you sign from Desktop, use this format. ssh-add --apple-use-keychain ~/.ssh/id_ed25519 ``` -2. Point Git at it. GitHub Desktop's signing preference does this for you; run - it by hand if you sign from the CLI instead: +2. Point Git at it. Run this **on the host**, in a host terminal -- the + container gets its own copy of `~/.gitconfig` at build time, so running it + inside the container configures only the container and silently leaves the + Mac unchanged: ```console git config --global gpg.format ssh @@ -89,9 +99,20 @@ configures, so if you sign from Desktop, use this format. forwarded into _this_ session doesn't have exactly one identity -- check `ssh-add -l` on the host first. -GitHub Desktop needs the key to exist as a real file on disk; it does not work -with agent-only or Secure Enclave-backed keys. A plain `ssh-keygen`-generated -key pair works better here than a hardware-backed one. +Use an ordinary `ssh-keygen`-generated key pair here. Keys that exist only +inside a Secure Enclave or an external agent, with no public key file on disk, +are a poor fit: `user.signingkey` has to name a real path, on the host and in +the container both. + +To confirm signing is actually working locally rather than assuming it, commit +and then check that the object really carries a signature: + +```console +git cat-file commit HEAD | head -20 +``` + +A locally signed commit has a `gpgsig` header (`BEGIN SSH SIGNATURE` for this +format). No header means the commit is unsigned no matter what the settings say. Signature _verification_ (`git log --show-signature`, the "Verified" badge on GitHub) is a separate concern from signing and isn't covered above -- see diff --git a/project-terms.txt b/project-terms.txt index 1e547f924..1b0c894a5 100755 --- a/project-terms.txt +++ b/project-terms.txt @@ -31,6 +31,7 @@ gpg gpg4win Gpg4win gpgconf +gpgsig gpgsign gpgsm gpgtunnel From 0ee219f2500b318ca690b718e783ad3801427b80 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 01:35:28 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Adocument?= =?UTF-8?q?=20the=20per-repo=20commit.gpgsign=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `commit.gpgsign = false` in .git/config beats `true` in the global config and turns signing off for every tool touching the clone, while every `--global` check keeps reporting that signing is enabled. Nothing in a diff shows it either, since .git/config isn't version controlled -- so it is worth ruling out early rather than after re-auditing the global config. Worth documenting rather than only fixing: the working tree is bind-mounted from the host, so .git is shared, and a `--local` setting applied from inside the container silently applies to the host clone as well. Co-Authored-By: Claude Sonnet 5 --- collections/_docs/agent-forwarding.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index e4b41599c..3569b1e83 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -153,6 +153,19 @@ If you use an actual OpenPGP key instead: `post-start.sh` writes lives in the container's filesystem, not a volume, so a rebuild removes it. It gets rewritten on the next attach as long as the agent still has exactly one identity at that point. +- **Everything looks configured, but commits still come out unsigned** -- check + for a per-repository override before re-checking anything global: + + ```console + git config --local --get commit.gpgsign + ``` + + `false` here beats `commit.gpgsign = true` in your global config, silently and + for every tool touching the clone. It is worth ruling out early: `.git/config` + isn't version controlled, so nothing in a PR can fix it and nothing in a diff + reveals it, and because the working tree is bind-mounted from the host, a + `--local` setting applied inside the container is applied to the host clone + too. Clear it with `git config --local --unset commit.gpgsign`. From d885f3c11e8c799ef2311f990ba6dc38f0026998 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 02:36:04 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=90=8B=F0=9F=94=A7=EF=BC=9Awrite=20th?= =?UTF-8?q?e=20forwarded=20signing=20key=20under=20$HOME,=20not=20the=20ho?= =?UTF-8?q?st's=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit post-start.sh wrote the forwarded agent's public key to user.signingkey's existing path, which VS Code copies verbatim from the host's gitconfig -- typically /Users//.ssh/id_ed25519.pub on a Mac, /home//... on Linux. Both live under a directory this container's non-root `node` user has no write access to (/Users and /home are root-owned, 755), so `mkdir -p` on that literal path failed with EACCES every time, on every host OS, not just macOS. `set -uo pipefail` without `-e` let the failure pass silently, so the script still printed "Commit signing ready" while nothing had been written -- confirmed live: git commit -S failed with "Couldn't load public key /Users/derek/.ssh/id_ed25519.pub: No such file or directory" right after a fresh rebuild reported success. Now writes to $HOME/.ssh/, a path the container user actually owns, and repoints the container's own copy of user.signingkey there instead of trying to recreate the host's path byte-for-byte. Verified with a real commit against the forwarded agent: signing succeeds and the object carries a gpgsig header. agent-forwarding.md updated to match: the host and container signingkey paths no longer need to be identical, since post-start.sh retargets the container's config to wherever it actually writes the file. Co-Authored-By: Claude Sonnet 5 --- .devcontainer/post-start.sh | 37 +++++++++++++++++++------- collections/_docs/agent-forwarding.md | 38 ++++++++++++++++++--------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh index ed96f354e..33aa84ce7 100755 --- a/.devcontainer/post-start.sh +++ b/.devcontainer/post-start.sh @@ -22,6 +22,15 @@ set -uo pipefail # directory" no matter how many times post-create.sh's `commit.gpgsign true` # hint is followed. # +# That copied path is also, almost always, a path this container's non-root +# user cannot create: a Mac's /Users/ or a Linux host's /home/ +# both live under a root-owned directory the container's `node` user has no +# write access to, so a bare `mkdir -p` on the host's literal path fails with +# EACCES regardless of host OS. So this retargets `user.signingkey`, in the +# container's own copy of the gitconfig only, at a path under $HOME that +# `node` actually owns, and writes the forwarded public key there instead of +# trying to recreate the host's path byte-for-byte. +# # If the forwarded agent is holding exactly one identity, write it out so that # path resolves. This can't sign with the wrong key even if it guessed wrong: # the actual signature still goes through the agent, keyed by fingerprint, so @@ -29,16 +38,24 @@ set -uo pipefail # of silently mis-signing. if [ "$(git config --global gpg.format 2>/dev/null || true)" = "ssh" ]; then signingkey="$(git config --global user.signingkey 2>/dev/null || true)" - if [ -n "${signingkey}" ] && [ ! -f "${signingkey}" ]; then - identities="$(ssh-add -L 2>/dev/null || true)" - count="$(printf '%s\n' "${identities}" | grep -c '^ssh-' || true)" - if [ "${count}" -eq 1 ]; then - mkdir -p -m 700 "$(dirname "${signingkey}")" - printf '%s\n' "${identities}" >"${signingkey}" - chmod 644 "${signingkey}" - echo "==> Commit signing ready (${signingkey}, from forwarded SSH agent)" - else - echo "==> Commit signing NOT ready: forwarded SSH agent has ${count} identities, need exactly 1 to write ${signingkey}" >&2 + if [ -n "${signingkey}" ]; then + container_signingkey="${HOME}/.ssh/$(basename "${signingkey}")" + if [ ! -f "${container_signingkey}" ]; then + identities="$(ssh-add -L 2>/dev/null || true)" + count="$(printf '%s\n' "${identities}" | grep -c '^ssh-' || true)" + if [ "${count}" -eq 1 ]; then + mkdir -p -m 700 "$(dirname "${container_signingkey}")" + printf '%s\n' "${identities}" >"${container_signingkey}" + chmod 644 "${container_signingkey}" + else + echo "==> Commit signing NOT ready: forwarded SSH agent has ${count} identities, need exactly 1 to write ${container_signingkey}" >&2 + fi + fi + if [ -f "${container_signingkey}" ]; then + if [ "${signingkey}" != "${container_signingkey}" ]; then + git config --global user.signingkey "${container_signingkey}" + fi + echo "==> Commit signing ready (${container_signingkey}, from forwarded SSH agent)" fi fi fi diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 3569b1e83..4bc1036ab 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -40,17 +40,23 @@ That gap matters because of how the two signing formats differ: `ssh-keygen -Y sign -f ...`. That `-f` argument is a **file path**, and `user.signingkey` in a gitconfig copied from your host points at a host path (typically `~/.ssh/id_ed25519.pub`) that has never existed in the - container. The forwarded agent doesn't help until that file exists. + container. The forwarded agent doesn't help until that file exists -- and the + container's non-root user usually can't even create it: a Mac's + `/Users/` or a Linux host's `/home/` both live under a root-owned + directory this container's `node` user has no write access to, so recreating + the host's path byte-for-byte fails with a permissions error regardless of + host OS. `.devcontainer/post-start.sh` closes that second gap. It runs on every container **start/attach**, unlike `post-create.sh`, which only ever runs once, when the container is first built -- too early, since the forwarded agent socket is a fresh, per-session thing set up on each attach. If `gpg.format` is `ssh` and the forwarded agent is holding exactly one identity, `post-start.sh` writes that -public key out to `user.signingkey`'s path. It deliberately does nothing if the -agent has zero identities (nothing to write) or more than one (no reliable way -to know which one you mean) -- watch its output on attach to see which case -you're in. +public key to a path under `$HOME/.ssh` in the container (not the host path +copied into `user.signingkey`) and repoints the container's own +`user.signingkey` at it. It deliberately does nothing if the agent has zero +identities (nothing to write) or more than one (no reliable way to know which +one you mean) -- watch its output on attach to see which case you're in. Either way, the actual cryptographic signing still happens on the host, via the forwarded agent. No private key material is ever copied into the container. @@ -101,8 +107,11 @@ and involves nothing on your machine. Use an ordinary `ssh-keygen`-generated key pair here. Keys that exist only inside a Secure Enclave or an external agent, with no public key file on disk, -are a poor fit: `user.signingkey` has to name a real path, on the host and in -the container both. +are a poor fit: `user.signingkey` has to name a real path on the host, and +`post-start.sh` needs a public key it can write out in the container. The two +paths don't need to match -- `post-start.sh` retargets the container's own +copy of `user.signingkey` to wherever it actually writes the file, under +`$HOME/.ssh`. To confirm signing is actually working locally rather than assuming it, commit and then check that the object really carries a signature: @@ -142,17 +151,20 @@ If you use an actual OpenPGP key instead: fact, not a container problem. Add the key on the host and reattach. - **`post-start.sh` reports more than one identity** -- it won't guess. Either unload the extra identities from the agent for this session, or, inside the - container, write the file yourself from the one you mean (path from - `git config --global user.signingkey`): + container, write the file yourself from the one you mean, then point + `user.signingkey` at it: ```console - ssh-add -L | grep > + ssh-add -L | grep > ~/.ssh/id_ed25519.pub + git config --global user.signingkey ~/.ssh/id_ed25519.pub ``` - **It worked before, stopped working after a container rebuild** -- the file - `post-start.sh` writes lives in the container's filesystem, not a volume, so a - rebuild removes it. It gets rewritten on the next attach as long as the agent - still has exactly one identity at that point. + `post-start.sh` writes, and the `user.signingkey` override pointing at it, + both live in the container's filesystem, not a volume, so a rebuild removes + them along with the host's copied-in gitconfig. Both get rewritten on the + next attach as long as the agent still has exactly one identity at that + point. - **Everything looks configured, but commits still come out unsigned** -- check for a per-repository override before re-checking anything global: From d63c63f83adcf3bd1085b2256eef23fc1d4fa383 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 02:51:53 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=90=8B=F0=9F=94=A7=EF=BC=9Aconfigure?= =?UTF-8?q?=20allowedSignersFile=20so=20signatures=20can=20be=20verified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signing already worked. Verification never did, and its failure mode is indistinguishable from signing being off, which is why it read as the former: $ git commit --allow-empty -S -m "Test signed commit" $ git log --show-signature -1 error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification No signature That `No signature` is the verifier reporting it could not run, not a statement about the commit -- `git cat-file commit HEAD` on the very same commit shows a `BEGIN SSH SIGNATURE` header. Nothing about the signing path was broken, so every re-check of `commit.gpgsign`, `user.signingkey` and the forwarded agent came back correct, and the `error:` line that says so is easy to lose above the commit it precedes. ssh-format verification needs a file mapping principals to the keys they may sign with, and Git ships no default location for one. So write `$HOME/.ssh/allowed_signers` next to the signing key post-start.sh already sets up, listing `user.email` against that key, and point `gpg.ssh.allowedSignersFile` at it. Scoped to this user's own key deliberately: other contributors' commits will report `No principal matched`, since verifying those means a shared allowed-signers file, which is a project decision and not one a container script should make on its own. Co-Authored-By: Claude Sonnet 5 --- .devcontainer/post-start.sh | 30 ++++++++++ collections/_docs/agent-forwarding.md | 81 ++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/.devcontainer/post-start.sh b/.devcontainer/post-start.sh index 33aa84ce7..2f722dd03 100755 --- a/.devcontainer/post-start.sh +++ b/.devcontainer/post-start.sh @@ -55,6 +55,36 @@ if [ "$(git config --global gpg.format 2>/dev/null || true)" = "ssh" ]; then if [ "${signingkey}" != "${container_signingkey}" ]; then git config --global user.signingkey "${container_signingkey}" fi + + # Signing and verifying are separate switches, and leaving the second one + # off makes the first one look broken. With only the above, `git commit + # -S` really does produce a signature -- `git cat-file commit HEAD` shows + # the `BEGIN SSH SIGNATURE` header -- but `git log --show-signature` + # answers: + # + # error: gpg.ssh.allowedSignersFile needs to be configured and exist + # for ssh signature verification + # No signature + # + # ssh-format verification needs a file mapping principals to the keys + # they may sign with, and Git ships no default location for one, so the + # verifier cannot run at all. `No signature` is it reporting that -- not + # a report about the commit, which is signed. Read as the latter, it + # sends you back to re-check signing settings that were correct the whole + # time, so write the file rather than leave that trap set. + email="$(git config --global user.email 2>/dev/null || true)" + if [ -n "${email}" ]; then + allowed_signers="${HOME}/.ssh/allowed_signers" + # Fields 1 and 2 only: `ssh-add -L` ends each line with the key's + # comment, and the allowed-signers grammar has no slot for one. + printf '%s %s\n' "${email}" \ + "$(awk '{print $1, $2}' "${container_signingkey}")" \ + >"${allowed_signers}" + git config --global gpg.ssh.allowedSignersFile "${allowed_signers}" + else + echo "==> Signature verification NOT configured: no user.email to attribute ${container_signingkey} to" >&2 + fi + echo "==> Commit signing ready (${container_signingkey}, from forwarded SSH agent)" fi fi diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 4bc1036ab..77e5eb353 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -41,11 +41,10 @@ That gap matters because of how the two signing formats differ: path**, and `user.signingkey` in a gitconfig copied from your host points at a host path (typically `~/.ssh/id_ed25519.pub`) that has never existed in the container. The forwarded agent doesn't help until that file exists -- and the - container's non-root user usually can't even create it: a Mac's - `/Users/` or a Linux host's `/home/` both live under a root-owned - directory this container's `node` user has no write access to, so recreating - the host's path byte-for-byte fails with a permissions error regardless of - host OS. + container's non-root user usually can't even create it: a Mac's `/Users/` + or a Linux host's `/home/` both live under a root-owned directory this + container's `node` user has no write access to, so recreating the host's path + byte-for-byte fails with a permissions error regardless of host OS. `.devcontainer/post-start.sh` closes that second gap. It runs on every container **start/attach**, unlike `post-create.sh`, which only ever runs once, when the @@ -109,8 +108,8 @@ Use an ordinary `ssh-keygen`-generated key pair here. Keys that exist only inside a Secure Enclave or an external agent, with no public key file on disk, are a poor fit: `user.signingkey` has to name a real path on the host, and `post-start.sh` needs a public key it can write out in the container. The two -paths don't need to match -- `post-start.sh` retargets the container's own -copy of `user.signingkey` to wherever it actually writes the file, under +paths don't need to match -- `post-start.sh` retargets the container's own copy +of `user.signingkey` to wherever it actually writes the file, under `$HOME/.ssh`. To confirm signing is actually working locally rather than assuming it, commit @@ -123,10 +122,59 @@ git cat-file commit HEAD | head -20 A locally signed commit has a `gpgsig` header (`BEGIN SSH SIGNATURE` for this format). No header means the commit is unsigned no matter what the settings say. -Signature _verification_ (`git log --show-signature`, the "Verified" badge on -GitHub) is a separate concern from signing and isn't covered above -- see -GitHub's docs on [commit signature verification][] if you need that working -locally too. +Prefer that check over `git log --show-signature` when the question is whether +_signing_ works: `cat-file` reads the commit, while `--show-signature` also has +to verify it, which is a separate mechanism that can fail on its own. See +[below](#when-show-signature-says-no-signature) for what that looks like. + +## Verifying signatures locally + +Verification is a distinct mechanism from signing, with its own configuration +and its own failure modes -- a commit can be perfectly signed and still fail to +verify here. For SSH-format signatures, Git needs an [allowed signers][] file +mapping each principal (an email address) to the keys it may sign with. Unlike +GPG, where the keyring is discovered automatically, Git has no default location +for this file, so verification cannot run at all until +`gpg.ssh.allowedSignersFile` names one. + +`post-start.sh` writes `$HOME/.ssh/allowed_signers` alongside the signing key it +already sets up, listing your `user.email` against that key, and points the +config at it. That is enough for `git log --show-signature` to report +`Good "git" signature`. + +Two things it deliberately does not attempt: + +- **Anyone else's commits.** The file lists your key and no one else's, so other + contributors' signed commits report `No principal matched`. Verifying those + means maintaining a shared allowed-signers file, which is a project-wide + decision rather than something a container script should invent. +- **Trust beyond your own attestation.** You are asserting that this key belongs + to this address. That makes local verification meaningful for catching a + misconfigured or swapped key; it is not third-party attestation the way + GitHub's "Verified" badge is. GitHub does its own check against the keys + registered on your account -- see its docs on [commit signature + verification][], and note that a key has to be added as a **signing** key + there, separately from the same key added for authentication. + +### When show-signature says "No signature" + +If the allowed-signers file is missing or unconfigured, +`git log --show-signature` prints (wrapped here for width): + +```console +error: gpg.ssh.allowedSignersFile needs to be configured and exist + for ssh signature verification +No signature +``` + +`No signature` here is the verifier reporting that it could not run -- not a +statement about the commit, which may well be signed. The wording invites the +opposite reading, and acting on it means re-checking `commit.gpgsign`, +`user.signingkey` and the agent, all of which were fine. Confirm with +`git cat-file commit HEAD` before changing any signing setting: a +`BEGIN SSH SIGNATURE` header means signing works and only verification needs +attention. Note that the `error:` line is easy to miss when it scrolls past +above the commit, or when a pager or tool shows only the commit body. ## Setting up GPG-format signing @@ -162,9 +210,13 @@ If you use an actual OpenPGP key instead: - **It worked before, stopped working after a container rebuild** -- the file `post-start.sh` writes, and the `user.signingkey` override pointing at it, both live in the container's filesystem, not a volume, so a rebuild removes - them along with the host's copied-in gitconfig. Both get rewritten on the - next attach as long as the agent still has exactly one identity at that - point. + them along with the host's copied-in gitconfig. Both get rewritten on the next + attach as long as the agent still has exactly one identity at that point. +- **`git log --show-signature` says `No signature`** -- establish that the + commit is actually unsigned before treating it as a signing problem, since + that message is also what a verifier that could not run prints. + `git cat-file commit HEAD` settles it; see + [above](#when-show-signature-says-no-signature). - **Everything looks configured, but commits still come out unsigned** -- check for a per-repository override before re-checking anything global: @@ -183,6 +235,7 @@ If you use an actual OpenPGP key instead: [#1775]: https://github.com/OpenINF/openinf.github.io/pull/1775 +[allowed signers]: https://man.openbsd.org/ssh-keygen#ALLOWED_SIGNERS [sharing Git credentials]: https://code.visualstudio.com/remote/advancedcontainers/sharing-git-credentials [commit signature verification]: https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification From 70946a990164a3723e092ddebb9f4e86a935c357 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 03:03:20 +0000 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Aclose=20tw?= =?UTF-8?q?o=20coherence=20gaps=20left=20in=20the=20verification=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The rebuild-wipes-it troubleshooting entry named only the signing-key file. `allowed_signers` and `gpg.ssh.allowedSignersFile` live under the same non-volume path and are wiped by a rebuild the same way, but weren't mentioned; a reader who rebuilt would find signing restored and verification silently not, with no entry pointing at why. Broadened the bullet and added a sibling entry for post-start.sh's other verification failure mode, missing user.email, to match the existing zero/multiple- identity entries in shape. - The new "No signature" subheading used a quoted phrase -- `### When show-signature says "No signature"` -- and both `[above]`/ `[below]` links pointed at `#when-show-signature-says-no-signature`. Rendering it through this repo's actual markdown-it-anchor pipeline shows the literal id is `when-show-signature-says-%22no-signature%22`: the slugifier percent-encodes quotes rather than stripping them, unlike GitHub's slugger. Both anchor links were dead. Dropped the quotes from the heading so the generated id matches what the links already assumed. Co-Authored-By: Claude Sonnet 5 --- collections/_docs/agent-forwarding.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 77e5eb353..279e01858 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -156,7 +156,7 @@ Two things it deliberately does not attempt: verification][], and note that a key has to be added as a **signing** key there, separately from the same key added for authentication. -### When show-signature says "No signature" +### When show-signature says No signature If the allowed-signers file is missing or unconfigured, `git log --show-signature` prints (wrapped here for width): @@ -207,11 +207,21 @@ If you use an actual OpenPGP key instead: git config --global user.signingkey ~/.ssh/id_ed25519.pub ``` -- **It worked before, stopped working after a container rebuild** -- the file - `post-start.sh` writes, and the `user.signingkey` override pointing at it, - both live in the container's filesystem, not a volume, so a rebuild removes - them along with the host's copied-in gitconfig. Both get rewritten on the next - attach as long as the agent still has exactly one identity at that point. +- **It worked before, stopped working after a container rebuild** -- the signing + key and `allowed_signers` files `post-start.sh` writes, and the + `user.signingkey`/`gpg.ssh.allowedSignersFile` overrides pointing at them, all + live in the container's filesystem, not a volume, so a rebuild removes them + along with the host's copied-in gitconfig. Everything gets rewritten on the + next attach as long as the agent still has exactly one identity at that point. +- **`post-start.sh` says "Signature verification NOT configured: no + user.email"** -- it needs `user.email` to know which principal to list against + your key in `allowed_signers`, and won't guess one. Set it (globally, on the + host, same as the other signing settings) and reattach: + + ```console + git config --global user.email you@example.com + ``` + - **`git log --show-signature` says `No signature`** -- establish that the commit is actually unsigned before treating it as a signing problem, since that message is also what a verifier that could not run prints. From cde98058d8e7654014b0779acf69f2ed3858b616 Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 03:14:34 +0000 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Ateach=20cs?= =?UTF-8?q?pell=20repoints,=20retargets,=20unconfigured?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Verify Markdown step failed on these three: cspell has no dictionary entry for them and agent-forwarding.md's rewrites introduced all three. Added them to project-terms.txt in their alphabetical slots, the same as gpgsig picked up two commits ago on this branch. Co-Authored-By: Claude Sonnet 5 --- project-terms.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project-terms.txt b/project-terms.txt index 1b0c894a5..f54c3c0bf 100755 --- a/project-terms.txt +++ b/project-terms.txt @@ -60,6 +60,8 @@ Potenti PowerShell Quux Renovatebot +repoints +retargets rubocop screencap scssify @@ -73,3 +75,4 @@ SLOCs smartcard soonish UI +unconfigured From f6ec8e7c3dbc20c3523bb0b9a24ecc44124dfdab Mon Sep 17 00:00:00 2001 From: Derek Lewis Date: Sun, 9 Aug 2026 03:22:33 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=93=96=F0=9F=94=A7=EF=BC=9Aswitch=20t?= =?UTF-8?q?he=20setup=20steps=20to=20bullets=20so=20ec-checker=20stops=20f?= =?UTF-8?q?ailing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's Test step runs editorconfig-checker, which this branch had never actually exercised end to end -- the earlier commits landed before this was ever pushed as a PR, so the "Lint and test" job's markdown-only path was the only thing that had run against them. It requires every continuation line's indentation to be a multiple of two spaces, repo-wide, no per-language exception. Both "Setting up ... signing" sections used numbered lists whose continuation lines -- code fences and follow-on sentences under each step -- were indented 3 spaces, matching a single-digit `1. ` marker. That's also what Prettier's markdown printer produces and keeps idempotent for ordered lists, and it's CommonMark-correct, but 3 isn't a multiple of 2. Padding the marker to `1. ` (making the continuation indent 4) satisfies ec-checker but not markdownlint's MD030, which wants exactly one space after either marker. No combination of ordered-list spacing satisfies every tool at once. Switched both lists to `-` bullets instead, which this same doc's Troubleshooting section already uses with nested fences at a 2-space indent -- already proven compatible with all three tools. Numbering carried no meaning nothing else in the doc depended on (no "step 2" cross-references), so nothing is lost. Also caught by the same rule: the wrapped second line of the "No signature" error message inside its code fence was indented 7 spaces to align under "error: "; reindented to 2. Co-Authored-By: Claude Sonnet 5 --- collections/_docs/agent-forwarding.md | 69 +++++++++++++-------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/collections/_docs/agent-forwarding.md b/collections/_docs/agent-forwarding.md index 279e01858..b1edd61c9 100644 --- a/collections/_docs/agent-forwarding.md +++ b/collections/_docs/agent-forwarding.md @@ -73,36 +73,35 @@ configured: commits made or squash-merged through github.com are signed server-side with GitHub's own web-flow key, which looks identical on the site and involves nothing on your machine. -1. Make sure the key you want to sign with is loaded into your platform's SSH - agent: +- Make sure the key you want to sign with is loaded into your platform's SSH + agent: - ```console - ssh-add -l - ``` + ```console + ssh-add -l + ``` - If it isn't listed, add it. On macOS, add `--apple-use-keychain` so it - survives a reboot instead of needing `ssh-add` again every session: + If it isn't listed, add it. On macOS, add `--apple-use-keychain` so it + survives a reboot instead of needing `ssh-add` again every session: - ```console - ssh-add --apple-use-keychain ~/.ssh/id_ed25519 - ``` + ```console + ssh-add --apple-use-keychain ~/.ssh/id_ed25519 + ``` -2. Point Git at it. Run this **on the host**, in a host terminal -- the - container gets its own copy of `~/.gitconfig` at build time, so running it - inside the container configures only the container and silently leaves the - Mac unchanged: +- Point Git at it. Run this **on the host**, in a host terminal -- the container + gets its own copy of `~/.gitconfig` at build time, so running it inside the + container configures only the container and silently leaves the Mac unchanged: - ```console - git config --global gpg.format ssh - git config --global user.signingkey ~/.ssh/id_ed25519.pub - git config --global commit.gpgsign true - ``` + ```console + git config --global gpg.format ssh + git config --global user.signingkey ~/.ssh/id_ed25519.pub + git config --global commit.gpgsign true + ``` -3. Reopen or rebuild the devcontainer and watch `post-start.sh`'s output on - attach. `Commit signing ready` means the public key was found in the - forwarded agent and written into the container. Anything else means the agent - forwarded into _this_ session doesn't have exactly one identity -- check - `ssh-add -l` on the host first. +- Reopen or rebuild the devcontainer and watch `post-start.sh`'s output on + attach. `Commit signing ready` means the public key was found in the forwarded + agent and written into the container. Anything else means the agent forwarded + into _this_ session doesn't have exactly one identity -- check `ssh-add -l` on + the host first. Use an ordinary `ssh-keygen`-generated key pair here. Keys that exist only inside a Secure Enclave or an external agent, with no public key file on disk, @@ -163,7 +162,7 @@ If the allowed-signers file is missing or unconfigured, ```console error: gpg.ssh.allowedSignersFile needs to be configured and exist - for ssh signature verification + for ssh signature verification No signature ``` @@ -180,18 +179,18 @@ above the commit, or when a pager or tool shows only the commit body. If you use an actual OpenPGP key instead: -1. Make sure `gpg-agent` on the host has your key and is reachable the normal - way (`gpg --list-secret-keys` should show it). -2. Leave `gpg.format` unset (or set it to `openpgp`), and set: +- Make sure `gpg-agent` on the host has your key and is reachable the normal way + (`gpg --list-secret-keys` should show it). +- Leave `gpg.format` unset (or set it to `openpgp`), and set: - ```console - git config --global user.signingkey - git config --global commit.gpgsign true - ``` + ```console + git config --global user.signingkey + git config --global commit.gpgsign true + ``` -3. Nothing in this repo's devcontainer needs configuring beyond what's already - there: `gnupg` ships in the base image, and the forwarded agent socket is all - `gpg` needs. +- Nothing in this repo's devcontainer needs configuring beyond what's already + there: `gnupg` ships in the base image, and the forwarded agent socket is all + `gpg` needs. ## Troubleshooting