diff --git a/Dockerfile.in b/Dockerfile.in index 7ebdcf5..59c2061 100644 --- a/Dockerfile.in +++ b/Dockerfile.in @@ -22,7 +22,11 @@ RUN apt-get update \ openssh-client \ && rm -rf /var/lib/apt/lists/* +# By default we will run as this user... RUN echo "git-sync:x:65533:65533::/tmp:/sbin/nologin" >> /etc/passwd +# ...but the user might choose a different UID and pass --add-user +# which needs to be able to write to /etc/passwd. +RUN chmod 0666 /etc/passwd ADD bin/{ARG_OS}_{ARG_ARCH}/{ARG_BIN} /{ARG_BIN} diff --git a/Makefile b/Makefile index b4dfb4a..6602863 100644 --- a/Makefile +++ b/Makefile @@ -177,6 +177,9 @@ test: $(BUILD_DIRS) " @./test_e2e.sh +test-tools: + @docker build -t $(REGISTRY)/test/test-sshd _test_tools/sshd + $(BUILD_DIRS): @mkdir -p $@ diff --git a/_test_tools/sshd/Dockerfile b/_test_tools/sshd/Dockerfile new file mode 100644 index 0000000..31efc1a --- /dev/null +++ b/_test_tools/sshd/Dockerfile @@ -0,0 +1,39 @@ +# Stolen from https://github.com/linuxkit/linuxkit/tree/master/pkg/sshd/ + +FROM alpine AS base + +RUN mkdir -p /out/etc/apk && cp -r /etc/apk/* /out/etc/apk/ +RUN apk add --no-cache --initdb -p /out \ + alpine-baselayout \ + apk-tools \ + busybox \ + ca-certificates \ + git \ + musl \ + openssh-server \ + tini \ + util-linux \ + wireguard-tools \ + && true + +############### + +FROM scratch + +ENTRYPOINT [] +WORKDIR / + +COPY --from=base /out/ / + +RUN mkdir -p /etc/ssh && rm /etc/motd +COPY sshd_config /etc/ssh/ +COPY sshd.sh / + +# Callers should mount a .ssh directory here. Our sshd.sh will copy it and +# manage permissions. +VOLUME /dot_ssh + +# Callers can SSH as user "test" +RUN echo "test:x:65533:65533::/home/test:/usr/bin/git-shell" >> /etc/passwd + +CMD ["/sbin/tini", "/sshd.sh"] diff --git a/_test_tools/sshd/README.md b/_test_tools/sshd/README.md new file mode 100644 index 0000000..3e0e316 --- /dev/null +++ b/_test_tools/sshd/README.md @@ -0,0 +1,52 @@ +# An SSHD for tests git-over-ssh + +DO NOT USE THIS FOR ANYTHING BUT TESTING GIT OVER SSH!!! + +## How to use it + +Build yourself a test image. We use example.com so you can't accidentally push +it. + +``` +$ docker build -t example.com/test/test-sshd . +...lots of output... +Successfully tagged example.com/test/test-sshd:latest +``` + +Generate keys for a fake user named "test". + +``` +$ mkdir -p dot_ssh + +$ ssh-keygen -f dot_ssh/id_test -P "" +Generating public/private rsa key pair. +Your identification has been saved in dot_ssh/id_test. +Your public key has been saved in dot_ssh/id_test.pub. +...lots of output... + +$ cat dot_ssh/id_test.pub > dot_ssh/authorized_keys +``` + +Run it. + +``` +$ docker run -d -v $(pwd)/dot_ssh:/dot_ssh:ro example.com/test/test-sshd +6d05b4111b03c66907031e3cd7587763f0e4fab6c50fac33c4a8284732b448ae +``` + +Find your IP. + +``` +$ docker inspect 6d05b4111b03c66907031e3cd7587763f0e4fab6c50fac33c4a8284732b448ae | jq -r .[0].NetworkSettings.IPAddress +192.168.1.2 +``` + +SSH to it. + +``` +$ ssh -i dot_ssh/id_test -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null test@192.168.9.2 +Warning: Permanently added '192.168.9.2' (ECDSA) to the list of known hosts. +fatal: Interactive git shell is not enabled. +hint: ~/git-shell-commands should exist and have read and execute access. +Connection to 192.168.9.2 closed. +``` diff --git a/_test_tools/sshd/sshd.sh b/_test_tools/sshd/sshd.sh new file mode 100755 index 0000000..757f5fe --- /dev/null +++ b/_test_tools/sshd/sshd.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +KEYS=$(find /etc/ssh -name 'ssh_host_*_key') +[ -z "$KEYS" ] && ssh-keygen -A >/dev/null 2>/dev/null + +# Copy creds for the test user, so we don't have to bake them into the image +# and so users don't have to manage permissions. +mkdir -p /home/test/.ssh +cp -a /dot_ssh/* /home/test/.ssh +chown -R test /home/test/.ssh +chmod 0700 /home/test/.ssh +chmod 0600 /home/test/.ssh/* + +exec /usr/sbin/sshd -D -e diff --git a/_test_tools/sshd/sshd_config b/_test_tools/sshd/sshd_config new file mode 100644 index 0000000..65a0a2a --- /dev/null +++ b/_test_tools/sshd/sshd_config @@ -0,0 +1,12 @@ +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys + +# To disable tunneled clear text passwords, change to no here! +PasswordAuthentication no + +# Change to no to disable s/key passwords +ChallengeResponseAuthentication no diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index e8ec321..60c9b70 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -94,6 +94,8 @@ var flSSHKnownHosts = flag.Bool("ssh-known-hosts", envBool("GIT_KNOWN_HOSTS", tr "enable SSH known_hosts verification") var flSSHKnownHostsFile = flag.String("ssh-known-hosts-file", envString("GIT_SSH_KNOWN_HOSTS_FILE", "/etc/git-secret/known_hosts"), "the known_hosts file to use") +var flAddUser = flag.Bool("add-user", envBool("GIT_SYNC_ADD_USER", false), + "add a record to /etc/passwd for the current UID/GID (needed to use SSH with a different UID)") var flCookieFile = flag.Bool("cookie-file", envBool("GIT_COOKIE_FILE", false), "use git cookiefile") @@ -241,6 +243,13 @@ func main() { os.Exit(1) } + if *flAddUser { + if err := addUser(); err != nil { + fmt.Fprintf(os.Stderr, "ERROR: can't write to /etc/passwd: %v\n", err) + os.Exit(1) + } + } + // This context is used only for git credentials initialization. There are no long-running operations like // `git clone`, so initTimeout set to 30 seconds should be enough. ctx, cancel := context.WithTimeout(context.Background(), initTimeout) @@ -390,6 +399,29 @@ func sleepForever() { os.Exit(0) } +// Put the current UID/GID into /etc/passwd so SSH can look it up. This +// assumes that we have the permissions to write to it. +func addUser() error { + home := os.Getenv("HOME") + if home == "" { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("can't get current working directory: %v", err) + } + home = cwd + } + + f, err := os.OpenFile("/etc/passwd", os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + str := fmt.Sprintf("git-sync:x:%d:%d::%s:/sbin/nologin\n", os.Getuid(), os.Getgid(), home) + _, err = f.WriteString(str) + return err +} + // updateSymlink atomically swaps the symlink to point at the specified // directory and cleans up the previous worktree. If there was a previous // worktree, this returns the path to it. diff --git a/docs/ssh.md b/docs/ssh.md index 8f7519a..20fa6f9 100644 --- a/docs/ssh.md +++ b/docs/ssh.md @@ -62,7 +62,7 @@ Secret (e.g. "git-creds" used in both above examples). - name: git-secret secret: secretName: git-creds - defaultMode: 288 # 0440 + defaultMode: 0400 # ... ``` @@ -103,6 +103,11 @@ that this is a Pod-wide setting, unlike the container `securityContext` above. # ... ``` +If you want git-sync to run as a different (non-root) UID and GID, you can +change these last blocks to any UID/GID you like. SSH demands that the current +UID be present in /etc/passwd, so in this case you will need to add the +`--add-user` flag to git-sync's args array. + **Note:** Kubernetes mounts the Secret with permissions 0444 by default (not restrictive enough to be used as an SSH key), so make sure you set the `defaultMode`. @@ -130,7 +135,7 @@ spec: - name: git-secret secret: secretName: git-creds - defaultMode: 0440 + defaultMode: 0400 containers: - name: git-sync image: k8s.gcr.io/git-sync:v3.1.1 diff --git a/test_e2e.sh b/test_e2e.sh index a7118e1..8e3ba81 100755 --- a/test_e2e.sh +++ b/test_e2e.sh @@ -66,6 +66,7 @@ function freencport() { # Build it make container REGISTRY=e2e VERSION=$(make -s version) +make test-tools REGISTRY=e2e RUNID="${RANDOM}${RANDOM}" DIR="" @@ -95,6 +96,12 @@ function clean_root() { mkdir -p "$ROOT" } +# Init SSH for test cases. +DOT_SSH="$DIR/dot_ssh" +mkdir -p "$DOT_SSH" +ssh-keygen -f "$DOT_SSH/id_test" -P "" >/dev/null +cat "$DOT_SSH/id_test.pub" > "$DOT_SSH/authorized_keys" + function finish() { if [ $? -ne 0 ]; then echo "The directory $DIR was not removed as it contains"\ @@ -118,8 +125,10 @@ function GIT_SYNC() { -v "$DIR":"$DIR":rw \ -v "$(pwd)/slow_git.sh":"$SLOW_GIT":ro \ -v "$(pwd)/askpass_git.sh":"$ASKPASS_GIT":ro \ + -v "$DOT_SSH/id_test":"/etc/git-secret/ssh":ro \ --env XDG_CONFIG_HOME=$DIR \ e2e/git-sync:$(make -s version)__$(go env GOOS)_$(go env GOARCH) \ + --add-user \ "$@" } @@ -963,5 +972,38 @@ fi rm -rf $SUBMODULE pass +############################################## +# Test SSH +############################################## +testcase "ssh" +echo "$TESTCASE" > "$REPO"/file +# Run a git-over-SSH server +CTR=$(docker run \ + -d \ + --rm \ + --label git-sync-e2e="$RUNID" \ + -v "$DOT_SSH":/dot_ssh:ro \ + -v "$REPO":/src:ro \ + e2e/test/test-sshd) +IP=$(docker inspect "$CTR" | jq -r .[0].NetworkSettings.IPAddress) +git -C "$REPO" commit -qam "$TESTCASE" +GIT_SYNC \ + --logtostderr \ + --v=5 \ + --one-time \ + --ssh \ + --ssh-known-hosts=false \ + --repo="test@$IP:/src" \ + --branch=master \ + --rev=HEAD \ + --root="$ROOT" \ + --dest="link" \ + > "$DIR"/log."$TESTCASE" 2>&1 +assert_link_exists "$ROOT"/link +assert_file_exists "$ROOT"/link/file +assert_file_eq "$ROOT"/link/file "$TESTCASE" +# Wrap up +pass + echo "cleaning up $DIR" rm -rf "$DIR"