Skip to content

Move secrets to Azure Key Vault #738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 3, 2025
Merged
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
54 changes: 54 additions & 0 deletions .github/actions/akv-secret/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Get Azure Key Vault Secrets

description: |
Get secrets from Azure Key Vault and store the results as masked step outputs,
environment variables, or files.

inputs:
vault:
required: true
description: Name of the Azure Key Vault.
secrets:
required: true
description: |
Comma- or newline-separated list of secret names in Azure Key Vault.
The output and encoding of secrets can be specified using this syntax:

SECRET ENCODING> $output:OUTPUT
SECRET ENCODING> $env:ENVAR
SECRET ENCODING> FILE

SECRET Name of the secret in Azure Key Vault.
ENCODING (optional) Encoding of the secret: base64.
OUTPUT Name of a step output variable.
ENVAR Name of an environment variable.
FILE File path (relative or absolute).

If no output format is specified the default is a step output variable
with the same name as the secret. I.e, SECRET > $output:SECRET.

Examples:

Assign output variable named `raw-var` to the raw value of the secret
`raw-secret`:

raw-secret > $output:raw-var

Assign output variable named `decoded-var` to the base64 decoded value
of the secret `encoded-secret`:

encoded-secret base64> $output:decoded-var

Download the secret named `tls-certificate` to the file path
`.certs/tls.cert`:

tls-certificate > .certs/tls.cert

Assign environment variable `ENV_SECRET` to the base64 decoded value of
the secret `encoded-secret`:

encoded-secret base64> $env:ENV_SECRET

runs:
using: node20
main: index.js
135 changes: 135 additions & 0 deletions .github/actions/akv-secret/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
const { spawnSync } = require('child_process');
const fs = require('fs');
const os = require('os');

// Note that we are not using the `@actions/core` package as it is not available
// without either committing node_modules/ to the repository, or using something
// like ncc to bundle the code.

// See https://github.com/actions/toolkit/blob/%40actions/core%401.1.0/packages/core/src/command.ts#L81-L87
const escapeData = (s) => {
return s
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
}

const writeCommand = (file, name, value) => {
// Unique delimiter to avoid conflicts with actual values
let delim;
for (let count = 0; ; count++) {
delim = `XXXXXX${count}`;
if (!name.includes(delim) && !value.includes(delim)) {
break;
}
}

fs.appendFileSync(file, `${name}<<${delim}${os.EOL}${value}${os.EOL}${delim}${os.EOL}`);
}

const setSecret = (value) => {
process.stdout.write(`::add-mask::${escapeData(value)}${os.EOL}`);
}

const setOutput = (name, value) => {
writeCommand(process.env.GITHUB_OUTPUT, name, value);
}

const exportVariable = (name, value) => {
writeCommand(process.env.GITHUB_ENV, name, value);
}

const logInfo = (message) => {
process.stdout.write(`${message}${os.EOL}`);
}

const setFailed = (error) => {
process.stdout.write(`::error::${escapeData(error.message)}${os.EOL}`);
process.exitCode = 1;
}

(async () => {
const vault = process.env.INPUT_VAULT;
const secrets = process.env.INPUT_SECRETS;
// Parse and normalize secret mappings
const secretMappings = secrets
.split(/[\n,]+/)
.map((entry) => entry.trim())
.filter((entry) => entry)
.map((entry) => {
const [input, encoding, output] = entry.split(/(\S+)?>/).map((part) => part?.trim());
return { input, encoding, output: output || `\$output:${input}` }; // Default output to $output:input if not specified
});

if (secretMappings.length === 0) {
throw new Error('No secrets provided.');
}

// Fetch secrets from Azure Key Vault
for (const { input: secretName, encoding, output } of secretMappings) {
let secretValue = '';

const az = spawnSync('az',
[
'keyvault',
'secret',
'show',
'--vault-name',
vault,
'--name',
secretName,
'--query',
'value',
'--output',
'tsv'
],
{
stdio: ['ignore', 'pipe', 'inherit'],
shell: true // az is a batch script on Windows
}
);

if (az.error) throw new Error(az.error, { cause: az.error });
if (az.status !== 0) throw new Error(`az failed with status ${az.status}`);

secretValue = az.stdout.toString('utf-8').trim();

// Mask the raw secret value in logs
setSecret(secretValue);

// Handle encoded values if specified
// Sadly we cannot use the `--encoding` parameter of the `az keyvault
// secret (show|download)` command as the former does not support it, and
// the latter must be used with `--file` (we could use /dev/stdout on UNIX
// but not on Windows).
if (encoding) {
switch (encoding.toLowerCase()) {
case 'base64':
secretValue = Buffer.from(secretValue, 'base64').toString();
break;
default:
// No decoding needed
}

setSecret(secretValue); // Mask the decoded value as well
}

if (output.startsWith('$env:')) {
// Environment variable
const envVarName = output.replace('$env:', '').trim();
exportVariable(envVarName, secretValue);
logInfo(`Secret set as environment variable: ${envVarName}`);
} else if (output.startsWith('$output:')) {
// GitHub Actions output variable
const outputName = output.replace('$output:', '').trim();
setOutput(outputName, secretValue);
logInfo(`Secret set as output variable: ${outputName}`);
} else {
// File output
const filePath = output.trim();
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, secretValue);
logInfo(`Secret written to file: ${filePath}`);
}
}
})().catch(setFailed);
Loading
Loading