From 37025040a7eab3b96fb0837cb63499971e740abb Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 30 Jan 2025 14:04:45 +0000 Subject: [PATCH 1/5] [docker-up] Extend parsing of DOCKERD_ARGS by "proxies" and contained fields, add tests Tool: gitpod/catfood.gitpod.cloud --- components/docker-up/docker-up/main.go | 99 +-------------- components/docker-up/dockerd/args.go | 144 ++++++++++++++++++++++ components/docker-up/dockerd/args_test.go | 62 ++++++++++ 3 files changed, 209 insertions(+), 96 deletions(-) create mode 100644 components/docker-up/dockerd/args.go create mode 100644 components/docker-up/dockerd/args_test.go diff --git a/components/docker-up/docker-up/main.go b/components/docker-up/docker-up/main.go index 43c3bb1fc9c81a..4385a784caf10b 100644 --- a/components/docker-up/docker-up/main.go +++ b/components/docker-up/docker-up/main.go @@ -9,11 +9,9 @@ package main import ( "archive/tar" - "bufio" "compress/gzip" "context" "embed" - "encoding/json" "fmt" "io" "os" @@ -25,6 +23,7 @@ import ( "syscall" "time" + "github.com/gitpod-io/gitpod/docker-up/dockerd" "github.com/rootless-containers/rootlesskit/pkg/sigproxy" sigproxysignal "github.com/rootless-containers/rootlesskit/pkg/sigproxy/signal" "github.com/sirupsen/logrus" @@ -57,7 +56,6 @@ var aptUpdated = false const ( dockerSocketFN = "/var/run/docker.sock" - gitpodUserId = 33333 containerIf = "eth0" ) @@ -118,7 +116,8 @@ func runWithinNetns() (err error) { ) } - userArgs, err := userArgs() + userArgsValue, _ := os.LookupEnv(DaemonArgs) + userArgs, err := dockerd.ParseUserArgs(log, userArgsValue) if err != nil { return xerrors.Errorf("cannot add user supplied docker args: %w", err) } @@ -192,98 +191,6 @@ func runWithinNetns() (err error) { return nil } -type ConvertUserArg func(arg, value string) ([]string, error) - -var allowedDockerArgs = map[string]ConvertUserArg{ - "remap-user": convertRemapUser, -} - -func userArgs() ([]string, error) { - userArgs, exists := os.LookupEnv(DaemonArgs) - args := []string{} - if !exists { - return args, nil - } - - var providedDockerArgs map[string]string - if err := json.Unmarshal([]byte(userArgs), &providedDockerArgs); err != nil { - return nil, xerrors.Errorf("unable to deserialize docker args: %w", err) - } - - for userArg, userValue := range providedDockerArgs { - converter, exists := allowedDockerArgs[userArg] - if !exists { - continue - } - - if converter != nil { - cargs, err := converter(userArg, userValue) - if err != nil { - return nil, xerrors.Errorf("could not convert %v - %v: %w", userArg, userValue, err) - } - args = append(args, cargs...) - - } else { - args = append(args, "--"+userArg, userValue) - } - } - - return args, nil -} - -func convertRemapUser(arg, value string) ([]string, error) { - id, err := strconv.Atoi(value) - if err != nil { - return nil, err - } - - for _, f := range []string{"/etc/subuid", "/etc/subgid"} { - err := adaptSubid(f, id) - if err != nil { - return nil, xerrors.Errorf("could not adapt subid files: %w", err) - } - } - - return []string{"--userns-remap", "gitpod"}, nil -} - -func adaptSubid(oldfile string, id int) error { - uid, err := os.Open(oldfile) - if err != nil { - return err - } - - newfile, err := os.Create(oldfile + ".new") - if err != nil { - return err - } - - mappingFmt := func(username string, id int, size int) string { return fmt.Sprintf("%s:%d:%d\n", username, id, size) } - - if id != 0 { - newfile.WriteString(mappingFmt("gitpod", 1, id)) - newfile.WriteString(mappingFmt("gitpod", gitpodUserId, 1)) - } else { - newfile.WriteString(mappingFmt("gitpod", gitpodUserId, 1)) - newfile.WriteString(mappingFmt("gitpod", 1, gitpodUserId-1)) - newfile.WriteString(mappingFmt("gitpod", gitpodUserId+1, 32200)) // map rest of user ids in the user namespace - } - - uidScanner := bufio.NewScanner(uid) - for uidScanner.Scan() { - l := uidScanner.Text() - if !strings.HasPrefix(l, "gitpod") { - newfile.WriteString(l + "\n") - } - } - - if err = os.Rename(newfile.Name(), oldfile); err != nil { - return err - } - - return nil -} - var prerequisites = map[string]func() error{ "dockerd": installDocker, "docker-compose": installDockerCompose, diff --git a/components/docker-up/dockerd/args.go b/components/docker-up/dockerd/args.go new file mode 100644 index 00000000000000..95500f0b8d4164 --- /dev/null +++ b/components/docker-up/dockerd/args.go @@ -0,0 +1,144 @@ +package dockerd + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/xerrors" +) + +const ( + gitpodUserId = 33333 +) + +type ConvertUserArg func(arg string, value interface{}) ([]string, error) + +var allowedDockerArgs = map[string]ConvertUserArg{ + "remap-user": convertRemapUser, + // TODO(gpl): Why this allow-list instead of a converter lookup only? + "proxies": nil, + "http-proxy": nil, + "https-proxy": nil, +} + +func ParseUserArgs(log *logrus.Entry, userArgs string) ([]string, error) { + if userArgs == "" { + return nil, nil + } + + var providedDockerArgs map[string]interface{} + if err := json.Unmarshal([]byte(userArgs), &providedDockerArgs); err != nil { + return nil, xerrors.Errorf("unable to deserialize docker args: %w", err) + } + + return mapUserArgs(log, providedDockerArgs) +} + +func mapUserArgs(log *logrus.Entry, jsonObj map[string]interface{}) ([]string, error) { + args := []string{} + for userArg, userValue := range jsonObj { + converter, exists := allowedDockerArgs[userArg] + if !exists { + // TODO(gpl): Why this allow-list instead of a converter lookup only? + continue + } + + if converter != nil { + cargs, err := converter(userArg, userValue) + if err != nil { + return nil, xerrors.Errorf("could not convert %v - %v: %w", userArg, userValue, err) + } + args = append(args, cargs...) + continue + } + + strValue, ok := (userValue).(string) + if ok { + args = append(args, fmt.Sprintf("--%s=%s", userArg, strValue)) + continue + } + + bValue, ok := (userValue).(bool) + if ok { + args = append(args, fmt.Sprintf("--%s=%t", userArg, bValue)) + continue + } + + obj, ok := (userValue).(map[string]interface{}) + if ok { + nestedArgs, err := mapUserArgs(log, obj) + if err != nil { + return nil, xerrors.Errorf("could not convert nested arg %v - %v: %w", userArg, userValue, err) + } + args = append(args, nestedArgs...) + continue + } + + log.WithField("arg", userArg).WithField("value", userValue).Warn("could not map userArg to dockerd argument, skipping.") + } + + return args, nil +} + +func convertRemapUser(arg string, value interface{}) ([]string, error) { + v, ok := (value).(string) + if !ok { + return nil, xerrors.Errorf("userns-remap expects a string argument") + } + + id, err := strconv.Atoi(v) + if err != nil { + return nil, err + } + + for _, f := range []string{"/etc/subuid", "/etc/subgid"} { + err := adaptSubid(f, id) + if err != nil { + return nil, xerrors.Errorf("could not adapt subid files: %w", err) + } + } + + return []string{"--userns-remap", "gitpod"}, nil +} + +func adaptSubid(oldfile string, id int) error { + uid, err := os.Open(oldfile) + if err != nil { + return err + } + + newfile, err := os.Create(oldfile + ".new") + if err != nil { + return err + } + + mappingFmt := func(username string, id int, size int) string { return fmt.Sprintf("%s:%d:%d\n", username, id, size) } + + if id != 0 { + newfile.WriteString(mappingFmt("gitpod", 1, id)) + newfile.WriteString(mappingFmt("gitpod", gitpodUserId, 1)) + } else { + newfile.WriteString(mappingFmt("gitpod", gitpodUserId, 1)) + newfile.WriteString(mappingFmt("gitpod", 1, gitpodUserId-1)) + newfile.WriteString(mappingFmt("gitpod", gitpodUserId+1, 32200)) // map rest of user ids in the user namespace + } + + uidScanner := bufio.NewScanner(uid) + for uidScanner.Scan() { + l := uidScanner.Text() + if !strings.HasPrefix(l, "gitpod") { + newfile.WriteString(l + "\n") + } + } + + if err = os.Rename(newfile.Name(), oldfile); err != nil { + return err + } + + return nil +} diff --git a/components/docker-up/dockerd/args_test.go b/components/docker-up/dockerd/args_test.go new file mode 100644 index 00000000000000..593d07ab48fe0e --- /dev/null +++ b/components/docker-up/dockerd/args_test.go @@ -0,0 +1,62 @@ +// Copyright (c) 2020 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package dockerd + +import ( + "reflect" + "sort" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestMapUserArgs(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected []string + wantErr bool + }{ + { + name: "empty input", + input: map[string]interface{}{}, + expected: []string{}, + wantErr: false, + }, + { + name: "tls and proxy settings", + input: map[string]interface{}{ + "proxies": map[string]interface{}{ + "http-proxy": "localhost:38080", + "https-proxy": "localhost:38081", + }, + }, + expected: []string{ + "--http-proxy=localhost:38080", + "--https-proxy=localhost:38081", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logrus.New().WithField("test", t.Name()) + got, err := mapUserArgs(log, tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("mapUserArgs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + sort.Strings(got) + sort.Strings(tt.expected) + // Sort both slices to ensure consistent comparison + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("mapUserArgs() = %v, want %v", got, tt.expected) + } + } + }) + } +} From cffb23c4d6a2b1d1089b440cc3a87faa2b35f273 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 30 Jan 2025 19:04:12 +0000 Subject: [PATCH 2/5] [image-builder-bob] Introduced forward-proxy and extended MapAuthorizer Details - proxy.go: make the core request-handling logic reusable - forward_proxy.go: re-used the proxy logic to setup a simple forwarding proxy without any mappings - auth.go: fixed multiple bugs, added tests and introduced handling of GITPOD_IMAGE_AUTH format Tool: gitpod/catfood.gitpod.cloud --- components/image-builder-bob/BUILD.yaml | 13 ++ components/image-builder-bob/cmd/proxy.go | 2 +- .../image-builder-bob/pkg/proxy/auth.go | 46 +++++- .../image-builder-bob/pkg/proxy/auth_test.go | 149 ++++++++++++++++++ .../pkg/proxy/forward_proxy.go | 92 +++++++++++ .../image-builder-bob/pkg/proxy/proxy.go | 67 +++++--- 6 files changed, 338 insertions(+), 31 deletions(-) create mode 100644 components/image-builder-bob/pkg/proxy/auth_test.go create mode 100644 components/image-builder-bob/pkg/proxy/forward_proxy.go diff --git a/components/image-builder-bob/BUILD.yaml b/components/image-builder-bob/BUILD.yaml index cafd239a3c9982..834c7e68051007 100644 --- a/components/image-builder-bob/BUILD.yaml +++ b/components/image-builder-bob/BUILD.yaml @@ -16,6 +16,19 @@ packages: - ["go", "mod", "tidy"] config: packaging: app +- name: lib + type: go + deps: + - components/common-go:lib + srcs: + - "cmd/*.go" + - "pkg/**/*.go" + - "main.go" + - "go.mod" + - "go.sum" + config: + packaging: library + dontTest: false - name: runc-facade type: go srcs: diff --git a/components/image-builder-bob/cmd/proxy.go b/components/image-builder-bob/cmd/proxy.go index 238f56eecc2475..8bc8f15bf35e84 100644 --- a/components/image-builder-bob/cmd/proxy.go +++ b/components/image-builder-bob/cmd/proxy.go @@ -37,7 +37,7 @@ var proxyCmd = &cobra.Command{ } authA, err := proxy.NewAuthorizerFromEnvVar(proxyOpts.AdditionalAuth) if err != nil { - log.WithError(err).WithField("auth", proxyOpts.Auth).Fatal("cannot unmarshal auth") + log.WithError(err).WithField("additionalAuth", proxyOpts.AdditionalAuth).Fatal("cannot unmarshal additionalAuth") } authP = authP.AddIfNotExists(authA) diff --git a/components/image-builder-bob/pkg/proxy/auth.go b/components/image-builder-bob/pkg/proxy/auth.go index fbd76e5269ee71..c25c1432575cd8 100644 --- a/components/image-builder-bob/pkg/proxy/auth.go +++ b/components/image-builder-bob/pkg/proxy/auth.go @@ -52,10 +52,27 @@ func (a MapAuthorizer) Authorize(host string) (user, pass string, err error) { }).Info("authorizing registry access") }() + // Strip any port from the host if present + host = strings.Split(host, ":")[0] + explicitHostMatcher := func() (authConfig, bool) { res, ok := a[host] return res, ok } + suffixHostMatcher := func() (authConfig, bool) { + var match *authConfig + for k, v := range a { + if strings.HasSuffix(host, k) { + if match == nil || len(k) > len(host) { + match = &v + } + } + } + if match == nil { + return authConfig{}, false + } + return *match, true + } ecrHostMatcher := func() (authConfig, bool) { if isECRRegistry(host) { res, ok := a[DummyECRRegistryDomain] @@ -71,7 +88,7 @@ func (a MapAuthorizer) Authorize(host string) (user, pass string, err error) { return authConfig{}, false } - matchers := []func() (authConfig, bool){explicitHostMatcher, ecrHostMatcher, dockerHubHostMatcher} + matchers := []func() (authConfig, bool){explicitHostMatcher, suffixHostMatcher, ecrHostMatcher, dockerHubHostMatcher} res, ok := authConfig{}, false for _, matcher := range matchers { res, ok = matcher() @@ -86,12 +103,13 @@ func (a MapAuthorizer) Authorize(host string) (user, pass string, err error) { user, pass = res.Username, res.Password if res.Auth != "" { - var auth []byte - auth, err = base64.StdEncoding.DecodeString(res.Auth) + var authBytes []byte + authBytes, err = base64.StdEncoding.DecodeString(res.Auth) if err != nil { return } - segs := strings.Split(string(auth), ":") + auth := strings.TrimSpace(string(authBytes)) + segs := strings.Split(auth, ":") if len(segs) < 2 { return } @@ -145,3 +163,23 @@ func NewAuthorizerFromEnvVar(content string) (auth MapAuthorizer, err error) { } return MapAuthorizer(res), nil } + +func NewAuthorizerFromGitpodImageAuth(content string) (auth MapAuthorizer, err error) { + if content == "" { + return nil, nil + } + + res := map[string]authConfig{} + hostsCredentials := strings.Split(content, ",") + for _, hostCredentials := range hostsCredentials { + parts := strings.SplitN(hostCredentials, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + log.Debug("Error parsing host credential for authorizer, skipping.") + continue + } + res[parts[0]] = authConfig{ + Auth: parts[1], + } + } + return MapAuthorizer(res), nil +} diff --git a/components/image-builder-bob/pkg/proxy/auth_test.go b/components/image-builder-bob/pkg/proxy/auth_test.go new file mode 100644 index 00000000000000..24e1ea7d5aaf7a --- /dev/null +++ b/components/image-builder-bob/pkg/proxy/auth_test.go @@ -0,0 +1,149 @@ +// Copyright (c) 2025 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package proxy + +import ( + "testing" +) + +func TestAuthorize(t *testing.T) { + type expectation struct { + user string + pass string + err string + } + tests := []struct { + name string + constructor func(string) (MapAuthorizer, error) + input string + testHost string + expected expectation + }{ + { + name: "docker auth format - valid credentials", + constructor: NewAuthorizerFromDockerEnvVar, + input: `{"auths": {"registry.example.com": {"auth": "dXNlcjpwYXNz"}}}`, // base64(user:pass) + testHost: "registry.example.com", + expected: expectation{ + user: "user", + pass: "pass", + }, + }, + { + name: "docker auth format - valid credentials - host with port", + constructor: NewAuthorizerFromDockerEnvVar, + input: `{"auths": {"registry.example.com": {"auth": "dXNlcjpwYXNz"}}}`, // base64(user:pass) + testHost: "registry.example.com:443", + expected: expectation{ + user: "user", + pass: "pass", + }, + }, + { + name: "docker auth format - invalid host", + constructor: NewAuthorizerFromDockerEnvVar, + input: `{"auths": {"registry.example.com": {"auth": "dXNlcjpwYXNz"}}}`, + testHost: "wrong.registry.com", + expected: expectation{ + user: "", + pass: "", + }, + }, + { + name: "env var format - valid credentials", + constructor: NewAuthorizerFromEnvVar, + input: `{"registry.example.com": {"auth": "dXNlcjpwYXNz"}}`, + testHost: "registry.example.com", + expected: expectation{ + user: "user", + pass: "pass", + }, + }, + { + name: "env var format - empty input", + constructor: NewAuthorizerFromEnvVar, + input: "", + testHost: "registry.example.com", + expected: expectation{ + user: "", + pass: "", + }, + }, + { + name: "gitpod format - valid credentials", + constructor: NewAuthorizerFromGitpodImageAuth, + input: "registry.example.com:dXNlcjpwYXNz", + testHost: "registry.example.com", + expected: expectation{ + user: "user", + pass: "pass", + }, + }, + { + name: "gitpod format - multiple hosts", + constructor: NewAuthorizerFromGitpodImageAuth, + input: "registry1.example.com:dXNlcjE6cGFzczEK,registry2.example.com:dXNlcjI6cGFzczIK", + testHost: "registry2.example.com", + expected: expectation{ + user: "user2", + pass: "pass2", + }, + }, + { + name: "gitpod format - invalid format", + constructor: NewAuthorizerFromGitpodImageAuth, + input: "invalid:format:with:toomany:colons", + testHost: "registry.example.com", + expected: expectation{ + user: "", + pass: "", + }, + }, + { + name: "gitpod format - empty input", + constructor: NewAuthorizerFromGitpodImageAuth, + input: "", + testHost: "registry.example.com", + expected: expectation{ + user: "", + pass: "", + }, + }, + { + name: "gitpod format - suffix match", + constructor: NewAuthorizerFromDockerEnvVar, + input: `{"auths": {"docker.io": {"auth": "dXNlcjpwYXNz"}}}`, + testHost: "registry.docker.io", + expected: expectation{ + user: "user", + pass: "pass", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth, err := tt.constructor(tt.input) + if err != nil { + if tt.expected.err == "" { + t.Errorf("Constructor failed: %s", err) + } + return + } + + actualUser, actualPassword, err := auth.Authorize(tt.testHost) + if (err != nil) != (tt.expected.err != "") { + t.Errorf("Authorize() error = %v, wantErr %v", err, tt.expected.err) + return + } + if actualUser != tt.expected.user { + t.Errorf("Authorize() actual user = %v, want %v", actualUser, tt.expected.user) + } + if actualPassword != tt.expected.pass { + t.Errorf("Authorize() actual password = %v, want %v", actualPassword, tt.expected.pass) + } + }) + } +} diff --git a/components/image-builder-bob/pkg/proxy/forward_proxy.go b/components/image-builder-bob/pkg/proxy/forward_proxy.go new file mode 100644 index 00000000000000..97371b7bdf3908 --- /dev/null +++ b/components/image-builder-bob/pkg/proxy/forward_proxy.go @@ -0,0 +1,92 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package proxy + +import ( + "context" + "net/http" + "net/http/httputil" + "net/url" + "strings" + "sync" + + "github.com/containerd/containerd/remotes/docker" + "github.com/gitpod-io/gitpod/common-go/log" +) + +func NewForwardProxy(authorizer func() docker.Authorizer, scheme string) (*ForwardProxy, error) { + return &ForwardProxy{ + authorizer: authorizer, + scheme: scheme, + proxies: make(map[string]*httputil.ReverseProxy), + }, nil +} + +// ForwardProxy acts as forward proxy, injecting authentication to requests +// It uses the same docker-specific retry-and-authenticate logic as the reverse/mirror proxy in proxy.go +type ForwardProxy struct { + authorizer func() docker.Authorizer + scheme string + + mu sync.Mutex + proxies map[string]*httputil.ReverseProxy +} + +// ServeHTTP serves the proxy +func (p *ForwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Prepare for forwarding + r.RequestURI = "" + + auth := p.authorizer() + r = r.WithContext(context.WithValue(ctx, CONTEXT_KEY_AUTHORIZER, auth)) // auth might be used in the forward proxy below + + err := auth.Authorize(ctx, r) + if err != nil { + log.WithError(err).Error("cannot authorize request") + http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) + return + } + + // Construct target URL + targetUrl, err := parseTargetURL(r.Host, p.scheme) + if err != nil { + log.WithError(err).Error("cannot parse host to determine target URL") + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + p.authenticatingProxy(targetUrl).ServeHTTP(w, r) +} + +func parseTargetURL(targetHost string, scheme string) (*url.URL, error) { + // to safely parse the URL, we need to make sure it has a scheme + parts := strings.Split(targetHost, "://") + if len(parts) == 1 { + targetHost = scheme + "://" + parts[0] + } else if len(parts) == 2 { + targetHost = scheme + "://" + parts[1] + } else { + targetHost = scheme + "://" + parts[len(parts)-1] + } + targetUrl, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + return targetUrl, nil +} + +func (p *ForwardProxy) authenticatingProxy(targetUrl *url.URL) *httputil.ReverseProxy { + p.mu.Lock() + defer p.mu.Unlock() + + if rp, ok := p.proxies[targetUrl.Host]; ok { + return rp + } + rp := createAuthenticatingReverseProxy(targetUrl) + p.proxies[targetUrl.Host] = rp + return rp +} diff --git a/components/image-builder-bob/pkg/proxy/proxy.go b/components/image-builder-bob/pkg/proxy/proxy.go index dbabc0d353fb48..1a31f8b4235e4b 100644 --- a/components/image-builder-bob/pkg/proxy/proxy.go +++ b/components/image-builder-bob/pkg/proxy/proxy.go @@ -19,7 +19,7 @@ import ( "github.com/hashicorp/go-retryablehttp" ) -const authKey = "authKey" +const CONTEXT_KEY_AUTHORIZER = "authKey" func NewProxy(host *url.URL, aliases map[string]Repo, mirrorAuth func() docker.Authorizer) (*Proxy, error) { if host.Host == "" || host.Scheme == "" { @@ -146,10 +146,11 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Host = host auth := proxy.mirrorAuth() - r = r.WithContext(context.WithValue(ctx, authKey, auth)) + r = r.WithContext(context.WithValue(ctx, CONTEXT_KEY_AUTHORIZER, auth)) r.RequestURI = "" - proxy.mirror(host).ServeHTTP(w, r) + targetUrl := &url.URL{Scheme: "https", Host: host} + proxy.mirror(targetUrl).ServeHTTP(w, r) return } @@ -161,7 +162,7 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Host = r.URL.Host auth := repo.Auth() - r = r.WithContext(context.WithValue(ctx, authKey, auth)) + r = r.WithContext(context.WithValue(ctx, CONTEXT_KEY_AUTHORIZER, auth)) err := auth.Authorize(ctx, r) if err != nil { @@ -200,7 +201,7 @@ func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy { log.WithError(err).Warn("saw error during CheckRetry") return false, err } - auth, ok := ctx.Value(authKey).(docker.Authorizer) + auth, ok := ctx.Value(CONTEXT_KEY_AUTHORIZER).(docker.Authorizer) if !ok || auth == nil { return false, nil } @@ -256,7 +257,7 @@ func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy { // @link https://golang.org/src/net/http/httputil/reverseproxy.go r.Header.Set("X-Forwarded-For", "127.0.0.1") - auth, ok := r.Context().Value(authKey).(docker.Authorizer) + auth, ok := r.Context().Value(CONTEXT_KEY_AUTHORIZER).(docker.Authorizer) if !ok || auth == nil { return } @@ -307,16 +308,41 @@ func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy { } // mirror produces an authentication-adding reverse proxy for given host -func (proxy *Proxy) mirror(host string) *httputil.ReverseProxy { +func (proxy *Proxy) mirror(targetUrl *url.URL) *httputil.ReverseProxy { proxy.mu.Lock() defer proxy.mu.Unlock() - if rp, ok := proxy.proxies[host]; ok { + if rp, ok := proxy.proxies[targetUrl.Host]; ok { return rp } - rp := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "https", Host: host}) + rp := createAuthenticatingReverseProxy(targetUrl) + proxy.proxies[targetUrl.Host] = rp + return rp +} + +func createAuthenticatingReverseProxy(targetUrl *url.URL) *httputil.ReverseProxy { + rp := httputil.NewSingleHostReverseProxy(targetUrl) + + client := CreateAuthenticatingDockerClient() + rp.Transport = &retryablehttp.RoundTripper{ + Client: client, + } + rp.ModifyResponse = func(r *http.Response) error { + if r.StatusCode == http.StatusBadGateway { + // BadGateway makes containerd retry - we don't want that because we retry the upstream + // requests internally. + r.StatusCode = http.StatusInternalServerError + r.Status = http.StatusText(http.StatusInternalServerError) + } + + return nil + } + return rp +} +// CreateAuthenticatingDockerClient creates a retryable http client that can authenticate against a docker registry, incl. handling it's idiosyncracies +func CreateAuthenticatingDockerClient() *retryablehttp.Client { client := retryablehttp.NewClient() client.RetryMax = 3 client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { @@ -324,11 +350,13 @@ func (proxy *Proxy) mirror(host string) *httputil.ReverseProxy { log.WithError(err).Warn("saw error during CheckRetry") return false, err } - auth, ok := ctx.Value(authKey).(docker.Authorizer) + auth, ok := ctx.Value(CONTEXT_KEY_AUTHORIZER).(docker.Authorizer) if !ok || auth == nil { + log.Warn("no authorizer found in context, won't retry.") return false, nil } if resp.StatusCode == http.StatusUnauthorized { + log.Debug("employing authorizer workaround for 401") // the docker authorizer only refreshes OAuth tokens after two // successive 401 errors for the same URL. Rather than issue the same // request multiple times to tickle the token-refreshing logic, just @@ -352,6 +380,7 @@ func (proxy *Proxy) mirror(host string) *httputil.ReverseProxy { } return true, nil } + if resp.StatusCode == http.StatusBadRequest { log.WithField("URL", resp.Request.URL.String()).Warn("bad request") return true, nil @@ -374,7 +403,7 @@ func (proxy *Proxy) mirror(host string) *httputil.ReverseProxy { // @link https://golang.org/src/net/http/httputil/reverseproxy.go r.Header.Set("X-Forwarded-For", "127.0.0.1") - auth, ok := r.Context().Value(authKey).(docker.Authorizer) + auth, ok := r.Context().Value(CONTEXT_KEY_AUTHORIZER).(docker.Authorizer) if !ok || auth == nil { return } @@ -382,19 +411,5 @@ func (proxy *Proxy) mirror(host string) *httputil.ReverseProxy { } client.ResponseLogHook = func(l retryablehttp.Logger, r *http.Response) {} - rp.Transport = &retryablehttp.RoundTripper{ - Client: client, - } - rp.ModifyResponse = func(r *http.Response) error { - if r.StatusCode == http.StatusBadGateway { - // BadGateway makes containerd retry - we don't want that because we retry the upstream - // requests internally. - r.StatusCode = http.StatusInternalServerError - r.Status = http.StatusText(http.StatusInternalServerError) - } - - return nil - } - proxy.proxies[host] = rp - return rp + return client } From 6839b7dbb8aaaf9f6e0b572d535b211c00ffc122 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 30 Jan 2025 19:23:33 +0000 Subject: [PATCH 3/5] [supervisor] Introduce subcommand "dockerd-proxy" and GITPOD_DOCKERD_PROXY_ENABLED for control docker-proxy is a MITM proxy to intercept HTTPS traffic. It does that to inject authentication for all registires configured Tool: gitpod/catfood.gitpod.cloud --- components/supervisor/BUILD.yaml | 1 + components/supervisor/cmd/dockerd-proxy.go | 101 ++++++ components/supervisor/go.mod | 40 ++- components/supervisor/go.sum | 110 +++--- components/supervisor/pkg/dockerd/certs.go | 186 ++++++++++ .../supervisor/pkg/dockerd/mitm_proxy.go | 325 ++++++++++++++++++ .../supervisor/pkg/supervisor/config.go | 3 + .../supervisor/pkg/supervisor/docker.go | 5 +- 8 files changed, 706 insertions(+), 65 deletions(-) create mode 100644 components/supervisor/cmd/dockerd-proxy.go create mode 100644 components/supervisor/pkg/dockerd/certs.go create mode 100644 components/supervisor/pkg/dockerd/mitm_proxy.go diff --git a/components/supervisor/BUILD.yaml b/components/supervisor/BUILD.yaml index 45807220c0aa8c..0aef866c98961e 100644 --- a/components/supervisor/BUILD.yaml +++ b/components/supervisor/BUILD.yaml @@ -14,6 +14,7 @@ packages: - components/ws-daemon-api/go:lib - components/ide-metrics-api/go:lib - components/public-api/go:lib + - components/image-builder-bob:lib env: - CGO_ENABLED=0 - GOOS=linux diff --git a/components/supervisor/cmd/dockerd-proxy.go b/components/supervisor/cmd/dockerd-proxy.go new file mode 100644 index 00000000000000..32f3afc49b516b --- /dev/null +++ b/components/supervisor/cmd/dockerd-proxy.go @@ -0,0 +1,101 @@ +// Copyright (c) 2025 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package cmd + +import ( + "context" + "net/http" + "os" + + "github.com/containerd/containerd/remotes/docker" + "github.com/spf13/cobra" + + log "github.com/gitpod-io/gitpod/common-go/log" + "github.com/gitpod-io/gitpod/image-builder/bob/pkg/proxy" + "github.com/gitpod-io/gitpod/supervisor/pkg/dockerd" +) + +var proxyOpts struct { + GitpodImageAuth string +} + +// dockerdProxyCmd represents the build command +var dockerdProxyCmd = &cobra.Command{ + Use: "dockerd-proxy", + Short: "Runs an authenticating proxy", + Run: func(cmd *cobra.Command, args []string) { + log.Init("dockerd-proxy", "", true, os.Getenv("SUPERVISOR_DEBUG_ENABLE") == "true") + log := log.WithField("command", "dockerd-proxy") + + auth, err := proxy.NewAuthorizerFromGitpodImageAuth(proxyOpts.GitpodImageAuth) + if err != nil { + log.WithError(err).WithField("gitpodImageAuth", proxyOpts.GitpodImageAuth).Fatal("cannot unmarshal gitpodImageAuth") + } + + //certDir := "/workspace/.dockerd-proxy/certs" + certDir, err := os.MkdirTemp("/tmp", "gitpod-dockerd-proxy-certs") + if err != nil { + log.WithError(err).Fatal("cannot create temporary directory for certificates") + } + certPath, keyPath, err := dockerd.EnsureProxyCaAndCertificatesInstalled(certDir) + if err != nil { + log.WithError(err).Fatal("failed to ensure proxy CA and certificates are installed") + } + + // Setup the (authenticating) MITM proxy to handle CONNECT requests + authorizer := func() docker.Authorizer { return docker.NewDockerAuthorizer(docker.WithAuthCreds(auth.Authorize)) } + mitmProxy, err := dockerd.CreateMitmProxy(certPath, keyPath, func(r *http.Request) *http.Request { + ctx := r.Context() + + auth := authorizer() + r = r.WithContext(context.WithValue(ctx, proxy.CONTEXT_KEY_AUTHORIZER, auth)) // install to context, as the proxy relies on it + + err = auth.Authorize(ctx, r) + if err != nil { + log.WithError(err).Error("cannot authorize request") + } + return r + }) + if err != nil { + log.Fatal(err) + } + + // Setup the (authenticating) forwarding proxy to handle all other requests + handler := func(scheme string) http.Handler { + httpProxy, err := proxy.NewForwardProxy(authorizer, scheme) + if err != nil { + log.Fatal(err) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + mitmProxy.ServeHTTP(w, r) + } else { + httpProxy.ServeHTTP(w, r) + } + }) + } + + log.Info("starting https dockerd proxy on :38081") + go (func() { + err := http.ListenAndServeTLS(":38081", certPath, keyPath, handler("https")) + if err != nil { + log.Fatal(err) + } + })() + log.Info("starting http dockerd proxy on :38080") + err = http.ListenAndServe(":38080", handler("http")) + if err != nil { + log.Fatal(err) + } + }, +} + +func init() { + rootCmd.AddCommand(dockerdProxyCmd) + + // These env vars start with `WORKSPACEKIT_` so that they aren't passed on to ring2 + dockerdProxyCmd.Flags().StringVar(&proxyOpts.GitpodImageAuth, "gitpod-image-auth", os.Getenv("WORKSPACEKIT_GITPOD_IMAGE_AUTH"), "docker credentials in the GITPOD_IMAGE_AUTH format") +} diff --git a/components/supervisor/go.mod b/components/supervisor/go.mod index 1cae07923dc268..51df34ad72b170 100644 --- a/components/supervisor/go.mod +++ b/components/supervisor/go.mod @@ -5,25 +5,27 @@ go 1.22 require ( github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 - github.com/cenkalti/backoff/v4 v4.2.0 + github.com/cenkalti/backoff/v4 v4.2.1 + github.com/containerd/containerd v1.7.25 github.com/creack/pty v1.1.18 - github.com/fsnotify/fsnotify v1.4.9 + github.com/fsnotify/fsnotify v1.6.0 github.com/gitpod-io/gitpod/common-go v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/components/public-api/go v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/content-service v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/content-service/api v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/gitpod-protocol v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/ide-metrics-api v0.0.0-00010101000000-000000000000 + github.com/gitpod-io/gitpod/image-builder/bob v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/supervisor/api v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/ws-daemon/api v0.0.0-00010101000000-000000000000 - github.com/gitpod-io/go-reaper v0.0.0-20241024192051-78d04cc2e25f github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 + github.com/hashicorp/go-retryablehttp v0.7.2 github.com/improbable-eng/grpc-web v0.14.0 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_model v0.3.0 @@ -33,7 +35,7 @@ require ( github.com/ramr/go-reaper v0.2.2 github.com/sirupsen/logrus v1.9.3 github.com/soheilhy/cmux v0.1.5 - github.com/spf13/cobra v1.4.0 + github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.31.0 golang.org/x/net v0.25.0 golang.org/x/sync v0.10.0 @@ -41,19 +43,25 @@ require ( golang.org/x/term v0.27.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.35.2 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/s2a-go v0.1.7 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/moby/locker v1.0.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.27.10 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect @@ -62,7 +70,7 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect ) require ( @@ -100,13 +108,13 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cilium/ebpf v0.4.0 // indirect + github.com/cilium/ebpf v0.9.1 // indirect github.com/configcat/go-sdk/v7 v7.6.0 // indirect - github.com/containerd/cgroups v1.0.4 // indirect - github.com/coreos/go-systemd/v22 v22.4.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/docker/go-units v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/camelcase v1.0.0 // indirect github.com/fatih/gomodifytags v1.14.0 // indirect @@ -114,7 +122,7 @@ require ( github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-ozzo/ozzo-validation v3.5.0+incompatible // indirect - github.com/godbus/dbus/v5 v5.0.4 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -123,7 +131,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb // indirect github.com/iancoleman/orderedmap v0.2.0 - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect @@ -136,8 +144,8 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/moredure/easygo v0.0.0-20220122214504-21cd2ebdd15b github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect - github.com/opencontainers/runtime-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -184,6 +192,8 @@ replace github.com/gitpod-io/gitpod/supervisor/api => ../supervisor-api/go // le replace github.com/gitpod-io/gitpod/ws-daemon/api => ../ws-daemon-api/go // leeway +replace github.com/gitpod-io/gitpod/image-builder/bob => ../image-builder-bob // leeway + replace k8s.io/api => k8s.io/api v0.29.3 // leeway indirect from components/common-go:lib replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.29.3 // leeway indirect from components/common-go:lib diff --git a/components/supervisor/go.sum b/components/supervisor/go.sum index 79c9e9c77cbc55..41bfe26f9b2132 100644 --- a/components/supervisor/go.sum +++ b/components/supervisor/go.sum @@ -11,9 +11,15 @@ cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQx cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ= cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY= cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/HdrHistogram/hdrhistogram-go v1.1.0 h1:6dpdDPTRoo78HxAJ6T1HfMiKSnqhgRRqzCuPshRkQ7I= github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= +github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d h1:wvStE9wLpws31NiWUx+38wny1msZ/tm+eL5xmm4Y7So= github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ= github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= @@ -67,22 +73,32 @@ github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8 h1:SjZ2GvvOononHOpK github.com/c9s/goprocinfo v0.0.0-20210130143923-c95fcf8c64a8/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.4.0 h1:QlHdikaxALkqWasW8hAC1mfR0jdmvbfaBdBPFmRSglA= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4= +github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/configcat/go-sdk/v7 v7.6.0 h1:CthQJ7DMz4bvUrpc8aek6VouJjisCvZCfuTG2gyNzL4= github.com/configcat/go-sdk/v7 v7.6.0/go.mod h1:2245V6Igy1Xz6GXvcYuK5z996Ct0VyzyuI470XS6aTw= -github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= -github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= -github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU= -github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/containerd v1.7.25 h1:khEQOAXOEJalRO228yzVsuASLH42vT7DIo9Ss+9SMFQ= +github.com/containerd/containerd v1.7.25/go.mod h1:tWfHzVI0azhw4CT2vaIjsb2CoV4LJ9PrMPaULAr21Ok= +github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= +github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -90,8 +106,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -107,11 +123,12 @@ github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= -github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsouza/fake-gcs-server v1.48.0 h1:CBjqlg0nout6XawFtLTKfdBP65SfE2kOnQs+FIOCV/U= github.com/fsouza/fake-gcs-server v1.48.0/go.mod h1:2F2TAO5Dttmzu8lXSyg9XG1o8lNfrMkw2m1VdVVSa00= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -119,8 +136,6 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/gitpod-io/go-reaper v0.0.0-20241024192051-78d04cc2e25f h1:jC8c/ONG+vsaxY6y17rM+Du7JN/faYfSBiMCFIi2NoA= -github.com/gitpod-io/go-reaper v0.0.0-20241024192051-78d04cc2e25f/go.mod h1:WJlnZLfag2J4+z28ZjM0CxgVqjYVYF8pnspnleDwrcA= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= @@ -150,8 +165,9 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -186,7 +202,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -215,8 +230,14 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaW github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/heptiolabs/healthcheck v0.0.0-20211123025425-613501dd5deb h1:tsEKRC3PU9rMw18w/uAptoijhgG4EvlA5kfJPtwrMDk= @@ -226,8 +247,8 @@ github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6 github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/improbable-eng/grpc-web v0.14.0 h1:GdoK+cXABdB+1keuqsV1drSFO2XLYIxqt/4Rj8SWGBk= github.com/improbable-eng/grpc-web v0.14.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -273,6 +294,12 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -296,10 +323,10 @@ github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= @@ -345,8 +372,8 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 h1:marA1XQDC7N870zmSFIoHZpIUduK80USeY0Rkuflgp4= github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -405,8 +432,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -417,9 +442,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -435,8 +459,6 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -450,8 +472,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -468,7 +488,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -476,15 +495,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -492,8 +508,6 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -511,8 +525,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -535,8 +547,8 @@ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJ google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -556,8 +568,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/components/supervisor/pkg/dockerd/certs.go b/components/supervisor/pkg/dockerd/certs.go new file mode 100644 index 00000000000000..797302899b6d4c --- /dev/null +++ b/components/supervisor/pkg/dockerd/certs.go @@ -0,0 +1,186 @@ +// Copyright (c) 2025 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package dockerd + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "os" + "path" + "time" + + log "github.com/gitpod-io/gitpod/common-go/log" +) + +func EnsureProxyCaAndCertificatesInstalled(certDir string) (certPath string, keyPath string, err error) { + // Generate certificates if they are not present, yet + if err := os.MkdirAll(certDir, 0755); err != nil { + return "", "", fmt.Errorf("failed to create cert directory: %w", err) + } + + // Create certificates and CA if they don't already exist + caPath := path.Join(certDir, "dockerd-proxy.pem") + certPath = path.Join(certDir, "proxy.crt") + keyPath = path.Join(certDir, "proxy.key") + _, statErr := os.Stat(certPath) + if statErr != nil { + if !os.IsNotExist(statErr) { + return "", "", fmt.Errorf("unexpected error while checking for certificate file: %w", statErr) + } + + if err := generateCAAndCerts(caPath, certPath, keyPath); err != nil { + return "", "", fmt.Errorf("failed to generate and save certificates: %w", err) + } + } + + // Install CA if not already installed + caInstallPath := "/etc/ssl/certs/dockerd-proxy.pem" + caBundleInstallPath := "/etc/ssl/certs/ca-certificates.crt" + _, statErr = os.Stat(caInstallPath) + if statErr == nil { + log.Infof("CA certificate already installed at %s, skipping install.", caInstallPath) + return certPath, keyPath, nil + } + if !os.IsNotExist(statErr) { + return "", "", fmt.Errorf("unexpected error while checking for installed CA file: %w", statErr) + } + + inFile, err := os.Open(caPath) + if err != nil { + return "", "", fmt.Errorf("cannot read CA file: %w", err) + } + defer inFile.Close() + buf, err := io.ReadAll(inFile) + if err != nil { + return "", "", fmt.Errorf("error reading certificat from file: %w", err) + } + + // Append CA to bundle + bundleFile, err := os.OpenFile(caBundleInstallPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return "", "", fmt.Errorf("cannot open CA bundle file: %w", err) + } + defer func() { + cerr := bundleFile.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(bundleFile, bytes.NewReader(buf)); err != nil { + return "", "", fmt.Errorf("cannot append to CA bundle file: %w", err) + } + if err = bundleFile.Sync(); err != nil { + return "", "", fmt.Errorf("cannot syncing CA bundle file: %w", err) + } + log.Infof("installed CA certificate to bundle at %s", caBundleInstallPath) + + // Install CA in own file + caFile, err := os.Create(caInstallPath) + if err != nil { + return "", "", fmt.Errorf("cannot create CA install file: %w", err) + } + defer func() { + cerr := caFile.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(caFile, bytes.NewReader(buf)); err != nil { + return "", "", fmt.Errorf("cannot write CA install file: %w", err) + } + if err = caFile.Sync(); err != nil { + return "", "", fmt.Errorf("cannot syncing CA install file: %w", err) + } + log.Infof("installed CA certificate at %s", caInstallPath) + + return certPath, keyPath, err +} + +// generateCAAndCerts generates a CA and a TLS certificate and saves them to the given paths +// As these are a) temporary for the lifetime for the workspace and b) only used inside the workspace to enable +// communication between dockerd and the dockerd-proxy, they don't need to be valid for long, nor do they need to be secure. +func generateCAAndCerts(caPath, crtPath, keyPath string) error { + // Generate TLS certificate + certPEM, keyPEM, err := generateSelfSignedTLSCertAndKey() + if err != nil { + return fmt.Errorf("failed to generate TLS certificate: %w", err) + } + + // Write certificate and key to files + if err := os.WriteFile(crtPath, certPEM, 0777); err != nil { + return fmt.Errorf("failed to write certificate file: %w", err) + } + + if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil { + return fmt.Errorf("failed to write key file: %w", err) + } + + // Write ca to file + if err := os.WriteFile(caPath, certPEM, 0777); err != nil { + return fmt.Errorf("failed to write ca file: %w", err) + } + + return nil +} + +func generateSelfSignedTLSCertAndKey() (certPEM, keyPEM []byte, err error) { + _, privKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate TLS private key: %w", err) + } + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + notBefore := time.Now() + notAfter := notBefore.AddDate(10, 0, 0) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Gitpod"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, + DNSNames: []string{"localhost"}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + //self-signed + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, privKey.Public(), privKey) + if err != nil { + return nil, nil, fmt.Errorf("Failed to create certificate: %w", err) + } + + // Encode certificate and private key to PEM + certPEM = pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: derBytes, + }) + + privBytes, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + return nil, nil, fmt.Errorf("Unable to marshal private key: %w", err) + } + keyPEM = pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: privBytes, + }) + + return certPEM, keyPEM, nil +} diff --git a/components/supervisor/pkg/dockerd/mitm_proxy.go b/components/supervisor/pkg/dockerd/mitm_proxy.go new file mode 100644 index 00000000000000..e0db8ca71f0c30 --- /dev/null +++ b/components/supervisor/pkg/dockerd/mitm_proxy.go @@ -0,0 +1,325 @@ +// Copyright (c) 2025 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package dockerd + +import ( + "bufio" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "math/big" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-retryablehttp" + + log "github.com/gitpod-io/gitpod/common-go/log" + "github.com/gitpod-io/gitpod/image-builder/bob/pkg/proxy" +) + +// Loosely based on https://eli.thegreenplace.net/2022/go-and-proxy-servers-part-2-https-proxies/ +type MitmProxy struct { + caCert *x509.Certificate + caKey any + modifier func(*http.Request) *http.Request + + certCacheMu sync.Mutex + certCache map[string]*tls.Certificate +} + +// CreateMitmProxy creates a new forwarding proxy that can handle CONNECT request, and intercept their payload. It should be passed the filenames +// for the certificate and private key of a certificate authority trusted by the +// client's machine. +func CreateMitmProxy(caCertFile, caKeyFile string, modifier func(*http.Request) *http.Request) (*MitmProxy, error) { + caCert, caKey, err := loadX509KeyPair(caCertFile, caKeyFile) + if err != nil { + return nil, fmt.Errorf("Error loading CA certificate/key: %w", err) + } + + return &MitmProxy{ + caCert: caCert, + caKey: caKey, + modifier: modifier, + certCache: make(map[string]*tls.Certificate), + }, nil +} + +func (p *MitmProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodConnect { + http.Error(w, "this proxy only supports CONNECT", http.StatusMethodNotAllowed) + } + + p.proxyConnect(w, req) +} + +// proxyConnect implements the MITM proxy for CONNECT tunnels. +func (p *MitmProxy) proxyConnect(w http.ResponseWriter, proxyReq *http.Request) { + log.Debugf("CONNECT requested to %v (from %v)", proxyReq.Host, proxyReq.RemoteAddr) + + // "Hijack" the client connection to get a TCP (or TLS) socket we can read + // and write arbitrary data to/from. + hj, ok := w.(http.Hijacker) + if !ok { + log.Error("http server doesn't support hijacking connection") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + clientConn, _, err := hj.Hijack() + if err != nil { + log.Error("http hijacking failed") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // proxyReq.Host will hold the CONNECT target host, which will typically have + // a port - e.g. example.org:443 + // To generate a fake certificate for example.org, we have to first split off + // the host from the port. + host, _, err := net.SplitHostPort(proxyReq.Host) + if err != nil { + log.WithError(err).Error("error splitting host/port") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // TODO(gpl): This might not work if the client asks for an IP, which is totally legit. + // If that ever happens, we can implement DNS discovery as outlined here: https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/#complication-1-whats-the-remote-hostname + tlsCert, err := p.getCertOrCreate(host) + + // Send an HTTP OK response back to the client; this initiates the CONNECT + // tunnel. From this point on the client will assume it's connected directly + // to the target. + if _, err := clientConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")); err != nil { + log.WithError(err).Error("error writing status to client") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Configure a new TLS server, pointing it at the client connection, using + // our certificate. This server will now pretend being the target. + tlsConfig := &tls.Config{ + PreferServerCipherSuites: true, + CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, + MinVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{*tlsCert}, + } + + tlsConn := tls.Server(clientConn, tlsConfig) + defer tlsConn.Close() + + // Create a buffered reader for the client connection; this is required to + // use http package functions with this connection. + connReader := bufio.NewReader(tlsConn) + + // Run the proxy in a loop until the client closes the connection. + for { + // Read an HTTP request from the client; the request is sent over TLS that + // connReader is configured to serve. The read will run a TLS handshake in + // the first invocation (we could also call tlsConn.Handshake explicitly + // before the loop, but this isn't necessary). + // Note that while the client believes it's talking across an encrypted + // channel with the target, the proxy gets these requests in "plain text" + // because of the MITM setup. + r, err := http.ReadRequest(connReader) + if err == io.EOF { + break + } else if err != nil { + log.WithError(err).Error("error reading request from client") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // We can dump the request; log it, modify it... + // if b, err := httputil.DumpRequest(r, false); err == nil { + // log.Printf("incoming request:\n%s\n", string(b)) + // } + + // Take the original request and changes its destination to be forwarded + // to the target server. + err = changeRequestToTarget(r, proxyReq.Host) + if err != nil { + log.WithError(err).WithField("targetHost", proxyReq.Host).Error("error parsing target URL") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // Inject authentication before sending it to the target + r = p.modifier(r) + // Also, use our special, retrying Docker client to inject authentication + client := proxy.CreateAuthenticatingDockerClient() + targetRequest := &retryablehttp.Request{Request: r} + + // Send the request to the target server and log the response. + resp, err := client.Do(targetRequest) + if err != nil { + log.WithError(err).Error("error sending request to target") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + // if b, err := httputil.DumpResponse(resp, false); err == nil { + // log.Printf("target response:\n%s\n", string(b)) + // } + defer resp.Body.Close() + + // Send the target server's response back to the client. + if err := resp.Write(tlsConn); err != nil { + log.WithError(err).Error("error writing response back") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } +} + +func (p *MitmProxy) getCertOrCreate(host string) (*tls.Certificate, error) { + p.certCacheMu.Lock() + defer p.certCacheMu.Unlock() + + if cert, ok := p.certCache[host]; ok { + // Before Go 1.23, cert.Leaf might be nil + if cert.Leaf == nil { + var err error + if cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil { + return nil, err + } + } + + if cert.Leaf.NotAfter.After(time.Now()) { + return cert, nil + } else { + delete(p.certCache, host) + } + } + + // Create a fake TLS certificate for the target host, signed by our CA. The + // certificate will be valid for 10 days - this number can be changed. + pemCert, pemKey, err := createCert([]string{host}, p.caCert, p.caKey, 240) + if err != nil { + return nil, err + } + tlsCert, err := tls.X509KeyPair(pemCert, pemKey) + if err != nil { + return nil, err + } + p.certCache[host] = &tlsCert + + return &tlsCert, nil +} + +// changeRequestToTarget modifies req to be re-routed to the given target; +// the target should be taken from the Host of the original tunnel (CONNECT) +// request. +func changeRequestToTarget(req *http.Request, targetHost string) error { + // TODO(gpl): It feels like we should allow using HTTP as well. But I'm not sure how to establish which protocol + // the client intends to use, because in the HTTPS requests nothing is explicitly stating HTTPS. Only the 443 + // ports indicates HTTPS - but that signal is not enough to only use HTTPS for that port. + // For the time being we assume anybody who uses CONNECT wants to use HTTPS. + if !strings.HasPrefix(targetHost, "https") { + targetHost = "https://" + targetHost + } + targetUrl, err := url.Parse(targetHost) + if err != nil { + return err + } + + targetUrl.Path = req.URL.Path + targetUrl.RawQuery = req.URL.RawQuery + req.URL = targetUrl + // Make sure this is unset for sending the request through a client + req.RequestURI = "" + return nil +} + +// createCert creates a new certificate/private key pair for the given domains, +// signed by the parent/parentKey certificate. hoursValid is the duration of +// the new certificate's validity. +func createCert(dnsNames []string, parent *x509.Certificate, parentKey crypto.PrivateKey, hoursValid int) (cert []byte, priv []byte, err error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("Failed to generate private key: %v", err) + } + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, fmt.Errorf("Failed to generate serial number: %v", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Sample MITM proxy"}, + }, + DNSNames: dnsNames, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Duration(hoursValid) * time.Hour), + + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, &privateKey.PublicKey, parentKey) + if err != nil { + return nil, nil, fmt.Errorf("Failed to create certificate: %v", err) + } + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if pemCert == nil { + log.Fatal("failed to encode certificate to PEM") + } + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, fmt.Errorf("Unable to marshal private key: %v", err) + } + pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + if pemCert == nil { + return nil, nil, fmt.Errorf("failed to encode key to PEM") + } + + return pemCert, pemKey, nil +} + +// loadX509KeyPair loads a certificate/key pair from files, and unmarshals them +// into data structures from the x509 package. Note that private key types in Go +// don't have a shared named interface and use `any` (for backwards +// compatibility reasons). +func loadX509KeyPair(certFile, keyFile string) (cert *x509.Certificate, key any, err error) { + cf, err := ioutil.ReadFile(certFile) + if err != nil { + return nil, nil, err + } + + kf, err := ioutil.ReadFile(keyFile) + if err != nil { + return nil, nil, err + } + certBlock, _ := pem.Decode(cf) + cert, err = x509.ParseCertificate(certBlock.Bytes) + if err != nil { + return nil, nil, err + } + + keyBlock, _ := pem.Decode(kf) + key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, nil, err + } + + return cert, key, nil +} diff --git a/components/supervisor/pkg/supervisor/config.go b/components/supervisor/pkg/supervisor/config.go index 4e41a2fba1f859..f4a7574280dcaf 100644 --- a/components/supervisor/pkg/supervisor/config.go +++ b/components/supervisor/pkg/supervisor/config.go @@ -350,6 +350,9 @@ type WorkspaceConfig struct { ConfigcatEnabled bool `env:"GITPOD_CONFIGCAT_ENABLED"` SSHGatewayCAPublicKey string `env:"GITPOD_SSH_CA_PUBLIC_KEY"` + + // DockerdProxyEnabled controls whether the dockerd proxy is enabled + DockerdProxyEnabled bool `env:"GITPOD_DOCKERD_PROXY_ENABLED"` } // WorkspaceGitpodToken is a list of tokens that should be added to supervisor's token service. diff --git a/components/supervisor/pkg/supervisor/docker.go b/components/supervisor/pkg/supervisor/docker.go index 4b25873ca66a8f..e405701cb3cce7 100644 --- a/components/supervisor/pkg/supervisor/docker.go +++ b/components/supervisor/pkg/supervisor/docker.go @@ -191,7 +191,10 @@ func listenToDockerSocket(parentCtx context.Context, term *terminal.Mux, cfg *Co } cmd := exec.CommandContext(ctx, "/usr/bin/docker-up") - cmd.Env = append(os.Environ(), "LISTEN_FDS=1") + env := os.Environ() + env = append(env, "LISTEN_FDS=1") + env = append(env, "DOCKERD_ARGS={ \"proxies\":{ \"http-proxy\": \"http://localhost:38080\", \"https-proxy\": \"https://localhost:38081\" } }") + cmd.Env = env cmd.ExtraFiles = []*os.File{socketFD} cmd.Stdout = stdout cmd.Stderr = stderr From 25244425e75277a880f6cdfb4ae752b2e90a24e6 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 30 Jan 2025 19:48:55 +0000 Subject: [PATCH 4/5] [ws-manager] Introduce GITPOD_DOCKERD_PROXY_ENABLED and if set, run docker-proxy in enclave and have supervisor configure dockerd for it Tool: gitpod/catfood.gitpod.cloud --- .../ws-manager-mk2/controllers/create.go | 18 ++++++++++++++++++ components/ws-manager-mk2/service/manager.go | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/components/ws-manager-mk2/controllers/create.go b/components/ws-manager-mk2/controllers/create.go index 28ae672c2bae62..ace101230124fa 100644 --- a/components/ws-manager-mk2/controllers/create.go +++ b/components/ws-manager-mk2/controllers/create.go @@ -25,6 +25,7 @@ import ( wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes" "github.com/gitpod-io/gitpod/common-go/tracing" + "github.com/gitpod-io/gitpod/common-go/util" csapi "github.com/gitpod-io/gitpod/content-service/api" regapi "github.com/gitpod-io/gitpod/registry-facade/api" "github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/constants" @@ -574,6 +575,23 @@ func createWorkspaceEnvironment(sctx *startWorkspaceContext) ([]corev1.EnvVar, e result = append(result, corev1.EnvVar{Name: "GIT_SSL_CAINFO", Value: customCAMountPath}) } + if sctx.Workspace.Annotations[wsk8s.WorkspaceDockerdProxyAnnotation] == util.BooleanTrueString { + var imageAuth string + for _, ev := range sctx.Workspace.Spec.UserEnvVars { + if ev.Name == "GITPOD_IMAGE_AUTH" { + imageAuth = ev.Value + break + } + } + if imageAuth != "" { + // Start the dockerd-proxy which injects all HTTP(S) requests with the credentials we got in GITPOD_IMAGE_AUTH + result = append(result, corev1.EnvVar{Name: "WORKSPACEKIT_RING2_ENCLAVE", Value: "/.supervisor/supervisor dockerd-proxy"}) + result = append(result, corev1.EnvVar{Name: "WORKSPACEKIT_GITPOD_IMAGE_AUTH", Value: string(imageAuth)}) + // Trigger supervisor to configure dockerd to use this proxy + result = append(result, corev1.EnvVar{Name: "GITPOD_DOCKERD_PROXY_ENABLED", Value: "true"}) + } + } + // System level env vars for _, e := range sctx.Workspace.Spec.SysEnvVars { env := corev1.EnvVar{ diff --git a/components/ws-manager-mk2/service/manager.go b/components/ws-manager-mk2/service/manager.go index ab5fe7113617c5..b901097b180208 100644 --- a/components/ws-manager-mk2/service/manager.go +++ b/components/ws-manager-mk2/service/manager.go @@ -225,6 +225,13 @@ func (wsm *WorkspaceManagerServer) StartWorkspace(ctx context.Context, req *wsma } } + for _, ev := range req.Spec.Envvars { + if ev.Name == "GITPOD_DOCKERD_PROXY_ENABLED" { + annotations[wsk8s.WorkspaceDockerdProxyAnnotation] = util.BooleanTrueString + break + } + } + envSecretName := fmt.Sprintf("%s-%s", req.Id, "env") userEnvVars, envData := extractWorkspaceUserEnv(envSecretName, req.Spec.Envvars, req.Spec.SysEnvvars) sysEnvVars := extractWorkspaceSysEnv(req.Spec.SysEnvvars) From 95778208714287825dceac90ebc34942bfe06774 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 30 Jan 2025 20:27:14 +0000 Subject: [PATCH 5/5] [server] Add DOCKERD_PROXY_ENABLED if feature flag "dockerd_proxy_enabled" is true Tool: gitpod/catfood.gitpod.cloud --- .../server/src/workspace/workspace-starter.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index ba58f253f6ebfd..ecdaa9252de663 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -632,7 +632,7 @@ export class WorkspaceStarter { } // build workspace image - const additionalAuth = await this.getAdditionalImageAuth(envVars); + const additionalAuth = this.getAdditionalImageAuth(envVars); instance = await this.buildWorkspaceImage( { span }, user, @@ -839,7 +839,7 @@ export class WorkspaceStarter { return undefined; } - private async getAdditionalImageAuth(envVars: ResolvedEnvVars): Promise> { + private getAdditionalImageAuth(envVars: ResolvedEnvVars): Map { const res = new Map(); const imageAuth = envVars.workspace.find((e) => e.name === EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME); if (!imageAuth) { @@ -1493,6 +1493,18 @@ export class WorkspaceStarter { envvars.push(ev); })(); + const isDockerdProxyEnabled = await getExperimentsClientForBackend().getValueAsync( + "dockerd_proxy_enabled", + false, + { user, projectId: workspace.projectId, gitpodHost: this.config.hostUrl.url.toString() }, + ); + if (isDockerdProxyEnabled) { + const ev = new EnvironmentVariable(); + ev.setName("DOCKERD_PROXY_ENABLED"); + ev.setValue("true"); + envvars.push(ev); + } + const portIndex = new Set(); const ports = (workspace.config.ports || []) .map((p) => {