diff --git a/.werft/build.yaml b/.werft/build.yaml index 49c58b9468ea2e..ed08859156a672 100644 --- a/.werft/build.yaml +++ b/.werft/build.yaml @@ -148,7 +148,7 @@ pod: sudo chown gitpod:gitpod $GOCACHE export GITHUB_TOKEN=$(echo $GITHUB_TOKEN | xargs) - export DOCKER_HOST=tcp://$NODENAME:2375 + export DOCKER_HOST=tcp://$NODENAME:2475 sudo chown -R gitpod:gitpod /workspace (cd .werft && yarn install && mv node_modules ..) | werft log slice prep diff --git a/.werft/debug.yaml b/.werft/debug.yaml new file mode 100644 index 00000000000000..b281a3ff320b14 --- /dev/null +++ b/.werft/debug.yaml @@ -0,0 +1,160 @@ +# debug using `werft run github -f -s .werft/build.js -j .werft/build.yaml -a debug=true` +pod: + serviceAccount: werft + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: dev/workload + operator: In + values: + - "builds" + volumes: + - name: monitoring-satellite-preview-token + secret: + secretName: monitoring-satellite-preview-token + - name: gcp-sa + secret: + secretName: gcp-sa-gitpod-dev-deployer + - name: gcp-sa-release + secret: + secretName: gcp-sa-gitpod-release-deployer + - name: gpsh-coredev-license + secret: + secretName: gpsh-coredev-license + - name: payment-provider-secret + secret: + secretName: payment-provider-secret + - name: payment-webhook-secret + secret: + secretName: payment-webhook-secret + - name: go-build-cache + hostPath: + path: /mnt/disks/ssd0/go-build-cache + type: DirectoryOrCreate + # - name: deploy-key + # secret: + # secretName: deploy-key + # - name: github-ssh-key + # secret: + # secretName: github-ssh-key + # defaultMode: 0600 + # - name: gitpod-test-tokens + # secret: + # secretName: gitpod-test-tokens + containers: + - name: testdb + image: mysql:5.7 + env: + - name: MYSQL_ROOT_PASSWORD + value: test + # Using the same port as in our Gitpod workspaces here + - name: MYSQL_TCP_PORT + value: 23306 + - name: build + image: eu.gcr.io/gitpod-core-dev/dev/dev-environment:as-migrate-mixins.73 + workingDir: /workspace + imagePullPolicy: Always + volumeMounts: + - name: monitoring-satellite-preview-token + mountPath: /mnt/secrets/monitoring-satellite-preview-token + - name: gcp-sa + mountPath: /mnt/secrets/gcp-sa + readOnly: true + - name: gcp-sa-release + mountPath: /mnt/secrets/gcp-sa-release + readOnly: true + - name: gpsh-coredev-license + mountPath: /mnt/secrets/gpsh-coredev + readOnly: true + - name: payment-webhook-secret + mountPath: /mnt/secrets/payment-webhook-config + readOnly: true + - name: payment-provider-secret + mountPath: /mnt/secrets/payment-provider-config + readOnly: true + - name: go-build-cache + mountPath: /go-build-cache + readOnly: false + # - name: deploy-key + # mountPath: /mnt/secrets/deploy-key + # readOnly: true + # - name: github-ssh-key + # mountPath: /mnt/secrets/github-ssh-key + # readOnly: true + env: + - name: LEEWAY_WORKSPACE_ROOT + value: /workspace + - name: LEEWAY_REMOTE_CACHE_BUCKET + {{- if eq .Repository.Ref "refs/heads/master" }} + value: gitpod-core-leeway-cache-master + {{- else }} + value: gitpod-core-leeway-cache-branch + {{- end }} + - name: GOPROXY + value: http://athens-athens-proxy.athens.svc.cluster.local:9999 + - name: GOCACHE + value: /go-build-cache + - name: WERFT_HOST + value: "werft.werft.svc.cluster.local:7777" + - name: NODENAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: NPM_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: npm-auth-token + key: npm-auth-token.json + - name: SLACK_NOTIFICATION_PATH + valueFrom: + secretKeyRef: + name: slack-path + key: slackPath + # used for GitHub releases (NOTE: for some reasons the token contains a trailing \n, is trimmed below) + - name: GITHUB_TOKEN + valueFrom: + secretKeyRef: + name: github-sh-release-token + key: token + # - name: GITPOD_TEST_TOKEN_GITHUB + # valueFrom: + # secretKeyRef: + # name: gitpod-test-tokens + # key: github-test-token.json + # - name: GITPOD_TEST_TOKEN_GITLAB + # valueFrom: + # secretKeyRef: + # name: gitpod-test-tokens + # key: gitlab-test-token.json + # - name: GITPOD_TEST_TOKEN_BITBUCKET + # valueFrom: + # secretKeyRef: + # name: gitpod-test-tokens + # key: bitbucket-test-token.json + - name: CODECOV_TOKEN + valueFrom: + secretKeyRef: + name: codecov + key: token + command: + - bash + - -c + - | + sleep 1 + set -Eeuo pipefail + + sudo chown gitpod:gitpod $GOCACHE + export GITHUB_TOKEN=$(echo $GITHUB_TOKEN | xargs) + + export DOCKER_HOST=tcp://$NODENAME:2475 + sudo chown -R gitpod:gitpod /workspace + + (cd .werft && yarn install && mv node_modules ..) | werft log slice prep + printf '{{ toJson . }}' > context.json + + leeway build components/supervisor/openssh:app + # npx ts-node .werft/build.ts +sidecars: +- testdb diff --git a/components/supervisor/BUILD.yaml b/components/supervisor/BUILD.yaml index 2bd25c2fbb6bde..5a957fd052257c 100644 --- a/components/supervisor/BUILD.yaml +++ b/components/supervisor/BUILD.yaml @@ -21,8 +21,8 @@ packages: - "supervisor-config.json" deps: - :app - - :dropbear - components/supervisor/frontend:app + - components/supervisor/openssh:app - components/workspacekit:app - components/workspacekit:fuse-overlayfs - components/gitpod-cli:app @@ -35,13 +35,3 @@ packages: image: - ${imageRepoBase}/supervisor:${version} - ${imageRepoBase}/supervisor:commit-${__git_commit} - - name: dropbear - type: generic - config: - commands: - - ["curl", "-OL", "https://matt.ucc.asn.au/dropbear/dropbear-2020.81.tar.bz2"] - - ["tar", "xjf", "dropbear-2020.81.tar.bz2"] - - ["sh", "-c", "cd dropbear-2020.81; ./configure --enable-static && sed -i '/clearenv();/d' svr-chansession.c && sed -i '/addnewvar(\"PATH\", DEFAULT_PATH);/d' svr-chansession.c && sed -i 's/filestat.st_mode & (S_IWGRP | S_IWOTH)/0/g' svr-authpubkey.c && make"] - - ["mv", "dropbear-2020.81/dropbear", "dropbear"] - - ["mv", "dropbear-2020.81/dropbearkey", "dropbearkey"] - - ["rm", "-rf", "dropbear-2020.81*"] diff --git a/components/supervisor/leeway.Dockerfile b/components/supervisor/leeway.Dockerfile index 5aa55a1f2dc691..5de4c78c36c823 100644 --- a/components/supervisor/leeway.Dockerfile +++ b/components/supervisor/leeway.Dockerfile @@ -16,9 +16,9 @@ COPY components-supervisor--app/supervisor \ components-workspacekit--fuse-overlayfs/fuse-overlayfs \ components-gitpod-cli--app/gitpod-cli \ ./ -WORKDIR "/.supervisor/dropbear" -COPY components-supervisor--dropbear/dropbear \ - components-supervisor--dropbear/dropbearkey \ - ./ -ENTRYPOINT ["/.supervisor/supervisor"] \ No newline at end of file +WORKDIR "/.supervisor/ssh" +COPY components-supervisor-openssh--app/usr/sbin/sshd . +COPY components-supervisor-openssh--app/usr/bin/ssh-keygen . + +ENTRYPOINT ["/.supervisor/supervisor"] diff --git a/components/supervisor/openssh/BUILD.yaml b/components/supervisor/openssh/BUILD.yaml new file mode 100644 index 00000000000000..013fba3cb562a0 --- /dev/null +++ b/components/supervisor/openssh/BUILD.yaml @@ -0,0 +1,15 @@ +packages: + - name: app + type: generic + deps: + - :docker-build + config: + commands: + - ["sh", "-c", "find . | grep layer.tar | xargs tar xfv"] + - ["rm", "-rf", "components-supervisor-openssh--docker-build"] + - name: docker-build + type: docker + srcs: + - "*.patch" + config: + dockerfile: leeway.Dockerfile diff --git a/components/supervisor/openssh/leeway.Dockerfile b/components/supervisor/openssh/leeway.Dockerfile new file mode 100644 index 00000000000000..53ca37a114fa51 --- /dev/null +++ b/components/supervisor/openssh/leeway.Dockerfile @@ -0,0 +1,71 @@ +# Copyright (c) 2021 ep76 +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This Dockerfile was taken from https://github.com/ep76/docker-openssh-static and adapted. +FROM alpine:3.14.2 AS builder + +ARG openssh_url=https://github.com/openssh/openssh-portable/archive/refs/tags/V_8_8_P1.tar.gz + +WORKDIR /build + +RUN apk add --no-cache \ + bash \ + autoconf \ + automake \ + curl \ + gcc \ + make \ + musl-dev \ + linux-headers \ + openssl-dev \ + openssl-libs-static \ + patch \ + zlib-dev \ + sed \ + xauth \ + zlib-static + +RUN curl -fsSL "${openssh_url}" | tar xz --strip-components=1 + +RUN autoreconf + +RUN ./configure \ + --prefix=/usr \ + --sysconfdir=/etc/ssh \ + --with-ldflags=-static \ + --with-privsep-user=nobody \ + --with-ssl-engine + +COPY supervisorenv.patch . +ENV aports=https://raw.githubusercontent.com/alpinelinux/aports/master/main/openssh +RUN curl -fsSL \ + "${aports}/{fix-utmp,fix-verify-dns-segfault,sftp-interactive}.patch" \ + | patch -p1 +RUN cat supervisorenv.patch | patch -p1 +RUN make install-nosysconf exec_prefix=/openssh + +RUN TEST_SSH_UNSAFE_PERMISSIONS=1 \ + make -C /build file-tests interop-tests unit SK_DUMMY_LIBRARY='' + +FROM scratch AS openssh-static +COPY --from=builder /openssh /usr +ENTRYPOINT [ "/usr/sbin/sshd" ] +CMD [ "-D", "-e" ] diff --git a/components/supervisor/openssh/supervisorenv.patch b/components/supervisor/openssh/supervisorenv.patch new file mode 100644 index 00000000000000..dad1babcd94cca --- /dev/null +++ b/components/supervisor/openssh/supervisorenv.patch @@ -0,0 +1,14 @@ +--- a/session.c 2021-10-29 07:07:35.794323753 +0000 ++++ b/session.c 2021-10-29 07:23:07.420640891 +0000 +@@ -1126,6 +1126,11 @@ + options.permit_user_env_allowlist); + } + ++ snprintf(buf, sizeof buf, "%.200s/%s/supervisor_env", ++ pw->pw_dir, _PATH_SSH_USER_DIR); ++ read_environment_file(&env, &envsize, buf, ++ options.permit_user_env_allowlist); ++ + #ifdef USE_PAM + /* + * Pull in any environment variables that may have diff --git a/components/supervisor/pkg/supervisor/ssh.go b/components/supervisor/pkg/supervisor/ssh.go new file mode 100644 index 00000000000000..0040492d238309 --- /dev/null +++ b/components/supervisor/pkg/supervisor/ssh.go @@ -0,0 +1,214 @@ +// 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 supervisor + +import ( + "bufio" + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/gitpod-io/gitpod/common-go/log" +) + +func newSSHServer(ctx context.Context, cfg *Config) (*sshServer, error) { + bin, err := os.Executable() + if err != nil { + return nil, xerrors.Errorf("cannot find executable path: %w", err) + } + + sshkey := filepath.Join(filepath.Dir(bin), "ssh", "sshkey") + if _, err := os.Stat(sshkey); err != nil { + err := prepareSSHKey(ctx, sshkey) + if err != nil { + return nil, xerrors.Errorf("unexpected error creating SSH key: %w", err) + } + } + err = writeSSHEnv(cfg) + if err != nil { + return nil, xerrors.Errorf("unexpected error creating SSH env: %w", err) + } + + return &sshServer{ + ctx: ctx, + cfg: cfg, + sshkey: sshkey, + }, nil +} + +type sshServer struct { + ctx context.Context + cfg *Config + + sshkey string +} + +// ListenAndServe listens on the TCP network address laddr and then handle packets on incoming connections. +func (s *sshServer) listenAndServe() error { + listener, err := net.Listen("tcp", fmt.Sprintf(":%v", s.cfg.SSHPort)) + if err != nil { + return err + } + + for { + conn, err := listener.Accept() + if err != nil { + log.WithError(err).Error("listening for SSH connection") + continue + } + + go s.handleConn(s.ctx, conn) + } +} + +func (s *sshServer) handleConn(ctx context.Context, conn net.Conn) { + bin, err := os.Executable() + if err != nil { + return + } + + defer conn.Close() + + openssh := filepath.Join(filepath.Dir(bin), "ssh", "sshd") + if _, err := os.Stat(openssh); err != nil { + return + } + + args := []string{ + "-iedD", "-f/dev/null", + "-oProtocol 2", + "-oAllowUsers gitpod", + "-oPasswordAuthentication no", + "-oChallengeResponseAuthentication no", + "-oPermitRootLogin no", + "-oLoginGraceTime 20", + "-oPrintLastLog no", + "-oPermitUserEnvironment yes", + "-oHostKey " + s.sshkey, + "-oPidFile /dev/null", + "-oUseDNS no", // Disable DNS lookups. + "-oSubsystem sftp internal-sftp", + "-oStrictModes no", // don't care for home directory and file permissions + } + + if os.Getenv("SUPERVISOR_DEBUG_ENABLE") != "" { + args = append(args, "-oLogLevel DEBUG") + } + + socketFD, err := conn.(*net.TCPConn).File() + if err != nil { + log.WithError(err).Error("cannot start SSH server") + return + } + defer socketFD.Close() + + log.WithField("args", args).Debug("sshd flags") + cmd := exec.CommandContext(ctx, openssh, args...) + cmd = runAsGitpodUser(cmd) + cmd.Env = buildChildProcEnv(s.cfg, nil) + cmd.ExtraFiles = []*os.File{socketFD} + cmd.Stderr = os.Stderr + cmd.Stdin = bufio.NewReader(socketFD) + cmd.Stdout = bufio.NewWriter(socketFD) + + err = cmd.Start() + if err != nil { + log.WithError(err).Error("cannot start SSH server: %w", err) + return + } + + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + log.Debug("sshd started") + + select { + case <-ctx.Done(): + if cmd.Process != nil { + _ = cmd.Process.Kill() + } + return + case err = <-done: + if err != nil { + log.WithError(err).Error("SSH server stopped") + } + } +} + +func prepareSSHKey(ctx context.Context, sshkey string) error { + bin, err := os.Executable() + if err != nil { + return xerrors.Errorf("cannot find executable path: %w", err) + } + + openssh := filepath.Join(filepath.Dir(bin), "ssh", "sshd") + if _, err := os.Stat(openssh); err != nil { + return xerrors.Errorf("cannot locate sshd binary in path %v", openssh) + } + + sshkeygen := filepath.Join(filepath.Dir(bin), "ssh", "ssh-keygen") + if _, err := os.Stat(sshkeygen); err != nil { + return xerrors.Errorf("cannot locate ssh-keygen (path %v)", sshkeygen) + } + + keycmd := exec.Command(sshkeygen, "-t", "rsa", "-q", "-N", "", "-f", sshkey) + // We need to force HOME because the Gitpod user might not have existed at the start of the container + // which makes the container runtime set an invalid HOME value. + keycmd.Env = func() []string { + env := os.Environ() + res := make([]string, 0, len(env)) + for _, e := range env { + if strings.HasPrefix(e, "HOME=") { + e = "HOME=/root" + } + res = append(res, e) + } + return res + }() + + _, err = keycmd.CombinedOutput() + if err != nil && !(err.Error() == "wait: no child processes" || err.Error() == "waitid: no child processes") { + return xerrors.Errorf("cannot create SSH hostkey file: %w", err) + } + + err = os.Chown(sshkey, gitpodUID, gitpodGID) + if err != nil { + return xerrors.Errorf("cannot chown SSH hostkey file: %w", err) + } + + return nil +} + +func writeSSHEnv(cfg *Config) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + d := filepath.Join(home, ".ssh") + err = os.MkdirAll(d, 0755) + if err != nil { + return xerrors.Errorf("cannot create $HOME/.ssh: %w", err) + } + + fn := filepath.Join(d, "supervisor_env") + env := strings.Join(buildChildProcEnv(cfg, nil), "\n") + err = os.WriteFile(fn, []byte(env), 0644) + if err != nil { + return xerrors.Errorf("cannot write %s: %w", fn, err) + } + + _ = exec.Command("chown", "-R", fmt.Sprintf("%d:%d", gitpodUID, gitpodGID), d).Run() + + return nil +} diff --git a/components/supervisor/pkg/supervisor/supervisor.go b/components/supervisor/pkg/supervisor/supervisor.go index 5cd7832162dcf8..a0718e5177169b 100644 --- a/components/supervisor/pkg/supervisor/supervisor.go +++ b/components/supervisor/pkg/supervisor/supervisor.go @@ -904,77 +904,17 @@ func stopWhenTasksAreDone(ctx context.Context, wg *sync.WaitGroup, shutdown chan func startSSHServer(ctx context.Context, cfg *Config, wg *sync.WaitGroup) { defer wg.Done() - bin, err := os.Executable() - if err != nil { - log.WithError(err).Error("cannot find executable path") - return - } - dropbear := filepath.Join(filepath.Dir(bin), "dropbear", "dropbear") - if _, err := os.Stat(dropbear); err != nil { - log.WithError(err).WithField("path", dropbear).Error("cannot locate dropebar binary") - return - } - dropbearkey := filepath.Join(filepath.Dir(bin), "dropbear", "dropbearkey") - if _, err := os.Stat(dropbearkey); err != nil { - log.WithError(err).WithField("path", dropbearkey).Error("cannot locate dropebarkey") - return - } - - hostkeyFN, err := ioutil.TempFile("", "hostkey") - if err != nil { - log.WithError(err).Error("cannot create hostkey file") - return - } - hostkeyFN.Close() - os.Remove(hostkeyFN.Name()) - - keycmd := exec.Command(dropbearkey, "-t", "rsa", "-f", hostkeyFN.Name()) - // We need to force HOME because the Gitpod user might not have existed at the start of the container - // which makes the container runtime set an invalid HOME value. - keycmd.Env = func() []string { - env := os.Environ() - res := make([]string, 0, len(env)) - for _, e := range env { - if strings.HasPrefix(e, "HOME=") { - e = "HOME=/root" - } - res = append(res, e) - } - return res - }() - out, err := keycmd.CombinedOutput() - if err != nil { - log.WithError(err).WithField("out", string(out)).Error("cannot create hostkey file") - return - } - _ = os.Chown(hostkeyFN.Name(), gitpodUID, gitpodGID) - - cmd := exec.Command(dropbear, "-F", "-E", "-w", "-s", "-p", fmt.Sprintf(":%d", cfg.SSHPort), "-r", hostkeyFN.Name()) - cmd = runAsGitpodUser(cmd) - cmd.Env = buildChildProcEnv(cfg, nil) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Start() - if err != nil { - log.WithError(err).Error("cannot start SSH server") - return - } - - done := make(chan error, 1) go func() { - done <- cmd.Wait() - }() - select { - case <-ctx.Done(): - if cmd.Process != nil { - cmd.Process.Kill() + ssh, err := newSSHServer(ctx, cfg) + if err != nil { + log.WithError(err).Error("err starting SSH server") } - return - case err = <-done: + + err = ssh.listenAndServe() if err != nil { - log.WithError(err).Error("SSH server stopped") + log.WithError(err).Error("err starting SSH server") } - } + }() } func startContentInit(ctx context.Context, cfg *Config, wg *sync.WaitGroup, cst ContentState) {