Skip to content

Add ssh_host_key router #8295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions components/ws-proxy/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down
7 changes: 5 additions & 2 deletions components/ws-proxy/pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion components/ws-proxy/pkg/proxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package proxy
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would produce a subtle failure mode: we'd get this error message once, but SSH access would be broken.

Not a reason not to merge, but maybe a follow up. Instead of just logging, we could bubble up the error and exit.

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"))
Expand Down
100 changes: 99 additions & 1 deletion components/ws-proxy/pkg/proxy/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ package proxy

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down