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