blob: 0b994c6b9f4ad18d22455fd34a6d0158262231f1 [file] [log] [blame] [view]
# Set up an SSH client in 5 minutes
SSHD is designed to easily allow setting up and using an SSH client in a few simple steps. The client needs to be configured
and then started before it can be used to connect to an SSH server. There are a few simple steps for creating a client
instance - for more details refer to the `SshClient` class.
## Creating an instance of the `SshClient` class
This is simply done by calling
```java
SshClient client = SshClient.setUpDefaultClient();
```
The call will create an instance with a default configuration suitable for most use cases - including ciphers,
compression, MACs, key exchanges, signatures, etc... If your code requires some special configuration, one can
look at the code for `setUpDefaultClient` and `checkConfig` as a reference for available options and configure
the SSH client the way you need.
## Set up client side security
The SSH client contains some security related configuration that one needs to consider
### `ServerKeyVerifier`
`client.setServerKeyVerifier(...);` sets up the server key verifier. As part of the SSH connection initialization
protocol, the server proves its "identity" by presenting a public key. The client can examine the key (e.g., present
it to the user via some UI) and decide whether to trust the server and continue with the connection setup. By default
the client is initialized with an `AcceptAllServerKeyVerifier` that simply logs a warning that an un-verified server
key was accepted. There are other out-of-the-box verifiers available in the code:
* `RejectAllServerKeyVerifier` - rejects all server key - usually used in tests or as a fallback verifier if none
of it predecesors validated the server key
* `RequiredServerKeyVerifier` - accepts only **one** specific server key (similar to certificate pinning for SSL)
* `KnownHostsServerKeyVerifier` - uses the [known_hosts](https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#Public_Keys_from_other_Hosts_.E2.80.93_.7E.2F.ssh.2Fknown_hosts)
file to validate the server key. One can use this class + some existing code to **update** the file when new servers are detected and their keys are accepted.
Of course, one can implement the verifier in whatever other manner is suitable for the specific code needs.
### `ClientIdentityLoader/KeyPairProvider`
One can set up the public/private keys to be used in case a password-less authentication is needed. By default, the client is configured to automatically
detect and use the identity files residing in the user's *~/.ssh* folder (e.g., *id_rsa*, *id_ecdsa*) and present them as part of the authentication process.
**Note:** if the identity files are encrypted via a password, one must configure a `FilePasswordProvider` so that the code can decrypt them before using
and presenting them to the server as part of the authentication process. Reading key files in PEM format (including encrypted ones) is supported by default
for the standard keys and formats. Using additional non-standard special features requires that the [Bouncy Castle](https://www.bouncycastle.org/) supporting
artifacts be available in the code's classpath.
#### Loading key files
In order to use password-less authentication the user needs to provide one or more `KeyPair`-s that are used to "prove" the client's identity for
the server. The code supports most if not all of the currently used key file formats. See `SshKeyDumpMain` class for example of how to load files - basically:
```java
KeyPairResourceLoader loader = SecurityUtils.getKeyPairResourceParser();
Collection<KeyPair> keys = loader.loadKeyPairs(null, filePath, passwordProvider);
```
For *PUTTY* key files one needs to include the *sshd-putty* module and use a different loader:
```java
Collection<KeyPair> keys = PuttyKeyUtils.DEFAULT_INSTANCE.loadKeyPairs(null, filePath, passwordProvider);
```
**Note:** reminder - a user's "identity" is the file that contains the **private** key - there is no need to provide the public key file since the
private key either already contains the public key in it, or it can be easily calculated from the private one.
Once the keys are loaded, one simply needs to provide them to the client session:
```java
try (ClientSession session = ...estblish initial session...) {
for (KeyPair kp : keys) {
session.addKeyIdentity(kp);
}
session.auth().await(...);
}
```
Instead of doing this on every session, it is possible to load the keys only **once** and then wrap them inside a `KeyIdentityProvider`
that is setup during *SshClient* setup:
```java
Collection<KeyPair> keys = ...load the keys ...
SshClient client = ...setup client...
client.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keys));
client.start();
```
The provided keys will be used for **all* the sessions - *Note:*
* One can **add** key identities to specific sessions.
* A similar effect can be achiveved for **passwords** by registering a `PasswordIdentityProvider` with the *SshClient*, and
thus forego the need to provide the password repeatedly for each session. In this context, one can go even one step forward
and provide a **combined** `AuthenticationIdentitiesProvider` that provides **both** passwords and key pairs. Both type of providers
are invoked with the established `SessionContext` so the user can actually pick which mechanism to use, what password/key to
use according to the server's identity.
#### Providing passwords for encrypted key files
The `FilePasswordProvider` is required for all private key files that are encrypted and being loaded (not just the "identity" ones). If the user
knows ahead of time that the file being currently decoded is not encrypted, a *null* provider may be used (if the file turns out to be encrypted
though an exception will be thrown in this case).
The `FilePasswordProvider`has support for a **retry mechanism** via its `handleDecodeAttemptResult`. When the code detects an encrypted private key,
it will start a loop where it prompts for the password, attempts to decode the key using the provided password and then informs the provider of
the outcome - success or failure. If failure is signaled, then the provider can decide whether to retry using a new password, abort (with exception)
or ignore. If the provider chooses to ignore the failure, then the code will make a best effort to proceed without the (undecoded) key.
The invoked methods are provided with a `NamedResource` that provides an indication of the key source "name" that is being attempted. This name
can be used in order to prompt the user interactively and provide a useful "hint" as to the password that needs to be provided. Furthermore, the
vast majority of the provided `NamedResource`-s also implement `IoResource` - which means that the code can find out what type of resource
is being attempted - e.g., a file [Path](https://docs.oracle.com/javase/8/docs/api/index.html?java/nio/file/Path.html),
a [URL](https://docs.oracle.com/javase/8/docs/api/java/net/URL.html), a [URI](https://docs.oracle.com/javase/8/docs/api/java/net/URI.html),
etc. - and modify it's behavior accordingly.
#### OpenSSH file format support
The code supports [OpenSSH](http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?rev=1.1&content-type=text/x-cvsweb-markup)
formatted files without any specific extra artifacts (although for reading _ed25519_ keys one needs to add the _EdDSA_ support artifacts). For
**encrypted** files only the the `bcrypt` key derivation function (KDF) is [currently supported](https://issues.apache.org/jira/browse/SSHD-708).
In this context, the maximum allowed number of rounds has been set to ~255 in order to protect the decryption process from
malformed or malicious data. However, since the protocol allows for 2^31 values, it is possible to modify the default by
calling `BCryptKdfOptions#setMaxAllowedRounds()` **programmatically** at any time - please note that
* The setting is **global** - i.e., affects all decryption attempts from then on and not just for the current SSH session or thread.
* The setting value is never allowed to be non-positive - any attempt to set such a value programmatically
throws an exception.
The usual _OpenSSH_ default seems to be 16, but users can ask for more (or less) by generating an encrypted key via
[`ssh-keygen -a NNN`](http://man7.org/linux/man-pages/man1/ssh-keygen.1.html). However, this comes at a cost:
>> -a rounds
>>
>> When saving a private key this option specifies the number of
>> KDF (key derivation function) rounds used. Higher numbers
>> result in slower passphrase verification
Various discussions on the net seem to indicate that 64 is the value at which many computers start to slow down noticeably, so
our default limit seems quite suitable (and beyond) for most cases we are likely to encounter "in the wild".
### `UserInteraction`
This interface is required for full support of `keyboard-interactive` authentication protocol as described in [RFC-4252 section 9](https://tools.ietf.org/html/rfc4252#section-9).
The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be
provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://tools.ietf.org/html/rfc4252#section-8).
While [RFC-4256](https://tools.ietf.org/html/rfc4256) support is the primary purpose of this interface, it can also be used to retrieve the server's
welcome banner as described in [RFC 4252 section 5.4](https://tools.ietf.org/html/rfc4252#section-5.4) as well as its initial identification string
as described in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
In this context, regardless of whether such interaction is configured, the default implementation for the client side contains code
that attempts to auto-detect a password prompt. If it detects it, then it attempts to use one of the registered passwords (if any) as
the interactive response to the server's challenge - (see client-side implementation of `UserAuthKeyboardInteractive#useCurrentPassword`
method). Basically, detection occurs by checking if the server sent **exactly one** challenge with no requested echo, and the challenge
string looks like `"... password ...:"` (**Note:** the auto-detection and password prompt detection patterns are configurable).
This interface can also be used to easily implement interactive password request from user for the `password` authentication protocol
as described in [RFC-4252 section 8](https://tools.ietf.org/html/rfc4252#section-8) via the `resolveAuthPasswordAttempt` method.
```java
/**
* Invoked during password authentication when no more pre-registered passwords are available
*
* @param session The {@link ClientSession} through which the request was received
* @return The password to use - {@code null} signals no more passwords available
* @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
*/
String resolveAuthPasswordAttempt(ClientSession session) throws Exception;
```
The interface can also be used to implement interactive key based authentication as described in [RFC-4252 section 7](https://tools.ietf.org/html/rfc4252#section-7)
via the `resolveAuthPublicKeyIdentityAttempt` method.
```java
/**
* Invoked during public key authentication when no more pre-registered keys are available
*
* @param session The {@link ClientSession} through which the request was received
* @return The {@link KeyPair} to use - {@code null} signals no more keys available
* @throws Exception if failed to handle the request - <B>Note:</B> may cause session termination
*/
KeyPair resolveAuthPublicKeyIdentityAttempt(ClientSession session) throws Exception;
```
## Using the `SshClient` to connect to a server
Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server.
**Note:** one can use a single `SshClient` instance to connnect to multiple servers as well as modifying the default
configuration (ciphers, MACs, keys, etc.) on a per-session manner (see more in the *Advanced usage* section).
Furthermore, one can change almost any configured `SshClient` parameter - although its influence on currently established
sessions depends on the actual changed configuration. Here is how a typical usage would look like
```java
SshClient client = SshClient.setUpDefaultClient();
// override any default configuration...
client.setSomeConfiguration(...);
client.setOtherConfiguration(...);
client.start();
// using the client for multiple sessions...
try (ClientSession session = client.connect(user, host, port)
.verify(...timeout...)
.getSession()) {
session.addPasswordIdentity(...password..); // for password-based authentication
// or
session.addPublicKeyIdentity(...key-pair...); // for password-less authentication
// Note: can add BOTH password AND public key identities - depends on the client/server security setup
session.auth().verify(...timeout...);
// start using the session to run commands, do SCP/SFTP, create local/remote port forwarding, etc...
}
// NOTE: this is just an example - one can open multiple concurrent sessions using the same client.
// No need to close the previous session before establishing a new one
try (ClientSession anotherSession = client.connect(otherUser, otherHost, port)
.verify(...timeout...)
.getSession()) {
anotherSession.addPasswordIdentity(...password..); // for password-based authentication
anotherSession.addPublicKeyIdentity(...key-pair...); // for password-less authentication
anotherSession.auth().verify(...timeout...);
// start using the session to run commands, do SCP/SFTP, create local/remote port forwarding, etc...
}
// exiting in an orderly fashion once the code no longer needs to establish SSH session
// NOTE: this can/should be done when the application exits.
client.stop();
```
## Configuring the protocol exchange phase
[RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2) does not specify when the client/server should send
their respective identification strings. All it states is that these strings must be available before KEX stage since they
participate in it. By default, the client sends its identification string immediately upon session being established. However,
this can be modified so that the client waits for the server's identification before sending its own.
```java
SshClient client = ...setup client...
PropertyResolverUtils.updateProperty(
client, CoreModuleProperties.SEND_IMMEDIATE_IDENTIFICATION.getName(), false);
client.start();
```
A similar configuration can be applied to sending the initial `SSH_MSG_KEXINIT` message - i.e., the client can be configured
to wait until the server's identification is received before sending the message. This is done in order to allow clients to
customize the KEX phase according to the parsed server identification.
```java
SshClient client = ...setup client...
PropertyResolverUtils.updateProperty(
client, CoreModuleProperties.SEND_IMMEDIATE_KEXINIT.getName(), false);
client.start();
```
**Note:** if immediate sending of the client's identification is disabled, `SSH_MSG_KEXINIT` message sending is also
automatically delayed until after the server's identification is received.
A viable configuration might be to send the client's identification immediately, but delay the client's `SSH_MSG_KEXINIT`
message sending until the server's identification is received so that the client can customize the session based on the
server's identity. This is a more likely configuration then delaying the client's own identification in order to be able
to cope with port multiplexors such as [sslh](http://www.rutschle.net/tech/sslh/README.html). Such multiplexors usually
require that the client send an initial packet immediately after connection is established so that they can analyze it
and route it to the correct server (_ssh_ in this case). If we delay the client's identification, then obviously no server
identification will ever be received since the multiplexor does not know how to route the connection.
## Keeping the session alive while no traffic
The client-side implementation supports several mechanisms for maintaining the session alive as far as the **server** is concerned
regardless of the user's own traffic:
* Sending `SSH_MSG_IGNORE` messages every once in a while.
This mechanism is along the lines of [PUTTY null packets configuration](https://patrickmn.com/aside/how-to-keep-alive-ssh-sessions/).
It generates small [`SSH_MSG_IGNORE`](https://tools.ietf.org/html/rfc4253#section-11.2) messages. The way to set this mechanism
up is via the `setSessionHeartbeat` API.
*Note:* the same effect can also be achieved by setting the relevant properties documented in `SessionHeartbeatController`, but
it is highly recommended to use the API - unless one needs to control these properties **externally** via `-Dxxx` JVM options.
* Sending `keepalive@...` [global requests](https://tools.ietf.org/html/rfc4254#section-4).
The feature is controlled via the `CoreModuleProperties#HEARTBEAT_REQUEST` and `HEARTBEAT_INTERVAL` properties - see the relevant
documentation for these features. The simplest way to activate this feature is to set the `HEARTBEAT_INTERVAL` property value
to the **milliseconds** value of the requested heartbeat interval.
This configuration only ensures that the **server** does not terminate the session due to no traffic. If the
incoming traffic from the server may also suffer from long "quiet" periods, one runs the risk of a **client** time-out. In order
to avoid this, it is possible to activate the `wantReply` option for the global request. This way, there is bound to be some
packet response (even if failure - which will be ignored by the heartbeat code). In order to activate this option one needs to
set the `HEARTBEAT_REPLY_WAIT` property value to a **positive** value specifying the number of **milliseconds** the client is
willing to wait for the server's reply to the global request.
* Customized user code
In order to support customized user code for this feature, the `ReservedSessionMessagesHandler` can be used to
implement any kind of user-defined heartbeat. *Note:* if the user configured such a mechanism, then the
`sendReservedHeartbeat` method **must** be implemented since the default throws `UnsupportedOperationException`
which will cause the session to be terminated the 1st time the method is invoked.
**Note(s):**
* Mechanisms are disabled by default - they need to be activated explicitly.
* Mechanisms can be activated either on the `SshClient` (for **global** setup) and/or
the `ClientSession` (for specific session configuration).
* The `keepalive@,,,,` mechanism **supersedes** the other mechanisms if activated.
* If specified timeout expires for the `wantReply` option then session will be **closed**.
* *Any* response - including [`SSH_MSH_REQUEST_FAILURE`](https://tools.ietf.org/html/rfc4254#page-4)
is considered a "good" response for the heartbeat request. In this context, a special patch
has been introduced in [SSHD-968](https://issues.apache.org/jira/browse/SSHD-968) that converts
an `SSH_MSG_UNIMPLEMENTED` response to such a global request into a `SSH_MSH_REQUEST_FAILURE`
since some servers have been found that violate the standard and reply with it to the request.
* When using the CLI, these options can be configured using the following `-o key=value` properties:
* `ClientAliveInterval` - if positive the defines the heartbeat interval in **seconds**.
* `ClientAliveUseNullPackets` - *true* if use the `SSH_MSG_IGNORE` mechanism, *false* if use global request (default).
* `ClientAliveReplyWait` - if positive, then activates the `wantReply` mechanism and specific the expected
response timeout in **seconds**.
## Running a command or opening a shell
### Running a single non-interactive command
```java
try (OutputStream stdout = ...create/obtain output stream...;
OutputStream stderr = ...create/obtain output stream...;
ClientChannel channel = session.createExecChannel(command)) {
channel.setOut(stdout);
channel.setErr(stderr);
channel.open().verify(...some timeout...);
// Wait (forever) for the channel to close - signalling command finished
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
}
// Parse/handle the command's output/error streams
```
If all one needs is to run a non-interactive command and then look at its string output, one can use several of the
available *ClientSession#executeRemoteCommand* overloaded methods.
### Running an interactive command/shell
If one needs to parse the command/shell output and then respond by sending the correct input, the code must use **separate** thread(s)
to read the STDOUT/STDERR and provide STDIN input. These threads must be up and running *before* opening the channel since data may start
to pour in even before the *await/verify* call returns. If this data is not consumed at a reasonable pace, then channel may block and eventually
even disconnect. Thus the thread(s) using the streams must be ready beforehand.
```java
// The same code can be used when opening a ChannelExec in order to run a single interactive command
try (ClientChannel channel = session.createShellChannel(/* use internal defaults */)) {
channel.setIn(...stdin...);
channel.setOut(...stdout...);
channel.setErr(...stderr...);
...spawn the servicing thread(s)....
try {
channel.open().verify(...some timeout...);
// Wait (forever) for the channel to close - signalling shell exited
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
} finally {
// ... stop the pumping threads ...
}
}
```
In such cases it is recommended to use the inverted streams in the relevant threads
```java
// The same code can be used when opening a ChannelExec in order to run a single interactive command
try (ClientChannel channel = session.createShellChannel(/* use internal defaults */)) {
try {
channel.open().verify(...some timeout...);
spawnStdinThread(channel.getInvertedIn());
spawnStdoutThread(channel.getInvertedOut());
spawnStderrThread(channel.getInvertedErr());
// Wait (forever) for the channel to close - signalling shell exited
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
} finally {
// ... stop the pumping threads ...
}
}
```
### Redirecting STDERR stream to STDOUT
One can use a combined STDOUT/STDERR stream instead of separate ones:
```java
///////////////////////// Non-interactive ///////////////////////////////
try (OutputStream mergedOutput = ...create/obtain output stream...;
ClientChannel channel = session.createExecChannel(command)) {
channel.setOut(mergedOutput);
channel.redirectErrorStream(true);
channel.open().verify(...some timeout...);
// Wait (forever) for the channel to close - signalling command finished
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
}
// Parse/handle the combined output/error streams
////////////////////////// Interactive ////////////////////////////////////
try (ClientChannel channel = session.createShellChannel(/* use internal defaults */)) {
try {
channel.redirectErrorStream(true);
channel.open().verify(...some timeout...);
spawnStdinThread(channel.getInvertedIn());
spawnCombinedOutputThread(channel.getInvertedOut());
// Wait (forever) for the channel to close - signalling shell exited
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 0L);
} finally {
// ... stop the pumping threads ...
}
}
```
**Note:** the call to *redirectErrorStream* must occur **before** channel is opened. Calling it afterwards has no effect - i.e.,
the last state before opening the stream determines the channel's behavior.
### PTY configuration
When running a command or opening a shell, there is an extra concern regarding the PTY configuration and/or the
reported environment variables. By default, unless specific instructions are provided, the code uses some internal
defaults - which however, might not be adequate for the specific client/server.
```java
// In order to override the PTY and/or environment
Map<String, ?> env = ...some environment...
PtyChannelConfiguration ptyConfig = ...some configuration...
try (ClientChannel channel = session.createShellChannel(ptyConfig, env)) {
... same code as before ...
}
```
One possible source of PTY configuration is code that provides some default initializations based on the detected O/S
type - `PtyChannelConfigurationMutator#setupSensitiveDefaultPtyConfiguration`. Of course, the user may use whatever other
considerations when opening such a channel.
**Caveat Emptor:** If the detected O/S type is Unix/Linux, then the `setupSensitiveDefaultPtyConfiguration` code issues an `stty` command
and parses the results (see `SttySupport` class). Since this involves using `System#exec` it is a source of concern as it may hang, throw
an exception, provide corrupted data, etc...