Merge pull request #218 from thockin/e2e_ssh

Support ssh as arbitrary users, add e2e for SSH
This commit is contained in:
Kubernetes Prow Robot 2020-01-07 08:30:19 -08:00 committed by GitHub
commit 705be50aa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 2 deletions

View File

@ -22,7 +22,11 @@ RUN apt-get update \
openssh-client \ openssh-client \
&& rm -rf /var/lib/apt/lists/* && 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 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} ADD bin/{ARG_OS}_{ARG_ARCH}/{ARG_BIN} /{ARG_BIN}

View File

@ -177,6 +177,9 @@ test: $(BUILD_DIRS)
" "
@./test_e2e.sh @./test_e2e.sh
test-tools:
@docker build -t $(REGISTRY)/test/test-sshd _test_tools/sshd
$(BUILD_DIRS): $(BUILD_DIRS):
@mkdir -p $@ @mkdir -p $@

View File

@ -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"]

View File

@ -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.
```

14
_test_tools/sshd/sshd.sh Executable file
View File

@ -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

View File

@ -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

View File

@ -94,6 +94,8 @@ var flSSHKnownHosts = flag.Bool("ssh-known-hosts", envBool("GIT_KNOWN_HOSTS", tr
"enable SSH known_hosts verification") "enable SSH known_hosts verification")
var flSSHKnownHostsFile = flag.String("ssh-known-hosts-file", envString("GIT_SSH_KNOWN_HOSTS_FILE", "/etc/git-secret/known_hosts"), 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") "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), var flCookieFile = flag.Bool("cookie-file", envBool("GIT_COOKIE_FILE", false),
"use git cookiefile") "use git cookiefile")
@ -241,6 +243,13 @@ func main() {
os.Exit(1) 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 // 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. // `git clone`, so initTimeout set to 30 seconds should be enough.
ctx, cancel := context.WithTimeout(context.Background(), initTimeout) ctx, cancel := context.WithTimeout(context.Background(), initTimeout)
@ -390,6 +399,29 @@ func sleepForever() {
os.Exit(0) 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 // updateSymlink atomically swaps the symlink to point at the specified
// directory and cleans up the previous worktree. If there was a previous // directory and cleans up the previous worktree. If there was a previous
// worktree, this returns the path to it. // worktree, this returns the path to it.

View File

@ -62,7 +62,7 @@ Secret (e.g. "git-creds" used in both above examples).
- name: git-secret - name: git-secret
secret: secret:
secretName: git-creds 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 **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 restrictive enough to be used as an SSH key), so make sure you set the
`defaultMode`. `defaultMode`.
@ -130,7 +135,7 @@ spec:
- name: git-secret - name: git-secret
secret: secret:
secretName: git-creds secretName: git-creds
defaultMode: 0440 defaultMode: 0400
containers: containers:
- name: git-sync - name: git-sync
image: k8s.gcr.io/git-sync:v3.1.1 image: k8s.gcr.io/git-sync:v3.1.1

View File

@ -66,6 +66,7 @@ function freencport() {
# Build it # Build it
make container REGISTRY=e2e VERSION=$(make -s version) make container REGISTRY=e2e VERSION=$(make -s version)
make test-tools REGISTRY=e2e
RUNID="${RANDOM}${RANDOM}" RUNID="${RANDOM}${RANDOM}"
DIR="" DIR=""
@ -95,6 +96,12 @@ function clean_root() {
mkdir -p "$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() { function finish() {
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "The directory $DIR was not removed as it contains"\ echo "The directory $DIR was not removed as it contains"\
@ -118,8 +125,10 @@ function GIT_SYNC() {
-v "$DIR":"$DIR":rw \ -v "$DIR":"$DIR":rw \
-v "$(pwd)/slow_git.sh":"$SLOW_GIT":ro \ -v "$(pwd)/slow_git.sh":"$SLOW_GIT":ro \
-v "$(pwd)/askpass_git.sh":"$ASKPASS_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 \ --env XDG_CONFIG_HOME=$DIR \
e2e/git-sync:$(make -s version)__$(go env GOOS)_$(go env GOARCH) \ e2e/git-sync:$(make -s version)__$(go env GOOS)_$(go env GOARCH) \
--add-user \
"$@" "$@"
} }
@ -963,5 +972,38 @@ fi
rm -rf $SUBMODULE rm -rf $SUBMODULE
pass 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" echo "cleaning up $DIR"
rm -rf "$DIR" rm -rf "$DIR"