diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..599f094
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,30 @@
+= SSHD Module
+
+== About
+
+This component provides a built-in SSH server for Jenkins.
+It's an alternative interface for the https://www.jenkins.io/doc/book/managing/cli/[Jenkins CLI], and commands can be invoked this way using any SSH client.
+
+NOTE: This is unrelated to https://plugins.jenkins.io/ssh-slaves/[SSH Build Agents]. In that case, the agents are the servers, and the Jenkins controller is the client.
+
+== Configuration
+
+Enable the built-in SSH server in _Manage Jenkins ยป Configure Global Security_.
+
+=== Advanced Configuration
+
+https://www.jenkins.io/doc/book/managing/system-properties/[System properties] can be used to configure hidden options.
+These are generally considered unsupported, i.e. may be removed at any time.
+
+* `org.jenkinsci.main.modules.sshd.SSHD.excludedKeyExchanges` is a comma-separated string of key exchange algorithms to disable.
+ By default, this disables SHA-1 based algorithms as they're no longer considered safe.
+ Use an empty string to disable no algorithms.
+ The names of supported, enabled, and disabled algorithms can be viewed using the https://www.jenkins.io/doc/book/system-administration/viewing-logs/[logger] `org.jenkinsci.main.modules.sshd.SSHD` during initialization on the level `FINE`.
+* `org.jenkinsci.main.modules.sshd.SSHD.excludedMacs` is a comma-separated string of HMAC algorithms to disable.
+ By default, this disables MD5 and truncated SHA-1 based algorithms as they're no longer considered safe.
+ Use an empty string to disable no algorithms.
+ The names of supported, enabled, and disabled algorithms can be viewed using the https://www.jenkins.io/doc/book/system-administration/viewing-logs/[logger] `org.jenkinsci.main.modules.sshd.SSHD` during initialization on the level `FINE`.
+
+== Changelog
+
+See link:CHANGELOG.md[CHANGELOG.md].
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 09332b4..1f4d4af 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
org.jenkins-ci.plugins
plugin
- 3.43
+ 4.7
@@ -31,7 +31,7 @@
2.7
-SNAPSHOT
- 2.89.3
+ 2.249.1
8
@@ -54,7 +54,7 @@
org.jenkins-ci.modules
ssh-cli-auth
- 1.5
+ 1.8
diff --git a/src/main/java/org/jenkinsci/main/modules/sshd/SSHD.java b/src/main/java/org/jenkinsci/main/modules/sshd/SSHD.java
index 82f6ff2..e48980b 100644
--- a/src/main/java/org/jenkinsci/main/modules/sshd/SSHD.java
+++ b/src/main/java/org/jenkinsci/main/modules/sshd/SSHD.java
@@ -12,6 +12,7 @@
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
import javax.annotation.CheckForSigned;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
@@ -20,12 +21,16 @@
import jenkins.model.GlobalConfiguration;
import jenkins.model.GlobalConfigurationCategory;
import jenkins.util.ServerTcpPort;
+import jenkins.util.SystemProperties;
import jenkins.util.Timer;
import net.sf.json.JSONObject;
+import org.apache.commons.lang.StringUtils;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.cipher.BuiltinCiphers;
import org.apache.sshd.common.cipher.Cipher;
+import org.apache.sshd.common.kex.KeyExchange;
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
+import org.apache.sshd.common.mac.Mac;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.UserAuth;
import org.jenkinsci.main.modules.instance_identity.InstanceIdentity;
@@ -44,7 +49,19 @@ public class SSHD extends GlobalConfiguration {
private static final List> ENABLED_CIPHERS = Arrays.>asList(
BuiltinCiphers.aes128ctr, BuiltinCiphers.aes192ctr, BuiltinCiphers.aes256ctr
);
-
+
+ /**
+ * Comma-separated string of key exchange names to disable. Defaults to a list of DH SHA1 key exchanges, gets its value from {@link SystemProperties}.
+ */
+ private static final String EXCLUDED_KEY_EXCHANGES = SystemProperties.getString(SSHD.class.getName() + ".excludedKeyExchanges",
+ "diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1");
+
+ /**
+ * Comma-separated string of key exchange names to disable. Defaults to a list of MD5 and truncated SHA-1 HMACs, gets its value from {@link SystemProperties}.
+ */
+ private static final String EXCLUDED_MACS = SystemProperties.getString(SSHD.class.getName() + ".excludedMacs",
+ "hmac-md5, hmac-md5-96, hmac-sha1-96");
+
@Override
public GlobalConfigurationCategory getCategory() {
return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class);
@@ -125,7 +142,7 @@ public void run() {
}
return activatedCiphers;
}
-
+
public synchronized void start() throws IOException, InterruptedException {
int port = this.port; // Capture local copy to prevent race conditions. Setting port to -1 after the check would blow up later.
if (port<0) return; // don't start it
@@ -137,6 +154,8 @@ public synchronized void start() throws IOException, InterruptedException {
sshd.setUserAuthFactories(Arrays.>asList(new UserAuthNamedFactory()));
sshd.setCipherFactories(getActivatedCiphers());
+ sshd.setKeyExchangeFactories(filterKeyExchanges(sshd.getKeyExchangeFactories()));
+ sshd.setMacFactories(filterMacs(sshd.getMacFactories()));
sshd.setPort(port);
sshd.setKeyPairProvider(new AbstractKeyPairProvider() {
@@ -158,7 +177,52 @@ public Iterable loadKeys() {
sshd.start();
LOGGER.info("Started SSHD at port " + sshd.getPort());
}
-
+
+ private List> filterMacs(List> macFactories) {
+ if (StringUtils.isBlank(EXCLUDED_MACS)) {
+ return macFactories;
+ }
+
+ List excludedNames = Arrays.stream(EXCLUDED_MACS.split(",")).filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toList());
+
+ List> filtered = new ArrayList<>();
+ for (NamedFactory macFactory : macFactories) {
+ final String name = macFactory.getName();
+ if (excludedNames.contains(name)) {
+ LOGGER.log(Level.CONFIG, "Excluding " + name);
+ } else {
+ LOGGER.log(Level.FINE, "Not excluding " + name);
+ filtered.add(macFactory);
+ }
+ }
+ return filtered;
+ }
+
+ /**
+ * Filter key exchanges based on configuration from {@link #EXCLUDED_KEY_EXCHANGES}.
+ * @param keyExchangeFactories the full list of key exchange factories
+ * @return a filtered list of key exchange factories
+ */
+ private List> filterKeyExchanges(List> keyExchangeFactories) {
+ if (StringUtils.isBlank(EXCLUDED_KEY_EXCHANGES)) {
+ return keyExchangeFactories;
+ }
+
+ List excludedNames = Arrays.stream(EXCLUDED_KEY_EXCHANGES.split(",")).filter(StringUtils::isNotBlank).map(String::trim).collect(Collectors.toList());
+
+ List> filtered = new ArrayList<>();
+ for (NamedFactory keyExchangeNamedFactory : keyExchangeFactories) {
+ final String name = keyExchangeNamedFactory.getName();
+ if (excludedNames.contains(name)) {
+ LOGGER.log(Level.CONFIG, "Excluding " + name);
+ } else {
+ LOGGER.log(Level.FINE, "Not excluding " + name);
+ filtered.add(keyExchangeNamedFactory);
+ }
+ }
+ return filtered;
+ }
+
public synchronized void restart() {
try {
if (sshd!=null) {