From d3f9c9fd5a0a3c9b587e1339c26f47b2bc0137f2 Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Sat, 3 Aug 2024 21:03:29 +0200 Subject: [PATCH] Lots of work on making easier interactive documentation, especially to make working with SSH public keys really easy. Next step is to do more validation in the UI. Specifically: * validate authorized keys * detection of accidental use of a private key Then, password based access can be disabled. --- Dockerfile | 2 +- cmd/converge/converge.go | 20 +++- cmd/converge/usage.go | 19 ++-- pkg/server/templates/about.templ | 132 ++++++++++++++++++++------- pkg/server/templates/downloads.templ | 12 +-- pkg/server/templates/usage.templ | 84 +++++++++-------- pkg/server/templates/usageinputs.go | 24 ++++- 7 files changed, 199 insertions(+), 94 deletions(-) diff --git a/Dockerfile b/Dockerfile index dfe85ee..a5d6b8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,4 @@ COPY --from=builder /opt/converge/bin/agent \ /opt/converge/static/ COPY --from=builder /opt/converge/static/ /opt/converge/static/ -ENTRYPOINT ["/opt/converge/bin/converge", "-d", "/opt/converge/static" ] +ENTRYPOINT ["/opt/converge/bin/converge", "-s", "/opt/converge/static", "-d", "/opt/converge/static" ] diff --git a/cmd/converge/converge.go b/cmd/converge/converge.go index 5c4edf4..7076b5a 100644 --- a/cmd/converge/converge.go +++ b/cmd/converge/converge.go @@ -48,14 +48,16 @@ func printHelp(msg string) { "an embedded SSH server to provide interactive access to the end-user. This works\n" + "both on linux and on windows.\n" + "\n" + - "-d : directory where static content of converge is placed" + "-s : directory where static content of converge is placed\n" + + "-d : directory where downloads of converge are placed\n" fmt.Fprintln(os.Stderr, helpText) os.Exit(1) } func main() { - downloadDir := "../static" + downloaddir := "." + staticdir := "../static" args := os.Args[1:] for len(args) > 0 && strings.HasPrefix(args[0], "-") { @@ -64,14 +66,20 @@ func main() { if len(args) <= 1 { printHelp("The -d option expects an argument") } - downloadDir = args[1] + downloaddir = args[1] + args = args[1:] + case "-s": + if len(args) <= 1 { + printHelp("The -s option expects an argument") + } + staticdir = args[1] args = args[1:] default: printHelp("Unknown option " + args[0]) } args = args[1:] } - log.Println("Content directory", downloadDir) + log.Println("Content directory", staticdir) if len(args) != 0 { printHelp("") @@ -158,7 +166,9 @@ func main() { // create filehandler with templating for html files. http.Handle("/docs/", http.StripPrefix("/docs/", http.HandlerFunc(pageHandler))) http.Handle("/static/", http.StripPrefix("/static/", - http.FileServer(http.Dir(downloadDir)))) + http.FileServer(http.Dir(staticdir)))) + http.Handle("/downloads/", http.StripPrefix("/downloads/", + http.FileServer(http.Dir(downloaddir)))) http.HandleFunc("/", catchAllHandler) // create usage generator diff --git a/cmd/converge/usage.go b/cmd/converge/usage.go index dc53a2a..8940851 100644 --- a/cmd/converge/usage.go +++ b/cmd/converge/usage.go @@ -2,16 +2,14 @@ package main import ( "converge/pkg/server/templates" - "log" "math/rand" "net/http" "os" "strconv" + "strings" ) func generateCLIExammple(w http.ResponseWriter, r *http.Request) { - log.Println("usage: got ", r.URL.Path) - err := r.ParseForm() if err != nil { http.Error(w, "Error parsing form", http.StatusBadRequest) @@ -27,14 +25,17 @@ func generateCLIExammple(w http.ResponseWriter, r *http.Request) { } remoteShells := r.Form["remote-shell"] localShells := r.Form["local-shell"] - keys := r.FormValue("ssh-keys") - log.Printf("remote_shells %v", remoteShells) - log.Printf("local_shells %v", localShells) - log.Printf("ssh-keys %v", keys) - + keysString := r.FormValue("ssh-keys") + sshPublicKeys := make([]string, 0) + for _, line := range strings.Split(keysString, "\n") { + line := strings.TrimSpace(line) + if line != "" { + sshPublicKeys = append(sshPublicKeys, line) + } + } access := getConvergeAccess(r, getAgentSshUser()) - usageInputs := templates.NewUsageInputs(id, remoteShells, localShells) + usageInputs := templates.NewUsageInputs(id, sshPublicKeys, remoteShells, localShells) err = templates.ShellUsage(access, usageInputs).Render(r.Context(), w) if err != nil { http.Error(w, err.Error(), 500) diff --git a/pkg/server/templates/about.templ b/pkg/server/templates/about.templ index 90ee0bf..a6a72f1 100644 --- a/pkg/server/templates/about.templ +++ b/pkg/server/templates/about.templ @@ -19,55 +19,123 @@ templ About() { the client and server together.

+

other tools

+ +

Using available existing tools such as + breakpoint in combination + with a websocket tunneling tool such as + wstunnel a similar solution can be + obtained. There are however some problems with these solutions that converge is + trying to address: +

+ +

+

    +
  • deployment: Breakpoint uses an embedded SSH server which is a really good idea but + uses the QUIC protocol for connecting to a rendez-vous server. The rendez-vous server than + exposes a random port for every client. This make deployment on kubernetes really hard + where fixed ports must be used and QUIC is also not a widely supported protocol.
  • +
  • The problem with the random ports can be solved by using wstunnel running together + with breakpoint server in a kubernetes pod, where wstunnel can forward traffic over an + extern websocket connection to the local random port that breakpoint server is listening on.
  • +
  • breakpoint leaves it open on how users install the breakpoint executable (agent).
  • +
  • Because of the hacky nature of this setup, it is very difficult for users to use + and troubleshoot when things go wrong.
  • +
+ +

+ Converve server addresses these issues in the following ways: +
    +
  • Use the websocket protocol both for agents and for clients, providing a fixed port and + a supported protocol for kubernetes deploymment.
  • +
  • Providing online documentation where the instructions take into account the + hostname and protocol where converge is running allowing users to cut and paste + instructions that can be used without modification. In the usage page the users + can even generate the correct agent startup commands and client connection commands + based on the type of shell they are connecting to.
  • +
  • Converge server provides out of the box downloads of required software. This makes sure + client and server are always up to date. In addition a protocol version check is done.
  • +
  • User-friendly error messages can be given to users in most case when things do not work + out because of wsproxy, an SSH proxy command that also talk to the server + to tell the user if a connection is accepted and if not why not.
  • +
  • A live screen showing the current sessions that are running.
  • +
  • Interactivity in the user's session with notifications about timeouts and a very + simple inactivity timmeout mechanism.
  • +
  • Possibility for the user to define his own shell.
  • +
  • Support for unix like bash shells and command prompt and powershell.
  • +
+

+

+ + +

how it works

+ +

+ The steps involved are as follows: +

    +
  • The agent connects to converge server. If no id is specified than a new id will + be generated. The ids specified by different agents must be unique. If the agent + specifies an id that is already in use, then a new id will be generated. + When started the agent will echo the commands to connect to it in its output. +
  • +
  • + Since the emmbedded SSH server in the agent will allow multiple clients to connect + to it, it wants to listen for copnnections. By default it cannot do this, it just + setup a connection to the converge server, but the converge server can in general + not connect back to it because of networking. Therefore, a multiplexing library is + used to establish multiple virtual connections over a single TCP connection. + The agent can now listen for connections from clients. +
  • +
  • The agent connects to the converge server using the commmand specified by the agent. + The converge server can then match the agent with the client based on the id and + the connectio at network level is established. +
  • +
  • The embedded SSH server now performs authentication, after successful login, + a shell is spwaned and the network connection of the user is connected to it. + The connection is practically identical to a regular terminal connection. To + achieve this some magic is used to make the shell beiieve it is connected to a + terminal. +
  • +
+

+ +

Security

+

The setup is such that the connection from client (end-user) to server (agent on CI job) is end-to-end encrypted. The Converge server itself is no more than a bitpipe which pumps data between client and agent.

+

Currently converge server still supports password based login but this will be disabled. + Image two people configuring an agent with the same id where one of the agents actually + gets it and other gets a new id. Now, with a password each user can access each other's + agents. This is of course highly confusing and undesirable. Converge server already support + authorized keys but this is not yet mandatory. I is made extremely easy through the + usage page to configure this, so the additional complexity should + not be an issue. +

+ +

SSH and SFTP

+

Both ssh and sftp are supported. Multiple shells are also allowed.

+

Timeouts

+

There is a timeout mechanism in the agent such that jobs do not hang indefinitely waiting for a connection. This mechanism is useful to make sure build agents do not keep build agents occupied for a long time. By default, the agent exits with status 0 when - the first client exits after logging in. This behavior as well as general expiry can be - controlled from within a shell session by touching a .hold file. After logging in, the - user can control expiry of the session as instructed by messages in the ssh session. - When the timeout of a session is near the user is informed about this with messages - in the shell. + the first client exits after logging in.

- -

end-to-end encryoption

-

ssh keys

-

agent options

-

client access

- -

Local clients: using ssh with a proxy command

- -

- wsproxy is a command that can be used as a proxy command for SSH which performs the connection to the - remote server. This command needs to be downloaded only once (see downloads). It does not depend on - the converge implementation but only on the websocket standards. Other tools that - provide a mapping of stdio to a websocket can also be used instead of wsproxy. +

When the user touches a .hold file, the agent keeps waiting for connections even + after the last client logs out, taking into account the timeout.

+

The sessions have an inactivity timeout. Any keypress on the keyboard by a user + is interpreted as activity.

-

Local clients: using SSH with a local TCP forwarding proxy

- -

- This option is less convenient than the proxy command because it requires two separate - commands to execute. -

- -

- Local clients can connect using regular ssh and sftp commands through a tunnel that - translates a local TCP port to a websocket connection in converge. See - the downloads section. - This runs a local client that allows SSH to port 10000 and connects to converge using - a websocket connection. -

Remote shell usage

diff --git a/pkg/server/templates/downloads.templ b/pkg/server/templates/downloads.templ index 2899ab1..c9125b9 100644 --- a/pkg/server/templates/downloads.templ +++ b/pkg/server/templates/downloads.templ @@ -17,20 +17,20 @@ templ Downloads() { agent - agent - agent.exe + agent + agent.exe The agent to run inside aa CI job wsproxy - wsproxy - wsproxy.exe + wsproxy + wsproxy.exe SSH proxy command that can be directly used by ssh tcptows - tcptows - tcptows.exe + tcptows + tcptows.exe You typically do not need to download this. It was used for testing at the beginning and can still be used as a generic TCP to WS tunnel for allowing regular diff --git a/pkg/server/templates/usage.templ b/pkg/server/templates/usage.templ index 2c830e3..6b2eeb0 100644 --- a/pkg/server/templates/usage.templ +++ b/pkg/server/templates/usage.templ @@ -12,16 +12,23 @@ templ AgentUsage(access models.ConvergeAccess, shells map[string]bool, usageInpu

if shells[BASH] { -
{`
-        `}curl http{access.Secure}://{access.HostPort}/static/agent > agent{`
+        
{addSshKeys(BASH, usageInputs.SshKeys)}
+        curl --fail-with-body http{access.Secure}://{access.HostPort}/downloads/agent > agent{`
         chmod 755 agent
         `}./agent --id {usageInputs.Id} ws{access.Secure}://{access.HostPort}{`
         rm -f agent
         `}
} - if shells[CMD] || shells[POWERSHELL] { -
{`
-        `}curl http{access.Secure}://{access.HostPort}/static/agent.exe > agent.exe{`
+    if shells[CMD]  {
+        
{addSshKeys(CMD, usageInputs.SshKeys)}
+        curl --fail-with-body http{access.Secure}://{access.HostPort}/downloads/agent.exe > agent.exe{`
+        `}agent --id {usageInputs.Id} ws{access.Secure}://{access.HostPort}{`
+        del agent.exe
+        `}
+ } + if shells[POWERSHELL] { +
{addSshKeys(POWERSHELL, usageInputs.SshKeys)}
+        curl --fail-with-body http{access.Secure}://{access.HostPort}/downloads/agent.exe > agent.exe{`
         `}agent --id {usageInputs.Id} ws{access.Secure}://{access.HostPort}{`
         del agent.exe
         `}
@@ -41,11 +48,41 @@ templ AgentUsage(access models.ConvergeAccess, shells map[string]bool, usageInpu

Connecting to the agent

+

The embedded ssh server in the agent supports both ssh and sftp. The user name is fixed + at { access.Username }. This is the user used to connect to the embedded + SSH server, after logging in however you will be running in a shell that is started + by the same user that started the agent. +

+
{`
           `}ssh -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}"  { access.Username }{"@localhost"}   {`
           `}sftp -oServerAliveInterval=10 -oProxyCommand="wsproxy ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}" { access.Username }{"@localhost"}   {`
           `}
+

This requires the wsproxy utility which is available in the + downloads section. This utility must be downloaded + only once since it is quite generic. It will warn you when it a newer version must + be downloaded. +

+ +

For other ssh clients that do not support the openssh ProxyCommand option, there is another + way to connect. In this method, a local port forwarder is started that forwards a local port + to the webserver. Then you can start an ssh client that connects to the local tcp port. +

+
{`
+         `}ssh -oServerAliveInterval=10 -p 10000  { access.Username }{"@localhost"}   {`
+         `}sftp -oServerAliveInterval=10 -p 10000 { access.Username }{"@localhost"}   {`
+         `}
+ +

This requires the tcptows utility which is available in the + downloads section. The utility must be started beforehand + using: +

+
{`
+         `}tcptows ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}   {`
+         `}tcptows ws{access.Secure}://{access.HostPort}/client/{usageInputs.Id}   {`
+         `}
+

Working with the agent

if shells[BASH] { @@ -90,25 +127,11 @@ templ AgentUsage(access models.ConvergeAccess, shells map[string]bool, usageInpu } -templ LocalShellUsage(access models.ConvergeAccess, shells map[string]bool, usageInput UsageInputs) { -
- if shells[BASH] { -

bash

- } - if shells[CMD] { -

cmd

- } - if shells[POWERSHELL] { -

powershell

- } -
-} templ ShellUsage(access models.ConvergeAccess, usageInputs UsageInputs) {
@AgentUsage(access, usageInputs.RemoteShells, usageInputs)
- @LocalShellUsage(access, usageInputs.LocalShells, usageInputs) } @@ -163,25 +186,6 @@ templ Usage(access models.ConvergeAccess) {
- - - - - -
                                         {`
-          `}# linux                                                 {`
-          `}echo "ssh-rsa dkddkdkkk a@b.c" > .authorized_keys       {`
-          `}echo "ssh-rsa adfadjfdf d@e.f" >> .authorized_keys      {`
-          `}                                                        {`
-          `}# windows                                               {`
-          `}echo ssh-rsa dkddkdkkk a@b.c > .authorized_keys         {`
-          `}echo ssh-rsa adfadjfdf d@e.f >> .authorized_keys
-                      
-

- Note that on windows you should not used quotes. -

- -