Skip to content

Commit 31701ba

Browse files
committed
Updated README.md to include the two new options
Fixed build.js to work on windows Fixed homedir lookup for windows Moved param names to const vars at the top and replaced all references
1 parent ef0ce0c commit 31701ba

File tree

5 files changed

+308
-24
lines changed

5 files changed

+308
-24
lines changed

README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,38 @@ In that case, you can set-up the different keys as multiple secrets and pass the
5050
${{ secrets.FIRST_KEY }}
5151
${{ secrets.NEXT_KEY }}
5252
${{ secrets.ANOTHER_KEY }}
53+
repo-mappings: |
54+
github.com/OWNERX/REPO1
55+
bitbucket.com/OWNERY/REPO2
56+
github.com/OWNERX/REPO3
5357
```
5458

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

57-
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
61+
Optionally, `repo-mappings` provides a list of git repos that correlate to the keys provided. If you specify `repo-mappings` you **MUST** specify the same number mappings as you provided `ssh-private-key` entries and they **MUST** be in the same order. Each mapping **MUST** be in the format of `{HOSTNAME}/{OWNER}/{REPO}` without any *https://*, *git@* , or *ssh://* prefix and using **slashes** not the mixed slashes and colons used in the ssh format.
62+
63+
These mappings are used to generate git config `insteadOf` entries to psuedo hostnames, where the pseudo hostnames are each assigned the associated `ssh-private-key`. See the [Repo Mappings](#repo-mappings) section for details on how this works.
64+
65+
There's one **caveat**, though, if you're not using `repo-mappings`: SSH servers may abort the connection attempt after a number of mismatching keys have been presented. So if, for example, you have
5866
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. If you don't need all of the keys at the same time, you could try to `run: kill $SSH_AGENT_PID` to kill the currently running `ssh-agent` and use the action again in a following step to start another instance.
5967

68+
### Dropping the http.extraHeader added by actions/checkout@v2
69+
If you are using (actions/checkout@v2)[], it adds an `AUTHORIZATION: basic ${GITHUB_TOKEN}` header to all git calls. This header can conflict with the `repo-mappings` in some apps (like `go get`). If you are having issues, try setting this option to `true`.
70+
```yaml
71+
# ... contens as before
72+
- uses: webfactory/ssh-agent@v0.4.0
73+
with:
74+
ssh-private-key: |
75+
${{ secrets.FIRST_KEY }}
76+
${{ secrets.NEXT_KEY }}
77+
${{ secrets.ANOTHER_KEY }}
78+
repo-mappings: |
79+
github.com/OWNERX/REPO1
80+
bitbucket.com/OWNERY/REPO2
81+
github.com/OWNERX/REPO3
82+
drop-extra-header: true
83+
```
84+
6085
## Exported variables
6186
The action exports the `SSH_AUTH_SOCK` and `SSH_AGENT_PID` environment variables through the Github Actions core module.
6287
The `$SSH_AUTH_SOCK` is used by several applications like git or rsync to connect to the SSH authentication agent.
@@ -113,14 +138,91 @@ To actually grant the SSH key access, you can – on GitHub – use at least two
113138

114139
* A [machine user](https://developer.github.com/v3/guides/managing-deploy-keys/#machine-users) can be used for more fine-grained permissions management and have access to multiple repositories with just one instance of the key being registered. It will, however, count against your number of users on paid GitHub plans.
115140

141+
## Repo Mappings
142+
When git connects over SSH, it sends the target path [see git connect.c](https://github.com/git/git/blob/e870325/connect.c#L1254), but GitHub glady accepts any valid ssh key without ensuring access to the specified path, only to then return 404. In order to work around this, we do three things:
143+
1. Parse `repo-mappings`
144+
2. Create git config `insteadOf` url-rewrite rules
145+
2. Configure per-host ssh details
146+
147+
### Parse repo-mappings
148+
Each mapping **MUST** be in the format of `{HOSTNAME}/{OWNER}/{REPO}` without any *https://*, *git@* , or *ssh://* prefix and using **slashes** not the mixed slashes and colons used in the ssh format. For the next two sections, we will use the following as our example mapping:
149+
```
150+
github.com/webfactory/ssh-agent
151+
```
152+
153+
### insteadOf Entries
154+
- A pseudo hostname is established using `{REPO}.{HOSTNAME}` (example: `ssh-agent.github.com`).
155+
- insteadOf entries are created in the **global** .gitconfig file for both https and ssh, forcing them to use the pseudo hostname over ssh:
156+
```
157+
git config url."git@http.{PSEUDOHOST}:{OWNER}/{REPO}".insteadOf "https://{HOSTNAME}/{OWNER}/{REPO}"
158+
git config url."git@ssh.{PSEUDOHOST}:{OWNER}/{REPO}".insteadOf "git@{HOSTNAME}:{OWNER}/{REPO}";
159+
```
160+
- The resulting .gitconfig looks something like (using the example):
161+
```
162+
[url "git@github.com:webfactory/ssh-agent"]
163+
insteadOf = https://ssh-agent.github.com/webfactory/ssh-agent
164+
[url "git@github.com:webfactory/ssh-agent"]
165+
insteadOf = git@github.com:webfactory/ssh-agent
166+
```
167+
168+
### Per-host SSH Entries
169+
For each mapping/key pair, we create custom named entries in `~/.ssh/config`:
170+
```
171+
Host http.{PSEUDOHOST}
172+
HostName {HOSTNAME}
173+
User git
174+
IdentityFile ~/.ssh/{PSEUDOHOST}
175+
IdentitiesOnly yes
176+
177+
Host ssh.{PSEUDOHOST}
178+
HostName {HOSTNAME}
179+
User git
180+
IdentityFile ~/.ssh/{PSEUDOHOST}
181+
IdentitiesOnly yes
182+
```
183+
184+
For the example, that is:
185+
```
186+
Host http.ssh-agent.github.com
187+
HostName github.com
188+
User git
189+
IdentityFile ~/.ssh/ssh-agent.github.com
190+
IdentitiesOnly yes
191+
192+
Host ssh.ssh-agent.github.com
193+
HostName github.com
194+
User git
195+
IdentityFile ~/.ssh/ssh-agent.github.com
196+
IdentitiesOnly yes
197+
```
198+
199+
Also note that we set `IdentitiesOnly`, which prevents ssh from trying every key when connecting to a host. This helps the caveat for (Using multiple keys)[#using-multiple-keys].
200+
116201
## Hacking
117202

118203
As a note to my future self, in order to work on this repo:
119204

120205
* Clone it
121206
* Run `yarn install` to fetch dependencies
122207
* _hack hack hack_
123-
* `node index.js`. Inputs are passed through `INPUT_` env vars with their names uppercased. Use `env "INPUT_SSH-PRIVATE-KEY=\`cat file\`" node index.js` for this action.
208+
* `node index.js`. Inputs are passed through `INPUT_` env vars with their names uppercased.
209+
210+
On *nix use:
211+
```bash
212+
env "INPUT_SSH-PRIVATE-KEY=\`cat file\`" node index.js
213+
```
214+
215+
On Windows (cmd):
216+
```cmd
217+
set /P INPUT_SSH-PRIVATE-KEY=< file
218+
node index.js
219+
```
220+
221+
On Windows (PowerShell):
222+
```ps
223+
${env:INPUT_SSH-PRIVATE-KEY} = (Get-Content .\test-keys -Raw); node index.js
224+
node index.js
225+
```
124226
* Run `npm run build` to update `dist/*`, which holds the files actually run
125227
* Read https://help.github.com/en/articles/creating-a-javascript-action if unsure.
126228
* Maybe update the README example when publishing a new version.

action.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ inputs:
44
ssh-private-key:
55
description: 'Private SSH key to register in the SSH agent'
66
required: true
7+
ssh-repo-mappings:
8+
description: 'Git Repo Mappings, order and count must match ssh-private-key'
9+
required: false
710
ssh-auth-sock:
811
description: 'Where to place the SSH Agent auth socket'
12+
required: false
13+
drop-extra-header:
14+
description: 'Remove the .gitconfig http.extraheader auth token added by actions/checkout@v2'
15+
required: false
16+
917
runs:
1018
using: 'node12'
1119
main: 'dist/index.js'

dist/index.js

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,27 +56,36 @@ module.exports = require("os");
5656
const core = __webpack_require__(470);
5757
const child_process = __webpack_require__(129);
5858
const fs = __webpack_require__(747);
59+
const os = __webpack_require__(87);
60+
61+
// Param names
62+
const privateKeyName = 'ssh-private-key';
63+
const repoMappingsName = 'repo-mappings';
64+
const authSockName = 'ssh-auth-sock';
65+
const dropExtraHeaderName = 'drop-extra-header';
5966

6067
try {
6168

62-
const home = process.env['HOME'];
69+
const home = os.homedir();
6370
const homeSsh = home + '/.ssh';
71+
const sshConfig = homeSsh + '/config';
72+
const sshKnownHosts = homeSsh + '/known_hosts';
6473

65-
const privateKey = core.getInput('ssh-private-key');
74+
const privateKey = core.getInput(privateKeyName);
6675

6776
if (!privateKey) {
68-
core.setFailed("The ssh-private-key argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.");
77+
core.setFailed(`The ${privateKeyName} argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.`);
6978

7079
return;
7180
}
7281

73-
console.log(`Adding GitHub.com keys to ${homeSsh}/known_hosts`);
82+
console.log(`Adding GitHub.com keys to ${sshKnownHosts}`);
7483
fs.mkdirSync(homeSsh, { recursive: true });
75-
fs.appendFileSync(`${homeSsh}/known_hosts`, '\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n');
76-
fs.appendFileSync(`${homeSsh}/known_hosts`, '\ngithub.com ssh-dss AAAAB3NzaC1kc3MAAACBANGFW2P9xlGU3zWrymJgI/lKo//ZW2WfVtmbsUZJ5uyKArtlQOT2+WRhcg4979aFxgKdcsqAYW3/LS1T2km3jYW/vr4Uzn+dXWODVk5VlUiZ1HFOHf6s6ITcZvjvdbp6ZbpM+DuJT7Bw+h5Fx8Qt8I16oCZYmAPJRtu46o9C2zk1AAAAFQC4gdFGcSbp5Gr0Wd5Ay/jtcldMewAAAIATTgn4sY4Nem/FQE+XJlyUQptPWMem5fwOcWtSXiTKaaN0lkk2p2snz+EJvAGXGq9dTSWHyLJSM2W6ZdQDqWJ1k+cL8CARAqL+UMwF84CR0m3hj+wtVGD/J4G5kW2DBAf4/bqzP4469lT+dF2FRQ2L9JKXrCWcnhMtJUvua8dvnwAAAIB6C4nQfAA7x8oLta6tT+oCk2WQcydNsyugE8vLrHlogoWEicla6cWPk7oXSspbzUcfkjN3Qa6e74PhRkc7JdSdAlFzU3m7LMkXo1MHgkqNX8glxWNVqBSc0YRdbFdTkL0C6gtpklilhvuHQCdbgB3LBAikcRkDp+FCVkUgPC/7Rw==\n');
84+
fs.appendFileSync(sshKnownHosts, '\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n');
85+
fs.appendFileSync(sshKnownHosts, '\ngithub.com ssh-dss AAAAB3NzaC1kc3MAAACBANGFW2P9xlGU3zWrymJgI/lKo//ZW2WfVtmbsUZJ5uyKArtlQOT2+WRhcg4979aFxgKdcsqAYW3/LS1T2km3jYW/vr4Uzn+dXWODVk5VlUiZ1HFOHf6s6ITcZvjvdbp6ZbpM+DuJT7Bw+h5Fx8Qt8I16oCZYmAPJRtu46o9C2zk1AAAAFQC4gdFGcSbp5Gr0Wd5Ay/jtcldMewAAAIATTgn4sY4Nem/FQE+XJlyUQptPWMem5fwOcWtSXiTKaaN0lkk2p2snz+EJvAGXGq9dTSWHyLJSM2W6ZdQDqWJ1k+cL8CARAqL+UMwF84CR0m3hj+wtVGD/J4G5kW2DBAf4/bqzP4469lT+dF2FRQ2L9JKXrCWcnhMtJUvua8dvnwAAAIB6C4nQfAA7x8oLta6tT+oCk2WQcydNsyugE8vLrHlogoWEicla6cWPk7oXSspbzUcfkjN3Qa6e74PhRkc7JdSdAlFzU3m7LMkXo1MHgkqNX8glxWNVqBSc0YRdbFdTkL0C6gtpklilhvuHQCdbgB3LBAikcRkDp+FCVkUgPC/7Rw==\n');
7786

7887
console.log("Starting ssh-agent");
79-
const authSock = core.getInput('ssh-auth-sock');
88+
const authSock = core.getInput(authSockName);
8089
let sshAgentOutput = ''
8190
if (authSock && authSock.length > 0) {
8291
sshAgentOutput = child_process.execFileSync('ssh-agent', ['-a', authSock]);
@@ -93,13 +102,88 @@ try {
93102
}
94103
}
95104

105+
// Do we need to drop the http.extraheader added by actions/checkout@v2?
106+
const dropExtraHeader = (core.getInput(dropExtraHeaderName).toLowerCase() === 'true');
107+
if (dropExtraHeader) {
108+
console.log("Dropping any existing http.extraheader git config");
109+
child_process.execSync(`git config --global http.https://github.com/.extraheader ''`);
110+
}
111+
112+
// Grab the repo mappings
113+
console.log("Parsing repo mappings");
114+
const repoMappingsInput = core.getInput(repoMappingsName);
115+
let repoMappings = null;
116+
if (repoMappingsInput) {
117+
repoMappings = new Array();
118+
repoMappingsInput.split(/\r?\n/).forEach(function(key) {
119+
// Get the hostname, org name, and repo name
120+
// format expected: sub.host.com/OWNER/REPO
121+
let parts = key.trim().match(/(.*)\/(.*)\/(.*)/);
122+
if (parts.length != 4) {
123+
throw `Invalid ${repoMappingsName} format at: ${key}`;
124+
}
125+
126+
// Add this to the array of mappings
127+
let mapping = {
128+
host: parts[1],
129+
owner: parts[2],
130+
repo: parts[3],
131+
pseudoHost: `${parts[3]}.${parts[1]}`
132+
};
133+
repoMappings.push(mapping);
134+
135+
// Create rewrites
136+
console.log(`Adding insteadOf entries in git config for ${key}`);
137+
child_process.execSync(`git config --global url."git@http.${mapping.pseudoHost}:${mapping.owner}/${mapping.repo}".insteadOf "https://${mapping.host}/${mapping.owner}/${mapping.repo}"`);
138+
child_process.execSync(`git config --global url."git@ssh.${mapping.pseudoHost}:${mapping.owner}/${mapping.repo}".insteadOf "git@${mapping.host}:${mapping.owner}/${mapping.repo}"`);
139+
});
140+
}
141+
142+
// Add private keys to ssh-agent
96143
console.log("Adding private key to agent");
97-
privateKey.split(/(?=-----BEGIN)/).forEach(function(key) {
98-
child_process.execSync('ssh-add -', { input: key.trim() + "\n" });
144+
const privateKeys = privateKey.split(/(?=-----BEGIN)/);
145+
if (repoMappings && privateKeys.length != repoMappings.length) {
146+
core.setFailed(`The number of ${privateKeyName} arguments and ${repoMappingsName} must match.`);
147+
148+
return;
149+
}
150+
151+
privateKeys.forEach(function(key, i) {
152+
if (repoMappings) {
153+
let mapping = repoMappings[i];
154+
let keyFile = `${mapping.pseudoHost}.key`;
155+
156+
// Since we can't specify hostname/user/host options in a ssh-add call...
157+
// Write the key to a file
158+
fs.writeFileSync(`${homeSsh}/${keyFile}`, key.replace("\r\n", "\n").trim() + "\n", { mode: '600' });
159+
160+
// Update ssh config
161+
let hostEntry = `\nHost http.${mapping.pseudoHost}\n`
162+
+ ` HostName ${mapping.host}\n`
163+
+ ` User git\n`
164+
+ ` IdentityFile ~/.ssh/${keyFile}\n`
165+
+ ` IdentitiesOnly yes\n`
166+
+ `\nHost ssh.${mapping.pseudoHost}\n`
167+
+ ` HostName ${mapping.host}\n`
168+
+ ` User git\n`
169+
+ ` IdentityFile ~/.ssh/${keyFile}\n`
170+
+ ` IdentitiesOnly yes\n`;
171+
172+
fs.appendFileSync(sshConfig, hostEntry);
173+
} else {
174+
// No mappings, just use ssh-add
175+
child_process.execSync('ssh-add -', { input: key.trim() + "\n" });
176+
}
99177
});
100178

101179
console.log("Keys added:");
102-
child_process.execSync('ssh-add -l', { stdio: 'inherit' });
180+
if (repoMappings) {
181+
repoMappings.forEach(function(key) {
182+
console.log(`~/.ssh/${key.pseudoHost}.key`);
183+
});
184+
} else {
185+
child_process.execSync('ssh-add -l', { stdio: 'inherit' });
186+
}
103187

104188
} catch (error) {
105189
core.setFailed(error.message);

0 commit comments

Comments
 (0)