From 83049c0393c4a234813fcc838b16a05c1ab364e8 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 09:03:49 -0500 Subject: [PATCH 1/6] fix(ssh): skip GPG signing key when gpg.format is ssh (#731) When the user has SSH-based commit signing configured (gpg.format=ssh), the GPG agent forwarding code no longer passes the SSH key path as --gitkey to setup-gpg. SSH signing keys are handled by the separate SSH signature helper path. --- cmd/ssh.go | 33 +++++++++++++++++++++++++-------- cmd/ssh_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 cmd/ssh_test.go diff --git a/cmd/ssh.go b/cmd/ssh.go index 453473871..82fd67b86 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -710,12 +710,7 @@ func (cmd *SSHCmd) setupGPGAgent( gpgExtraSocketPath := strings.TrimSpace(string(gpgExtraSocketBytes)) log.Debugf("[GPG] detected gpg-agent socket path %s", gpgExtraSocketPath) - gitGpgKey, err := exec.Command("git", []string{"config", "user.signingKey"}...).Output() - if err != nil { - log.Debugf("[GPG] no git signkey detected, skipping") - } else { - log.Debugf("[GPG] detected git sign key %s", gitGpgKey) - } + gitKey := gpgSigningKey(log) cmd.ReverseForwardPorts = append(cmd.ReverseForwardPorts, gpgExtraSocketPath) @@ -735,8 +730,7 @@ func (cmd *SSHCmd) setupGPGAgent( forwardAgent = append(forwardAgent, "--debug") } - if len(gitGpgKey) > 0 { - gitKey := strings.TrimSpace(string(gitGpgKey)) + if gitKey != "" { forwardAgent = append(forwardAgent, "--gitkey") forwardAgent = append(forwardAgent, gitKey) } @@ -770,6 +764,29 @@ func (cmd *SSHCmd) setupGPGAgent( return nil } +// gpgSigningKey returns the user's GPG signing key from git config, +// or empty string if no key is configured or the signing format is SSH +// (SSH signing keys are handled by the separate SSH signature helper). +func gpgSigningKey(log log.Logger) string { + format, err := exec.Command("git", "config", "--get", "gpg.format").Output() + if err == nil && strings.TrimSpace(string(format)) == "ssh" { + log.Debugf( + "[GPG] gpg.format is ssh, skipping GPG signing key (handled by SSH signing helper)", + ) + return "" + } + + key, err := exec.Command("git", "config", "--get", "user.signingKey").Output() + if err != nil { + log.Debugf("[GPG] no git signkey detected, skipping") + return "" + } + + result := strings.TrimSpace(string(key)) + log.Debugf("[GPG] detected git sign key %s", result) + return result +} + func startSSHKeepAlive( ctx context.Context, client *ssh.Client, diff --git a/cmd/ssh_test.go b/cmd/ssh_test.go new file mode 100644 index 000000000..a6783f7b3 --- /dev/null +++ b/cmd/ssh_test.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/skevetter/log" + "github.com/stretchr/testify/assert" +) + +func writeGitConfig(t *testing.T, content string) { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", home) + t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(home, ".gitconfig")) + err := os.WriteFile(filepath.Join(home, ".gitconfig"), []byte(content), 0o600) + assert.NoError(t, err) +} + +func TestGpgSigningKey_GPGFormat(t *testing.T) { + writeGitConfig(t, "[user]\n\tsigningKey = TESTKEY123\n") + result := gpgSigningKey(log.Discard) + assert.Equal(t, "TESTKEY123", result) +} + +func TestGpgSigningKey_SSHFormat_Skipped(t *testing.T) { + writeGitConfig( + t, + "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = /home/user/.ssh/id_ed25519.pub\n", + ) + result := gpgSigningKey(log.Discard) + assert.Empty(t, result) +} + +func TestGpgSigningKey_NoKeyConfigured(t *testing.T) { + writeGitConfig(t, "[user]\n\tname = Test\n") + result := gpgSigningKey(log.Discard) + assert.Empty(t, result) +} + +func TestGpgSigningKey_X509Format_Returned(t *testing.T) { + writeGitConfig(t, "[gpg]\n\tformat = x509\n[user]\n\tsigningKey = /path/to/cert\n") + result := gpgSigningKey(log.Discard) + assert.Equal(t, "/path/to/cert", result) +} From a82e6527559cbf105fc9b25a33fd99c75e08dd45 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 09:09:14 -0500 Subject: [PATCH 2/6] fix(gpg): make signing key setup non-fatal in setup-gpg (#731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting user.signingKey in the container is optional — if it fails, GPG agent forwarding and the SSH server should still start. This prevents a bad signing key configuration from tearing down the entire tunnel. --- cmd/agent/workspace/setup_gpg.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/agent/workspace/setup_gpg.go b/cmd/agent/workspace/setup_gpg.go index 26cb8841b..ecd546f66 100644 --- a/cmd/agent/workspace/setup_gpg.go +++ b/cmd/agent/workspace/setup_gpg.go @@ -135,10 +135,8 @@ func (cmd *SetupGPGCmd) Run(ctx context.Context, log log.Logger) error { if gpgConf.GitKey != "" { log.Debugf("Setup git signing key") - err = gitcredentials.SetupGpgGitKey(gpgConf.GitKey) - if err != nil { - log.Errorf("Setup git signing key: %v", err) - return err + if err := gitcredentials.SetupGpgGitKey(gpgConf.GitKey); err != nil { + log.Warnf("Setup git signing key failed (non-fatal): %v", err) } } From 52edd73df54165cf3f49810080514fb4577e10ce Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 09:11:56 -0500 Subject: [PATCH 3/6] test(e2e): add GPG forwarding with SSH signing format test (#731) Validates that workspace starts successfully when GPG agent forwarding is enabled and the host has gpg.format=ssh with an SSH signing key. This is the exact scenario reported in issue #731. --- e2e/tests/ssh/ssh.go | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index 7bc39200c..c3a6a1130 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -61,6 +61,65 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord }, ) + ginkgo.It( + "should start workspace with GPG forwarding when host uses SSH signing format", + ginkgo.Label("gpg"), + ginkgo.SpecTimeout(5*time.Minute), + func(ctx ginkgo.SpecContext) { + if runtime.GOOS == osWindows { + ginkgo.Skip("skipping on windows") + } + + tempDir, err := framework.CopyToTempDir("tests/ssh/testdata/gpg-forwarding") + framework.ExpectNoError(err) + + f := framework.NewDefaultFramework(initialDir + "/bin") + _ = f.DevPodProviderAdd(ctx, "docker") + err = f.DevPodProviderUse(ctx, "docker") + framework.ExpectNoError(err) + + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevPodWorkspaceDelete(cleanupCtx, tempDir) + framework.CleanupTempDir(initialDir, tempDir) + }) + + // Configure host git to use SSH signing format with an SSH key, + // simulating the reporter's setup from issue #731. + sshKeyDir, err := os.MkdirTemp("", "devpod-gpg-ssh-test") + framework.ExpectNoError(err) + defer func() { _ = os.RemoveAll(sshKeyDir) }() + + keyPath := filepath.Join(sshKeyDir, "id_ed25519") + // #nosec G204 -- test command with controlled arguments + err = exec.Command( + "ssh-keygen", "-t", "ed25519", "-f", keyPath, "-N", "", "-q", + ).Run() + framework.ExpectNoError(err) + + // Create a temporary gitconfig with SSH signing format + gitConfigDir := ginkgo.GinkgoT().TempDir() + gitConfigPath := filepath.Join(gitConfigDir, ".gitconfig") + gitConfigContent := "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = " + + keyPath + ".pub\n\tname = Test\n\temail = test@test.com\n" + err = os.WriteFile(gitConfigPath, []byte(gitConfigContent), 0o600) + framework.ExpectNoError(err) + ginkgo.GinkgoT().Setenv("GIT_CONFIG_GLOBAL", gitConfigPath) + + // Start workspace with GPG agent forwarding enabled. + // Before the fix, this would fail because setup-gpg would try + // to use the SSH key path as a GPG key and crash. + err = f.DevPodUp(ctx, tempDir, "--gpg-agent-forwarding") + framework.ExpectNoError(err) + + // Verify SSH server is running and reachable + devpodSSHDeadline := time.Now().Add(20 * time.Second) + devpodSSHCtx, cancelSSH := context.WithDeadline(ctx, devpodSSHDeadline) + defer cancelSSH() + err = f.DevPodSSHEchoTestString(devpodSSHCtx, tempDir) + framework.ExpectNoError(err) + }, + ) + ginkgo.It( "should set up git SSH signature helper and sign a commit", ginkgo.SpecTimeout(7*time.Minute), From 21b22641e20a28f5871b0b7a1151539d3d80da70 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 18:40:21 -0500 Subject: [PATCH 4/6] style(e2e): use DeferCleanup for consistent test cleanup (#731) --- e2e/tests/ssh/ssh.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index c3a6a1130..262a62199 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -87,7 +87,7 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord // simulating the reporter's setup from issue #731. sshKeyDir, err := os.MkdirTemp("", "devpod-gpg-ssh-test") framework.ExpectNoError(err) - defer func() { _ = os.RemoveAll(sshKeyDir) }() + ginkgo.DeferCleanup(func() { _ = os.RemoveAll(sshKeyDir) }) keyPath := filepath.Join(sshKeyDir, "id_ed25519") // #nosec G204 -- test command with controlled arguments From ee0d75efab7df2d6d3a1b6e914b9da535a632363 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 19:21:12 -0500 Subject: [PATCH 5/6] style(e2e): remove unnecessary comments from GPG forwarding test --- e2e/tests/ssh/ssh.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index 262a62199..6fbc99fc2 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -83,8 +83,6 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord framework.CleanupTempDir(initialDir, tempDir) }) - // Configure host git to use SSH signing format with an SSH key, - // simulating the reporter's setup from issue #731. sshKeyDir, err := os.MkdirTemp("", "devpod-gpg-ssh-test") framework.ExpectNoError(err) ginkgo.DeferCleanup(func() { _ = os.RemoveAll(sshKeyDir) }) @@ -96,7 +94,6 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord ).Run() framework.ExpectNoError(err) - // Create a temporary gitconfig with SSH signing format gitConfigDir := ginkgo.GinkgoT().TempDir() gitConfigPath := filepath.Join(gitConfigDir, ".gitconfig") gitConfigContent := "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = " + @@ -105,13 +102,9 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord framework.ExpectNoError(err) ginkgo.GinkgoT().Setenv("GIT_CONFIG_GLOBAL", gitConfigPath) - // Start workspace with GPG agent forwarding enabled. - // Before the fix, this would fail because setup-gpg would try - // to use the SSH key path as a GPG key and crash. err = f.DevPodUp(ctx, tempDir, "--gpg-agent-forwarding") framework.ExpectNoError(err) - // Verify SSH server is running and reachable devpodSSHDeadline := time.Now().Add(20 * time.Second) devpodSSHCtx, cancelSSH := context.WithDeadline(ctx, devpodSSHDeadline) defer cancelSSH() From f885098b143e31009f4e993b5ce70a579c5bfc0f Mon Sep 17 00:00:00 2001 From: Samuel K Date: Mon, 13 Apr 2026 19:28:30 -0500 Subject: [PATCH 6/6] style(e2e): use GinkgoT().TempDir() consistently for temp dirs --- e2e/tests/ssh/ssh.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index 6fbc99fc2..0bf53f424 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -83,9 +83,7 @@ var _ = ginkgo.Describe("devpod ssh test suite", ginkgo.Label("ssh"), ginkgo.Ord framework.CleanupTempDir(initialDir, tempDir) }) - sshKeyDir, err := os.MkdirTemp("", "devpod-gpg-ssh-test") - framework.ExpectNoError(err) - ginkgo.DeferCleanup(func() { _ = os.RemoveAll(sshKeyDir) }) + sshKeyDir := ginkgo.GinkgoT().TempDir() keyPath := filepath.Join(sshKeyDir, "id_ed25519") // #nosec G204 -- test command with controlled arguments