From acee8a1b103f3469a575df3ca42eeb39818dbb65 Mon Sep 17 00:00:00 2001 From: Ollie Hughes Date: Thu, 6 Jul 2017 16:21:09 +0100 Subject: [PATCH] Git SSH configuration using properties By default, Spring Cloud Config Server uses SSH configuration files such as ~/.ssh/known_hosts and /etc/ssh/ssh_config when connecting to Git repositories using an SSH URI. In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. For cases such as these, SSH configuration can be set using Java properties. In order to activate property based SSH configuration, the property spring.cloud.config.server.git.ignoreLocalSshSettings must be set to true --- docs/pom.xml | 4 +- .../main/asciidoc/spring-cloud-config.adoc | 72 ++++- .../config/ConfigServerAutoConfiguration.java | 2 +- .../server/config/TransportConfiguration.java | 101 +++++++ .../JGitEnvironmentRepository.java | 22 -- .../ssh/PropertyBasedSshSessionFactory.java | 75 ++++++ .../server/ssh/SshPropertyValidator.java | 110 ++++++++ .../config/server/ssh/SshUriProperties.java | 251 ++++++++++++++++++ .../server/ssh/SshUriPropertyProcessor.java | 76 ++++++ ...ransportConfigurationIntegrationTests.java | 229 ++++++++++++++++ .../JGitEnvironmentRepositoryTests.java | 38 --- .../PropertyBasedSshSessionFactoryTest.java | 160 +++++++++++ .../server/ssh/SshPropertyValidatorTest.java | 185 +++++++++++++ .../ssh/SshUriPropertyProcessorTest.java | 138 ++++++++++ .../src/test/resources/ssh/key | 27 ++ .../resources/ssh/ssh-nested-settings.yml | 41 +++ .../resources/ssh/ssh-private-key-block.yml | 35 +++ .../resources/ssh/ssh-private-key-newline.yml | 10 + 18 files changed, 1513 insertions(+), 63 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java create mode 100644 spring-cloud-config-server/src/test/resources/ssh/key create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml create mode 100644 spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml diff --git a/docs/pom.xml b/docs/pom.xml index 38eabee8..b41839ae 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,8 +6,10 @@ org.springframework.cloud spring-cloud-config - 1.3.2.BUILD-SNAPSHOT + 1.4.0.BUILD-SNAPSHOT + .. + pom Spring Cloud Config Docs Spring Cloud Docs diff --git a/docs/src/main/asciidoc/spring-cloud-config.adoc b/docs/src/main/asciidoc/spring-cloud-config.adoc index 503bb6f0..55146702 100644 --- a/docs/src/main/asciidoc/spring-cloud-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-config.adoc @@ -360,7 +360,77 @@ by the default credential provider chain. AWS EC2 instances may use http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html[IAM Roles for EC2 Instances]. Note: The aws-java-sdk-core jar is an optional dependency. If the aws-java-sdk-core jar is not on your -classpath, then the AWS Code Commit credential provider will not be created regardless of the git server URI. +classpath, then the AWS Code Commit credential provider will not be created regardless of the git server URI. + +===== Git SSH configuration using properties + +By default, Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories using an SSH URI. +In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. For cases such as these, SSH configuration can be set using +Java properties. In order to activate property based SSH configuration, the property `spring.cloud.config.server.git.ignoreLocalSshSettings` must be set to `true`. +Example: + +[source,yaml] +---- + spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo1.git + ignoreLocalSshSettings: true + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- + +---- + +.SSH Configuration properties +|=== +|Property Name |Remarks + +|*ignoreLocalSshSettings* +|If true, use property based SSH config instead of file based + +|*privateKey* +|Valid SSH private key. Must be set if `ignoreLocalSshSettings` is true and Git URI is SSH format + +|*hostKey* +|Valid SSH host key. Must be set if `hostKeyAlgorithm` is also set + +|*hostKeyAlgorithm* +|One of `ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384 ,ecdsa-sha2-nistp521`. Must be set if `hostKey` is also set + +|*strictHostKeyChecking* +|`true` or `false`. If false, ignore errors with host key +|=== + ===== Placeholders in Git Search Paths diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java index b1dff1ca..8a7a4140 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerAutoConfiguration.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Import; @ConditionalOnBean(ConfigServerConfiguration.Marker.class) @EnableConfigurationProperties(ConfigServerProperties.class) @Import({ EnvironmentRepositoryConfiguration.class, CompositeConfiguration.class, ResourceRepositoryConfiguration.class, - ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class }) + ConfigServerEncryptionConfiguration.class, ConfigServerMvcConfiguration.class, TransportConfiguration.class }) public class ConfigServerAutoConfiguration { } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java new file mode 100644 index 00000000..f5199cd1 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/TransportConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.config; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.*; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.ssh.PropertyBasedSshSessionFactory; +import org.springframework.cloud.config.server.ssh.SshUriProperties; +import org.springframework.cloud.config.server.ssh.SshUriPropertyProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configure a callback to set up a property based SSH settings before running a transport command (such as clone or fetch) + * + * @author Ollie Hughes + */ +@Configuration +@EnableConfigurationProperties(SshUriProperties.class) +public class TransportConfiguration { + + @ConditionalOnMissingBean(TransportConfigCallback.class) + @Bean + public TransportConfigCallback propertiesBasedSshTransportCallback(final SshUriProperties sshUriProperties) { + if(sshUriProperties.isIgnoreLocalSshSettings()) { + return new PropertiesBasedSshTransportConfigCallback(sshUriProperties); + } + else return new FileBasedSshTransportConfigCallback(sshUriProperties); + } + + /** + * Configure JGit transport command to use a SSH session factory that is configured using properties defined + * in {@link SshUriProperties} + */ + public static class PropertiesBasedSshTransportConfigCallback implements TransportConfigCallback { + + private SshUriProperties sshUriProperties; + + public PropertiesBasedSshTransportConfigCallback(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public SshUriProperties getSshUriProperties() { + return sshUriProperties; + } + + @Override + public void configure(Transport transport) { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory( + new PropertyBasedSshSessionFactory( + new SshUriPropertyProcessor(sshUriProperties).getSshKeysByHostname(), new JSch())); + } + } + + /** + * Configure JGit transport command to use a default SSH session factory based on local machines SSH config. + * Allow strict host key checking to be set. + */ + public static class FileBasedSshTransportConfigCallback implements TransportConfigCallback { + + private SshUriProperties sshUriProperties; + + public FileBasedSshTransportConfigCallback(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public SshUriProperties getSshUriProperties() { + return sshUriProperties; + } + + @Override + public void configure(Transport transport) { + SshSessionFactory.setInstance(new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host hc, Session session) { + session.setConfig("StrictHostKeyChecking", + sshUriProperties.isStrictHostKeyChecking() ? "yes" : "no"); + } + }); + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java index c28370de..8df63428 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepository.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; - import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; @@ -43,9 +42,6 @@ import org.eclipse.jgit.errors.NoRemoteRepositoryException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig.Host; -import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.TagOpt; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.util.FileUtils; @@ -56,8 +52,6 @@ import org.springframework.core.io.UrlResource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.jcraft.jsch.Session; - import static org.springframework.util.StringUtils.hasText; /** @@ -81,8 +75,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository */ private int timeout = 5; - private boolean initialized; - /** * Flag to indicate that the repository should be cloned on startup (not on * demand). Generally leads to slower startup but faster first query. @@ -174,7 +166,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository @Override public void afterPropertiesSet() throws Exception { Assert.state(getUri() != null, "You need to configure a uri for the git repository"); - initialize(); if (this.cloneOnStart) { initClonedRepository(); } @@ -184,7 +175,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository * Get the working directory ready. */ public String refresh(String label) { - initialize(); Git git = null; try { git = createGitClient(); @@ -422,18 +412,6 @@ public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository } } - private void initialize() { - if (!this.initialized) { - SshSessionFactory.setInstance(new JschConfigSessionFactory() { - @Override - protected void configure(Host hc, Session session) { - session.setConfig("StrictHostKeyChecking", isStrictHostKeyChecking() ? "yes" : "no"); - } - }); - this.initialized = true; - } - } - private void configureCommand(TransportCommand command) { command.setTimeout(this.timeout); if (this.transportConfigCallback != null) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java new file mode 100644 index 00000000..93493a43 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.config.server.ssh; + +import com.jcraft.jsch.HostKey; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.eclipse.jgit.util.Base64; +import org.eclipse.jgit.util.FS; + +import java.util.Map; + + +/** + * In a cloud environment local SSH config files such as `.known_hosts` may not be suitable for providing + * configuration settings due to ephemeral filesystems. This flag enables SSH config to be provided as application + * properties + * @author William Tran + * @author Ollie Hughes + */ +public class PropertyBasedSshSessionFactory extends JschConfigSessionFactory { + + private final Map sshKeysByHostname; + private final JSch jSch; + + public PropertyBasedSshSessionFactory(Map sshKeysByHostname, JSch jSch) { + this.sshKeysByHostname = sshKeysByHostname; + this.jSch = jSch; + } + + @Override + protected void configure(Host hc, Session session) { + SshUriProperties sshProperties = sshKeysByHostname.get(hc.getHostName()); + String hostKeyAlgorithm = sshProperties.getHostKeyAlgorithm(); + if (hostKeyAlgorithm != null) { + session.setConfig("server_host_key", hostKeyAlgorithm); + } + if (sshProperties.getHostKey() == null || !sshProperties.isStrictHostKeyChecking()) { + session.setConfig("StrictHostKeyChecking", "no"); + } else { + session.setConfig("StrictHostKeyChecking", "yes"); + } + } + + @Override + protected Session createSession(Host hc, String user, String host, int port, FS fs) throws JSchException { + if (sshKeysByHostname.containsKey(host)) { + SshUriProperties sshUriProperties = sshKeysByHostname.get(host); + jSch.addIdentity(host, sshUriProperties.getPrivateKey().getBytes(), null, null); + if (sshUriProperties.getHostKey() != null) { + HostKey hostkey = new HostKey(host, Base64.decode(sshUriProperties.getHostKey())); + jSch.getHostKeyRepository().add(hostkey, null); + } + return jSch.getSession(user, host, port); + } + throw new JSchException("no keys configured for hostname " + host); + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java new file mode 100644 index 00000000..011effe7 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshPropertyValidator.java @@ -0,0 +1,110 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.KeyPair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import javax.annotation.PostConstruct; +import java.util.*; + +import static java.lang.String.format; +import static org.springframework.util.StringUtils.hasText; + +/** + * Validate SSH related properties + * + * @author Ollie Hughes + */ +@Component +@EnableConfigurationProperties(SshUriProperties.class) +public class SshPropertyValidator { + + private final SshUriProperties sshUriProperties; + private final JSch jsch = new JSch(); + private static final Set VALID_HOST_KEY_ALGORITHMS = new LinkedHashSet<>(Arrays.asList( + "ssh-dss","ssh-rsa","ecdsa-sha2-nistp256","ecdsa-sha2-nistp384","ecdsa-sha2-nistp521")); + private static final String GIT_PROPERTY_PREFIX = "spring.cloud.config.server.git."; + + @Autowired + public SshPropertyValidator(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + static boolean isSshUri(Object uri) { + return uri != null && (uri.toString().startsWith("ssh") || uri.toString().startsWith("git")); + } + + @PostConstruct + public void validateSshConfigurationProperties() { + List allRepoProperties = new ArrayList<>(); + allRepoProperties.add(sshUriProperties); + Map repos = sshUriProperties.getRepos(); + if (repos != null) { + allRepoProperties.addAll(repos.values()); + } + for (SshUriProperties repoProperties : allRepoProperties) { + if(isSshUri(repoProperties.getUri()) && sshUriProperties.isIgnoreLocalSshSettings()){ + validatePrivateKeyPresent(); + validatePrivateKeyFormat(); + validateAlgorithmSpecifiedWhenHostKeySet(); + validateHostKeySpecifiedWhenAlgorithmSet(); + validateHostKeyAlgorithmSupported(); + } + } + } + + protected void validatePrivateKeyFormat() { + try { + KeyPair.load(jsch, sshUriProperties.getPrivateKey().getBytes(), null); + } catch (JSchException e) { + throw new IllegalStateException(format("Property '%sprivateKey' contains an invalid value", GIT_PROPERTY_PREFIX)); + } + } + + protected void validateHostKeyAlgorithmSupported() { + if (hasText(sshUriProperties.getHostKeyAlgorithm())) { + Assert.state(VALID_HOST_KEY_ALGORITHMS.contains(sshUriProperties.getHostKeyAlgorithm()), + format("Property '%shostKeyAlgorithm' must be one of %s", GIT_PROPERTY_PREFIX, VALID_HOST_KEY_ALGORITHMS)); + } + } + + protected void validatePrivateKeyPresent() { + Assert.state(sshUriProperties.getPrivateKey() != null, + format("Property '%sprivateKey' must be set when '%signoreLocalSshSettings' is set to 'true'", GIT_PROPERTY_PREFIX, GIT_PROPERTY_PREFIX)); + } + + protected void validateHostKeySpecifiedWhenAlgorithmSet() { + if (hasText(sshUriProperties.getHostKeyAlgorithm())) { + Assert.state(hasText(sshUriProperties.getHostKey()), + format("Property '%shostKey' must be set when 'hostKeyAlgorithm' is specified", GIT_PROPERTY_PREFIX)); + } + } + + protected void validateAlgorithmSpecifiedWhenHostKeySet() { + if (hasText(sshUriProperties.getHostKey())) { + Assert.state(hasText(sshUriProperties.getHostKeyAlgorithm()), + format("Property '%shostKeyAlgorithm' must be set when 'hostKey' is specified", GIT_PROPERTY_PREFIX)); + } + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java new file mode 100644 index 00000000..8d036d5a --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriProperties.java @@ -0,0 +1,251 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.config.server.ssh; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Data container for property based SSH config + * + * @author Ollie Hughes + */ +@ConfigurationProperties("spring.cloud.config.server.git") +public class SshUriProperties { + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private String privateKey; + private String username; + private String password; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + + private Map repos = new HashMap<>(); + + public SshUriProperties(String uri, String hostKeyAlgorithm, String hostKey, String privateKey, String username, String password, boolean ignoreLocalSshSettings, boolean strictHostKeyChecking, Map repos) { + this.uri = uri; + this.hostKeyAlgorithm = hostKeyAlgorithm; + this.hostKey = hostKey; + this.privateKey = privateKey; + this.username = username; + this.password = password; + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + this.strictHostKeyChecking = strictHostKeyChecking; + this.repos = repos; + } + + public SshUriProperties() { + } + + public static SshUriPropertiesBuilder builder() { + return new SshUriPropertiesBuilder(); + } + + public boolean isSshUri() { + return uri != null && !uri.startsWith("http"); + } + + public String getHostname() { + if (getUri() == null) { + return null; + } + + if (getUri().matches("^[a-z]+://.*")) { + return UriComponentsBuilder.fromUriString(uri).build().getHost(); + } + else if (getUri().indexOf('@') < getUri().indexOf(':')) { + return getUri().substring(getUri().indexOf('@') + 1, uri.indexOf(':')); + } + else if (getUri().startsWith("ssh:") && getUri().indexOf('@') > 0) { + String postAt = getUri().substring(getUri().indexOf('@') + 1); + return postAt.substring(0, postAt.indexOf(":")); + } + else return null; + } + + public String getUri() { + return this.uri; + } + + public String getHostKeyAlgorithm() { + return this.hostKeyAlgorithm; + } + + public String getHostKey() { + return this.hostKey; + } + + public String getPrivateKey() { + return this.privateKey; + } + + public String getUsername() { + return this.username; + } + + public String getPassword() { + return this.password; + } + + public boolean isIgnoreLocalSshSettings() { + return this.ignoreLocalSshSettings; + } + + public boolean isStrictHostKeyChecking() { + return this.strictHostKeyChecking; + } + + public Map getRepos() { + return this.repos; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setHostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + } + + public void setHostKey(String hostKey) { + this.hostKey = hostKey; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setIgnoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + } + + public void setStrictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + } + + public void setRepos(Map repos) { + this.repos = repos; + } + + @Override + public int hashCode() { + return Objects.hash(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SshUriProperties other = (SshUriProperties) obj; + return Objects.equals(this.uri, other.uri) + && Objects.equals(this.hostKeyAlgorithm, other.hostKeyAlgorithm) + && Objects.equals(this.hostKey, other.hostKey) + && Objects.equals(this.privateKey, other.privateKey) + && Objects.equals(this.username, other.username) + && Objects.equals(this.password, other.password) + && Objects.equals(this.ignoreLocalSshSettings, other.ignoreLocalSshSettings) + && Objects.equals(this.strictHostKeyChecking, other.strictHostKeyChecking); + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties(uri=" + this.getUri() + " hostKeyAlgorithm=" + this.getHostKeyAlgorithm() + ", hostKey=" + this.getHostKey() + ", privateKey=" + this.getPrivateKey() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ", ignoreLocalSshSettings=" + this.isIgnoreLocalSshSettings() + ", strictHostKeyChecking=" + this.isStrictHostKeyChecking() + ", repos=" + this.getRepos() + ")"; + } + + public static class SshUriPropertiesBuilder { + private String uri; + private String hostKeyAlgorithm; + private String hostKey; + private String privateKey; + private String username; + private String password; + private boolean ignoreLocalSshSettings; + private boolean strictHostKeyChecking = true; + private Map repos; + + SshUriPropertiesBuilder() { + } + + public SshUriProperties.SshUriPropertiesBuilder uri(String uri) { + this.uri = uri; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder hostKeyAlgorithm(String hostKeyAlgorithm) { + this.hostKeyAlgorithm = hostKeyAlgorithm; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder hostKey(String hostKey) { + this.hostKey = hostKey; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder privateKey(String privateKey) { + this.privateKey = privateKey; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder username(String username) { + this.username = username; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder password(String password) { + this.password = password; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder ignoreLocalSshSettings(boolean ignoreLocalSshSettings) { + this.ignoreLocalSshSettings = ignoreLocalSshSettings; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder strictHostKeyChecking(boolean strictHostKeyChecking) { + this.strictHostKeyChecking = strictHostKeyChecking; + return this; + } + + public SshUriProperties.SshUriPropertiesBuilder repos(Map repos) { + this.repos = repos; + return this; + } + + public SshUriProperties build() { + return new SshUriProperties(uri, hostKeyAlgorithm, hostKey, privateKey, username, password, ignoreLocalSshSettings, strictHostKeyChecking, repos); + } + + public String toString() { + return "org.springframework.cloud.config.server.ssh.SshUriProperties.SshUriPropertiesBuilder(uri=" + this.uri + "hostKeyAlgorithm=" + this.hostKeyAlgorithm + ", hostKey=" + this.hostKey + ", privateKey=" + this.privateKey + ", username=" + this.username + ", password=" + this.password + ", ignoreLocalSshSettings=" + this.ignoreLocalSshSettings + ", strictHostKeyChecking=" + this.strictHostKeyChecking + ", repos=" + this.repos + ")"; + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java new file mode 100644 index 00000000..791dcda9 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.config.server.ssh; + +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.cloud.config.server.ssh.SshPropertyValidator.isSshUri; + +/** + * Check if Git repo properties refer to an SSH based transport then filter and extract the properties + * @author William Tran + * @author Ollie Hughes + */ +public class SshUriPropertyProcessor { + + private final SshUriProperties sshUriProperties; + + public SshUriPropertyProcessor(SshUriProperties sshUriProperties) { + this.sshUriProperties = sshUriProperties; + } + + public Map getSshKeysByHostname() { + return extractNestedProperties(sshUriProperties); + } + + private Map extractNestedProperties(SshUriProperties uriProperties) { + Map sshUriPropertyMap = new HashMap<>(); + String parentUri = uriProperties.getUri(); + if (isSshUri(parentUri) && getHostname(parentUri) != null) { + sshUriPropertyMap.put(getHostname(parentUri), uriProperties); + } + Map repos = uriProperties.getRepos(); + if(repos != null) { + for (SshUriProperties repoProperties : repos.values()) { + String repoUri = repoProperties.getUri(); + if (isSshUri(repoUri) && getHostname(repoUri) != null) { + sshUriPropertyMap.put(getHostname(repoUri), repoProperties); + } + } + } + return sshUriPropertyMap; + } + + private String getHostname(String uri) { + if (uri == null) { + return null; + } + else if (uri.matches("^[a-z]+://.*")) { + return UriComponentsBuilder.fromUriString(uri).build().getHost(); + } + else if (uri.indexOf('@') < uri.indexOf(':')) { + return uri.substring(uri.indexOf('@') + 1, uri.indexOf(':')); + } + else if (uri.startsWith("ssh:") && uri.indexOf('@') > 0) { + String postAt = uri.substring(uri.indexOf('@') + 1); + return postAt.substring(0, postAt.indexOf(":")); + } + else return null; + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java new file mode 100644 index 00000000..b22b3166 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/TransportConfigurationIntegrationTests.java @@ -0,0 +1,229 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server; + +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.util.FS; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.config.server.config.TransportConfiguration; +import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository; +import org.springframework.cloud.config.server.ssh.SshPropertyValidator; +import org.springframework.cloud.config.server.ssh.SshUriProperties; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.lang.reflect.Method; + +import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Integration tests for property based SSH config support + * @author Ollie Hughes + */ +public class TransportConfigurationIntegrationTests { + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-private-key-block",}) + @ActiveProfiles({"test", "git"}) + public static class PropertyBasedCallbackTest { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void propertyBasedTransportCallbackIsConfigured() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + } + } + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-private-key-newline" + }) + @ActiveProfiles({"test", "git"}) + public static class PrivateKeyPropertyWithLineBreaks { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void privateKeyPropertyWithLineBreaks() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + + TransportConfiguration.PropertiesBasedSshTransportConfigCallback configCallback = + (TransportConfiguration.PropertiesBasedSshTransportConfigCallback) transportConfigCallback; + assertThat(configCallback.getSshUriProperties().getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_1))); + } + } + + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.config.name:ssh/ssh-nested-settings" + }) + @ActiveProfiles({"test", "git"}) + public static class SshPropertiesWithinNestedRepo { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void sshPropertiesWithinNestedRepo() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.PropertiesBasedSshTransportConfigCallback.class))); + + TransportConfiguration.PropertiesBasedSshTransportConfigCallback configCallback = + (TransportConfiguration.PropertiesBasedSshTransportConfigCallback) transportConfigCallback; + SshUriProperties sshUriProperties = configCallback.getSshUriProperties(); + assertThat(sshUriProperties.getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_1))); + + assertThat(sshUriProperties.getRepos().get("repo1"), is(notNullValue())); + assertThat(sshUriProperties.getRepos().get("repo1").getPrivateKey(), is(equalTo(TestProperties.TEST_PRIVATE_KEY_2))); + } + } + + @RunWith(SpringRunner.class) + @SpringBootTest(classes = {ConfigServerApplication.class, TransportConfiguration.class, SshPropertyValidator.class}, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "spring.cloud.config.server.git.uri=git@gitserver.com:team/repo.git", + "spring.cloud.config.server.git.ignoreLocalSshSettings=false",}) + @ActiveProfiles({"test", "git"}) + public static class FileBasedCallbackTest { + + @Autowired + private MultipleJGitEnvironmentRepository jGitEnvironmentRepository; + + @Test + public void fileBasedTransportCallbackIsConfigured() throws Exception { + TransportConfigCallback transportConfigCallback = jGitEnvironmentRepository.getTransportConfigCallback(); + assertThat(transportConfigCallback, is(instanceOf(TransportConfiguration.FileBasedSshTransportConfigCallback.class))); + } + + + @Test + public void strictHostKeyCheckShouldCheck() throws Exception { + String uri = "git+ssh://git@somegitserver/somegitrepo"; + SshSessionFactory.setInstance(null); + jGitEnvironmentRepository.setUri(uri); + jGitEnvironmentRepository.setBasedir(new File("./mybasedir")); + assertTrue(jGitEnvironmentRepository.isStrictHostKeyChecking()); + jGitEnvironmentRepository.setCloneOnStart(true); + try { + // this will throw but we don't care about connecting. + jGitEnvironmentRepository.afterPropertiesSet(); + } catch (Exception e) { + final OpenSshConfig.Host hc = OpenSshConfig.get(FS.detect()).lookup("github.com"); + JschConfigSessionFactory factory = (JschConfigSessionFactory) SshSessionFactory.getInstance(); + // There's no public method that can be used to inspect the ssh + // configuration, so we'll reflect + // the configure method to allow us to check that the config + // property is set as expected. + Method configure = factory.getClass().getDeclaredMethod("configure", OpenSshConfig.Host.class, + Session.class); + configure.setAccessible(true); + Session session = mock(Session.class); + ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); + configure.invoke(factory, hc, session); + verify(session).setConfig(keyCaptor.capture(), valueCaptor.capture()); + configure.setAccessible(false); + assertTrue("yes".equals(valueCaptor.getValue())); + } + } + } + + private static class TestProperties { + private static final String TEST_PRIVATE_KEY_1 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\n" + + "yyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\n" + + "uP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\n" + + "ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\n" + + "TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n" + + "+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\n" + + "jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\n" + + "lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\n" + + "xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n" + + "4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n" + + "7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n" + + "3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\n" + + "q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\n" + + "mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\n" + + "lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\n" + + "ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\n" + + "ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\n" + + "BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\n" + + "zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\n" + + "uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n" + + "54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\n" + + "E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n" + + "4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n" + + "/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n" + + "2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n" + + "-----END RSA PRIVATE KEY-----"; + + private static final String TEST_PRIVATE_KEY_2 = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX\n" + + "IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF\n" + + "ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud\n" + + "1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i\n" + + "oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W\n" + + "DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd\n" + + "fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b\n" + + "BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG\n" + + "EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj\n" + + "5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8\n" + + "+AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe\n" + + "pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG\n" + + "ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ\n" + + "xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW\n" + + "dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi\n" + + "PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX\n" + + "VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z\n" + + "FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R\n" + + "gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4\n" + + "VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV\n" + + "cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee\n" + + "KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N\n" + + "CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs\n" + + "q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J\n" + + "69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT\n" + + "-----END RSA PRIVATE KEY-----\n"; + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java index ef105132..0d67d4ac 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/JGitEnvironmentRepositoryTests.java @@ -33,7 +33,6 @@ import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -60,19 +59,14 @@ import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; -import org.eclipse.jgit.transport.JschConfigSessionFactory; -import org.eclipse.jgit.transport.OpenSshConfig; -import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.TrackingRefUpdate; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; -import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.mockito.ArgumentCaptor; import org.springframework.cloud.config.environment.Environment; import org.springframework.cloud.config.server.support.AwsCodeCommitCredentialProvider; import org.springframework.cloud.config.server.support.GitCredentialsProviderFactory; @@ -677,38 +671,6 @@ public class JGitEnvironmentRepositoryTests { } - @Test - public void strictHostKeyCheckShouldCheck() throws Exception { - String uri = "git+ssh://git@somegitserver/somegitrepo"; - SshSessionFactory.setInstance(null); - JGitEnvironmentRepository envRepository = new JGitEnvironmentRepository(this.environment); - envRepository.setUri(uri); - envRepository.setBasedir(new File("./mybasedir")); - assertTrue(envRepository.isStrictHostKeyChecking()); - envRepository.setCloneOnStart(true); - try { - // this will throw but we don't care about connecting. - envRepository.afterPropertiesSet(); - } catch (Exception e) { - final OpenSshConfig.Host hc = OpenSshConfig.get(FS.detect()).lookup("github.com"); - JschConfigSessionFactory factory = (JschConfigSessionFactory) SshSessionFactory.getInstance(); - // There's no public method that can be used to inspect the ssh - // configuration, so we'll reflect - // the configure method to allow us to check that the config - // property is set as expected. - Method configure = factory.getClass().getDeclaredMethod("configure", OpenSshConfig.Host.class, - Session.class); - configure.setAccessible(true); - Session session = mock(Session.class); - ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); - configure.invoke(factory, hc, session); - verify(session).setConfig(keyCaptor.capture(), valueCaptor.capture()); - configure.setAccessible(false); - assertTrue("yes".equals(valueCaptor.getValue())); - } - } - @Test public void shouldPrintStacktraceIfDebugEnabled() throws Exception { final Log mockLogger = mock(Log.class); diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java new file mode 100644 index 00000000..fbcb3461 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/PropertyBasedSshSessionFactoryTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import com.jcraft.jsch.*; +import org.eclipse.jgit.transport.OpenSshConfig.Host; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +/** + * Unit tests for property based SSH config processor + * @author William Tran + * @author Ollie Hughes + */ +@RunWith(MockitoJUnitRunner.class) +public class PropertyBasedSshSessionFactoryTest { + + private static final String HOST_KEY = "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMzCa0AcNbahUFjFYJHIilhJOhKFHuDOOuY+/HqV9kALftitwNYo6dQ+tC9IK5JVZCZfqKfDWVMxspcPDf9eMoE="; + private static final String HOST_KEY_ALGORITHM = "ecdsa-sha2-nistp256"; + private static final String PRIVATE_KEY = getResourceAsString("/ssh/key"); + private PropertyBasedSshSessionFactory factory; + @Mock + private Host hc; + @Mock + private Session session; + @Mock + private JSch jSch; + @Mock + private HostKeyRepository hostKeyRepository; + + @Test + public void strictHostKeyCheckingIsOptional() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + + verify(session).setConfig("StrictHostKeyChecking", "no"); + verifyNoMoreInteractions(session); + } + + @Test + public void strictHostKeyCheckingIsUsed() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + + verify(session).setConfig("StrictHostKeyChecking", "yes"); + verifyNoMoreInteractions(session); + } + + @Test + public void hostKeyAlgorithmIsSpecified() { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("ssh://gitlab.example.local:3322/somerepo.git") + .hostKeyAlgorithm(HOST_KEY_ALGORITHM) + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.configure(hc, session); + verify(session).setConfig("server_host_key", HOST_KEY_ALGORITHM); + verify(session).setConfig("StrictHostKeyChecking", "yes"); + verifyNoMoreInteractions(session); + } + + @Test + public void privateKeyIsUsed() throws Exception { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("git@gitlab.example.local:someorg/somerepo.git") + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.createSession(hc, null, sshKey.getHostname(), 22, null); + verify(jSch).addIdentity("gitlab.example.local", PRIVATE_KEY.getBytes(), null, null); + } + + @Test + public void hostKeyIsUsed() throws Exception { + SshUriProperties sshKey = new SshUriProperties.SshUriPropertiesBuilder() + .uri("git@gitlab.example.local:someorg/somerepo.git") + .hostKey(HOST_KEY) + .privateKey(PRIVATE_KEY) + .build(); + setupSessionFactory(sshKey); + + factory.createSession(hc, null, sshKey.getHostname(), 22, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(HostKey.class); + verify(hostKeyRepository).add(captor.capture(), any(UserInfo.class)); + HostKey hostKey = captor.getValue(); + Assert.assertEquals("gitlab.example.local", hostKey.getHost()); + Assert.assertEquals(HOST_KEY, hostKey.getKey()); + } + + private void setupSessionFactory(SshUriProperties sshKey) { + Map sshKeysByHostname = new HashMap<>(); + sshKeysByHostname.put(sshKey.getHostname(), sshKey); + factory = new PropertyBasedSshSessionFactory(sshKeysByHostname, jSch) ; + when(hc.getHostName()).thenReturn(sshKey.getHostname()); + when(jSch.getHostKeyRepository()).thenReturn(hostKeyRepository); + } + + public static String getResourceAsString(String path) { + try { + Resource resource = new ClassPathResource(path); + try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + StringBuilder builder = new StringBuilder(); + String line = ""; + while ((line = br.readLine()) != null) { + builder.append(line).append('\n'); + } + return builder.toString(); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java new file mode 100644 index 00000000..3fe69c98 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshPropertyValidatorTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import org.junit.Test; + +import static org.mockito.Mockito.*; + +/** + * Unit tests for property based SSH config validators + * + * @author Ollie Hughes + */ +public class SshPropertyValidatorTest { + + private static final String SSH_URI = "git@gitserver.com:team/repo1.git"; + + private static final String VALID_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\n" + + "yyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\n" + + "uP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\n" + + "ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\n" + + "TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n" + + "+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\n" + + "jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\n" + + "lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\n" + + "xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n" + + "4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n" + + "7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n" + + "3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\n" + + "q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\n" + + "mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\n" + + "lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\n" + + "ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\n" + + "ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\n" + + "BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\n" + + "zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\n" + + "uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n" + + "54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\n" + + "E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n" + + "4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n" + + "/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n" + + "2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n" + + "-----END RSA PRIVATE KEY-----"; + + private static final String VALID_HOST_KEY = "AAAAB3NzaC1yc2EAAAADAQABAAABAQDg6/W/5cbk/npvzpae7ZEa54F4rkwh2V3NiuqVZ5hWr+8O4/6SmrS7yBvRHAFeAJNb0LOCjE/7tjd1fqUx+QU1ATCtwkOhuwG8Ubzkx23mMZlrwEvx7XEfBoLN7Lw9fXjWDtTTgFB1AxCQ2pGGiNG0QCwyA4HViDHVU+ibwkRlzuDJG0tnp5Qpo3DXkHwFNdqWNfVrIZ6q2xbyeoJjKjnR215T0ehmuWFmKqG+uMNe/LQ6IOiK0F5+gr7rgPxNLAYYqyhraAnBeHn5gapsSzYJmFpoAHWvN7OUwHcJ88D9qUkKi4VKxYiuK69u3z825Xj2cLTfj9JiHCfV8cTo9GL"; + + @Test + public void supportedParametersSuccesful() throws Exception { + SshUriProperties validSettings = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey(VALID_PRIVATE_KEY) + .hostKey(VALID_HOST_KEY) + .hostKeyAlgorithm("ssh-rsa") + .build(); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(validSettings)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(1)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(1)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(1)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(1)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(1)).validateHostKeySpecifiedWhenAlgorithmSet(); + } + + @Test(expected = IllegalStateException.class) + public void invalidPrivateKeyFails() throws Exception { + + SshUriProperties invalidKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(invalidKey); + sshPropertyValidator.validateSshConfigurationProperties(); + + } + + @Test(expected = IllegalStateException.class) + public void missingPrivateKeyFails() throws Exception { + + SshUriProperties missingKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingKey); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void hostKeyWithMissingAlgoFails() throws Exception { + + SshUriProperties missingAlgo = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKey("some_host") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingAlgo); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void algoWithMissingHostKeyFails() throws Exception { + + SshUriProperties missingHostKey = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKeyAlgorithm("some_host_algo") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(missingHostKey); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test(expected = IllegalStateException.class) + public void unsupportedAlgoFails() throws Exception { + + SshUriProperties unsupportedAlgo = SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .hostKey("some_host_key") + .hostKeyAlgorithm("unsupported") + .build(); + + SshPropertyValidator sshPropertyValidator = new SshPropertyValidator(unsupportedAlgo); + sshPropertyValidator.validateSshConfigurationProperties(); + } + + @Test + public void validatorNotRunIfIgnoreLocalSettingsFalse() throws Exception { + + SshUriProperties useLocal = (SshUriProperties.builder() + .uri(SSH_URI) + .ignoreLocalSshSettings(false) + .privateKey("invalid_key") + .build()); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(useLocal)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + } + + @Test + public void validatorNotRunIfHttpsUri() throws Exception { + + SshUriProperties httpsUri = (SshUriProperties.builder() + .uri("https://somerepo.com/team/project.git") + .ignoreLocalSshSettings(true) + .privateKey("invalid_key") + .build()); + + SshPropertyValidator sshPropertyValidator = spy(new SshPropertyValidator(httpsUri)); + sshPropertyValidator.validateSshConfigurationProperties(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyFormat(); + verify(sshPropertyValidator, times(0)).validateAlgorithmSpecifiedWhenHostKeySet(); + verify(sshPropertyValidator, times(0)).validatePrivateKeyPresent(); + verify(sshPropertyValidator, times(0)).validateHostKeyAlgorithmSupported(); + verify(sshPropertyValidator, times(0)).validateHostKeySpecifiedWhenAlgorithmSet(); + } +} \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java new file mode 100644 index 00000000..cc3457b0 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/ssh/SshUriPropertyProcessorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.config.server.ssh; + +import org.eclipse.jgit.transport.SshSessionFactory; +import org.junit.After; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +/** + * Unit tests for property based SSH config processor + * @author William Tran + * @author Ollie Hughes + */ +public class SshUriPropertyProcessorTest { + + private static final String PRIVATE_KEY1 = "privateKey"; + private static final String HOST_KEY1 = "hostKey"; + private static final String ALGO1 = "ssh-rsa"; + private static final String URI1 = "git@gitlab.test.local:wtran/my-repo"; + private static final String HOST1 = "gitlab.test.local"; + private static final String PRIVATE_KEY2 = "privateKey2"; + private static final String URI2 = "git@gitlab2.test.local:wtran/my-repo"; + private static final String HOST2 = "gitlab2.test.local"; + + @After + public void cleanup() { + SshSessionFactory.setInstance(null); + } + + @Test + public void testSingleSshUriProperties() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(mainRepoPropertiesFixture()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + assertThat(sshKeysByHostname.values(), hasSize(1)); + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + } + + @Test + public void testMultipleSshUriPropertiess() { + SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); + addRepoProperties(sshUriProperties, SshUriProperties.builder() + .uri(URI2) + .privateKey(PRIVATE_KEY2) + .build()); + + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); + + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + + sshKey = sshKeysByHostname.get(HOST2); + + assertThat(sshKeysByHostname.values(), hasSize(2)); + + assertThat(sshKey.getHostname(), is(equalTo(HOST2))); + + assertThat(sshKey.getHostKeyAlgorithm(), is(nullValue())); + + assertThat(sshKey.getHostKey(), is(nullValue())); + + assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY2))); + } + + @Test + public void testSameHostnameDifferentKeysFirstOneWins() { + SshUriProperties sshUriProperties = mainRepoPropertiesFixture(); + addRepoProperties(sshUriProperties, SshUriProperties.builder().uri(URI1) + .privateKey(PRIVATE_KEY1) + .hostKey(HOST_KEY1) + .hostKeyAlgorithm(ALGO1) + .build()); + + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(sshUriProperties); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + + assertThat(sshKeysByHostname.values(), hasSize(1)); + + SshUriProperties sshKey = sshKeysByHostname.get(HOST1); + assertMainRepo(sshKey); + } + + @Test + public void testNoSshUriPropertiess() { + SshUriPropertyProcessor sshUriPropertyProcessor = new SshUriPropertyProcessor(new SshUriProperties()); + Map sshKeysByHostname = sshUriPropertyProcessor.getSshKeysByHostname(); + assertThat(sshKeysByHostname.values(), hasSize(0)); + } + + private SshUriProperties mainRepoPropertiesFixture() { + + return SshUriProperties.builder() + .uri(URI1) + .hostKeyAlgorithm(ALGO1) + .hostKey(HOST_KEY1) + .privateKey(PRIVATE_KEY1) + .build(); + } + + private void addRepoProperties(SshUriProperties mainRepoProperties, SshUriProperties repoProperties) { + Map repos = new HashMap<>(); + repos.put("repo2", repoProperties); + mainRepoProperties.setRepos(repos); + } + + private void assertMainRepo(SshUriProperties sshKey) { + assertThat(sshKey.getHostname(), is(equalTo(HOST1))); + assertThat(sshKey.getHostKeyAlgorithm(), is(equalTo(ALGO1))); + assertThat(sshKey.getHostKey(), is(equalTo(HOST_KEY1))); + assertThat(sshKey.getPrivateKey(), is(equalTo(PRIVATE_KEY1))); + } + +} diff --git a/spring-cloud-config-server/src/test/resources/ssh/key b/spring-cloud-config-server/src/test/resources/ssh/key new file mode 100644 index 00000000..f1586c20 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAwQTXywKTzJEYXQ/LOR/hDciu4tJ1Av8zQQqi4ekL2hnOUzA0 +XecMmoYp31j3aWbT8RFNaUxFObfG7uGUWG21NlciqN7NtRkzTceygwBl25J9UEIo +iSHkS2arMhSIQ/B5TIFmL8fucsMUzqZ0LFwd2NR5k+BL5Ip/g6Gp6NpY2lNlxG3W +EaI/ee73vp8fJd9xge1o5Y/WFjCdP2TVfBuMTzswtjooakhQMg16c3YxcpK3Utbc +J4gaPDAGSd9eu5YuxSnTyC4s+FMPsKF3zEu+kTb5iutiCvO4lFdvlhURN2WBecRt +Am4nrMHEmLp3Yy440UTSOAzUUdxlfyltxvdOnQIDAQABAoIBABpQtbb4z/u44HB3 +ocZVAVLmxGkqNkfFUVCRyakxI6uFjyqqTMlsobxDhFvt/jVBjq20JTCw9FMtKhMR +cC8qLeWNtaz+S19gYy08h6ryl/B6pVkW2/3uP/jDs51tTTKuC2uRzMPA51KAJEsU +SIxvtqYEOdseXlp3Q3FHzilv82M4dKQCUXO/BbYV1jtgGHsJIJIcJhb8JIuXGScq +krm+GrzXpejF8FjpEFJ+VRbh/wjbMqUD5/9KimtJ9tXK5MKaxZ2aerYVONrPIbRH +Ij2tKS2o+cEBL9b0E2GHSGQyzs+g4+eTmsf56O8MWAoh5uib3pKXi84wxWi1ETQd ++NIGuUECgYEA9VZuTCbJVEriqptZKw8BNn7xIlXkmABEWAmIlo8U4NodjP/WIyIB +FtwYS4jUxfJhLdgs8mzb7O+EY6F+lusQBcAViRfk6a0VL9qrzYAY21d1X0hRtt4t +NKixjQ1hESao/CQDOonsYIqolXlDsvYxDX0tWXkVjiXPVk71oPtaF80CgYEAyWhT +j1v6XtFQT4KAQ10pQEbXBet9ae4M6WALAghdROMY60zX6XdXNGugycy3T9s9q712 +gxglhhhQO+RpEtaVOXvOzZv5aczrDqPwYOX1jJbIaOT4Iv2szjI518akRTef+ANW +t+mETUBf+qYKhW91lfSmi5PUPrWBs5z8j9qcohECgYEA1XgfAKO/ClkeYNRLAexM +Yd6Scg3KmXeh//hLoLVetUcT71usN2WfDQT1HsQwXwxHAlzWvxl5QvAlgYtDWo2t +o1M/acogpDK1/K2IT4bmGfQ/bGjAsR0guHEQDGtXylSZNMshBA5+XoJHrceV4GHW +zKXej7uLxeOgRLjvgOMzwQECgYAWSjJ9OiuyVK3zrritO+E12OExKYe+snbBWTy+ +o9hWqF94ow9+KhP/nC22R6696sCfgh9ckM6OUaCHkFbEm/T14KrZfHqU5/XXr/MJ +dkm/nmxdl4GStcprI4ndRIGnm2lb1FrDjddziT1WCww9CkHUL7hC2EB0FDhffW9C +KGqPUQKBgQCd85DH751a6EKN8dyKUyf3sP8BraTsc26DFk3q+vfhzSyJF4wxIVWa +kmjx6kfGTu0+ED0UXWi5LnMSvne4rGU1j3rQyHs71t6JLVcFl8j5i8g/n91FA87C +EIni0JgBvgbDNlM04mMFE6K0u5+Rt3Nx/yywW/w9nQUrzNio7MP+8w== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml new file mode 100644 index 00000000..28b09e86 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-nested-settings.yml @@ -0,0 +1,41 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo1.git + ignoreLocalSshSettings: true + privateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\nyyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\nuP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\nayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\nTOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\njfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\nlDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\nxdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\nq4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\nmRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\nlKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\nZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\nZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\nBT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\nzf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\nuC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\nE1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n-----END RSA PRIVATE KEY-----" + repos: + repo1: + uri: git@gitserver.com:team/repo2.git + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml new file mode 100644 index 00000000..3db045f3 --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-block.yml @@ -0,0 +1,35 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo.git + ignoreLocalSshSettings: true + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI + yyxp38YcuZG9cUDBAxby+K2TKmwHaC1Wf1QTwbPuCRdIPrDwRz+FLoegm3iDLCmn + uP6rjZDneYsqfU1sSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km + ayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq + TOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir + +diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk + jfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt + lDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI + xdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V + 4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG + 7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu + 3reKgm0CgYEA03Sx8JgF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8 + q4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq + mRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa + lKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M + ZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv + ZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit + BT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO + zf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw + uC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF + 54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS + E1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx + 4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr + /syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq + 2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ== + -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml new file mode 100644 index 00000000..399ba87a --- /dev/null +++ b/spring-cloud-config-server/src/test/resources/ssh/ssh-private-key-newline.yml @@ -0,0 +1,10 @@ +spring: + cloud: + config: + server: + git: + uri: git@gitserver.com:team/repo.git + ignoreLocalSshSettings: true + privateKey: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAoqyz6YaYMTr7L8GLPSQpAQXaM04gRx4CCsGK2kfLQdw4BlqI\nyyxp38YcuZG9cUDBAxby+K2TKmwHaC1R61QTwbPuCRdIPrDwRz+FLoegm3iDLCmn\nuP6rjZDneYsqfU1KSdrOwIbCnONfDdvYL/vnZC/o8DDMlk5Orw2SfHkT3pq0o8km\nayBwN4Sf3bpyWTY0oZcmNeSCCoIdE59k8Pa7/t9bwY9caLj05C3DEsjucc7Ei/Eq\nTOyGyobtXwaya5CqKLUHes74Poz1aEP/yVFdUud91uezd8ZK1P1t5/ZKA3R6aHir\n+diDJ2/GQ2tD511FW46yw+EtBUJTO6ADVv4UnQIDAQABAoIBAF+5qwEfX82QfKFk\njfADqFFexUDtl1biFKeJrpC2MKhn01wByH9uejrhFKQqW8UaKroLthyZ34DWIyGt\nlDnHGv0gSVF2LuAdNLdobJGt49e4+c9yD61vxzm97Eh8mRs08SM2q/VlF35E2fmI\nxdWusUImYzd8L9e+6tRd8zZl9UhG5vR5XIstKqxC6S0g79aAt0hasE4Gw1FKOf2V\n4mlL15atjQSKCPdOicuyc4zpjAtU1A9AfF51iG8oOUuJebPW8tCftfOQxaeGFgMG\n7M9aai1KzXR6M5IBAKEv31yBvz/SHTneP7oZXNLeC1GIR420PKybmeZdNK8BbEAu\n3reKgm0CgYEA03Sx8JoF5UBsIvFPpP1fjSlTgKryM5EJR6KQtj5e4YfyxccJepN8\nq4MrqDfNKleG/a1acEtDMhBNovU7Usp2QIP7zpAeioHBOhmE5WSieZGc3icOGWWq\nmRkdulSONruqWKv76ZoluxftekE03bDhZDNlcCgmrslEKB/ufHd2oc8CgYEAxPFa\nlKOdSeiYFV5CtvO8Ro8em6rGpSsVz4qkPxbeBqUDCb9KXHhq6YrhRxOIfQJKfT7M\nZFCn8ArJXKgOGu+KsvwIErFHF9g2jJMG4DOUTpkQgi2yveihFxcmz/AltyVXgrnv\nZWQbAerH77pdKKhNivLGgEv72GYawdYjYNjemdMCgYA2kEMmMahZyrDcp2YEzfit\nBT/t0K6kzcUWPgWXcSqsiZcEn+J7RbmCzFskkhmX1nQX23adyV3yejB+X0dKisHO\nzf/ZAmlPFkJVCqa3RquCMSfIT02dEhXeYZPBM/Zqeyxuqxpa4hLgX0FBLbhFiFHw\nuC5xrXql2XuD2xF//peXEwKBgQC+pa28Cg7vRxxCQzduB9CQtWc55j3aEjVQ7bNF\n54sS/5ZLT0Ra8677WZfuyDfuW9NkHvCZg4Ku2qJG8eCFrrGjxlrCTZ62tHVJ6+JS\nE1xUIdRbUIWhVZrr0VufG6hG/P0T7Y6Tpi6G0pKtvMkF3LcD9TS3adboix8H2ZXx\n4L7MRQKBgQC0OO3qqNXOjIVYWOoqXLybOY/Wqu9lxCAgGyCYaMcstnBI7W0MZTBr\n/syluvGsaFc1sE7MMGOOzKi1tF4YvDmSnzA/R1nmaPguuD9fOA+w7Pwkv5vLvuJq\n2U7EeNwxq1I1L3Ag6E7wH4BHLHd4TKaZR6agFkn8oomz71yZPGjuZQ==\n-----END RSA PRIVATE KEY-----" + hostKey: somekey + hostKeyAlgorithm: ssh-rsa \ No newline at end of file