From 838a4704c81f8daef8b330bd2bb933091ccf2f03 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Jun 2025 12:06:17 +0200 Subject: [PATCH 1/4] Support absolute paths for post_start_hook --- lib/God.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/God.js b/lib/God.js index 91880ea375..467124a481 100644 --- a/lib/God.js +++ b/lib/God.js @@ -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)) { From d036bc69b29f4fa37b04ca84e242cb01c093fa3f Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Jun 2025 15:51:28 +0200 Subject: [PATCH 2/4] Fix some earlier bugs in exception handling --- lib/God.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/God.js b/lib/God.js index 467124a481..235243bf7b 100644 --- a/lib/God.js +++ b/lib/God.js @@ -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.isAbsolute(post_start_hook) ? 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)) { @@ -228,14 +228,16 @@ 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, From 161695a2838dca91bc071ce76b89f3c4de2b7b35 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Jun 2025 12:02:16 +0200 Subject: [PATCH 3/4] Add --with-secret option to make it request secret at startup, and pass to post_start_hook --- lib/Daemon.js | 5 ++- lib/God.js | 1 + lib/God/ActionMethods.js | 13 ++++++ lib/binaries/CLI.js | 97 +++++++++++++++++++++++++++++----------- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/lib/Daemon.js b/lib/Daemon.js index 03e48b164c..d5b3fccbea 100644 --- a/lib/Daemon.js +++ b/lib/Daemon.js @@ -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(); diff --git a/lib/God.js b/lib/God.js index 235243bf7b..a62fc4ee53 100644 --- a/lib/God.js +++ b/lib/God.js @@ -245,6 +245,7 @@ God.executeApp = function executeApp(env, cb) { 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); diff --git a/lib/God/ActionMethods.js b/lib/God/ActionMethods.js index f84b40fa15..781a40661e 100644 --- a/lib/God/ActionMethods.js +++ b/lib/God/ActionMethods.js @@ -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) { diff --git a/lib/binaries/CLI.js b/lib/binaries/CLI.js index 36f8af6dc1..7c953eb9b7 100644 --- a/lib/binaries/CLI.js +++ b/lib/binaries/CLI.js @@ -2,6 +2,7 @@ process.env.PM2_USAGE = 'CLI'; +var util = require('util'); var cst = require('../../constants.js'); var commander = require('commander'); @@ -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') @@ -265,6 +293,7 @@ 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) @@ -272,36 +301,52 @@ commander.command('start [name|namespace|file|ecosystem|id...]') 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 [params]') From 748e2dcefeec998d9fd03dcebacc52db34c8a89d Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Jun 2025 17:16:54 +0200 Subject: [PATCH 4/4] Add tests --- test/e2e.sh | 1 + test/e2e/cli/with-secret.sh | 22 +++++++++++++++++++ .../post_start_hook_with-secret.js | 8 +++++++ test/fixtures/post_start_hook/process.yaml | 5 +++++ 4 files changed, 36 insertions(+) create mode 100644 test/e2e/cli/with-secret.sh create mode 100644 test/fixtures/post_start_hook/post_start_hook_with-secret.js create mode 100644 test/fixtures/post_start_hook/process.yaml diff --git a/test/e2e.sh b/test/e2e.sh index a21c62a691..6f64d73de6 100644 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -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')"` diff --git a/test/e2e/cli/with-secret.sh b/test/e2e/cli/with-secret.sh new file mode 100644 index 0000000000..64461ac154 --- /dev/null +++ b/test/e2e/cli/with-secret.sh @@ -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 diff --git a/test/fixtures/post_start_hook/post_start_hook_with-secret.js b/test/fixtures/post_start_hook/post_start_hook_with-secret.js new file mode 100644 index 0000000000..39fcb31b0a --- /dev/null +++ b/test/fixtures/post_start_hook/post_start_hook_with-secret.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function hook(info, cb) { + if (info.stdin) { + info.stdin.write('secret: ' + info.getStartupSecret()); + } + cb(null); +} diff --git a/test/fixtures/post_start_hook/process.yaml b/test/fixtures/post_start_hook/process.yaml new file mode 100644 index 0000000000..6d85dd344c --- /dev/null +++ b/test/fixtures/post_start_hook/process.yaml @@ -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