Skip to content

Commit 4fcb25e

Browse files
authored
Randomize SSH auth socket, kill agent to support non-ephemeral, self hosted runners (@thommyhh, #27)
Thanks to @thommyhh for this contribution! Unless the `SSH_AUTH_SOCK` is configured explicitly, this change will make the SSH agent use a random file name for the socket. That way, multiple, concurrent SSH agents can be used on non-ephemeral, self-hosted runners. A new post-action step will automatically clean up the running agent at the end of a job. Be aware of the possible security implications: Two jobs running on the same runner might be able to access each other's socket and thus access repositories and/or hosts.
1 parent a82ae3c commit 4fcb25e

9 files changed

+424
-20
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ The `ssh-agent` will load all of the keys and try each one in order when establi
5757
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
5858
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.
5959

60+
## Exported variables
61+
The action exports `SSH_AUTH_SOCK` and `SSH_AGENT_PID` through the Github Actions core module.
62+
The `$SSH_AUTH_SOCK` is used by several applications like git or rsync to connect to the SSH authentication agent.
63+
The `$SSH_AGENT_PID` contains the process id of the agent. This is used to kill the agent in post job action.
64+
6065
## Known issues and limitations
6166

6267
### Currently OS X and Linux only
@@ -116,7 +121,7 @@ As a note to my future self, in order to work on this repo:
116121
* Run `npm install` to fetch dependencies
117122
* _hack hack hack_
118123
* `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.
119-
* Run `./node_modules/.bin/ncc build index.js` to update `dist/index.js`, which is the file actually run
124+
* Run `npm run build` to update `dist/*`, which holds the files actually run
120125
* Read https://help.github.com/en/articles/creating-a-javascript-action if unsure.
121126
* Maybe update the README example when publishing a new version.
122127

action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ inputs:
66
required: true
77
ssh-auth-sock:
88
description: 'Where to place the SSH Agent auth socket'
9-
default: /tmp/ssh-auth.sock
109
runs:
1110
using: 'node12'
1211
main: 'dist/index.js'
12+
post: 'dist/cleanup.js'
1313
branding:
1414
icon: loader
1515
color: 'yellow'

cleanup.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const core = require('@actions/core')
2+
const { execSync } = require('child_process')
3+
4+
try {
5+
// Kill the started SSH agent
6+
console.log('Stopping SSH agent')
7+
execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' })
8+
} catch (error) {
9+
core.setFailed(error.message)
10+
}

dist/cleanup.js

+332
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
module.exports =
2+
/******/ (function(modules, runtime) { // webpackBootstrap
3+
/******/ "use strict";
4+
/******/ // The module cache
5+
/******/ var installedModules = {};
6+
/******/
7+
/******/ // The require function
8+
/******/ function __webpack_require__(moduleId) {
9+
/******/
10+
/******/ // Check if module is in cache
11+
/******/ if(installedModules[moduleId]) {
12+
/******/ return installedModules[moduleId].exports;
13+
/******/ }
14+
/******/ // Create a new module (and put it into the cache)
15+
/******/ var module = installedModules[moduleId] = {
16+
/******/ i: moduleId,
17+
/******/ l: false,
18+
/******/ exports: {}
19+
/******/ };
20+
/******/
21+
/******/ // Execute the module function
22+
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
23+
/******/
24+
/******/ // Flag the module as loaded
25+
/******/ module.l = true;
26+
/******/
27+
/******/ // Return the exports of the module
28+
/******/ return module.exports;
29+
/******/ }
30+
/******/
31+
/******/
32+
/******/ __webpack_require__.ab = __dirname + "/";
33+
/******/
34+
/******/ // the startup function
35+
/******/ function startup() {
36+
/******/ // Load entry module and return exports
37+
/******/ return __webpack_require__(175);
38+
/******/ };
39+
/******/
40+
/******/ // run startup
41+
/******/ return startup();
42+
/******/ })
43+
/************************************************************************/
44+
/******/ ({
45+
46+
/***/ 87:
47+
/***/ (function(module) {
48+
49+
module.exports = require("os");
50+
51+
/***/ }),
52+
53+
/***/ 129:
54+
/***/ (function(module) {
55+
56+
module.exports = require("child_process");
57+
58+
/***/ }),
59+
60+
/***/ 175:
61+
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
62+
63+
const core = __webpack_require__(470)
64+
const { execSync } = __webpack_require__(129)
65+
66+
try {
67+
// Kill the started SSH agent
68+
console.log('Stopping SSH agent')
69+
execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' })
70+
} catch (error) {
71+
core.setFailed(error.message)
72+
}
73+
74+
75+
/***/ }),
76+
77+
/***/ 431:
78+
/***/ (function(__unusedmodule, exports, __webpack_require__) {
79+
80+
"use strict";
81+
82+
Object.defineProperty(exports, "__esModule", { value: true });
83+
const os = __webpack_require__(87);
84+
/**
85+
* Commands
86+
*
87+
* Command Format:
88+
* ##[name key=value;key=value]message
89+
*
90+
* Examples:
91+
* ##[warning]This is the user warning message
92+
* ##[set-secret name=mypassword]definitelyNotAPassword!
93+
*/
94+
function issueCommand(command, properties, message) {
95+
const cmd = new Command(command, properties, message);
96+
process.stdout.write(cmd.toString() + os.EOL);
97+
}
98+
exports.issueCommand = issueCommand;
99+
function issue(name, message = '') {
100+
issueCommand(name, {}, message);
101+
}
102+
exports.issue = issue;
103+
const CMD_PREFIX = '##[';
104+
class Command {
105+
constructor(command, properties, message) {
106+
if (!command) {
107+
command = 'missing.command';
108+
}
109+
this.command = command;
110+
this.properties = properties;
111+
this.message = message;
112+
}
113+
toString() {
114+
let cmdStr = CMD_PREFIX + this.command;
115+
if (this.properties && Object.keys(this.properties).length > 0) {
116+
cmdStr += ' ';
117+
for (const key in this.properties) {
118+
if (this.properties.hasOwnProperty(key)) {
119+
const val = this.properties[key];
120+
if (val) {
121+
// safely append the val - avoid blowing up when attempting to
122+
// call .replace() if message is not a string for some reason
123+
cmdStr += `${key}=${escape(`${val || ''}`)};`;
124+
}
125+
}
126+
}
127+
}
128+
cmdStr += ']';
129+
// safely append the message - avoid blowing up when attempting to
130+
// call .replace() if message is not a string for some reason
131+
const message = `${this.message || ''}`;
132+
cmdStr += escapeData(message);
133+
return cmdStr;
134+
}
135+
}
136+
function escapeData(s) {
137+
return s.replace(/\r/g, '%0D').replace(/\n/g, '%0A');
138+
}
139+
function escape(s) {
140+
return s
141+
.replace(/\r/g, '%0D')
142+
.replace(/\n/g, '%0A')
143+
.replace(/]/g, '%5D')
144+
.replace(/;/g, '%3B');
145+
}
146+
//# sourceMappingURL=command.js.map
147+
148+
/***/ }),
149+
150+
/***/ 470:
151+
/***/ (function(__unusedmodule, exports, __webpack_require__) {
152+
153+
"use strict";
154+
155+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
156+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
157+
return new (P || (P = Promise))(function (resolve, reject) {
158+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
159+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
160+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
161+
step((generator = generator.apply(thisArg, _arguments || [])).next());
162+
});
163+
};
164+
Object.defineProperty(exports, "__esModule", { value: true });
165+
const command_1 = __webpack_require__(431);
166+
const path = __webpack_require__(622);
167+
/**
168+
* The code to exit an action
169+
*/
170+
var ExitCode;
171+
(function (ExitCode) {
172+
/**
173+
* A code indicating that the action was successful
174+
*/
175+
ExitCode[ExitCode["Success"] = 0] = "Success";
176+
/**
177+
* A code indicating that the action was a failure
178+
*/
179+
ExitCode[ExitCode["Failure"] = 1] = "Failure";
180+
})(ExitCode = exports.ExitCode || (exports.ExitCode = {}));
181+
//-----------------------------------------------------------------------
182+
// Variables
183+
//-----------------------------------------------------------------------
184+
/**
185+
* sets env variable for this action and future actions in the job
186+
* @param name the name of the variable to set
187+
* @param val the value of the variable
188+
*/
189+
function exportVariable(name, val) {
190+
process.env[name] = val;
191+
command_1.issueCommand('set-env', { name }, val);
192+
}
193+
exports.exportVariable = exportVariable;
194+
/**
195+
* exports the variable and registers a secret which will get masked from logs
196+
* @param name the name of the variable to set
197+
* @param val value of the secret
198+
*/
199+
function exportSecret(name, val) {
200+
exportVariable(name, val);
201+
// the runner will error with not implemented
202+
// leaving the function but raising the error earlier
203+
command_1.issueCommand('set-secret', {}, val);
204+
throw new Error('Not implemented.');
205+
}
206+
exports.exportSecret = exportSecret;
207+
/**
208+
* Prepends inputPath to the PATH (for this action and future actions)
209+
* @param inputPath
210+
*/
211+
function addPath(inputPath) {
212+
command_1.issueCommand('add-path', {}, inputPath);
213+
process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
214+
}
215+
exports.addPath = addPath;
216+
/**
217+
* Gets the value of an input. The value is also trimmed.
218+
*
219+
* @param name name of the input to get
220+
* @param options optional. See InputOptions.
221+
* @returns string
222+
*/
223+
function getInput(name, options) {
224+
const val = process.env[`INPUT_${name.replace(' ', '_').toUpperCase()}`] || '';
225+
if (options && options.required && !val) {
226+
throw new Error(`Input required and not supplied: ${name}`);
227+
}
228+
return val.trim();
229+
}
230+
exports.getInput = getInput;
231+
/**
232+
* Sets the value of an output.
233+
*
234+
* @param name name of the output to set
235+
* @param value value to store
236+
*/
237+
function setOutput(name, value) {
238+
command_1.issueCommand('set-output', { name }, value);
239+
}
240+
exports.setOutput = setOutput;
241+
//-----------------------------------------------------------------------
242+
// Results
243+
//-----------------------------------------------------------------------
244+
/**
245+
* Sets the action status to failed.
246+
* When the action exits it will be with an exit code of 1
247+
* @param message add error issue message
248+
*/
249+
function setFailed(message) {
250+
process.exitCode = ExitCode.Failure;
251+
error(message);
252+
}
253+
exports.setFailed = setFailed;
254+
//-----------------------------------------------------------------------
255+
// Logging Commands
256+
//-----------------------------------------------------------------------
257+
/**
258+
* Writes debug message to user log
259+
* @param message debug message
260+
*/
261+
function debug(message) {
262+
command_1.issueCommand('debug', {}, message);
263+
}
264+
exports.debug = debug;
265+
/**
266+
* Adds an error issue
267+
* @param message error issue message
268+
*/
269+
function error(message) {
270+
command_1.issue('error', message);
271+
}
272+
exports.error = error;
273+
/**
274+
* Adds an warning issue
275+
* @param message warning issue message
276+
*/
277+
function warning(message) {
278+
command_1.issue('warning', message);
279+
}
280+
exports.warning = warning;
281+
/**
282+
* Begin an output group.
283+
*
284+
* Output until the next `groupEnd` will be foldable in this group
285+
*
286+
* @param name The name of the output group
287+
*/
288+
function startGroup(name) {
289+
command_1.issue('group', name);
290+
}
291+
exports.startGroup = startGroup;
292+
/**
293+
* End an output group.
294+
*/
295+
function endGroup() {
296+
command_1.issue('endgroup');
297+
}
298+
exports.endGroup = endGroup;
299+
/**
300+
* Wrap an asynchronous function call in a group.
301+
*
302+
* Returns the same type as the function itself.
303+
*
304+
* @param name The name of the group
305+
* @param fn The function to wrap in the group
306+
*/
307+
function group(name, fn) {
308+
return __awaiter(this, void 0, void 0, function* () {
309+
startGroup(name);
310+
let result;
311+
try {
312+
result = yield fn();
313+
}
314+
finally {
315+
endGroup();
316+
}
317+
return result;
318+
});
319+
}
320+
exports.group = group;
321+
//# sourceMappingURL=core.js.map
322+
323+
/***/ }),
324+
325+
/***/ 622:
326+
/***/ (function(module) {
327+
328+
module.exports = require("path");
329+
330+
/***/ })
331+
332+
/******/ });

0 commit comments

Comments
 (0)