Skip to content

Commit 86e2900

Browse files
committed
Add ssh private key passphrase input
1 parent a1b46c2 commit 86e2900

4 files changed

Lines changed: 89 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
# Optional.
3838
private-key: ${{ secrets.PRIVATE_KEY }}
3939

40+
# Passphrase for encrypted private keys.
41+
# Optional.
42+
private-key-passphrase: ${{ secrets.PRIVATE_KEY_PASSPHRASE }}
43+
4044
# Content of `~/.ssh/known_hosts` file. The public SSH keys for a
4145
# host may be obtained using the utility `ssh-keyscan`.
4246
# For example: `ssh-keyscan deployer.org`.

action.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ inputs:
2727
default: ''
2828
description: The private key for connecting to remote hosts.
2929

30+
private-key-passphrase:
31+
required: false
32+
default: ''
33+
description: Passphrase for the private key.
34+
3035
known-hosts:
3136
required: false
3237
default: ''

dist/index.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16106,6 +16106,38 @@ function exportVariable(name, val) {
1610616106
issueCommand("set-env", { name }, convertedVal);
1610716107
}
1610816108
/**
16109+
* Registers a secret which will get masked from logs
16110+
*
16111+
* @param secret - Value of the secret to be masked
16112+
* @remarks
16113+
* This function instructs the Actions runner to mask the specified value in any
16114+
* logs produced during the workflow run. Once registered, the secret value will
16115+
* be replaced with asterisks (***) whenever it appears in console output, logs,
16116+
* or error messages.
16117+
*
16118+
* This is useful for protecting sensitive information such as:
16119+
* - API keys
16120+
* - Access tokens
16121+
* - Authentication credentials
16122+
* - URL parameters containing signatures (SAS tokens)
16123+
*
16124+
* Note that masking only affects future logs; any previous appearances of the
16125+
* secret in logs before calling this function will remain unmasked.
16126+
*
16127+
* @example
16128+
* ```typescript
16129+
* // Register an API token as a secret
16130+
* const apiToken = "abc123xyz456";
16131+
* setSecret(apiToken);
16132+
*
16133+
* // Now any logs containing this value will show *** instead
16134+
* console.log(`Using token: ${apiToken}`); // Outputs: "Using token: ***"
16135+
* ```
16136+
*/
16137+
function setSecret(secret) {
16138+
issueCommand("add-mask", {}, secret);
16139+
}
16140+
/**
1610916141
* Gets the value of an input.
1611016142
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
1611116143
* Returns an empty string if the value is not defined.
@@ -36650,10 +36682,30 @@ async function ssh() {
3665036682
let privateKey = getInput("private-key");
3665136683
if (privateKey !== "") {
3665236684
privateKey = privateKey.replace(/\r/g, "").trim() + "\n";
36653-
const p = $`ssh-add -`;
36654-
p.stdin.write(privateKey);
36655-
p.stdin.end();
36656-
await p;
36685+
const passphrase = getInput("private-key-passphrase");
36686+
if (passphrase === "") {
36687+
const p = $`ssh-add -`;
36688+
p.stdin.write(privateKey);
36689+
p.stdin.end();
36690+
await p;
36691+
} else {
36692+
setSecret(passphrase);
36693+
const keyPath = `${process.env["RUNNER_TEMP"] ?? "/tmp"}/deployer-ssh-key`;
36694+
const askpassPath = `${process.env["RUNNER_TEMP"] ?? "/tmp"}/deployer-ssh-askpass.sh`;
36695+
fs.writeFileSync(keyPath, privateKey, { mode: 384 });
36696+
fs.writeFileSync(askpassPath, `#!/bin/sh\nprintf '%s\\n' \"$DEPLOYER_SSH_KEY_PASSPHRASE\"\n`, { mode: 448 });
36697+
try {
36698+
process.env["DEPLOYER_SSH_KEY_PASSPHRASE"] = passphrase;
36699+
process.env["SSH_ASKPASS"] = askpassPath;
36700+
process.env["SSH_ASKPASS_REQUIRE"] = "force";
36701+
process.env["DISPLAY"] = process.env["DISPLAY"] ?? ":0";
36702+
await $`ssh-add ${keyPath}`;
36703+
} finally {
36704+
delete process.env["DEPLOYER_SSH_KEY_PASSPHRASE"];
36705+
fs.rmSync(keyPath, { force: true });
36706+
fs.rmSync(askpassPath, { force: true });
36707+
}
36708+
}
3665736709
}
3665836710
const knownHosts = getInput("known-hosts");
3665936711
if (knownHosts !== "") {

src/index.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,30 @@ async function ssh(): Promise<void> {
4040
let privateKey = core.getInput('private-key')
4141
if (privateKey !== '') {
4242
privateKey = privateKey.replace(/\r/g, '').trim() + '\n'
43-
const p = $`ssh-add -`
44-
p.stdin.write(privateKey)
45-
p.stdin.end()
46-
await p
43+
const passphrase = core.getInput('private-key-passphrase')
44+
if (passphrase === '') {
45+
const p = $`ssh-add -`
46+
p.stdin.write(privateKey)
47+
p.stdin.end()
48+
await p
49+
} else {
50+
core.setSecret(passphrase)
51+
const keyPath = `${process.env['RUNNER_TEMP'] ?? '/tmp'}/deployer-ssh-key`
52+
const askpassPath = `${process.env['RUNNER_TEMP'] ?? '/tmp'}/deployer-ssh-askpass.sh`
53+
fs.writeFileSync(keyPath, privateKey, { mode: 0o600 })
54+
fs.writeFileSync(askpassPath, `#!/bin/sh\nprintf '%s\\n' \"$DEPLOYER_SSH_KEY_PASSPHRASE\"\n`, { mode: 0o700 })
55+
try {
56+
process.env['DEPLOYER_SSH_KEY_PASSPHRASE'] = passphrase
57+
process.env['SSH_ASKPASS'] = askpassPath
58+
process.env['SSH_ASKPASS_REQUIRE'] = 'force'
59+
process.env['DISPLAY'] = process.env['DISPLAY'] ?? ':0'
60+
await $`ssh-add ${keyPath}`
61+
} finally {
62+
delete process.env['DEPLOYER_SSH_KEY_PASSPHRASE']
63+
fs.rmSync(keyPath, { force: true })
64+
fs.rmSync(askpassPath, { force: true })
65+
}
66+
}
4767
}
4868

4969
const knownHosts = core.getInput('known-hosts')

0 commit comments

Comments
 (0)