Skip to content

Commit b79520d

Browse files
committed
add ssh_host_key router
1 parent 6497329 commit b79520d

File tree

4 files changed

+140
-8
lines changed

4 files changed

+140
-8
lines changed

components/ws-proxy/cmd/run.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,10 @@ var runCmd = &cobra.Command{
112112
}
113113
}
114114

115-
go proxy.NewWorkspaceProxy(cfg.Ingress, cfg.Proxy, proxy.HostBasedRouter(cfg.Ingress.Header, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffix, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffixRegex), workspaceInfoProvider).MustServe()
116-
log.Infof("started proxying on %s", cfg.Ingress.HTTPAddress)
117-
115+
// SSH Gateway
116+
var signers []ssh.Signer
118117
flist, err := os.ReadDir("/mnt/host-key")
119118
if err == nil && len(flist) > 0 {
120-
var signers []ssh.Signer
121119
for _, f := range flist {
122120
if f.IsDir() {
123121
continue
@@ -143,6 +141,9 @@ var runCmd = &cobra.Command{
143141
}
144142
}
145143

144+
go proxy.NewWorkspaceProxy(cfg.Ingress, cfg.Proxy, proxy.HostBasedRouter(cfg.Ingress.Header, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffix, cfg.Proxy.GitpodInstallation.WorkspaceHostSuffixRegex), workspaceInfoProvider, signers).MustServe()
145+
log.Infof("started proxying on %s", cfg.Ingress.HTTPAddress)
146+
146147
log.Info("🚪 ws-proxy is up and running")
147148
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
148149
log.WithError(err).Fatal(err, "problem starting ws-proxy")

components/ws-proxy/pkg/proxy/proxy.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/gorilla/mux"
1414
"github.com/klauspost/cpuid/v2"
15+
"golang.org/x/crypto/ssh"
1516

1617
"github.com/gitpod-io/gitpod/common-go/log"
1718
)
@@ -22,15 +23,17 @@ type WorkspaceProxy struct {
2223
Config Config
2324
WorkspaceRouter WorkspaceRouter
2425
WorkspaceInfoProvider WorkspaceInfoProvider
26+
SSHHostSigners []ssh.Signer
2527
}
2628

2729
// NewWorkspaceProxy creates a new workspace proxy.
28-
func NewWorkspaceProxy(ingress HostBasedIngressConfig, config Config, workspaceRouter WorkspaceRouter, workspaceInfoProvider WorkspaceInfoProvider) *WorkspaceProxy {
30+
func NewWorkspaceProxy(ingress HostBasedIngressConfig, config Config, workspaceRouter WorkspaceRouter, workspaceInfoProvider WorkspaceInfoProvider, signers []ssh.Signer) *WorkspaceProxy {
2931
return &WorkspaceProxy{
3032
Ingress: ingress,
3133
Config: config,
3234
WorkspaceRouter: workspaceRouter,
3335
WorkspaceInfoProvider: workspaceInfoProvider,
36+
SSHHostSigners: signers,
3437
}
3538
}
3639

@@ -95,7 +98,7 @@ func (p *WorkspaceProxy) Handler() (http.Handler, error) {
9598
return nil, err
9699
}
97100
ideRouter, portRouter, blobserveRouter := p.WorkspaceRouter(r, p.WorkspaceInfoProvider)
98-
installWorkspaceRoutes(ideRouter, handlerConfig, p.WorkspaceInfoProvider)
101+
installWorkspaceRoutes(ideRouter, handlerConfig, p.WorkspaceInfoProvider, p.SSHHostSigners)
99102
err = installWorkspacePortRoutes(portRouter, handlerConfig, p.WorkspaceInfoProvider)
100103
if err != nil {
101104
return nil, err

components/ws-proxy/pkg/proxy/routes.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package proxy
77
import (
88
"bytes"
99
"context"
10+
"encoding/base64"
1011
"encoding/json"
1112
"fmt"
1213
"io"
@@ -22,6 +23,7 @@ import (
2223
"github.com/gorilla/handlers"
2324
"github.com/gorilla/mux"
2425
"github.com/sirupsen/logrus"
26+
"golang.org/x/crypto/ssh"
2527
"golang.org/x/xerrors"
2628

2729
"github.com/gitpod-io/gitpod/common-go/log"
@@ -68,13 +70,18 @@ func NewRouteHandlerConfig(config *Config, opts ...RouteHandlerConfigOpt) (*Rout
6870
type RouteHandler = func(r *mux.Router, config *RouteHandlerConfig)
6971

7072
// installWorkspaceRoutes configures routing of workspace and IDE requests.
71-
func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip WorkspaceInfoProvider) {
73+
func installWorkspaceRoutes(r *mux.Router, config *RouteHandlerConfig, ip WorkspaceInfoProvider, hostKeyList []ssh.Signer) {
7274
r.Use(logHandler)
7375

7476
// Note: the order of routes defines their priority.
7577
// Routes registered first have priority over those that come afterwards.
7678
routes := newIDERoutes(config, ip)
7779

80+
// if host key is not empty, we use /__ws_proxy/ssh_host_key to provider public host key
81+
if len(hostKeyList) > 0 {
82+
routes.HandleSSHHostKeyRoute(r.Path("/__ws_proxy/ssh_host_key"), hostKeyList)
83+
}
84+
7885
// The favicon warants special handling, because we pull that from the supervisor frontend
7986
// rather than the IDE.
8087
faviconRouter := r.Path("/favicon.ico").Subrouter()
@@ -132,6 +139,29 @@ type ideRoutes struct {
132139
workspaceMustExistHandler mux.MiddlewareFunc
133140
}
134141

142+
func (ir *ideRoutes) HandleSSHHostKeyRoute(route *mux.Route, hostKeyList []ssh.Signer) {
143+
shk := make([]struct {
144+
Type string `json:"type"`
145+
HostKey string `json:"host_key"`
146+
}, len(hostKeyList))
147+
for i, hk := range hostKeyList {
148+
shk[i].Type = hk.PublicKey().Type()
149+
shk[i].HostKey = base64.StdEncoding.EncodeToString(hk.PublicKey().Marshal())
150+
}
151+
byt, err := json.Marshal(shk)
152+
if err != nil {
153+
log.WithError(err).Error("ssh_host_key router setup failed")
154+
return
155+
}
156+
r := route.Subrouter()
157+
r.Use(logRouteHandlerHandler("HandleSSHHostKeyRoute"))
158+
r.Use(ir.Config.CorsHandler)
159+
r.NewRoute().HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
160+
rw.Header().Add("Content-Type", "application/json")
161+
rw.Write(byt)
162+
})
163+
}
164+
135165
func (ir *ideRoutes) HandleDirectIDERoute(route *mux.Route) {
136166
r := route.Subrouter()
137167
r.Use(logRouteHandlerHandler("HandleDirectIDERoute"))

components/ws-proxy/pkg/proxy/routes_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ package proxy
66

77
import (
88
"context"
9+
"crypto/rand"
10+
"crypto/rsa"
11+
"crypto/x509"
12+
"encoding/json"
13+
"encoding/pem"
914
"fmt"
1015
"io"
1116
"net"
@@ -18,6 +23,7 @@ import (
1823

1924
"github.com/google/go-cmp/cmp"
2025
"github.com/sirupsen/logrus"
26+
"golang.org/x/crypto/ssh"
2127

2228
"github.com/gitpod-io/gitpod/common-go/log"
2329
"github.com/gitpod-io/gitpod/common-go/util"
@@ -662,7 +668,7 @@ func TestRoutes(t *testing.T) {
662668
Header: "",
663669
}
664670

665-
proxy := NewWorkspaceProxy(ingress, cfg, router, &fakeWsInfoProvider{infos: workspaces})
671+
proxy := NewWorkspaceProxy(ingress, cfg, router, &fakeWsInfoProvider{infos: workspaces}, nil)
666672
handler, err := proxy.Handler()
667673
if err != nil {
668674
t.Fatalf("cannot create proxy handler: %q", err)
@@ -733,6 +739,98 @@ func (p *fakeWsInfoProvider) WorkspaceCoords(wsProxyPort string) *WorkspaceCoord
733739
return nil
734740
}
735741

742+
func TestSSHGatewayRouter(t *testing.T) {
743+
generatePrivateKey := func() ssh.Signer {
744+
prik, err := rsa.GenerateKey(rand.Reader, 2048)
745+
if err != nil {
746+
return nil
747+
}
748+
b := pem.EncodeToMemory(&pem.Block{
749+
Bytes: x509.MarshalPKCS1PrivateKey(prik),
750+
Type: "RSA PRIVATE KEY",
751+
})
752+
signal, err := ssh.ParsePrivateKey(b)
753+
if err != nil {
754+
return nil
755+
}
756+
return signal
757+
}
758+
759+
tests := []struct {
760+
Name string
761+
Input []ssh.Signer
762+
Expected int
763+
}{
764+
{"one hostkey", []ssh.Signer{generatePrivateKey()}, 1},
765+
{"multi hostkey", []ssh.Signer{generatePrivateKey(), generatePrivateKey()}, 2},
766+
}
767+
for _, test := range tests {
768+
t.Run(test.Name, func(t *testing.T) {
769+
router := HostBasedRouter(hostBasedHeader, wsHostSuffix, wsHostNameRegex)
770+
ingress := HostBasedIngressConfig{
771+
HTTPAddress: "8080",
772+
HTTPSAddress: "9090",
773+
Header: "",
774+
}
775+
776+
proxy := NewWorkspaceProxy(ingress, config, router, &fakeWsInfoProvider{infos: workspaces}, test.Input)
777+
handler, err := proxy.Handler()
778+
if err != nil {
779+
t.Fatalf("cannot create proxy handler: %q", err)
780+
}
781+
782+
rec := httptest.NewRecorder()
783+
handler.ServeHTTP(rec, modifyRequest(httptest.NewRequest("GET", workspaces[0].URL+"__ws_proxy/ssh_host_key", nil),
784+
addHostHeader,
785+
))
786+
resp := rec.Result()
787+
body, _ := io.ReadAll(resp.Body)
788+
resp.Body.Close()
789+
if resp.StatusCode != 200 {
790+
t.Fatalf("status code should be 200, but got %d", resp.StatusCode)
791+
}
792+
var hostkeys []map[string]interface{}
793+
fmt.Println(string(body))
794+
err = json.Unmarshal(body, &hostkeys)
795+
if err != nil {
796+
t.Fatal(err)
797+
}
798+
t.Log(hostkeys, len(hostkeys), test.Expected)
799+
800+
if len(hostkeys) != test.Expected {
801+
t.Fatalf("hostkey length is not expected")
802+
}
803+
})
804+
}
805+
}
806+
807+
func TestNoSSHGatewayRouter(t *testing.T) {
808+
t.Run("TestNoSSHGatewayRouter", func(t *testing.T) {
809+
router := HostBasedRouter(hostBasedHeader, wsHostSuffix, wsHostNameRegex)
810+
ingress := HostBasedIngressConfig{
811+
HTTPAddress: "8080",
812+
HTTPSAddress: "9090",
813+
Header: "",
814+
}
815+
816+
proxy := NewWorkspaceProxy(ingress, config, router, &fakeWsInfoProvider{infos: workspaces}, nil)
817+
handler, err := proxy.Handler()
818+
if err != nil {
819+
t.Fatalf("cannot create proxy handler: %q", err)
820+
}
821+
rec := httptest.NewRecorder()
822+
handler.ServeHTTP(rec, modifyRequest(httptest.NewRequest("GET", workspaces[0].URL+"__ws_proxy/ssh_host_key", nil),
823+
addHostHeader,
824+
))
825+
resp := rec.Result()
826+
resp.Body.Close()
827+
if resp.StatusCode != 401 {
828+
t.Fatalf("status code should be 401, but got %d", resp.StatusCode)
829+
}
830+
})
831+
832+
}
833+
736834
func TestRemoveSensitiveCookies(t *testing.T) {
737835
var (
738836
domain = "test-domain.com"

0 commit comments

Comments
 (0)