From 17860381fc86af3610aac3383c3a29f67d36070c Mon Sep 17 00:00:00 2001 From: Michael Carlson Date: Mon, 23 Nov 2020 21:58:33 -0700 Subject: [PATCH 1/4] add git deploy key wrapper functionality --- README.md | 12 ++++++++--- action.yml | 3 +++ cleanup.js | 8 +++++++ dist/cleanup.js | 8 +++++++ .../git-deploy-key-wrapper.sh | 8 +++++-- dist/index.js | 9 ++++++++ index.js | 9 ++++++++ scripts/build.js | 18 +++++++++++++--- wrapper/git-deploy-key-wrapper.sh | 21 +++++++++++++++++++ 9 files changed, 88 insertions(+), 8 deletions(-) rename wrapper/ssh-deploy-key-wrapper.sh => dist/git-deploy-key-wrapper.sh (55%) create mode 100644 wrapper/git-deploy-key-wrapper.sh diff --git a/README.md b/README.md index 18e26de..b9a85cb 100644 --- a/README.md +++ b/README.md @@ -57,14 +57,20 @@ The `ssh-agent` will load all of the keys and try each one in order when establi There's one **caveat**, though: SSH servers may abort the connection attempt after a number of mismatching keys have been presented. So if, for example, you have six different keys loaded into the `ssh-agent`, but the server aborts after five unknown keys, the last key (which might be the right one) will never even be tried. -Also, when using **Github deploy keys**, GitHub servers will accept the first known key. But since deploy keys are scoped to a single repository, you might get the error message `fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.` if the wrong key/repository combination is tried. +You might want to [try a wrapper script around `ssh`](https://gist.github.com/mpdude/e56fcae5bc541b95187fa764aafb5e6d) that can pick the right key, based on key comments. See [our blog post](https://www.webfactory.de/blog/using-multiple-ssh-deploy-keys-with-github) for the full story. -In both cases, you might want to [try a wrapper script around `ssh`](https://gist.github.com/mpdude/e56fcae5bc541b95187fa764aafb5e6d) that can pick the right key, based on key comments. See [our blog post](https://www.webfactory.de/blog/using-multiple-ssh-deploy-keys-with-github) for the full story. +Also, when using **Github deploy keys**, GitHub servers will accept the first known key. But since deploy keys are scoped to a single repository, you might get the error message `fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.` if the wrong key/repository combination is tried. +For this scenario, you'll want to set `use-git-deploy-key-wrapper` to `true` and create your key with a comment that has the git SSH url in it. For example: +``` +ssh-keygen -t ed25519 -a 100 -C "ssh://git@github.com/ORGANIZATION/REPO.git" -m PEM -N "" -f ~/.ssh/REPO -q +``` +**Please note that `git@github.com` is followed by a `/`, not a `:`** ## Exported variables -The action exports the `SSH_AUTH_SOCK` and `SSH_AGENT_PID` environment variables through the Github Actions core module. +The action exports the `SSH_AUTH_SOCK`, `SSH_AGENT_PID` and `GIT_SSH_COMMAND` environment variables through the Github Actions core module. The `$SSH_AUTH_SOCK` is used by several applications like git or rsync to connect to the SSH authentication agent. The `$SSH_AGENT_PID` contains the process id of the agent. This is used to kill the agent in post job action. +The `$GIT_SSH_COMMAND` contains the path to the `git-deploy-key-wrapper` shell script. Git will use this value for SSH interactions instead of the SSH executable. ## Known issues and limitations diff --git a/action.yml b/action.yml index e43c44f..0a58dae 100644 --- a/action.yml +++ b/action.yml @@ -6,6 +6,9 @@ inputs: required: true ssh-auth-sock: description: 'Where to place the SSH Agent auth socket' + use-git-deploy-key-wrapper: + description: 'Enable usage of git deploy key wrapper for multiple repositories with individual deploy keys' + default: false runs: using: 'node12' main: 'dist/index.js' diff --git a/cleanup.js b/cleanup.js index f90cddd..8db86cf 100644 --- a/cleanup.js +++ b/cleanup.js @@ -5,6 +5,14 @@ try { // Kill the started SSH agent console.log('Stopping SSH agent') execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' }) + + const home = process.env['HOME']; + const homeSsh = `${home}/.ssh`; + const gitSSHWrapperPath = path.join(homeSsh, 'git-deploy-key-wrapper.sh'); + if (fs.existsSync(gitSSHWrapperPath)) { + console.log('Removing ssh git SSH wrapper'); + fs.unlinkSync(gitSSHWrapperPath); + } } catch (error) { console.log(error.message); console.log('Error stopping the SSH agent, proceeding anyway'); diff --git a/dist/cleanup.js b/dist/cleanup.js index c8081be..b5840be 100644 --- a/dist/cleanup.js +++ b/dist/cleanup.js @@ -129,6 +129,14 @@ try { // Kill the started SSH agent console.log('Stopping SSH agent') execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' }) + + const home = process.env['HOME']; + const homeSsh = `${home}/.ssh`; + const gitSSHWrapperPath = path.join(homeSsh, 'git-deploy-key-wrapper.sh'); + if (fs.existsSync(gitSSHWrapperPath)) { + console.log('Removing ssh git SSH wrapper'); + fs.unlinkSync(gitSSHWrapperPath); + } } catch (error) { console.log(error.message); console.log('Error stopping the SSH agent, proceeding anyway'); diff --git a/wrapper/ssh-deploy-key-wrapper.sh b/dist/git-deploy-key-wrapper.sh similarity index 55% rename from wrapper/ssh-deploy-key-wrapper.sh rename to dist/git-deploy-key-wrapper.sh index 06da0db..d314204 100644 --- a/wrapper/ssh-deploy-key-wrapper.sh +++ b/dist/git-deploy-key-wrapper.sh @@ -2,6 +2,7 @@ # The last argument is the command to be executed on the remote end, which is something # like "git-upload-pack 'webfactory/ssh-agent.git'". We need the repo path only, so we +# Terraform ends up bing "git-upload-pack '/webfactory/ssh-agent.git'" # loop over this last argument to get the last part of if. for last in ${!#}; do :; done @@ -12,6 +13,9 @@ trap "rm -f $key_file" EXIT eval last=$last # Try to pick the right key -ssh-add -L | grep --word-regexp --max-count=1 $last > $key_file +# No "--word-regexp" because Terraforms usage of git ends up as +# "git-upload-pack 'webfactory/ssh-agent.git'". "--word-regexp" will not match it. +# Other integrations still work without "--word-regexp" +ssh-add -L | grep --max-count=1 $last > $key_file -ssh -i $key_file "$@" \ No newline at end of file +ssh -i $key_file "$@" diff --git a/dist/index.js b/dist/index.js index 173d8ea..06fb070 100644 --- a/dist/index.js +++ b/dist/index.js @@ -163,6 +163,15 @@ try { console.log("Keys added:"); child_process.execSync('ssh-add -l', { stdio: 'inherit' }); + const useGitSSHWrapper = core.getInput('use-git-deploy-key-wrapper'); + if(useGitSSHWrapper) { + const gitSSHWrapperFileName = 'git-deploy-key-wrapper.sh'; + const gitSSHWrapperPath = path.join(homeSsh, gitSSHWrapperFileName); + fs.copyFileSync(path.join(process.cwd(), gitSSHWrapperFileName), gitSSHWrapperPath); + fs.chmodSync(gitSSHWrapperPath, "755"); + + core.exportVariable('GIT_SSH_COMMAND', gitSSHWrapperPath); + } } catch (error) { core.setFailed(error.message); } diff --git a/index.js b/index.js index 7ee6fe7..22b4491 100644 --- a/index.js +++ b/index.js @@ -46,6 +46,15 @@ try { console.log("Keys added:"); child_process.execSync('ssh-add -l', { stdio: 'inherit' }); + const useGitSSHWrapper = core.getInput('use-git-deploy-key-wrapper'); + if(useGitSSHWrapper) { + const gitSSHWrapperFileName = 'git-deploy-key-wrapper.sh'; + const gitSSHWrapperPath = path.join(homeSsh, gitSSHWrapperFileName); + fs.copyFileSync(path.join(process.cwd(), gitSSHWrapperFileName), gitSSHWrapperPath); + fs.chmodSync(gitSSHWrapperPath, "755"); + + core.exportVariable('GIT_SSH_COMMAND', gitSSHWrapperPath); + } } catch (error) { core.setFailed(error.message); } diff --git a/scripts/build.js b/scripts/build.js index eecd192..4b7a9a1 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -5,9 +5,14 @@ const fs = require('fs') const buildDir = path.join(process.cwd(), 'build') const distDir = path.join(process.cwd(), 'dist') -const buildIndexJs = path.join(buildDir, 'index.js') -const distIndexJs = path.join(distDir, 'index.js') -const distCleanupJs = path.join(distDir, 'cleanup.js') +const gitSSHWrapperFileName = 'git-deploy-key-wrapper.sh'; +const gitSSHWrapper = path.join(process.cwd(), 'wrapper', gitSSHWrapperFileName); + +const buildIndexJs = path.join(buildDir, 'index.js'); +const buildGitSSHWrapper = path.join(buildDir, gitSSHWrapperFileName); +const distIndexJs = path.join(distDir, 'index.js'); +const distGitSSHWrapper = path.join(distDir, gitSSHWrapperFileName); +const distCleanupJs = path.join(distDir, 'cleanup.js'); if (!fs.existsSync(buildDir)) { fs.mkdirSync(buildDir) @@ -29,6 +34,13 @@ if (fs.existsSync(distCleanupJs)) { } fs.renameSync(buildIndexJs, distCleanupJs) +console.log(`Copying "${gitSSHWrapperFileName}"`); +fs.copyFileSync(gitSSHWrapper, buildGitSSHWrapper); +if (fs.existsSync(distGitSSHWrapper)) { + fs.unlinkSync(distGitSSHWrapper); +} +fs.renameSync(buildGitSSHWrapper, distGitSSHWrapper); + console.log('Cleaning up...') fs.rmdirSync(buildDir) diff --git a/wrapper/git-deploy-key-wrapper.sh b/wrapper/git-deploy-key-wrapper.sh new file mode 100644 index 0000000..d314204 --- /dev/null +++ b/wrapper/git-deploy-key-wrapper.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# The last argument is the command to be executed on the remote end, which is something +# like "git-upload-pack 'webfactory/ssh-agent.git'". We need the repo path only, so we +# Terraform ends up bing "git-upload-pack '/webfactory/ssh-agent.git'" +# loop over this last argument to get the last part of if. +for last in ${!#}; do :; done + +# Don't use "exec" to run "ssh" below; then the trap won't work. +key_file=$(mktemp -u) +trap "rm -f $key_file" EXIT + +eval last=$last + +# Try to pick the right key +# No "--word-regexp" because Terraforms usage of git ends up as +# "git-upload-pack 'webfactory/ssh-agent.git'". "--word-regexp" will not match it. +# Other integrations still work without "--word-regexp" +ssh-add -L | grep --max-count=1 $last > $key_file + +ssh -i $key_file "$@" From 0128ae9f5988b50b794a0734b8b70ad25754ade6 Mon Sep 17 00:00:00 2001 From: Michael Carlson Date: Mon, 23 Nov 2020 22:03:39 -0700 Subject: [PATCH 2/4] update readme wording --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b9a85cb..4142863 100644 --- a/README.md +++ b/README.md @@ -15,26 +15,27 @@ GitHub Actions only have access to the repository they run for. So, in order to ## Usage 1. Create an SSH key with sufficient access privileges. For security reasons, don't use your personal SSH key but set up a dedicated one for use in GitHub Actions. See below for a few hints if you are unsure about this step. -2. Make sure you don't have a passphrase set on the private key. -3. In your repository, go to the *Settings > Secrets* menu and create a new secret. In this example, we'll call it `SSH_PRIVATE_KEY`. Put the contents of the *private* SSH key file into the contents field.
+1. Make sure you don't have a passphrase set on the private key. +1. In your repository, go to the *Settings > Secrets* menu and create a new secret. In this example, we'll call it `SSH_PRIVATE_KEY`. Put the contents of the *private* SSH key file into the contents field.
This key should start with `-----BEGIN ... PRIVATE KEY-----`, consist of many lines and ends with `-----END ... PRIVATE KEY-----`. -4. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v1` line. - -```yaml -# .github/workflows/my-workflow.yml -jobs: - my_job: - ... - steps: - - actions/checkout@v1 - # Make sure the @v0.4.1 matches the current version of the - # action - - uses: webfactory/ssh-agent@v0.4.1 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - ... other steps -``` -5. If, for some reason, you need to change the location of the SSH agent socket, you can use the `ssh-auth-sock` input to provide a path. +1. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v1` line. + + ```yaml + # .github/workflows/my-workflow.yml + jobs: + my_job: + ... + steps: + - actions/checkout@v1 + # Make sure the @v0.4.1 matches the current version of the + # action + - uses: webfactory/ssh-agent@v0.4.1 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - ... other steps + ``` +1. If you are using GitHub deploy keys, set the `use-git-deploy-key-wrapper` input variable to `true`. +1. If, for some reason, you need to change the location of the SSH agent socket, you can use the `ssh-auth-sock` input to provide a path. ### Using multiple keys @@ -60,7 +61,7 @@ six different keys loaded into the `ssh-agent`, but the server aborts after five You might want to [try a wrapper script around `ssh`](https://gist.github.com/mpdude/e56fcae5bc541b95187fa764aafb5e6d) that can pick the right key, based on key comments. See [our blog post](https://www.webfactory.de/blog/using-multiple-ssh-deploy-keys-with-github) for the full story. Also, when using **Github deploy keys**, GitHub servers will accept the first known key. But since deploy keys are scoped to a single repository, you might get the error message `fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists.` if the wrong key/repository combination is tried. -For this scenario, you'll want to set `use-git-deploy-key-wrapper` to `true` and create your key with a comment that has the git SSH url in it. For example: +For this scenario, you'll want to set `use-git-deploy-key-wrapper` input variable to `true` and create your key with a comment that has the git SSH url in it. For example: ``` ssh-keygen -t ed25519 -a 100 -C "ssh://git@github.com/ORGANIZATION/REPO.git" -m PEM -N "" -f ~/.ssh/REPO -q ``` From 701a67051546d70d952f235ad7a5af5b7bd50fbc Mon Sep 17 00:00:00 2001 From: Michael Carlson Date: Mon, 23 Nov 2020 22:05:59 -0700 Subject: [PATCH 3/4] add whitespace to readme for readability --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4142863..74f9626 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,11 @@ ssh-keygen -t ed25519 -a 100 -C "ssh://git@github.com/ORGANIZATION/REPO.git" -m ## Exported variables The action exports the `SSH_AUTH_SOCK`, `SSH_AGENT_PID` and `GIT_SSH_COMMAND` environment variables through the Github Actions core module. + The `$SSH_AUTH_SOCK` is used by several applications like git or rsync to connect to the SSH authentication agent. + The `$SSH_AGENT_PID` contains the process id of the agent. This is used to kill the agent in post job action. + The `$GIT_SSH_COMMAND` contains the path to the `git-deploy-key-wrapper` shell script. Git will use this value for SSH interactions instead of the SSH executable. ## Known issues and limitations From 8b6489657d0b0f88ebcb53bf267d92e71094fc23 Mon Sep 17 00:00:00 2001 From: Michael Carlson Date: Mon, 23 Nov 2020 22:11:57 -0700 Subject: [PATCH 4/4] fix script comment --- wrapper/git-deploy-key-wrapper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrapper/git-deploy-key-wrapper.sh b/wrapper/git-deploy-key-wrapper.sh index d314204..ec99586 100644 --- a/wrapper/git-deploy-key-wrapper.sh +++ b/wrapper/git-deploy-key-wrapper.sh @@ -14,7 +14,7 @@ eval last=$last # Try to pick the right key # No "--word-regexp" because Terraforms usage of git ends up as -# "git-upload-pack 'webfactory/ssh-agent.git'". "--word-regexp" will not match it. +# "git-upload-pack '/webfactory/ssh-agent.git'". "--word-regexp" will not match it. # Other integrations still work without "--word-regexp" ssh-add -L | grep --max-count=1 $last > $key_file