From ae5f4867fd8ca63b32aab250ff96d22a278a852b Mon Sep 17 00:00:00 2001 From: Pudong Zheng Date: Mon, 21 Feb 2022 09:45:07 +0000 Subject: [PATCH] add ssh_host_key router --- components/ws-proxy/cmd/run.go | 9 +- components/ws-proxy/pkg/proxy/proxy.go | 7 +- components/ws-proxy/pkg/proxy/routes.go | 32 +++++- components/ws-proxy/pkg/proxy/routes_test.go | 100 ++++++++++++++++++- 4 files changed, 140 insertions(+), 8 deletions(-) diff --git a/components/ws-proxy/cmd/run.go b/components/ws-proxy/cmd/run.go index 0140fd7417bab0..4c36ca129b357d 100644 --- a/components/ws-proxy/cmd/run.go +++ b/components/ws-proxy/cmd/run.go @@ -112,12 +112,10 @@ var runCmd = &cobra.Command{ } } - go proxy.NewWorkspaceProxy(cfg.Ingress, cfg.Proxy, proxy.HostBasedRouter(cfg.Ingress.Header, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffix, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffixRegex), workspaceInfoProvider).MustServe() - log.Infof("started proxying on %s", cfg.Ingress.HTTPAddress) - + // SSH Gateway + var signers []ssh.Signer flist, err := os.ReadDir("/mnt/host-key") if err == nil && len(flist) > 0 { - var signers []ssh.Signer for _, f := range flist { if f.IsDir() { continue @@ -143,6 +141,9 @@ var runCmd = &cobra.Command{ } } + go proxy.NewWorkspaceProxy(cfg.Ingress, cfg.Proxy, proxy.HostBasedRouter(cfg.Ingress.Header, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffix, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffixRegex), workspaceInfoProvider, signers).MustServe() + log.Infof("started proxying on %s", cfg.Ingress.HTTPAddress) + log.Info("🚪 ws-proxy is up and running") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { log.WithError(err).Fatal(err, "problem starting ws-proxy") diff --git a/components/ws-proxy/pkg/proxy/proxy.go b/components/ws-proxy/pkg/proxy/proxy.go index 82d898cec5bcf2..ee792686e160fb 100644 --- a/components/ws-proxy/pkg/proxy/proxy.go +++ b/components/ws-proxy/pkg/proxy/proxy.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/mux" "github.com/klauspost/cpuid/v2" + "golang.org/x/crypto/ssh" "github.com/gitpod-io/gitpod/common-go/log" ) @@ -22,15 +23,17 @@ type WorkspaceProxy struct { Config Config WorkspaceRouter WorkspaceRouter WorkspaceInfoProvider WorkspaceInfoProvider + SSHHostSigners []ssh.Signer } // NewWorkspaceProxy creates a new workspace proxy. -func NewWorkspaceProxy(ingress HostBasedIngressConfig, config Config, workspaceRouter WorkspaceRouter, workspaceInfoProvider WorkspaceInfoProvider) *WorkspaceProxy { +func NewWorkspaceProxy(ingress HostBasedIngressConfig, config Config, workspaceRouter WorkspaceRouter, workspaceInfoProvider WorkspaceInfoProvider, signers []ssh.Signer) *WorkspaceProxy { return &WorkspaceProxy{ Ingress: ingress, Config: config, WorkspaceRouter: workspaceRouter, WorkspaceInfoProvider: workspaceInfoProvider, + SSHHostSigners: signers, } } @@ -95,7 +98,7 @@ func (p *WorkspaceProxy) Handler() (http.Handler, error) { return nil, err } ideRouter, portRouter, blobserveRouter := p.WorkspaceRouter(r, p.WorkspaceInfoProvider) - installWorkspaceRoutes(ideRouter, handlerConfig, p.WorkspaceInfoProvider) + installWorkspaceRoutes(ideRouter, handlerConfig, p.WorkspaceInfoProvider, p.SSHHostSigners) err = installWorkspacePortRoutes(portRouter, handlerConfig, p.WorkspaceInfoProvider) if err != nil { return nil, err diff --git a/components/ws-proxy/pkg/proxy/routes.go b/components/ws-proxy/pkg/proxy/routes.go index 1ea27970246a3f..b7229ec023c0f1 100644 --- a/components/ws-proxy/pkg/proxy/routes.go +++ b/components/ws-proxy/pkg/proxy/routes.go @@ -7,6 +7,7 @@ package proxy import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -22,6 +23,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" "golang.org/x/xerrors" "github.com/gitpod-io/gitpod/common-go/log" @@ -68,13 +70,18 @@ func NewRouteHandlerConfig(config *Config, opts ...RouteHandlerConfigOpt) (*Rout type RouteHandler = func(r *mux.Router, config *RouteHandlerConfig) // installWorkspaceRoutes configures routing of workspace and IDE requests. -func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip WorkspaceInfoProvider) { +func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip WorkspaceInfoProvider, hostKeyList []ssh.Signer) { r.Use(logHandler) // Note: the order of routes defines their priority. // Routes registered first have priority over those that come afterwards. routes := newIDERoutes(config, ip) + // if host key is not empty, we use /_ssh/host_keys to provider public host key + if len(hostKeyList) > 0 { + routes.HandleSSHHostKeyRoute(r.Path("/_ssh/host_keys"), hostKeyList) + } + // The favicon warants special handling, because we pull that from the supervisor frontend // rather than the IDE. faviconRouter := r.Path("/favicon.ico").Subrouter() @@ -132,6 +139,29 @@ type ideRoutes struct { workspaceMustExistHandler mux.MiddlewareFunc } +func (ir *ideRoutes) HandleSSHHostKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) { + shk := make([]struct { + Type string `json:"type"` + HostKey string `json:"host_key"` + }, len(hostKeyList)) + for i, hk := range hostKeyList { + shk[i].Type = hk.PublicKey().Type() + shk[i].HostKey = base64.StdEncoding.EncodeToString(hk.PublicKey().Marshal()) + } + byt, err := json.Marshal(shk) + if err != nil { + log.WithError(err).Error("ssh_host_key router setup failed") + return + } + r := route.Subrouter() + r.Use(logRouteHandlerHandler("HandleSSHHostKeyRoute")) + r.Use(ir.Config.CorsHandler) + r.NewRoute().HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.Header().Add("Content-Type", "application/json") + rw.Write(byt) + }) +} + func (ir *ideRoutes) HandleDirectIDERoute(route *mux.Route) { r := route.Subrouter() r.Use(logRouteHandlerHandler("HandleDirectIDERoute")) diff --git a/components/ws-proxy/pkg/proxy/routes_test.go b/components/ws-proxy/pkg/proxy/routes_test.go index f506c21baae6e7..c2f0c0fd2dde75 100644 --- a/components/ws-proxy/pkg/proxy/routes_test.go +++ b/components/ws-proxy/pkg/proxy/routes_test.go @@ -6,6 +6,11 @@ package proxy import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" "fmt" "io" "net" @@ -18,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" "github.com/gitpod-io/gitpod/common-go/log" "github.com/gitpod-io/gitpod/common-go/util" @@ -662,7 +668,7 @@ func TestRoutes(t *testing.T) { Header: "", } - proxy := NewWorkspaceProxy(ingress, cfg, router, &fakeWsInfoProvider{infos: workspaces}) + proxy := NewWorkspaceProxy(ingress, cfg, router, &fakeWsInfoProvider{infos: workspaces}, nil) handler, err := proxy.Handler() if err != nil { t.Fatalf("cannot create proxy handler: %q", err) @@ -733,6 +739,98 @@ func (p *fakeWsInfoProvider) WorkspaceCoords(wsProxyPort string) *WorkspaceCoord return nil } +func TestSSHGatewayRouter(t *testing.T) { + generatePrivateKey := func() ssh.Signer { + prik, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil + } + b := pem.EncodeToMemory(&pem.Block{ + Bytes: x509.MarshalPKCS1PrivateKey(prik), + Type: "RSA PRIVATE KEY", + }) + signal, err := ssh.ParsePrivateKey(b) + if err != nil { + return nil + } + return signal + } + + tests := []struct { + Name string + Input []ssh.Signer + Expected int + }{ + {"one hostkey", []ssh.Signer{generatePrivateKey()}, 1}, + {"multi hostkey", []ssh.Signer{generatePrivateKey(), generatePrivateKey()}, 2}, + } + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + router := HostBasedRouter(hostBasedHeader, wsHostSuffix, wsHostNameRegex) + ingress := HostBasedIngressConfig{ + HTTPAddress: "8080", + HTTPSAddress: "9090", + Header: "", + } + + proxy := NewWorkspaceProxy(ingress, config, router, &fakeWsInfoProvider{infos: workspaces}, test.Input) + handler, err := proxy.Handler() + if err != nil { + t.Fatalf("cannot create proxy handler: %q", err) + } + + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, modifyRequest(httptest.NewRequest("GET", workspaces[0].URL+"_ssh/host_keys", nil), + addHostHeader, + )) + resp := rec.Result() + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("status code should be 200, but got %d", resp.StatusCode) + } + var hostkeys []map[string]interface{} + fmt.Println(string(body)) + err = json.Unmarshal(body, &hostkeys) + if err != nil { + t.Fatal(err) + } + t.Log(hostkeys, len(hostkeys), test.Expected) + + if len(hostkeys) != test.Expected { + t.Fatalf("hostkey length is not expected") + } + }) + } +} + +func TestNoSSHGatewayRouter(t *testing.T) { + t.Run("TestNoSSHGatewayRouter", func(t *testing.T) { + router := HostBasedRouter(hostBasedHeader, wsHostSuffix, wsHostNameRegex) + ingress := HostBasedIngressConfig{ + HTTPAddress: "8080", + HTTPSAddress: "9090", + Header: "", + } + + proxy := NewWorkspaceProxy(ingress, config, router, &fakeWsInfoProvider{infos: workspaces}, nil) + handler, err := proxy.Handler() + if err != nil { + t.Fatalf("cannot create proxy handler: %q", err) + } + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, modifyRequest(httptest.NewRequest("GET", workspaces[0].URL+"_ssh/host_keys", nil), + addHostHeader, + )) + resp := rec.Result() + resp.Body.Close() + if resp.StatusCode != 401 { + t.Fatalf("status code should be 401, but got %d", resp.StatusCode) + } + }) + +} + func TestRemoveSensitiveCookies(t *testing.T) { var ( domain = "test-domain.com"