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) {