Add support for GitHub Deployment Keys through key comments (#59)

Fixes #30, closes #38.
This commit is contained in:
Matthias Pigulla 2021-02-19 14:37:34 +01:00 committed by GitHub
parent 85353917a2
commit 4d06ea6a33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 10 deletions

View file

@ -7,7 +7,7 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest] os: [ubuntu-latest, macOS-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Setup key - name: Setup key
uses: ./ uses: ./
with: with:
@ -21,7 +21,7 @@ jobs:
os: [ubuntu-latest, macOS-latest] os: [ubuntu-latest, macOS-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Setup key - name: Setup key
uses: ./ uses: ./
with: with:
@ -32,7 +32,7 @@ jobs:
container: container:
image: ubuntu:latest image: ubuntu:latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- run: apt update && apt install -y openssh-client - run: apt update && apt install -y openssh-client
- name: Setup key - name: Setup key
uses: ./ uses: ./
@ -40,3 +40,21 @@ jobs:
ssh-private-key: | ssh-private-key: |
${{ secrets.DEMO_KEY }} ${{ secrets.DEMO_KEY }}
${{ secrets.DEMO_KEY_2 }} ${{ secrets.DEMO_KEY_2 }}
deployment_keys_demo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup key
uses: ./
with:
ssh-private-key: |
${{ secrets.MPDUDE_TEST_1_DEPLOY_KEY }}
${{ secrets.MPDUDE_TEST_2_DEPLOY_KEY }}
- run: |
git clone https://github.com/mpdude/test-1.git test-1-http
git clone git@github.com:mpdude/test-1.git test-1-git
git clone ssh://git@github.com/mpdude/test-1.git test-1-git-ssh
git clone https://github.com/mpdude/test-2.git test-2-http
git clone git@github.com:mpdude/test-2.git test-2-git
git clone ssh://git@github.com/mpdude/test-2.git test-2-git-ssh

View file

@ -3,13 +3,15 @@
This action This action
* starts the `ssh-agent`, * starts the `ssh-agent`,
* exports the `SSH_AUTH_SOCK` environment variable, * exports the `SSH_AUTH_SOCK` environment variable,
* loads a private SSH key into the agent and * loads one or several private SSH key into the agent and
* configures `known_hosts` for GitHub.com. * configures `known_hosts` for GitHub.com.
It should work in all GitHub Actions virtual environments, including container-based workflows. It should work in all GitHub Actions virtual environments, including container-based workflows.
Windows and Docker support is, however, somewhat new. Since we have little feedback from the field, things might not run so smooth for you as we'd hope. If Windows and/or Docker-based workflows work well for you, leave a :+1: at https://github.com/webfactory/ssh-agent/pull/17. Windows and Docker support is, however, somewhat new. Since we have little feedback from the field, things might not run so smooth for you as we'd hope. If Windows and/or Docker-based workflows work well for you, leave a :+1: at https://github.com/webfactory/ssh-agent/pull/17.
Also, using multiple GitHub deployment keys is supported; keys are mapped to repositories by using SSH key comments (see below).
## Why? ## Why?
When running a GitHub Action workflow to stage your project, run tests or build images, you might need to fetch additional libraries or _vendors_ from private repositories. When running a GitHub Action workflow to stage your project, run tests or build images, you might need to fetch additional libraries or _vendors_ from private repositories.
@ -22,7 +24,7 @@ GitHub Actions only have access to the repository they run for. So, in order to
2. Make sure you don't have a passphrase set on the private key. 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. <br> 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. <br>
This key should start with `-----BEGIN ... PRIVATE KEY-----`, consist of many lines and ends with `-----END ... PRIVATE KEY-----`. 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. 4. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v2` line.
```yaml ```yaml
# .github/workflows/my-workflow.yml # .github/workflows/my-workflow.yml
@ -30,7 +32,7 @@ jobs:
my_job: my_job:
... ...
steps: steps:
- actions/checkout@v1 - actions/checkout@v2
# Make sure the @v0.4.1 matches the current version of the # Make sure the @v0.4.1 matches the current version of the
# action # action
- uses: webfactory/ssh-agent@v0.4.1 - uses: webfactory/ssh-agent@v0.4.1
@ -58,12 +60,18 @@ You can set up different keys as different secrets and pass them all to the acti
The `ssh-agent` will load all of the keys and try each one in order when establishing SSH connections. The `ssh-agent` will load all of the keys and try each one in order when establishing SSH connections.
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 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. But when you're using GitHub Deploy Keys, read on!
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. ### Support for GitHub Deploy Keys
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. When using **Github deploy keys**, GitHub servers will accept the _first_ known key. But since deploy keys are scoped to a single repository, this might not be the key needed to access a particular repository. Thus, you will 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.
To support picking the right key in this use case, this action scans _key comments_ and will set up extra Git and SSH configuration to make things work.
1. When creating the deploy key for a repository like `git@github.com:owner/repo.git` or `https://github.com/owner/repo`, put that URL into the key comment.
2. After keys have been added to the agent, this action will scan the key comments.
3. For key comments containing such URLs, a Git config setting is written that uses [`url.<base>.insteadof`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf). It will redirect `git` requests to URLs starting with either `https://github.com/owner/repo` or `git@github.com:owner/repo` to a fake hostname/URL like `git@...some.hash...:owner/repo`.
4. An SSH configuration section is generated that applies to the fake hostname. It will map the SSH connection back to `github.com`, while at the same time pointing SSH to a file containing the appropriate key's public part. That will make SSH use the right key when connecting to GitHub.com.
## Exported variables ## 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` and `SSH_AGENT_PID` environment variables through the Github Actions core module.

35
dist/index.js vendored
View file

@ -119,6 +119,7 @@ const core = __webpack_require__(470);
const child_process = __webpack_require__(129); const child_process = __webpack_require__(129);
const fs = __webpack_require__(747); const fs = __webpack_require__(747);
const os = __webpack_require__(87); const os = __webpack_require__(87);
const crypto = __webpack_require__(417);
try { try {
const privateKey = core.getInput('ssh-private-key'); const privateKey = core.getInput('ssh-private-key');
@ -175,6 +176,33 @@ try {
console.log("Keys added:"); console.log("Keys added:");
child_process.execSync('ssh-add -l', { stdio: 'inherit' }); child_process.execSync('ssh-add -l', { stdio: 'inherit' });
child_process.execFileSync('ssh-add', ['-L']).toString().split(/\r?\n/).forEach(function(key) {
let parts = key.match(/\bgithub.com[:/](.*)(?:\.git)?\b/);
if (parts == null) {
return;
}
let ownerAndRepo = parts[1];
let sha256 = crypto.createHash('sha256').update(key).digest('hex');
fs.writeFileSync(`${homeSsh}/${sha256}`, key + "\n", { mode: '600' });
child_process.execSync(`git config --global --replace-all url."git@${sha256}:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`);
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`);
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`);
let sshConfig = `\nHost ${sha256}\n`
+ ` HostName github.com\n`
+ ` User git\n`
+ ` IdentityFile ${homeSsh}/${sha256}\n`
+ ` IdentitiesOnly yes\n`;
fs.appendFileSync(`${homeSsh}/config`, sshConfig);
console.log(`Added deploy-key mapping: Use key "${key}" for GitHub repository ${ownerAndRepo}`);
});
} catch (error) { } catch (error) {
core.setFailed(error.message); core.setFailed(error.message);
} }
@ -189,6 +217,13 @@ module.exports = require("child_process");
/***/ }), /***/ }),
/***/ 417:
/***/ (function(module) {
module.exports = require("crypto");
/***/ }),
/***/ 431: /***/ 431:
/***/ (function(__unusedmodule, exports, __webpack_require__) { /***/ (function(__unusedmodule, exports, __webpack_require__) {

View file

@ -2,6 +2,7 @@ const core = require('@actions/core');
const child_process = require('child_process'); const child_process = require('child_process');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const crypto = require('crypto');
try { try {
const privateKey = core.getInput('ssh-private-key'); const privateKey = core.getInput('ssh-private-key');
@ -58,6 +59,33 @@ try {
console.log("Keys added:"); console.log("Keys added:");
child_process.execSync('ssh-add -l', { stdio: 'inherit' }); child_process.execSync('ssh-add -l', { stdio: 'inherit' });
child_process.execFileSync('ssh-add', ['-L']).toString().split(/\r?\n/).forEach(function(key) {
let parts = key.match(/\bgithub.com[:/](.*)(?:\.git)?\b/);
if (parts == null) {
return;
}
let ownerAndRepo = parts[1];
let sha256 = crypto.createHash('sha256').update(key).digest('hex');
fs.writeFileSync(`${homeSsh}/${sha256}`, key + "\n", { mode: '600' });
child_process.execSync(`git config --global --replace-all url."git@${sha256}:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`);
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`);
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`);
let sshConfig = `\nHost ${sha256}\n`
+ ` HostName github.com\n`
+ ` User git\n`
+ ` IdentityFile ${homeSsh}/${sha256}\n`
+ ` IdentitiesOnly yes\n`;
fs.appendFileSync(`${homeSsh}/config`, sshConfig);
console.log(`Added deploy-key mapping: Use key "${key}" for GitHub repository ${ownerAndRepo}`);
});
} catch (error) { } catch (error) {
core.setFailed(error.message); core.setFailed(error.message);
} }