Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple SSH keys for use in submodules #802

Merged
merged 2 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,10 @@ OPTIONS
Use SSH for git authentication and operations.

--ssh-key-file <string>, $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
Expand Down
3 changes: 3 additions & 0 deletions _test_tools/sshd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 33 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -2463,8 +2482,10 @@ OPTIONS
Use SSH for git authentication and operations.

--ssh-key-file <string>, $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
Expand Down
142 changes: 131 additions & 11 deletions test_e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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"
Expand All @@ -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
##############################################
Expand Down
Loading