Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2700,9 +2700,11 @@
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

Check warning on line 2706 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 2703-2706 are not covered by tests
+ " %* ");
w.newLine();
}
ssh.toFile().setExecutable(true, true);
Expand All @@ -2724,8 +2726,10 @@
w.newLine();
w.write("fi");
w.newLine();
String verboseFlag = isSshVerboseEnabled() ? " -vvv" : "";

Check warning on line 2729 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2729 is only partially covered, one branch is missing
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);
Expand Down Expand Up @@ -2768,6 +2772,25 @@
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) {

Check warning on line 2784 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2784 is only partially covered, one branch is missing
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);

Check warning on line 2789 in src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 2785-2789 are not covered by tests
}
return false;
}

private String launchCommandIn(ArgumentListBuilder args, File workDir, EnvVars env)
throws GitException, InterruptedException {
return launchCommandIn(args, workDir, environment, TIMEOUT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

private SshHostKeyVerificationStrategy<? extends HostKeyVerifierFactory> sshHostKeyVerificationStrategy;

private boolean sshVerbose = false;

@Override
public @NonNull GlobalConfigurationCategory getCategory() {
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
Expand All @@ -35,6 +37,29 @@
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();
}

Check warning on line 61 in src/main/java/org/jenkinsci/plugins/gitclient/GitHostKeyVerificationConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 48-61 are not covered by tests

public static @NonNull GitHostKeyVerificationConfiguration get() {
return GlobalConfiguration.all().getInstance(GitHostKeyVerificationConfiguration.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:section title="Git Host Key Verification Configuration">
<f:dropdownDescriptorSelector field="sshHostKeyVerificationStrategy" title="Host Key Verification Strategy"/>
<f:entry title="SSH Verbose Mode" field="sshVerbose">
<f:checkbox title="Enable verbose SSH output for diagnostics"
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."/>
</f:entry>
</f:section>
</j:jelly>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,7 @@
*
* @author Mark Waite
*/
@WithJenkins
class CliGitAPISecurityTest {

@TempDir
Expand Down Expand Up @@ -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);
}
}
}
Loading