Skip to content

Commit 4d06ea6

Browse files
authored
Add support for GitHub Deployment Keys through key comments (#59)
Fixes #30, closes #38.
1 parent 8535391 commit 4d06ea6

File tree

4 files changed

+99
-10
lines changed

4 files changed

+99
-10
lines changed

.github/workflows/demo.yml

+21-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
os: [ubuntu-latest, macOS-latest, windows-latest]
88
runs-on: ${{ matrix.os }}
99
steps:
10-
- uses: actions/checkout@v1
10+
- uses: actions/checkout@v2
1111
- name: Setup key
1212
uses: ./
1313
with:
@@ -21,7 +21,7 @@ jobs:
2121
os: [ubuntu-latest, macOS-latest]
2222
runs-on: ${{ matrix.os }}
2323
steps:
24-
- uses: actions/checkout@v1
24+
- uses: actions/checkout@v2
2525
- name: Setup key
2626
uses: ./
2727
with:
@@ -32,11 +32,29 @@ jobs:
3232
container:
3333
image: ubuntu:latest
3434
steps:
35-
- uses: actions/checkout@v1
35+
- uses: actions/checkout@v2
3636
- run: apt update && apt install -y openssh-client
3737
- name: Setup key
3838
uses: ./
3939
with:
4040
ssh-private-key: |
4141
${{ secrets.DEMO_KEY }}
4242
${{ secrets.DEMO_KEY_2 }}
43+
44+
deployment_keys_demo:
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v2
48+
- name: Setup key
49+
uses: ./
50+
with:
51+
ssh-private-key: |
52+
${{ secrets.MPDUDE_TEST_1_DEPLOY_KEY }}
53+
${{ secrets.MPDUDE_TEST_2_DEPLOY_KEY }}
54+
- run: |
55+
git clone https://github.com/mpdude/test-1.git test-1-http
56+
git clone [email protected]:mpdude/test-1.git test-1-git
57+
git clone ssh://[email protected]/mpdude/test-1.git test-1-git-ssh
58+
git clone https://github.com/mpdude/test-2.git test-2-http
59+
git clone [email protected]:mpdude/test-2.git test-2-git
60+
git clone ssh://[email protected]/mpdude/test-2.git test-2-git-ssh

README.md

+15-7
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
This action
44
* starts the `ssh-agent`,
55
* exports the `SSH_AUTH_SOCK` environment variable,
6-
* loads a private SSH key into the agent and
6+
* loads one or several private SSH key into the agent and
77
* configures `known_hosts` for GitHub.com.
88

99
It should work in all GitHub Actions virtual environments, including container-based workflows.
1010

1111
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.
1212

13+
Also, using multiple GitHub deployment keys is supported; keys are mapped to repositories by using SSH key comments (see below).
14+
1315
## Why?
1416

1517
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,15 +24,15 @@ GitHub Actions only have access to the repository they run for. So, in order to
2224
2. Make sure you don't have a passphrase set on the private key.
2325
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>
2426
This key should start with `-----BEGIN ... PRIVATE KEY-----`, consist of many lines and ends with `-----END ... PRIVATE KEY-----`.
25-
4. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v1` line.
27+
4. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v2` line.
2628

2729
```yaml
2830
# .github/workflows/my-workflow.yml
2931
jobs:
3032
my_job:
3133
...
3234
steps:
33-
- actions/checkout@v1
35+
- actions/checkout@v2
3436
# Make sure the @v0.4.1 matches the current version of the
3537
# action
3638
- uses: webfactory/[email protected]
@@ -58,12 +60,18 @@ You can set up different keys as different secrets and pass them all to the acti
5860

5961
The `ssh-agent` will load all of the keys and try each one in order when establishing SSH connections.
6062

61-
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
62-
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.
63+
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!
64+
65+
### Support for GitHub Deploy Keys
66+
67+
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.
6368

64-
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.
69+
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.
6570

66-
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.
71+
1. When creating the deploy key for a repository like `[email protected]:owner/repo.git` or `https://github.com/owner/repo`, put that URL into the key comment.
72+
2. After keys have been added to the agent, this action will scan the key comments.
73+
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 `[email protected]:owner/repo` to a fake hostname/URL like `[email protected]...:owner/repo`.
74+
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.
6775

6876
## Exported variables
6977
The action exports the `SSH_AUTH_SOCK` and `SSH_AGENT_PID` environment variables through the Github Actions core module.

dist/index.js

+35
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const core = __webpack_require__(470);
119119
const child_process = __webpack_require__(129);
120120
const fs = __webpack_require__(747);
121121
const os = __webpack_require__(87);
122+
const crypto = __webpack_require__(417);
122123

123124
try {
124125
const privateKey = core.getInput('ssh-private-key');
@@ -175,6 +176,33 @@ try {
175176
console.log("Keys added:");
176177
child_process.execSync('ssh-add -l', { stdio: 'inherit' });
177178

179+
child_process.execFileSync('ssh-add', ['-L']).toString().split(/\r?\n/).forEach(function(key) {
180+
let parts = key.match(/\bgithub.com[:/](.*)(?:\.git)?\b/);
181+
182+
if (parts == null) {
183+
return;
184+
}
185+
186+
let ownerAndRepo = parts[1];
187+
let sha256 = crypto.createHash('sha256').update(key).digest('hex');
188+
189+
fs.writeFileSync(`${homeSsh}/${sha256}`, key + "\n", { mode: '600' });
190+
191+
child_process.execSync(`git config --global --replace-all url."git@${sha256}:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`);
192+
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "[email protected]:${ownerAndRepo}"`);
193+
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "ssh://[email protected]/${ownerAndRepo}"`);
194+
195+
let sshConfig = `\nHost ${sha256}\n`
196+
+ ` HostName github.com\n`
197+
+ ` User git\n`
198+
+ ` IdentityFile ${homeSsh}/${sha256}\n`
199+
+ ` IdentitiesOnly yes\n`;
200+
201+
fs.appendFileSync(`${homeSsh}/config`, sshConfig);
202+
203+
console.log(`Added deploy-key mapping: Use key "${key}" for GitHub repository ${ownerAndRepo}`);
204+
});
205+
178206
} catch (error) {
179207
core.setFailed(error.message);
180208
}
@@ -189,6 +217,13 @@ module.exports = require("child_process");
189217

190218
/***/ }),
191219

220+
/***/ 417:
221+
/***/ (function(module) {
222+
223+
module.exports = require("crypto");
224+
225+
/***/ }),
226+
192227
/***/ 431:
193228
/***/ (function(__unusedmodule, exports, __webpack_require__) {
194229

index.js

+28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const core = require('@actions/core');
22
const child_process = require('child_process');
33
const fs = require('fs');
44
const os = require('os');
5+
const crypto = require('crypto');
56

67
try {
78
const privateKey = core.getInput('ssh-private-key');
@@ -58,6 +59,33 @@ try {
5859
console.log("Keys added:");
5960
child_process.execSync('ssh-add -l', { stdio: 'inherit' });
6061

62+
child_process.execFileSync('ssh-add', ['-L']).toString().split(/\r?\n/).forEach(function(key) {
63+
let parts = key.match(/\bgithub.com[:/](.*)(?:\.git)?\b/);
64+
65+
if (parts == null) {
66+
return;
67+
}
68+
69+
let ownerAndRepo = parts[1];
70+
let sha256 = crypto.createHash('sha256').update(key).digest('hex');
71+
72+
fs.writeFileSync(`${homeSsh}/${sha256}`, key + "\n", { mode: '600' });
73+
74+
child_process.execSync(`git config --global --replace-all url."git@${sha256}:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`);
75+
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "[email protected]:${ownerAndRepo}"`);
76+
child_process.execSync(`git config --global --add url."git@${sha256}:${ownerAndRepo}".insteadOf "ssh://[email protected]/${ownerAndRepo}"`);
77+
78+
let sshConfig = `\nHost ${sha256}\n`
79+
+ ` HostName github.com\n`
80+
+ ` User git\n`
81+
+ ` IdentityFile ${homeSsh}/${sha256}\n`
82+
+ ` IdentitiesOnly yes\n`;
83+
84+
fs.appendFileSync(`${homeSsh}/config`, sshConfig);
85+
86+
console.log(`Added deploy-key mapping: Use key "${key}" for GitHub repository ${ownerAndRepo}`);
87+
});
88+
6189
} catch (error) {
6290
core.setFailed(error.message);
6391
}

0 commit comments

Comments
 (0)