Skip to content

Commit 2174cfa

Browse files
[JENKINS-71461] GIT_SSH_COMMAND diagnostics fail with some host key verification strategies
1 parent 2953704 commit 2174cfa

4 files changed

Lines changed: 230 additions & 2 deletions

File tree

src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2700,9 +2700,11 @@ Path createWindowsGitSSH(Path key, String user, Path knownHosts) throws IOExcept
27002700
w.newLine();
27012701
w.write("setlocal enabledelayedexpansion");
27022702
w.newLine();
2703+
String verboseFlag = isSshVerboseEnabled() ? " -vvv" : "";
27032704
w.write("\"" + sshexe.getAbsolutePath()
27042705
+ "\" -i \"!JENKINS_GIT_SSH_KEYFILE!\" -l \"!JENKINS_GIT_SSH_USERNAME!\" "
2705-
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " %* ");
2706+
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag
2707+
+ " %* ");
27062708
w.newLine();
27072709
}
27082710
ssh.toFile().setExecutable(true, true);
@@ -2724,8 +2726,10 @@ Path createUnixGitSSH(Path key, String user, Path knownHosts) throws IOException
27242726
w.newLine();
27252727
w.write("fi");
27262728
w.newLine();
2729+
String verboseFlag = isSshVerboseEnabled() ? " -vvv" : "";
27272730
w.write("ssh -i \"$JENKINS_GIT_SSH_KEYFILE\" -l \"$JENKINS_GIT_SSH_USERNAME\" "
2728-
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + " \"$@\"");
2731+
+ getHostKeyFactory().forCliGit(listener).getVerifyHostKeyOption(knownHosts) + verboseFlag
2732+
+ " \"$@\"");
27292733
w.newLine();
27302734
}
27312735
return createNonBusyExecutable(ssh);
@@ -2768,6 +2772,25 @@ private String launchCommandIn(ArgumentListBuilder args, File workDir) throws Gi
27682772
return launchCommandIn(args, workDir, environment);
27692773
}
27702774

2775+
/**
2776+
* Safely check if SSH verbose mode is enabled.
2777+
* Returns false if Jenkins instance is not available (e.g., during tests).
2778+
*
2779+
* @return true if SSH verbose mode is enabled, false otherwise
2780+
*/
2781+
private boolean isSshVerboseEnabled() {
2782+
try {
2783+
jenkins.model.Jenkins instance = jenkins.model.Jenkins.getInstanceOrNull();
2784+
if (instance != null) {
2785+
return GitHostKeyVerificationConfiguration.get().isSshVerbose();
2786+
}
2787+
} catch (Exception e) {
2788+
// If we can't get the configuration, default to false
2789+
LOGGER.log(Level.FINE, "Unable to get SSH verbose configuration", e);
2790+
}
2791+
return false;
2792+
}
2793+
27712794
private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars env)
27722795
throws GitException, InterruptedException {
27732796
return launchCommandIn(args, workDir, environment, TIMEOUT);

src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class GitHostKeyVerificationConfiguration extends GlobalConfiguration imp
1616

1717
private SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> sshHostKeyVerificationStrategy;
1818

19+
private boolean sshVerbose = false;
20+
1921
@Override
2022
public @NonNull GlobalConfigurationCategory getCategory() {
2123
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
@@ -35,6 +37,29 @@ public void setSshHostKeyVerificationStrategy(
3537
save();
3638
}
3739

40+
/**
41+
* Check if SSH verbose mode is enabled.
42+
* When enabled, SSH commands will include -vvv flag for detailed diagnostic output.
43+
* This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable.
44+
*
45+
* @return true if SSH verbose mode is enabled, false otherwise
46+
*/
47+
public boolean isSshVerbose() {
48+
return sshVerbose;
49+
}
50+
51+
/**
52+
* Set SSH verbose mode.
53+
* When enabled, SSH commands will include -vvv flag for detailed diagnostic output.
54+
* This helps troubleshoot SSH connection issues without requiring GIT_SSH_COMMAND environment variable.
55+
*
56+
* @param sshVerbose true to enable SSH verbose mode, false to disable
57+
*/
58+
public void setSshVerbose(boolean sshVerbose) {
59+
this.sshVerbose = sshVerbose;
60+
save();
61+
}
62+
3863
public static @NonNull GitHostKeyVerificationConfiguration get() {
3964
return GlobalConfiguration.all().getInstance(GitHostKeyVerificationConfiguration.class);
4065
}

src/main/resources/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration/config.jelly

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@
22
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
33
<f:section title="Git Host Key Verification Configuration">
44
<f:dropdownDescriptorSelector field="sshHostKeyVerificationStrategy" title="Host Key Verification Strategy"/>
5+
<f:entry title="SSH Verbose Mode" field="sshVerbose">
6+
<f:checkbox title="Enable verbose SSH output for diagnostics"
7+
help="When enabled, SSH commands will include -vvv flag for detailed diagnostic output. This helps troubleshoot SSH connection issues without requiring the GIT_SSH_COMMAND environment variable."/>
8+
</f:entry>
59
</f:section>
610
</j:jelly>

src/test/java/org/jenkinsci/plugins/gitclient/CliGitAPISecurityTest.java

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.junit.jupiter.params.provider.Arguments;
2525
import org.junit.jupiter.params.provider.MethodSource;
2626
import org.jvnet.hudson.test.Issue;
27+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
2728

2829
/**
2930
* Security test that proves the environment variable approach prevents
@@ -35,6 +36,7 @@
3536
*
3637
* @author Mark Waite
3738
*/
39+
@WithJenkins
3840
class CliGitAPISecurityTest {
3941

4042
@TempDir
@@ -286,4 +288,178 @@ private void executeWrapper(Path wrapper, Path keyFile) throws Exception {
286288
// That's fine - we're just checking for injection
287289
}
288290
}
291+
292+
/**
293+
* Test that SSH verbose mode is disabled by default (no -vvv flag)
294+
*/
295+
@Test
296+
@Issue("JENKINS-71461")
297+
void testSshVerboseModeDisabledByDefault() throws Exception {
298+
workspace = new File(tempDir, "test-default");
299+
workspace.mkdirs();
300+
301+
Path keyFile = createMockSSHKey(workspace);
302+
Path knownHosts = Files.createTempFile("known_hosts", "");
303+
304+
try {
305+
// When Jenkins is not available, SSH verbose should default to false
306+
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
307+
.in(workspace)
308+
.using("git")
309+
.getClient();
310+
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
311+
312+
Path sshWrapper;
313+
if (isWindows()) {
314+
sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);
315+
} else {
316+
sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);
317+
}
318+
319+
String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);
320+
321+
// Verify -vvv flag is NOT present
322+
assertFalse(
323+
wrapperContent.contains("-vvv"),
324+
"Wrapper should NOT contain -vvv flag when verbose mode is disabled");
325+
326+
} finally {
327+
Files.deleteIfExists(knownHosts);
328+
}
329+
}
330+
331+
/**
332+
* Test that SSH verbose mode adds -vvv flag when enabled
333+
*/
334+
@Test
335+
@Issue("JENKINS-71461")
336+
void testSshVerboseModeEnabled() throws Exception {
337+
// Skip if Jenkins instance is not available
338+
if (jenkins.model.Jenkins.getInstanceOrNull() == null) {
339+
return;
340+
}
341+
342+
workspace = new File(tempDir, "test-verbose");
343+
workspace.mkdirs();
344+
345+
Path keyFile = createMockSSHKey(workspace);
346+
Path knownHosts = Files.createTempFile("known_hosts", "");
347+
348+
try {
349+
// Enable SSH verbose mode
350+
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);
351+
352+
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
353+
.in(workspace)
354+
.using("git")
355+
.getClient();
356+
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
357+
358+
Path sshWrapper;
359+
if (isWindows()) {
360+
sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);
361+
} else {
362+
sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);
363+
}
364+
365+
String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);
366+
367+
// Verify -vvv flag IS present
368+
assertTrue(
369+
wrapperContent.contains("-vvv"), "Wrapper should contain -vvv flag when verbose mode is enabled");
370+
371+
} finally {
372+
// Reset to default
373+
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
374+
Files.deleteIfExists(knownHosts);
375+
}
376+
}
377+
378+
/**
379+
* Test that SSH verbose mode flag is placed correctly in Unix wrapper
380+
*/
381+
@Test
382+
@Issue("JENKINS-71461")
383+
void testUnixSshVerboseFlagPlacement() throws Exception {
384+
// Skip if Jenkins instance is not available or on Windows
385+
if (jenkins.model.Jenkins.getInstanceOrNull() == null || isWindows()) {
386+
return;
387+
}
388+
389+
workspace = new File(tempDir, "test-unix-verbose");
390+
workspace.mkdirs();
391+
392+
Path keyFile = createMockSSHKey(workspace);
393+
Path knownHosts = Files.createTempFile("known_hosts", "");
394+
395+
try {
396+
// Enable SSH verbose mode
397+
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);
398+
399+
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
400+
.in(workspace)
401+
.using("git")
402+
.getClient();
403+
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
404+
Path sshWrapper = git.createUnixGitSSH(keyFile, "testuser", knownHosts);
405+
406+
String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);
407+
408+
// Verify -vvv appears before "$@" (which represents additional args)
409+
int vvvIndex = wrapperContent.indexOf("-vvv");
410+
int argsIndex = wrapperContent.indexOf("\"$@\"");
411+
assertTrue(vvvIndex > 0, "-vvv flag should be present");
412+
assertTrue(argsIndex > 0, "\"$@\" should be present");
413+
assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before \"$@\"");
414+
415+
} finally {
416+
// Reset to default
417+
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
418+
Files.deleteIfExists(knownHosts);
419+
}
420+
}
421+
422+
/**
423+
* Test that SSH verbose mode flag is placed correctly in Windows wrapper
424+
*/
425+
@Test
426+
@Issue("JENKINS-71461")
427+
void testWindowsSshVerboseFlagPlacement() throws Exception {
428+
// Skip if Jenkins instance is not available or on Unix
429+
if (jenkins.model.Jenkins.getInstanceOrNull() == null || !isWindows()) {
430+
return; // Skip on Unix
431+
}
432+
433+
workspace = new File(tempDir, "test-windows-verbose");
434+
workspace.mkdirs();
435+
436+
Path keyFile = createMockSSHKey(workspace);
437+
Path knownHosts = Files.createTempFile("known_hosts", "");
438+
439+
try {
440+
// Enable SSH verbose mode
441+
GitHostKeyVerificationConfiguration.get().setSshVerbose(true);
442+
443+
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars())
444+
.in(workspace)
445+
.using("git")
446+
.getClient();
447+
CliGitAPIImpl git = (CliGitAPIImpl) gitClient;
448+
Path sshWrapper = git.createWindowsGitSSH(keyFile, "testuser", knownHosts);
449+
450+
String wrapperContent = Files.readString(sshWrapper, StandardCharsets.UTF_8);
451+
452+
// Verify -vvv appears before %* (which represents additional args)
453+
int vvvIndex = wrapperContent.indexOf("-vvv");
454+
int argsIndex = wrapperContent.indexOf("%*");
455+
assertTrue(vvvIndex > 0, "-vvv flag should be present");
456+
assertTrue(argsIndex > 0, "%* should be present");
457+
assertTrue(vvvIndex < argsIndex, "-vvv flag should appear before %*");
458+
459+
} finally {
460+
// Reset to default
461+
GitHostKeyVerificationConfiguration.get().setSshVerbose(false);
462+
Files.deleteIfExists(knownHosts);
463+
}
464+
}
289465
}

0 commit comments

Comments
 (0)