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