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