From 2174cfa6e4c15d52046b7e380acb13482daccd16 Mon Sep 17 00:00:00 2001 From: Akash Manna Date: Thu, 18 Dec 2025 01:22:40 +0530 Subject: [PATCH] [JENKINS-71461] GIT_SSH_COMMAND diagnostics fail with some host key verification strategies --- .../plugins/gitclient/CliGitAPIImpl.java | 27 ++- .../GitHostKeyVerificationConfiguration.java | 25 +++ .../config.jelly | 4 + .../gitclient/CliGitAPISecurityTest.java | 176 ++++++++++++++++++ 4 files changed, 230 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java index 8fe922e6c8..6cf49ff3b6 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java @@ -2700,9 +2700,11 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept w.newLine(); w.write("setlocal enabledelayedexpansion"); w.newLine(); + String verboseFlag = isSshVerboseEnabled() ? " -vvv" : ""; w.write("\"" + sshexe.getAbsolutePath() + "\" -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" " - + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* "); + + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag + + " %* "); w.newLine(); } ssh.toFile().setExecutable(true, true); @@ -2724,8 +2726,10 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException w.newLine(); w.write("fi"); w.newLine(); + String verboseFlag = isSshVerboseEnabled() ? " -vvv" : ""; w.write("ssh -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" " - + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\""); + + getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag + + " \"$@\""); w.newLine(); } return createNonBusyExecutable(ssh); @@ -2768,6 +2772,25 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir) throws Gi return launchCommandIn(args, workDir, environment); } + /** + * Safely check if SSH verbose mode is enabled. + * Returns false if Jenkins instance is not available (e.g., during tests). + * + * @return true if SSH verbose mode is enabled, false otherwise + */ + private boolean isSshVerboseEnabled() { + try { + jenkins.model.Jenkins instance = jenkins.model.Jenkins.getInstanceOrNull(); + if (instance != null) { + return GitHostKeyVerificationConfiguration.get().isSshVerbose(); + } + } catch (Exception e) { + // If we can't get the configuration, default to false + LOGGER.log(Level.FINE, "Unable to get SSH verbose configuration", e); + } + return false; + } + private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars env) throws GitException, InterruptedException { return launchCommandIn(args, workDir, environment, TIMEOUT); diff --git a/src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java b/src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java index a3831d55a5..c22d4b481c 100644 --- a/src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java +++ b/src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java @@ -16,6 +16,8 @@ public class GitHostKeyVerificationConfiguration extends GlobalConfiguration imp private SshHostKeyVerificationStrategy sshHostKeyVerificationStrategy; + private boolean sshVerbose = false; + @Override public @NonNull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); @@ -35,6 +37,29 @@ public void setSshHostKeyVerificationStrategy( save(); } + /** + * Check if SSH verbose mode is enabled. + * When enabled, SSH commands will include -vvv flag for detailed diagnostic output. + * This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable. + * + * @return true if SSH verbose mode is enabled, false otherwise + */ + public boolean isSshVerbose() { + return sshVerbose; + } + + /** + * Set SSH verbose mode. + * When enabled, SSH commands will include -vvv flag for detailed diagnostic output. + * This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable. + * + * @param sshVerbose true to enable SSH verbose mode, false to disable + */ + public void setSshVerbose(boolean sshVerbose) { + this.sshVerbose = sshVerbose; + save(); + } + public static @NonNull GitHostKeyVerificationConfiguration get() { return GlobalConfiguration.all().getInstance(GitHostKeyVerificationConfiguration.class); } diff --git a/src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly b/src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly index 3461e5681e..8e8e35e599 100644 --- a/src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly @@ -2,5 +2,9 @@ + + + \ No newline at end of file diff --git a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISecurityTest.java b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISecurityTest.java index 60ade1b268..2ea1e228a3 100644 --- a/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISecurityTest.java +++ b/src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISecurityTest.java @@ -24,6 +24,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; /** * Security test that proves the environment variable approach prevents @@ -35,6 +36,7 @@ * * @author Mark Waite */ +@WithJenkins class CliGitAPISecurityTest { @TempDir @@ -286,4 +288,178 @@ private void executeWrapper(Path wrapper, Path keyFile) throws Exception { // That's fine - we're just checking for injection } } + + /** + * Test that SSH verbose mode is disabled by default (no -vvv flag) + */ + @Test + @Issue("JENKINS-71461") + void testSshVerboseModeDisabledByDefault() throws Exception { + workspace = new File(tempDir, "test-default"); + workspace.mkdirs(); + + Path keyFile = createMockSSHKey(workspace); + Path knownHosts = Files.createTempFile("known_hosts", ""); + + try { + // When Jenkins is not available, SSH verbose should default to false + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(workspace) + .using("git") + .getClient(); + CliGitAPIImpl git = (CliGitAPIImpl) gitClient; + + Path sshWrapper; + if (isWindows()) { + sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts); + } else { + sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts); + } + + String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8); + + // Verify -vvv flag is NOT present + assertFalse( + wrapperContent.contains("-vvv"), + "Wrapper should NOT contain -vvv flag when verbose mode is disabled"); + + } finally { + Files.deleteIfExists(knownHosts); + } + } + + /** + * Test that SSH verbose mode adds -vvv flag when enabled + */ + @Test + @Issue("JENKINS-71461") + void testSshVerboseModeEnabled() throws Exception { + // Skip if Jenkins instance is not available + if (jenkins.model.Jenkins.getInstanceOrNull() == null) { + return; + } + + workspace = new File(tempDir, "test-verbose"); + workspace.mkdirs(); + + Path keyFile = createMockSSHKey(workspace); + Path knownHosts = Files.createTempFile("known_hosts", ""); + + try { + // Enable SSH verbose mode + GitHostKeyVerificationConfiguration.get().setSshVerbose(true); + + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(workspace) + .using("git") + .getClient(); + CliGitAPIImpl git = (CliGitAPIImpl) gitClient; + + Path sshWrapper; + if (isWindows()) { + sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts); + } else { + sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts); + } + + String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8); + + // Verify -vvv flag IS present + assertTrue( + wrapperContent.contains("-vvv"), "Wrapper should contain -vvv flag when verbose mode is enabled"); + + } finally { + // Reset to default + GitHostKeyVerificationConfiguration.get().setSshVerbose(false); + Files.deleteIfExists(knownHosts); + } + } + + /** + * Test that SSH verbose mode flag is placed correctly in Unix wrapper + */ + @Test + @Issue("JENKINS-71461") + void testUnixSshVerboseFlagPlacement() throws Exception { + // Skip if Jenkins instance is not available or on Windows + if (jenkins.model.Jenkins.getInstanceOrNull() == null || isWindows()) { + return; + } + + workspace = new File(tempDir, "test-unix-verbose"); + workspace.mkdirs(); + + Path keyFile = createMockSSHKey(workspace); + Path knownHosts = Files.createTempFile("known_hosts", ""); + + try { + // Enable SSH verbose mode + GitHostKeyVerificationConfiguration.get().setSshVerbose(true); + + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(workspace) + .using("git") + .getClient(); + CliGitAPIImpl git = (CliGitAPIImpl) gitClient; + Path sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts); + + String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8); + + // Verify -vvv appears before "$@" (which represents additional args) + int vvvIndex = wrapperContent.indexOf("-vvv"); + int argsIndex = wrapperContent.indexOf("\"$@\""); + assertTrue(vvvIndex > 0, "-vvv flag should be present"); + assertTrue(argsIndex > 0, "\"$@\" should be present"); + assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before \"$@\""); + + } finally { + // Reset to default + GitHostKeyVerificationConfiguration.get().setSshVerbose(false); + Files.deleteIfExists(knownHosts); + } + } + + /** + * Test that SSH verbose mode flag is placed correctly in Windows wrapper + */ + @Test + @Issue("JENKINS-71461") + void testWindowsSshVerboseFlagPlacement() throws Exception { + // Skip if Jenkins instance is not available or on Unix + if (jenkins.model.Jenkins.getInstanceOrNull() == null || !isWindows()) { + return; // Skip on Unix + } + + workspace = new File(tempDir, "test-windows-verbose"); + workspace.mkdirs(); + + Path keyFile = createMockSSHKey(workspace); + Path knownHosts = Files.createTempFile("known_hosts", ""); + + try { + // Enable SSH verbose mode + GitHostKeyVerificationConfiguration.get().setSshVerbose(true); + + GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()) + .in(workspace) + .using("git") + .getClient(); + CliGitAPIImpl git = (CliGitAPIImpl) gitClient; + Path sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts); + + String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8); + + // Verify -vvv appears before %* (which represents additional args) + int vvvIndex = wrapperContent.indexOf("-vvv"); + int argsIndex = wrapperContent.indexOf("%*"); + assertTrue(vvvIndex > 0, "-vvv flag should be present"); + assertTrue(argsIndex > 0, "%* should be present"); + assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before %*"); + + } finally { + // Reset to default + GitHostKeyVerificationConfiguration.get().setSshVerbose(false); + Files.deleteIfExists(knownHosts); + } + } }