Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/Daemon.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ Daemon.prototype.innerStart = function(cb) {
ping : God.ping,
getVersion : God.getVersion,
getReport : God.getReport,
reloadLogs : God.reloadLogs
reloadLogs : God.reloadLogs,

setStartupSecret : God.setStartupSecret,
hasStartupSecret : God.hasStartupSecret,
});

this.startLogic();
Expand Down
13 changes: 8 additions & 5 deletions lib/God.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ God.executeApp = function executeApp(env, cb) {
var post_start_hook = env_copy['post_start_hook'];
if (post_start_hook) {
// Full path script resolution
var hook_path = path.resolve(clu.pm2_env.cwd, post_start_hook);
var hook_path = path.isAbsolute(post_start_hook) ? hook_path : path.resolve(clu.pm2_env.cwd || '', post_start_hook);

// If script does not exist after resolution
if (!fs.existsSync(hook_path)) {
Expand All @@ -228,21 +228,24 @@ God.executeApp = function executeApp(env, cb) {
ckd = ckd.toString();
hook_path = ckd;
}
else
// Throw critical error
return new Error(`post_start_hook not found: ${post_start_hook}`);
else {
console.error(`post_start_hook not found: ${post_start_hook}`);
return hooksDoneCb(clu);
}
}
try {
var hookFn = require(hook_path);
if (typeof hookFn !== 'function') {
throw new Error('post_start_hook module.exports must be a function');
console.error('post_start_hook module.exports must be a function');
return hooksDoneCb(clu);
}
hookFn({
pid: clu.process.pid,
stdin: clu.stdin,
stdout: clu.stdout,
stderr: clu.stderr,
pm2_env: clu.pm2_env,
getStartupSecret: God.getStartupSecret,
}, function (hook_err) {
if (hook_err) {
console.error('post_start_hook returned error:', hook_err);
Expand Down
13 changes: 13 additions & 0 deletions lib/God/ActionMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,19 @@ module.exports = function(God) {
return cb(null, report);
});
};
God.setStartupSecret = function(arg, cb) {
var secret = arg.secret;
// 1-time getter to prevent attacks where a 2nd post_start_hook could be used to exfiltrate the secret
God.getStartupSecret = function() {
var result = secret;
secret = null;
return result;
};
return cb(null, {success: true});
};
God.hasStartupSecret = function(arg, cb) {
return cb(null, {success: true, hasStartupSecret: !!God.getStartupSecret});
}
};

function filterBadProcess(pro) {
Expand Down
97 changes: 71 additions & 26 deletions lib/binaries/CLI.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

process.env.PM2_USAGE = 'CLI';

var util = require('util');
var cst = require('../../constants.js');

var commander = require('commander');
Expand Down Expand Up @@ -256,6 +257,33 @@ function patchCommanderArg(cmd) {
//
// Start command
//
function promptStartupSecret() {
return new Promise((resolve) => {
const rl = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});

rl.stdoutMuted = true;

rl.question('Enter startup secret: ', function(secret) {
rl.close();
rl.output.write('\n');
resolve(secret);
});

rl._writeToOutput = function _writeToOutput(stringToWrite) {
if (rl.stdoutMuted) {
// rl.output.write("*");
} else {
rl.output.write(stringToWrite);
}
};
});
}

var executeRemoteP = util.promisify(pm2.Client.executeRemote).bind(pm2.Client);

commander.command('start [name|namespace|file|ecosystem|id...]')
.option('--watch', 'Watch folder for changes')
.option('--fresh', 'Rebuild Dockerfile')
Expand All @@ -265,43 +293,60 @@ commander.command('start [name|namespace|file|ecosystem|id...]')
.option('--image-name [name]', 'with --dist; set the exported image name')
.option('--node-version [major]', 'with --container, set a specific major Node.js version')
.option('--dockerdaemon', 'for debugging purpose')
.option('--with-secret', 'read secret from stdin on startup. It will be available to the first post_start_hook that runs.')
.description('start and daemonize an app')
.action(function(cmd, opts) {
if (opts.container == true && opts.dist == true)
return pm2.dockerMode(cmd, opts, 'distribution');
else if (opts.container == true)
return pm2.dockerMode(cmd, opts, 'development');

if (cmd == "-") {
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (cmd) {
process.stdin.pause();
pm2._startJson(cmd, commander, 'restartProcessId', 'pipe');
var setSecretP;
if (opts.withSecret === true) {
setSecretP = executeRemoteP('hasStartupSecret', {}).then(function(result) {
if (result.hasStartupSecret === true) {
return;
} else {
return promptStartupSecret().then(function (secret) {
return executeRemoteP('setStartupSecret', {secret});
});
}
});
} else {
setSecretP = Promise.resolve();
}
else {
// Commander.js patch
cmd = patchCommanderArg(cmd);
if (cmd.length === 0) {
cmd = [cst.APP_CONF_DEFAULT_FILE];
}
let acc = []
forEachLimit(cmd, 1, function(script, next) {
pm2.start(script, commander, (err, apps) => {
acc = acc.concat(apps)
next(err)
setSecretP.then(function() {
if (cmd == "-") {
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function (cmd) {
process.stdin.pause();
pm2._startJson(cmd, commander, 'restartProcessId', 'pipe');
});
}, function(err, dt) {
if (err && err.message &&
(err.message.includes('Script not found') === true ||
err.message.includes('NOT AVAILABLE IN PATH') === true)) {
pm2.exitCli(1)
}
else {
// Commander.js patch
cmd = patchCommanderArg(cmd);
if (cmd.length === 0) {
cmd = [cst.APP_CONF_DEFAULT_FILE];
}
else
pm2.speedList(err ? 1 : 0, acc);
});
}
let acc = []
forEachLimit(cmd, 1, function(script, next) {
pm2.start(script, commander, (err, apps) => {
acc = acc.concat(apps)
next(err)
});
}, function(err, dt) {
if (err && err.message &&
(err.message.includes('Script not found') === true ||
err.message.includes('NOT AVAILABLE IN PATH') === true)) {
pm2.exitCli(1)
}
else
pm2.speedList(err ? 1 : 0, acc);
});
}
});
});

commander.command('trigger <id|proc_name|namespace|all> <action_name> [params]')
Expand Down
1 change: 1 addition & 0 deletions test/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ runTest ./test/e2e/cli/smart-start.sh
runTest ./test/e2e/cli/args.sh
runTest ./test/e2e/cli/attach.sh
runTest ./test/e2e/cli/serve.sh
runTest ./test/e2e/cli/with-secret.sh

SUPV6=`node -e "require('semver').lt(process.versions.node, '6.0.0') ? console.log('<6') : console.log('>6')"`

Expand Down
22 changes: 22 additions & 0 deletions test/e2e/cli/with-secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/../include.sh"

cd $file_path/post_start_hook
rm with-secret.log || true

$pm2 kill
$pm2 start process.yaml --only with-secret --with-secret <<< "secret1234" > pm2-with-secret.log

sleep 1
grep "Enter startup secret:" pm2-with-secret.log > /dev/null
grep "secret: secret1234" with-secret.log > /dev/null
spec "should prompt for the secret and pass it to the post_start_hook"

rm with-secret.log

$pm2 restart process.yaml with-secret
sleep 1
grep "secret: null" with-secret.log > /dev/null
spec "should not prompt again and pass null for the secret to post_start_hook on restart "

$pm2 delete all
8 changes: 8 additions & 0 deletions test/fixtures/post_start_hook/post_start_hook_with-secret.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = function hook(info, cb) {
if (info.stdin) {
info.stdin.write('secret: ' + info.getStartupSecret());
}
cb(null);
}
5 changes: 5 additions & 0 deletions test/fixtures/post_start_hook/process.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- name: with-secret
script: echo.js
post_start_hook: post_start_hook_with-secret.js
merge_logs: true
out_file: with-secret.log
Loading