JCLOUDS-516: Add ssh agent support via sch agentproxy

This commit is contained in:
Pasi Niemi 2014-03-19 22:58:02 +02:00 committed by Andrew Phillips
parent 59c10e18e8
commit 63d6d97553
14 changed files with 182 additions and 32 deletions

View File

@ -17,6 +17,7 @@
package org.jclouds.compute.functions;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import java.util.concurrent.TimeUnit;
@ -49,7 +50,7 @@ public class CreateSshClientOncePortIsListeningOnNode implements Function<NodeMe
@Inject(optional = true)
SshClient.Factory sshFactory;
private final OpenSocketFinder openSocketFinder;
private final long timeoutMs;
@ -65,7 +66,7 @@ public class CreateSshClientOncePortIsListeningOnNode implements Function<NodeMe
checkState(sshFactory != null, "ssh requested, but no SshModule configured");
checkNotNull(node.getCredentials(), "no credentials found for node %s", node.getId());
checkNotNull(node.getCredentials().identity, "no login identity found for node %s", node.getId());
checkNotNull(node.getCredentials().credential, "no credential found for %s on node %s", node
checkArgument(node.getCredentials().credential != null || sshFactory.isAgentAvailable(), "no credential or ssh agent found for %s on node %s", node
.getCredentials().identity, node.getId());
HostAndPort socket = openSocketFinder.findOpenSocketOnNode(node, node.getLoginPort(),
timeoutMs, TimeUnit.MILLISECONDS);

View File

@ -29,9 +29,8 @@ import com.google.common.net.HostAndPort;
public interface SshClient {
interface Factory {
SshClient create(HostAndPort socket, LoginCredentials credentials);
boolean isAgentAvailable();
}
String getUsername();

View File

@ -228,7 +228,6 @@ public abstract class BaseComputeServiceLiveTest extends BaseComputeServiceConte
NodeMetadata node = get(nodes, 0);
LoginCredentials good = node.getCredentials();
assert good.identity != null : nodes;
assert good.credential != null : nodes;
for (Entry<? extends NodeMetadata, ExecResponse> response : client.runScriptOnNodesMatching(
runningInGroup(group), "hostname",
@ -507,7 +506,6 @@ public abstract class BaseComputeServiceLiveTest extends BaseComputeServiceConte
assertNotNull(node.getCredentials());
if (node.getCredentials().identity != null) {
assertNotNull(node.getCredentials().identity);
assertNotNull(node.getCredentials().credential);
sshPing(node, taskName);
}
}

View File

@ -73,6 +73,7 @@ public class Pems {
public static final String CERTIFICATE_X509_MARKER = "-----BEGIN CERTIFICATE-----";
public static final String PUBLIC_X509_MARKER = "-----BEGIN PUBLIC KEY-----";
public static final String PUBLIC_PKCS1_MARKER = "-----BEGIN RSA PUBLIC KEY-----";
public static final String PROC_TYPE_ENCRYPTED = "Proc-Type: 4,ENCRYPTED";
private static class PemProcessor<T> implements ByteProcessor<T> {
private interface ResultParser<T> {

View File

@ -19,6 +19,7 @@ package org.jclouds.domain;
import static org.jclouds.crypto.Pems.PRIVATE_PKCS1_MARKER;
import static org.jclouds.crypto.Pems.PRIVATE_PKCS8_MARKER;
import org.jclouds.crypto.Pems;
import org.jclouds.javax.annotation.Nullable;
import com.google.common.base.Optional;
@ -155,6 +156,15 @@ public class LoginCredentials extends Credentials {
return (privateKey != null) ? privateKey.orNull() : null;
}
/**
* @return true if there is a private key attached that is not encrypted
*/
public boolean hasUnencryptedPrivateKey() {
return getPrivateKey() != null
&& !getPrivateKey().isEmpty()
&& !getPrivateKey().contains(Pems.PROC_TYPE_ENCRYPTED);
}
/**
* @return the optional private ssh key of the user or null
*/

View File

@ -86,6 +86,16 @@
<artifactId>jsch</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.jsch</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.connector-factory</artifactId>
<version>0.0.7</version>
</dependency>
</dependencies>
<profiles>

View File

@ -56,6 +56,7 @@ import org.jclouds.util.Closeables2;
import org.jclouds.util.Strings2;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
@ -66,6 +67,7 @@ import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.agentproxy.Connector;
/**
* This class needs refactoring. It is not thread safe.
@ -124,25 +126,28 @@ public class JschSshClient implements SshClient {
public JschSshClient(ProxyConfig proxyConfig, BackoffLimitedRetryHandler backoffLimitedRetryHandler, HostAndPort socket,
LoginCredentials loginCredentials, int timeout) {
LoginCredentials loginCredentials, int timeout, Optional<Connector> agentConnector) {
this.user = checkNotNull(loginCredentials, "loginCredentials").getUser();
this.host = checkNotNull(socket, "socket").getHostText();
checkArgument(socket.getPort() > 0, "ssh port must be greater then zero" + socket.getPort());
checkArgument(loginCredentials.getPassword() != null || loginCredentials.getPrivateKey() != null,
"you must specify a password or a key");
checkArgument(loginCredentials.getPassword() != null || loginCredentials.hasUnencryptedPrivateKey() || agentConnector.isPresent(),
"you must specify a password, a key or an SSH agent needs to be available");
this.backoffLimitedRetryHandler = checkNotNull(backoffLimitedRetryHandler, "backoffLimitedRetryHandler");
if (loginCredentials.getPrivateKey() == null) {
if (loginCredentials.getPassword() != null) {
this.toString = String.format("%s:pw[%s]@%s:%d", loginCredentials.getUser(),
base16().lowerCase().encode(md5().hashString(loginCredentials.getPassword(), UTF_8).asBytes()), host,
socket.getPort());
} else {
} else if (loginCredentials.hasUnencryptedPrivateKey()) {
String fingerPrint = fingerprintPrivateKey(loginCredentials.getPrivateKey());
String sha1 = sha1PrivateKey(loginCredentials.getPrivateKey());
this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", loginCredentials.getUser(),
fingerPrint, sha1, host, socket.getPort());
fingerPrint, sha1, host, socket.getPort());
} else {
this.toString = String.format("%s:rsa[ssh-agent]@%s:%d", loginCredentials.getUser(), host, socket.getPort());
}
sessionConnection = SessionConnection.builder().hostAndPort(HostAndPort.fromParts(host, socket.getPort())).loginCredentials(
loginCredentials).proxy(checkNotNull(proxyConfig, "proxyConfig")).connectTimeout(timeout).sessionTimeout(timeout).build();
loginCredentials).proxy(checkNotNull(proxyConfig, "proxyConfig")).connectTimeout(timeout).sessionTimeout(timeout)
.agentConnector(agentConnector).build();
}
@Override

View File

@ -17,7 +17,6 @@
package org.jclouds.ssh.jsch;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import org.jclouds.domain.Credentials;
@ -34,8 +33,13 @@ import com.jcraft.jsch.Proxy;
import com.jcraft.jsch.ProxyHTTP;
import com.jcraft.jsch.ProxySOCKS5;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
public final class SessionConnection implements Connection<Session> {
private Optional<Connector> agentConnector;
public static Builder builder() {
return new Builder();
}
@ -47,6 +51,7 @@ public final class SessionConnection implements Connection<Session> {
private Optional<Proxy> proxy = Optional.absent();
private int connectTimeout;
private int sessionTimeout;
private Optional<Connector> agentConnector;
/**
* @see SessionConnection#getHostAndPort()
@ -114,7 +119,7 @@ public final class SessionConnection implements Connection<Session> {
}
public SessionConnection build() {
return new SessionConnection(hostAndPort, loginCredentials, proxy, connectTimeout, sessionTimeout);
return new SessionConnection(hostAndPort, loginCredentials, proxy, connectTimeout, sessionTimeout, agentConnector);
}
public Builder from(SessionConnection in) {
@ -122,15 +127,21 @@ public final class SessionConnection implements Connection<Session> {
.connectTimeout(in.connectTimeout).sessionTimeout(in.sessionTimeout);
}
public Builder agentConnector(Optional<Connector> agentConnector) {
this.agentConnector = agentConnector;
return this;
}
}
private SessionConnection(HostAndPort hostAndPort, LoginCredentials loginCredentials, Optional<Proxy> proxy,
int connectTimeout, int sessionTimeout) {
int connectTimeout, int sessionTimeout, Optional<Connector> agentConnector) {
this.hostAndPort = checkNotNull(hostAndPort, "hostAndPort");
this.loginCredentials = checkNotNull(loginCredentials, "loginCredentials for %", hostAndPort);
this.connectTimeout = connectTimeout;
this.sessionTimeout = sessionTimeout;
this.proxy = checkNotNull(proxy, "proxy for %", hostAndPort);
this.agentConnector = checkNotNull(agentConnector, "agentConnector for %", hostAndPort);
}
private static final byte[] emptyPassPhrase = new byte[0];
@ -160,11 +171,12 @@ public final class SessionConnection implements Connection<Session> {
session.setTimeout(sessionTimeout);
if (loginCredentials.getPrivateKey() == null) {
session.setPassword(loginCredentials.getPassword());
} else {
checkArgument(!loginCredentials.getPrivateKey().contains("Proc-Type: 4,ENCRYPTED"),
"JschSshClientModule does not support private keys that require a passphrase");
} else if (loginCredentials.hasUnencryptedPrivateKey()) {
byte[] privateKey = loginCredentials.getPrivateKey().getBytes();
jsch.addIdentity(loginCredentials.getUser(), privateKey, null, emptyPassPhrase);
} else if (agentConnector.isPresent()) {
JSch.setConfig("PreferredAuthentications", "publickey");
jsch.setIdentityRepository(new RemoteIdentityRepository(agentConnector.get()));
}
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no");

View File

@ -28,11 +28,15 @@ import org.jclouds.ssh.SshClient;
import org.jclouds.ssh.config.ConfiguresSshClient;
import org.jclouds.ssh.jsch.JschSshClient;
import com.google.common.base.Optional;
import com.google.common.net.HostAndPort;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.jcraft.jsch.agentproxy.AgentProxyException;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.ConnectorFactory;
/**
*
@ -50,6 +54,16 @@ public class JschSshClientModule extends AbstractModule {
@Inject(optional = true)
int timeout = 60000;
Optional<Connector> agentConnector = getAgentConnector();
Optional<Connector> getAgentConnector() {
try {
return Optional.of(ConnectorFactory.getDefault().createConnector());
} catch (final AgentProxyException e) {
return Optional.absent();
}
}
private final ProxyConfig proxyConfig;
private final BackoffLimitedRetryHandler backoffLimitedRetryHandler;
private final Injector injector;
@ -63,9 +77,14 @@ public class JschSshClientModule extends AbstractModule {
@Override
public SshClient create(HostAndPort socket, LoginCredentials credentials) {
SshClient client = new JschSshClient(proxyConfig, backoffLimitedRetryHandler, socket, credentials, timeout);
SshClient client = new JschSshClient(proxyConfig, backoffLimitedRetryHandler, socket, credentials, timeout, getAgentConnector());
injector.injectMembers(client);// add logger
return client;
}
@Override
public boolean isAgentAvailable() {
return agentConnector.isPresent();
}
}
}

View File

@ -104,6 +104,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.sshj</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.connector-factory</artifactId>
<version>0.0.7</version>
</dependency>
</dependencies>
<profiles>

View File

@ -17,15 +17,19 @@
package org.jclouds.sshj;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.util.List;
import javax.annotation.Resource;
import javax.inject.Named;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.common.Buffer.BufferException;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile;
import net.schmizz.sshj.userauth.method.AuthMethod;
import org.jclouds.domain.LoginCredentials;
import org.jclouds.logging.Logger;
@ -33,9 +37,18 @@ import org.jclouds.sshj.SshjSshClient.Connection;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HostAndPort;
import com.jcraft.jsch.agentproxy.AgentProxy;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.Identity;
import com.jcraft.jsch.agentproxy.sshj.AuthAgent;
public class SSHClientConnection implements Connection<SSHClient> {
private Optional<Connector> agentConnector;
public static Builder builder() {
return new Builder();
}
@ -46,6 +59,7 @@ public class SSHClientConnection implements Connection<SSHClient> {
protected LoginCredentials loginCredentials;
protected int connectTimeout;
protected int sessionTimeout;
protected Optional<Connector> agentConnector;
/**
* @see SSHClientConnection#getHostAndPort()
@ -79,8 +93,16 @@ public class SSHClientConnection implements Connection<SSHClient> {
return this;
}
/**
* @see SSHClientConnection#getAgentConnector()
*/
public Builder agentConnector(Optional<Connector> agentConnector) {
this.agentConnector = agentConnector;
return this;
}
public SSHClientConnection build() {
return new SSHClientConnection(hostAndPort, loginCredentials, connectTimeout, sessionTimeout);
return new SSHClientConnection(hostAndPort, loginCredentials, connectTimeout, sessionTimeout, agentConnector);
}
protected Builder fromSSHClientConnection(SSHClientConnection in) {
@ -90,11 +112,12 @@ public class SSHClientConnection implements Connection<SSHClient> {
}
private SSHClientConnection(HostAndPort hostAndPort, LoginCredentials loginCredentials, int connectTimeout,
int sessionTimeout) {
this.hostAndPort = hostAndPort;
this.loginCredentials = loginCredentials;
int sessionTimeout, Optional<Connector> agentConnector) {
this.hostAndPort = checkNotNull(hostAndPort, "hostAndPort");
this.loginCredentials = checkNotNull(loginCredentials, "loginCredentials for %", hostAndPort);
this.connectTimeout = connectTimeout;
this.sessionTimeout = sessionTimeout;
this.agentConnector = checkNotNull(agentConnector, "agentConnector for %", hostAndPort);
}
@Resource
@ -136,10 +159,13 @@ public class SSHClientConnection implements Connection<SSHClient> {
ssh.connect(hostAndPort.getHostText(), hostAndPort.getPortOrDefault(22));
if (loginCredentials.getPassword() != null) {
ssh.authPassword(loginCredentials.getUser(), loginCredentials.getPassword());
} else {
} else if (loginCredentials.hasUnencryptedPrivateKey()) {
OpenSSHKeyFile key = new OpenSSHKeyFile();
key.init(loginCredentials.getPrivateKey(), null);
ssh.authPublickey(loginCredentials.getUser(), key);
} else if (agentConnector.isPresent()) {
AgentProxy proxy = new AgentProxy(agentConnector.get());
ssh.auth(loginCredentials.getUser(), getAuthMethods(proxy));
}
return ssh;
}
@ -175,6 +201,14 @@ public class SSHClientConnection implements Connection<SSHClient> {
return sessionTimeout;
}
/**
*
* @return Ssh agent connector
*/
public Optional<Connector> getAgentConnector() {
return agentConnector;
}
/**
*
* @return the current ssh or {@code null} if not connected
@ -206,4 +240,12 @@ public class SSHClientConnection implements Connection<SSHClient> {
"sessionTimeout", sessionTimeout).toString();
}
private static List<AuthMethod> getAuthMethods(AgentProxy agent) throws BufferException {
ImmutableList.Builder<AuthMethod> identities = ImmutableList.builder();
for (Identity identity : agent.getIdentities()) {
identities.add(new AuthAgent(agent, identity));
}
return identities.build();
}
}

View File

@ -68,6 +68,7 @@ import org.jclouds.util.Closeables2;
import org.jclouds.util.Throwables2;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
@ -76,6 +77,7 @@ import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.net.HostAndPort;
import com.google.inject.Inject;
import com.jcraft.jsch.agentproxy.Connector;
/**
* This class needs refactoring. It is not thread safe.
@ -141,25 +143,28 @@ public class SshjSshClient implements SshClient {
private final BackoffLimitedRetryHandler backoffLimitedRetryHandler;
public SshjSshClient(BackoffLimitedRetryHandler backoffLimitedRetryHandler, HostAndPort socket,
LoginCredentials loginCredentials, int timeout) {
LoginCredentials loginCredentials, int timeout, Optional<Connector> agentConnector) {
this.user = checkNotNull(loginCredentials, "loginCredentials").getUser();
this.host = checkNotNull(socket, "socket").getHostText();
checkArgument(socket.getPort() > 0, "ssh port must be greater then zero" + socket.getPort());
checkArgument(loginCredentials.getPassword() != null || loginCredentials.getPrivateKey() != null,
"you must specify a password or a key");
checkArgument(loginCredentials.getPassword() != null || loginCredentials.hasUnencryptedPrivateKey() || agentConnector.isPresent(),
"you must specify a password, a key or an SSH agent needs to be available");
this.backoffLimitedRetryHandler = checkNotNull(backoffLimitedRetryHandler, "backoffLimitedRetryHandler");
if (loginCredentials.getPrivateKey() == null) {
if (loginCredentials.getPassword() != null) {
this.toString = String.format("%s:pw[%s]@%s:%d", loginCredentials.getUser(),
base16().lowerCase().encode(md5().hashString(loginCredentials.getPassword(), UTF_8).asBytes()), host,
socket.getPort());
} else {
} else if (loginCredentials.hasUnencryptedPrivateKey()) {
String fingerPrint = fingerprintPrivateKey(loginCredentials.getPrivateKey());
String sha1 = sha1PrivateKey(loginCredentials.getPrivateKey());
this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", loginCredentials.getUser(),
fingerPrint, sha1, host, socket.getPort());
} else {
this.toString = String.format("%s:rsa[ssh-agent]@%s:%d", loginCredentials.getUser(),
host, socket.getPort());
}
sshClientConnection = SSHClientConnection.builder().hostAndPort(HostAndPort.fromParts(host, socket.getPort()))
.loginCredentials(loginCredentials).connectTimeout(timeout).sessionTimeout(timeout).build();
.loginCredentials(loginCredentials).connectTimeout(timeout).sessionTimeout(timeout).agentConnector(agentConnector).build();
}
@Override

View File

@ -25,11 +25,15 @@ import org.jclouds.ssh.SshClient;
import org.jclouds.ssh.config.ConfiguresSshClient;
import org.jclouds.sshj.SshjSshClient;
import com.google.common.base.Optional;
import com.google.common.net.HostAndPort;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Scopes;
import com.jcraft.jsch.agentproxy.AgentProxyException;
import com.jcraft.jsch.agentproxy.Connector;
import com.jcraft.jsch.agentproxy.ConnectorFactory;
/**
*
@ -42,11 +46,22 @@ public class SshjSshClientModule extends AbstractModule {
bind(SshClient.Factory.class).to(Factory.class).in(Scopes.SINGLETON);
}
private static class Factory implements SshClient.Factory {
@Named(Constants.PROPERTY_CONNECTION_TIMEOUT)
@Inject(optional = true)
int timeout = 60000;
Optional<Connector> agentConnector = getAgentConnector();
Optional<Connector> getAgentConnector() {
try {
return Optional.of(ConnectorFactory.getDefault().createConnector());
} catch (final AgentProxyException e) {
return Optional.absent();
}
}
private final BackoffLimitedRetryHandler backoffLimitedRetryHandler;
private final Injector injector;
@ -58,9 +73,15 @@ public class SshjSshClientModule extends AbstractModule {
@Override
public SshClient create(HostAndPort socket, LoginCredentials credentials) {
SshClient client = new SshjSshClient(backoffLimitedRetryHandler, socket, credentials, timeout);
SshClient client = new SshjSshClient(backoffLimitedRetryHandler, socket, credentials, timeout, getAgentConnector());
injector.injectMembers(client);// add logger
return client;
}
@Override
public boolean isAgentAvailable() {
return agentConnector.isPresent();
}
}
}

View File

@ -469,6 +469,23 @@
<package>com.google</package>
</packages>
</exception>
<exception>
<conflictingDependencies>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.core</artifactId>
<version>0.0.7</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.connector-factory</artifactId>
<version>0.0.7</version>
</dependency>
</conflictingDependencies>
<packages>
<package>com.jcraft.jsch.agentproxy</package>
</packages>
</exception>
</exceptions>
<ignoredResources>
<!-- For all the jetty packages -->