Skip to content

Latest commit

 

History

History
467 lines (346 loc) · 25 KB

client-setup.md

File metadata and controls

467 lines (346 loc) · 25 KB

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

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 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 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:

    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:

    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:

    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:

    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 FilePasswordProviderhas 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, a URL, a URI, etc. - and modify it's behavior accordingly.

OpenSSH file format support

The code supports OpenSSH 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. 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. 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. 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.

While RFC-4256 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 as well as its initial identification string as described in RFC 4253 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 via the resolveAuthPasswordAttempt method.

/**
 * 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 via the resolveAuthPublicKeyIdentityAttempt method.

/**
 * 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

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 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.

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.

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. 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. It generates small SSH_MSG_IGNORE 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.

    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 is considered a "good" response for the heartbeat request. In this context, a special patch has been introduced in 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

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.

// 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

// 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:

///////////////////////// 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.

// 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...