diff --git a/README.md b/README.md index 5dbf5564a..43ac44098 100644 --- a/README.md +++ b/README.md @@ -317,8 +317,10 @@ OPTIONS Use SSH for git authentication and operations. --ssh-key-file , $GITSYNC_SSH_KEY_FILE - The SSH key to use when using --ssh. If not specified, this - defaults to "/etc/git-secret/ssh". + The SSH key(s) to use when using --ssh. This flag may be specified + more than once and the environment variable will be parsed like + PATH - using a colon (':') to separate elements. If not specified, + this defaults to "/etc/git-secret/ssh". --ssh-known-hosts, $GITSYNC_SSH_KNOWN_HOSTS Enable SSH known_hosts verification when using --ssh. If not diff --git a/_test_tools/sshd/Dockerfile b/_test_tools/sshd/Dockerfile index 9878794a2..9b05b1d0e 100644 --- a/_test_tools/sshd/Dockerfile +++ b/_test_tools/sshd/Dockerfile @@ -47,6 +47,9 @@ COPY sshd.sh / # manage permissions. VOLUME /dot_ssh +# Callers should mount a directory with git repos here. +VOLUME /git + # Callers can SSH as user "test" RUN echo "test:x:65533:65533::/home/test:/usr/bin/git-shell" >> /etc/passwd diff --git a/main.go b/main.go index 572cb06ff..800a3886b 100644 --- a/main.go +++ b/main.go @@ -118,6 +118,23 @@ func envString(def string, key string, alts ...string) string { return def } +func envStringArray(def string, key string, alts ...string) []string { + parse := func(s string) []string { + return strings.Split(s, ":") + } + + if val := os.Getenv(key); val != "" { + return parse(val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(val) + } + } + return parse(def) +} + func envBoolOrError(def bool, key string, alts ...string) (bool, error) { parse := func(val string) (bool, error) { parsed, err := strconv.ParseBool(val) @@ -437,9 +454,9 @@ func main() { flSSH := pflag.Bool("ssh", envBool(false, "GITSYNC_SSH", "GIT_SYNC_SSH"), "use SSH for git operations") - flSSHKeyFile := pflag.String("ssh-key-file", - envString("/etc/git-secret/ssh", "GITSYNC_SSH_KEY_FILE", "GIT_SYNC_SSH_KEY_FILE", "GIT_SSH_KEY_FILE"), - "the SSH key to use") + flSSHKeyFiles := pflag.StringArray("ssh-key-file", + envStringArray("/etc/git-secret/ssh", "GITSYNC_SSH_KEY_FILE", "GIT_SYNC_SSH_KEY_FILE", "GIT_SSH_KEY_FILE"), + "the SSH key(s) to use") flSSHKnownHosts := pflag.Bool("ssh-known-hosts", envBool(true, "GITSYNC_SSH_KNOWN_HOSTS", "GIT_SYNC_KNOWN_HOSTS", "GIT_KNOWN_HOSTS"), "enable SSH known_hosts verification") @@ -697,7 +714,7 @@ func main() { if *flCookieFile { handleConfigError(log, true, "ERROR: only one of --ssh and --cookie-file may be specified") } - if *flSSHKeyFile == "" { + if len(*flSSHKeyFiles) == 0 { handleConfigError(log, true, "ERROR: --ssh-key-file must be specified when --ssh is set") } if *flSSHKnownHosts { @@ -821,8 +838,8 @@ func main() { } if *flSSH { - if err := git.SetupGitSSH(*flSSHKnownHosts, *flSSHKeyFile, *flSSHKnownHostsFile); err != nil { - log.Error(err, "can't set up git SSH", "keyFile", *flSSHKeyFile, "knownHosts", *flSSHKnownHosts, "knownHostsFile", *flSSHKnownHostsFile) + if err := git.SetupGitSSH(*flSSHKnownHosts, *flSSHKeyFiles, *flSSHKnownHostsFile); err != nil { + log.Error(err, "can't set up git SSH", "keyFile", *flSSHKeyFiles, "knownHosts", *flSSHKnownHosts, "knownHostsFile", *flSSHKnownHostsFile) os.Exit(1) } } @@ -1928,7 +1945,7 @@ func (git *repoSync) StoreCredentials(ctx context.Context, username, password st return nil } -func (git *repoSync) SetupGitSSH(setupKnownHosts bool, pathToSSHSecret, pathToSSHKnownHosts string) error { +func (git *repoSync) SetupGitSSH(setupKnownHosts bool, pathsToSSHSecrets []string, pathToSSHKnownHosts string) error { git.log.V(1).Info("setting up git SSH credentials") // If the user sets GIT_SSH_COMMAND we try to respect it. @@ -1937,10 +1954,12 @@ func (git *repoSync) SetupGitSSH(setupKnownHosts bool, pathToSSHSecret, pathToSS sshCmd = "ssh" } - if _, err := os.Stat(pathToSSHSecret); err != nil { - return fmt.Errorf("can't access SSH key file %s: %w", pathToSSHSecret, err) + for _, p := range pathsToSSHSecrets { + if _, err := os.Stat(p); err != nil { + return fmt.Errorf("can't access SSH key file %s: %w", p, err) + } + sshCmd += fmt.Sprintf(" -i %s", p) } - sshCmd += fmt.Sprintf(" -i %s", pathToSSHSecret) if setupKnownHosts { if _, err := os.Stat(pathToSSHKnownHosts); err != nil { @@ -2463,8 +2482,10 @@ OPTIONS Use SSH for git authentication and operations. --ssh-key-file , $GITSYNC_SSH_KEY_FILE - The SSH key to use when using --ssh. If not specified, this - defaults to "/etc/git-secret/ssh". + The SSH key(s) to use when using --ssh. This flag may be specified + more than once and the environment variable will be parsed like + PATH - using a colon (':') to separate elements. If not specified, + this defaults to "/etc/git-secret/ssh". --ssh-known-hosts, $GITSYNC_SSH_KNOWN_HOSTS Enable SSH known_hosts verification when using --ssh. If not diff --git a/test_e2e.sh b/test_e2e.sh index 6ef852c40..a322e2db9 100755 --- a/test_e2e.sh +++ b/test_e2e.sh @@ -238,10 +238,16 @@ function wait_for_sync() { # 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" -chmod -R g+r "$DOT_SSH" +for i in $(seq 1 3); do + mkdir -p "$DOT_SSH/$i" + ssh-keygen -f "$DOT_SSH/$i/id_test" -P "" >/dev/null + cp -a "$DOT_SSH/$i/id_test" "$DOT_SSH/$i/id_local" # for outside-of-container use + mkdir -p "$DOT_SSH/server/$i" + cat "$DOT_SSH/$i/id_test.pub" > "$DOT_SSH/server/$i/authorized_keys" +done +# Allow files to be read inside containers running as a different UID. +# Note: this does not include the *.local forms. +chmod g+r "$DOT_SSH"/*/id_test* "$DOT_SSH"/server/* TEST_TOOLS="_test_tools" SLOW_GIT_FETCH="$TEST_TOOLS/git_slow_fetch.sh" @@ -279,7 +285,9 @@ function GIT_SYNC() { -v "$(pwd)/$TEST_TOOLS":"/$TEST_TOOLS":ro \ --env "$EXECHOOK_ENVKEY=$EXECHOOK_ENVVAL" \ -v "$RUNLOG":/var/log/runs \ - -v "$DOT_SSH/id_test":"/etc/git-secret/ssh":ro \ + -v "$DOT_SSH/1/id_test":"/ssh/secret.1":ro \ + -v "$DOT_SSH/2/id_test":"/ssh/secret.2":ro \ + -v "$DOT_SSH/3/id_test":"/ssh/secret.3":ro \ "${IMAGE}" \ -v=6 \ --add-user \ @@ -2690,31 +2698,62 @@ function e2e::submodule_sync_relative() { } ############################################## -# Test SSH +# Test SSH with bad key ############################################## -function e2e::auth_ssh() { +function e2e::auth_ssh_wrong_key() { echo "$FUNCNAME" > "$REPO/file" - # Run a git-over-SSH server + # Run a git-over-SSH server. Use key #1. CTR=$(docker_run \ - -v "$DOT_SSH":/dot_ssh:ro \ + -v "$DOT_SSH/server/3":/dot_ssh:ro \ -v "$REPO":/src:ro \ e2e/test/sshd) IP=$(docker_ip "$CTR") git -C "$REPO" commit -qam "$FUNCNAME" - # First sync + # Try to sync with key #2. + GIT_SYNC \ + --one-time \ + --repo="test@$IP:/src" \ + --root="$ROOT" \ + --link="link" \ + --ssh \ + --ssh-key-file="/ssh/secret.2" \ + --ssh-known-hosts=false \ + || true + + # check for failure + assert_file_absent "$ROOT/link/file" +} + +############################################## +# Test SSH +############################################## +function e2e::auth_ssh() { + # Run a git-over-SSH server. Use key #3 to exercise the multi-key logic. + CTR=$(docker_run \ + -v "$DOT_SSH/server/3":/dot_ssh:ro \ + -v "$REPO":/git/repo:ro \ + e2e/test/sshd) + IP=$(docker_ip "$CTR") + + # Configure the repo. echo "$FUNCNAME 1" > "$REPO/file" git -C "$REPO" commit -qam "$FUNCNAME 1" GIT_SYNC \ --period=100ms \ - --repo="test@$IP:/src" \ + --repo="test@$IP:/git/repo" \ --root="$ROOT" \ --link="link" \ --ssh \ + --ssh-key-file="/ssh/secret.1" \ + --ssh-key-file="/ssh/secret.2" \ + --ssh-key-file="/ssh/secret.3" \ --ssh-known-hosts=false \ & + + # First sync wait_for_sync "${MAXWAIT}" assert_link_exists "$ROOT/link" assert_file_exists "$ROOT/link/file" @@ -2739,6 +2778,87 @@ function e2e::auth_ssh() { assert_metric_eq "${METRIC_GOOD_SYNC_COUNT}" 3 } +############################################## +# Test submodules over SSH with different keys +############################################## +function e2e::submodule_sync_over_ssh_different_keys() { + # Init nested submodule repo + NESTED_SUBMODULE_REPO_NAME="nested-sub" + NESTED_SUBMODULE="$WORK/$NESTED_SUBMODULE_REPO_NAME" + mkdir "$NESTED_SUBMODULE" + + git -C "$NESTED_SUBMODULE" init -q -b "$MAIN_BRANCH" + config_repo "$NESTED_SUBMODULE" + echo "nested-submodule" > "$NESTED_SUBMODULE/nested-submodule.file" + git -C "$NESTED_SUBMODULE" add nested-submodule.file + git -C "$NESTED_SUBMODULE" commit -aqm "init nested-submodule.file" + + # Run a git-over-SSH server. Use key #1. + CTR_SUBSUB=$(docker_run \ + -v "$DOT_SSH/server/1":/dot_ssh:ro \ + -v "$NESTED_SUBMODULE":/git/repo:ro \ + e2e/test/sshd) + IP_SUBSUB=$(docker_ip "$CTR_SUBSUB") + + # Tell local git not to do host checking and to use the test keys. + export GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i $DOT_SSH/1/id_local -i $DOT_SSH/2/id_local" + + # Init submodule repo + SUBMODULE_REPO_NAME="sub" + SUBMODULE="$WORK/$SUBMODULE_REPO_NAME" + mkdir "$SUBMODULE" + + git -C "$SUBMODULE" init -q -b "$MAIN_BRANCH" + config_repo "$SUBMODULE" + echo "submodule" > "$SUBMODULE/submodule.file" + git -C "$SUBMODULE" add submodule.file + git -C "$SUBMODULE" commit -aqm "init submodule.file" + + # Add nested submodule to submodule repo + git -C "$SUBMODULE" submodule add -q "test@$IP_SUBSUB:/git/repo" "$NESTED_SUBMODULE_REPO_NAME" + git -C "$SUBMODULE" commit -aqm "add nested submodule" + + # Run a git-over-SSH server. Use key #2. + CTR_SUB=$(docker_run \ + -v "$DOT_SSH/server/2":/dot_ssh:ro \ + -v "$SUBMODULE":/git/repo:ro \ + e2e/test/sshd) + IP_SUB=$(docker_ip "$CTR_SUB") + + # Add the submodule to the main repo + git -C "$REPO" submodule add -q "test@$IP_SUB:/git/repo" "$SUBMODULE_REPO_NAME" + git -C "$REPO" commit -aqm "add submodule" + git -C "$REPO" submodule update --recursive --remote > /dev/null 2>&1 + + # Run a git-over-SSH server. Use key #3. + CTR=$(docker_run \ + -v "$DOT_SSH/server/3":/dot_ssh:ro \ + -v "$REPO":/git/repo:ro \ + e2e/test/sshd) + IP=$(docker_ip "$CTR") + + GIT_SYNC \ + --period=100ms \ + --repo="test@$IP:/git/repo" \ + --root="$ROOT" \ + --link="link" \ + --ssh \ + --ssh-key-file="/ssh/secret.1" \ + --ssh-key-file="/ssh/secret.2" \ + --ssh-key-file="/ssh/secret.3" \ + --ssh-known-hosts=false \ + & + wait_for_sync "${MAXWAIT}" + assert_link_exists "$ROOT/link" + assert_file_exists "$ROOT/link/file" + assert_file_exists "$ROOT/link/$SUBMODULE_REPO_NAME/submodule.file" + assert_file_exists "$ROOT/link/$SUBMODULE_REPO_NAME/$NESTED_SUBMODULE_REPO_NAME/nested-submodule.file" + assert_metric_eq "${METRIC_GOOD_SYNC_COUNT}" 1 + + rm -rf $SUBMODULE + rm -rf $NESTED_SUBMODULE +} + ############################################## # Test sparse-checkout files ##############################################