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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
# Optional.
private-key: ${{ secrets.PRIVATE_KEY }}

# Passphrase for encrypted private keys.
# Optional.
private-key-passphrase: ${{ secrets.PRIVATE_KEY_PASSPHRASE }}

# Content of `~/.ssh/known_hosts` file. The public SSH keys for a
# host may be obtained using the utility `ssh-keyscan`.
# For example: `ssh-keyscan deployer.org`.
Expand Down
5 changes: 5 additions & 0 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ inputs:
default: ''
description: The private key for connecting to remote hosts.

private-key-passphrase:
required: false
default: ''
description: Passphrase for the private key.

known-hosts:
required: false
default: ''
Expand Down
60 changes: 56 additions & 4 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16106,6 +16106,38 @@ function exportVariable(name, val) {
issueCommand("set-env", { name }, convertedVal);
}
/**
* Registers a secret which will get masked from logs
*
* @param secret - Value of the secret to be masked
* @remarks
* This function instructs the Actions runner to mask the specified value in any
* logs produced during the workflow run. Once registered, the secret value will
* be replaced with asterisks (***) whenever it appears in console output, logs,
* or error messages.
*
* This is useful for protecting sensitive information such as:
* - API keys
* - Access tokens
* - Authentication credentials
* - URL parameters containing signatures (SAS tokens)
*
* Note that masking only affects future logs; any previous appearances of the
* secret in logs before calling this function will remain unmasked.
*
* @example
* ```typescript
* // Register an API token as a secret
* const apiToken = "abc123xyz456";
* setSecret(apiToken);
*
* // Now any logs containing this value will show *** instead
* console.log(`Using token: ${apiToken}`); // Outputs: "Using token: ***"
* ```
*/
function setSecret(secret) {
issueCommand("add-mask", {}, secret);
}
/**
* Gets the value of an input.
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
* Returns an empty string if the value is not defined.
Expand Down Expand Up @@ -36650,10 +36682,30 @@ async function ssh() {
let privateKey = getInput("private-key");
if (privateKey !== "") {
privateKey = privateKey.replace(/\r/g, "").trim() + "\n";
const p = $`ssh-add -`;
p.stdin.write(privateKey);
p.stdin.end();
await p;
const passphrase = getInput("private-key-passphrase");
if (passphrase === "") {
const p = $`ssh-add -`;
p.stdin.write(privateKey);
p.stdin.end();
await p;
} else {
setSecret(passphrase);
const keyPath = `${process.env["RUNNER_TEMP"] ?? "/tmp"}/deployer-ssh-key`;
const askpassPath = `${process.env["RUNNER_TEMP"] ?? "/tmp"}/deployer-ssh-askpass.sh`;
fs.writeFileSync(keyPath, privateKey, { mode: 384 });
fs.writeFileSync(askpassPath, `#!/bin/sh\nprintf '%s\\n' \"$DEPLOYER_SSH_KEY_PASSPHRASE\"\n`, { mode: 448 });
try {
process.env["DEPLOYER_SSH_KEY_PASSPHRASE"] = passphrase;
process.env["SSH_ASKPASS"] = askpassPath;
process.env["SSH_ASKPASS_REQUIRE"] = "force";
process.env["DISPLAY"] = process.env["DISPLAY"] ?? ":0";
await $`ssh-add ${keyPath}`;
} finally {
delete process.env["DEPLOYER_SSH_KEY_PASSPHRASE"];
fs.rmSync(keyPath, { force: true });
fs.rmSync(askpassPath, { force: true });
}
}
}
const knownHosts = getInput("known-hosts");
if (knownHosts !== "") {
Expand Down
28 changes: 24 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,30 @@ async function ssh(): Promise<void> {
let privateKey = core.getInput('private-key')
if (privateKey !== '') {
privateKey = privateKey.replace(/\r/g, '').trim() + '\n'
const p = $`ssh-add -`
p.stdin.write(privateKey)
p.stdin.end()
await p
const passphrase = core.getInput('private-key-passphrase')
if (passphrase === '') {
const p = $`ssh-add -`
p.stdin.write(privateKey)
p.stdin.end()
await p
} else {
core.setSecret(passphrase)
const keyPath = `${process.env['RUNNER_TEMP'] ?? '/tmp'}/deployer-ssh-key`
const askpassPath = `${process.env['RUNNER_TEMP'] ?? '/tmp'}/deployer-ssh-askpass.sh`
fs.writeFileSync(keyPath, privateKey, { mode: 0o600 })
fs.writeFileSync(askpassPath, `#!/bin/sh\nprintf '%s\\n' \"$DEPLOYER_SSH_KEY_PASSPHRASE\"\n`, { mode: 0o700 })
try {
process.env['DEPLOYER_SSH_KEY_PASSPHRASE'] = passphrase
process.env['SSH_ASKPASS'] = askpassPath
process.env['SSH_ASKPASS_REQUIRE'] = 'force'
process.env['DISPLAY'] = process.env['DISPLAY'] ?? ':0'
await $`ssh-add ${keyPath}`
} finally {
delete process.env['DEPLOYER_SSH_KEY_PASSPHRASE']
fs.rmSync(keyPath, { force: true })
fs.rmSync(askpassPath, { force: true })
}
}
}

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