Zero Trust *ssh.Client

Clint DovholukClint Dovholuk
4 min read

A few years ago, the OpenZiti project developed and published two client tools to make ssh and scp available over an OpenZiti overlay network without requiring the sshd port to be exposed to the internet. If interested, read the original posts about zssh and zscp. Continuing with the belief that security-related code should be open source and auditable, the project is available on GitHub.

The OpenZiti project provides SDKs that developers can use to create secure connections. The zssh client demonstrates that adopting an OpenZiti SDK into an application is no harder than developing any application that uses traditional IP-based, underlay connectivity.

Secondly, though uncommon, there are still vulnerabilities found in ssh. Just this year (2024) RegreSSHion was discovered and was given a staggering CVSS score of 9.8. Scores greater than 9 are generally deemed a "patch this as soon as possible" type of CVE. Yes, today's ssh clients are incredibly robust, but if it's easy to remove a substantial portion of attackers from attacking a service by using a zero trust overlay network like OpenZiti, why wouldn't you?

Using *ssh.Client

The go ecosystem provides extended packages from the golang.org/x/* modules. One of these modules is golang.org/x/crypto. Within this module, there is an ssh package that provides everything needed to make a functional ssh client. In there is ssh.Client, the main thing you'll interact with. This struct, along with ssh.NewClientConn and the Golang standard library, provides all the functionality needed to create a simple ssh client.

Shown below is all the code needed to make a very contrived ssh example. Any errors are ignored, and all values are hard-coded or expected as arguments to the program to keep the example small. In total, there are fewer than 30 total lines. Hopefully, the example is straightforward enough to understand.

package main

import (
    "golang.org/x/crypto/ssh"
    "os"
)

func main() {
    key, _ := os.ReadFile(os.Args[1])
    signer, _ := ssh.ParsePrivateKey(key)
    config := &ssh.ClientConfig{
        User:            "ubuntu",
        Auth:            []ssh.AuthMethod{ssh.PublicKeys(signer)},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    sshClient, _ := ssh.Dial("tcp", os.Args[2], config)
    defer sshClient.Close()
    session, _ := sshClient.NewSession()
    defer session.Close()
    session.RequestPty("xterm", 80, 40, ssh.TerminalModes{})
    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin
    session.Shell()
    session.Wait()
}

Try it out! That's really all there is to it. You'll be able to ssh to any machine that uses key-based authentication. Although it's not a robust example, it demonstrates the overall idea and shows off how amazing the go ecosystem can be. Note that ssh.InsecureIgnoreHostKey is used for the HostKeyCallback, to keep the example short. See zssh's implementation if interested

Layering in Zero Trust Connectivity

The Golang standard library is well thought out. The abstractions in place make it amazing for building applications embedding zero trust. Adapting an application that uses normal IP-based connectivity (like the ssh example shown above that uses "tcp") to use an OpenZiti SDK is generally straightforward. From the example above, a single line needs to be changed: the line that creates the ssh.Client. This line:

    sshClient, _ := ssh.Dial("tcp", host, config)

Creating the sshClient needs to be adapted away from using IP-based underlay networking. Instead of "tcp" and "remote-machine-name:22", it needs to use a zero trust connection provided by the OpenZiti Golang SDK. Below is a simplified function that uses an OpenZiti identity file to create an OpenZiti context and dial an OpenZiti service, creating a Golang net.Conn that can be used to create an ssh.Client. Again, the example omits error handling and robustness for simplicity's sake, and looks like this:

func obtainZitiConn() net.Conn {
    cfg, _ := ziti.NewConfigFromFile(os.Args[3])
    ctx, _ := ziti.NewContext(cfg)
    dialOptions := &ziti.DialOptions{
        Identity:       host,
    }
    c, _ := ctx.DialWithOptions("zsshSvc", dialOptions)
    return c
}

With the IP-based underlay net.Conn connection replaced with a zero trust connection, an ssh.Client can be created by replacing the call to ssh.Dial, and instead using a call to ssh.NewClientConn combined with a call to ssh.NewClient. With the ssh.Dial line adapted, it looks like this:

    //adapted sshClient, _ := ssh.Dial("tcp", host, config)
    c, chans, reqs, _ := ssh.NewClientConn(obtainZitiConn(), "", config)
    sshClient := ssh.NewClient(c, chans, reqs)

Everything else in the example remains identical; these are the only lines that need to change! The full source for zssh is available on GitHub at https://github.com/openziti-test-kitchen/zssh. You'll find the examples shown above in that repo as individual, compilable go files available in the example folder.

If you're interested in zssh, the OpenZiti project and zero trust in general, check out the next article. It focuses on using zssh and using OpenZiti's OIDC-based authentication mechanisms and uses Keycloak for federated authentication to GitHub or Google.

Share the Project

If you find this interesting, please consider starring the projects on GitHub. It really does help to support the project! And if you haven't seen it yet, check out https://zrok.io. It's a totally free sharing platform built on OpenZiti and uses the OpenZiti Golang SDK and is also all open source!

Tell us how you're using OpenZiti on X twitter, reddit, or over at our Discourse. Or, if you prefer, check out our content on YouTube if that's more your speed. Regardless of how, we'd love to hear from you.

3
Subscribe to my newsletter

Read articles from Clint Dovholuk directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Clint Dovholuk
Clint Dovholuk