Skip to content

Commit 25202f8

Browse files
authored
Move secrets to Azure Key Vault (#738)
We should standardise on a location (and naming convention) to store secrets used as part of the build process for `microsoft/git`. Azure Key Vault is an approved store for secrets inside of Microsoft, and so we should migrate any existing secrets from GitHub environment secrets to AKV. Access to the AKV is via Managed Identity and [federated authentication through GitHub Actions](https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect). The names of the secrets, and other configuration for accessing the Key Vault, remain in GitHub environment secrets. Forks of the project can therefore define the same set of environment secrets pointing at their own AKV to utilise the same build process. For reference, the set of secrets that must be defined for the workflow are as follows: Secret Name|Description -|- `AZURE_VAULT`|Name of the Azure Key Vault containing build secrets. `AZURE_CLIENT_ID`|Client ID of the Managed Identity with access to the Key Vault. `AZURE_TENANT_ID`|Tenant ID where the Managed Identity resides. `AZURE_SUBSCRIPTION_ID`|Subscription ID where the Key Vault resides. `WIN_CODESIGN_CERT_SECRET_NAME`|Name of the AKV secret containing the base64 encoded Windows Authenticode signing certificate. `WIN_CODESIGN_PASS_SECRET_NAME`|Name of the AKV secret containing the password for the Windows Authenticode signing certificate. `WIN_GPG_PRIVATE_SECRET_NAME`|Name of the AKV secret containing the GPG private key used to sign the Windows package. `WIN_GPG_KEYGRIP_SECRET_NAME`|Name of the AKV secret containing the keygrip for the GPG key used to sign the Windows package. `WIN_GPG_PASS_SECRET_NAME`|Name of the AKV secret containing the passphrase for the GPG private key used to sign the Windows package. `APPLE_APPCERT_SECRET_NAME`|Name of the AKV secret containing the base64 encoded Apple Application Certificate for macOS. `APPLE_APPCERT_PASS_SECRET_NAME`|Name of the AKV secret containing the password for the Apple Application Certificate. `APPLE_INSTCERT_SECRET_NAME`|Name of the AKV secret containing the base64 encode Apple InstallationCertificate for macOS. `APPLE_INSTCERT_PASS_SECRET_NAME`|Name of the AKV secret containing the password for the Apple Installation Certificate. `APPLE_TEAM_ID_SECRET_NAME`|Name of the AKV secret containing the Apple Developer Team ID. `APPLE_DEVELOPER_ID_SECRET_NAME`|Name of the AKV secret containing the Apple Developer ID account email address for developer signing. `APPLE_DEVELOPER_PASSWORD_SECRET_NAME`|Name of the AKV secret containing the Apple Developer ID account password for developer signing. `APPLE_APPSIGN_ID_SECRET_NAME`|Name of the AKV secret containing the Apple Application Signing ID. `APPLE_INSTSIGN_ID_SECRET_NAME`|Name of the AKV secret containing the Apple Installation Signing ID. `LINUX_GPG_PUBLIC_SECRET_NAME`|Name of the AKV secret containing the base64 encoded GPG public key used for Debian package signing. `LINUX_GPG_PRIVATE_SECRET_NAME`|Name of the AKV secret containing the base64 encoded GPG private key used for Debian package signing. `LINUX_GPG_PASS_SECRET_NAME`|Name of the AKV secret containing the password for the GPG signing key used for the Debian package. `LINUX_GPG_KEYGRIP_SECRET_NAME`|Name of the AKV secret containing the keygrip of the GPG signing key used for the Debian package.
2 parents 1915438 + c056b79 commit 25202f8

File tree

3 files changed

+287
-71
lines changed

3 files changed

+287
-71
lines changed

.github/actions/akv-secret/action.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Get Azure Key Vault Secrets
2+
3+
description: |
4+
Get secrets from Azure Key Vault and store the results as masked step outputs,
5+
environment variables, or files.
6+
7+
inputs:
8+
vault:
9+
required: true
10+
description: Name of the Azure Key Vault.
11+
secrets:
12+
required: true
13+
description: |
14+
Comma- or newline-separated list of secret names in Azure Key Vault.
15+
The output and encoding of secrets can be specified using this syntax:
16+
17+
SECRET ENCODING> $output:OUTPUT
18+
SECRET ENCODING> $env:ENVAR
19+
SECRET ENCODING> FILE
20+
21+
SECRET Name of the secret in Azure Key Vault.
22+
ENCODING (optional) Encoding of the secret: base64.
23+
OUTPUT Name of a step output variable.
24+
ENVAR Name of an environment variable.
25+
FILE File path (relative or absolute).
26+
27+
If no output format is specified the default is a step output variable
28+
with the same name as the secret. I.e, SECRET > $output:SECRET.
29+
30+
Examples:
31+
32+
Assign output variable named `raw-var` to the raw value of the secret
33+
`raw-secret`:
34+
35+
raw-secret > $output:raw-var
36+
37+
Assign output variable named `decoded-var` to the base64 decoded value
38+
of the secret `encoded-secret`:
39+
40+
encoded-secret base64> $output:decoded-var
41+
42+
Download the secret named `tls-certificate` to the file path
43+
`.certs/tls.cert`:
44+
45+
tls-certificate > .certs/tls.cert
46+
47+
Assign environment variable `ENV_SECRET` to the base64 decoded value of
48+
the secret `encoded-secret`:
49+
50+
encoded-secret base64> $env:ENV_SECRET
51+
52+
runs:
53+
using: node20
54+
main: index.js

.github/actions/akv-secret/index.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const { spawnSync } = require('child_process');
2+
const fs = require('fs');
3+
const os = require('os');
4+
5+
// Note that we are not using the `@actions/core` package as it is not available
6+
// without either committing node_modules/ to the repository, or using something
7+
// like ncc to bundle the code.
8+
9+
// See https://github.com/actions/toolkit/blob/%40actions/core%401.1.0/packages/core/src/command.ts#L81-L87
10+
const escapeData = (s) => {
11+
return s
12+
.replace(/%/g, '%25')
13+
.replace(/\r/g, '%0D')
14+
.replace(/\n/g, '%0A')
15+
}
16+
17+
const writeCommand = (file, name, value) => {
18+
// Unique delimiter to avoid conflicts with actual values
19+
let delim;
20+
for (let count = 0; ; count++) {
21+
delim = `XXXXXX${count}`;
22+
if (!name.includes(delim) && !value.includes(delim)) {
23+
break;
24+
}
25+
}
26+
27+
fs.appendFileSync(file, `${name}<<${delim}${os.EOL}${value}${os.EOL}${delim}${os.EOL}`);
28+
}
29+
30+
const setSecret = (value) => {
31+
process.stdout.write(`::add-mask::${escapeData(value)}${os.EOL}`);
32+
}
33+
34+
const setOutput = (name, value) => {
35+
writeCommand(process.env.GITHUB_OUTPUT, name, value);
36+
}
37+
38+
const exportVariable = (name, value) => {
39+
writeCommand(process.env.GITHUB_ENV, name, value);
40+
}
41+
42+
const logInfo = (message) => {
43+
process.stdout.write(`${message}${os.EOL}`);
44+
}
45+
46+
const setFailed = (error) => {
47+
process.stdout.write(`::error::${escapeData(error.message)}${os.EOL}`);
48+
process.exitCode = 1;
49+
}
50+
51+
(async () => {
52+
const vault = process.env.INPUT_VAULT;
53+
const secrets = process.env.INPUT_SECRETS;
54+
// Parse and normalize secret mappings
55+
const secretMappings = secrets
56+
.split(/[\n,]+/)
57+
.map((entry) => entry.trim())
58+
.filter((entry) => entry)
59+
.map((entry) => {
60+
const [input, encoding, output] = entry.split(/(\S+)?>/).map((part) => part?.trim());
61+
return { input, encoding, output: output || `\$output:${input}` }; // Default output to $output:input if not specified
62+
});
63+
64+
if (secretMappings.length === 0) {
65+
throw new Error('No secrets provided.');
66+
}
67+
68+
// Fetch secrets from Azure Key Vault
69+
for (const { input: secretName, encoding, output } of secretMappings) {
70+
let secretValue = '';
71+
72+
const az = spawnSync('az',
73+
[
74+
'keyvault',
75+
'secret',
76+
'show',
77+
'--vault-name',
78+
vault,
79+
'--name',
80+
secretName,
81+
'--query',
82+
'value',
83+
'--output',
84+
'tsv'
85+
],
86+
{
87+
stdio: ['ignore', 'pipe', 'inherit'],
88+
shell: true // az is a batch script on Windows
89+
}
90+
);
91+
92+
if (az.error) throw new Error(az.error, { cause: az.error });
93+
if (az.status !== 0) throw new Error(`az failed with status ${az.status}`);
94+
95+
secretValue = az.stdout.toString('utf-8').trim();
96+
97+
// Mask the raw secret value in logs
98+
setSecret(secretValue);
99+
100+
// Handle encoded values if specified
101+
// Sadly we cannot use the `--encoding` parameter of the `az keyvault
102+
// secret (show|download)` command as the former does not support it, and
103+
// the latter must be used with `--file` (we could use /dev/stdout on UNIX
104+
// but not on Windows).
105+
if (encoding) {
106+
switch (encoding.toLowerCase()) {
107+
case 'base64':
108+
secretValue = Buffer.from(secretValue, 'base64').toString();
109+
break;
110+
default:
111+
// No decoding needed
112+
}
113+
114+
setSecret(secretValue); // Mask the decoded value as well
115+
}
116+
117+
if (output.startsWith('$env:')) {
118+
// Environment variable
119+
const envVarName = output.replace('$env:', '').trim();
120+
exportVariable(envVarName, secretValue);
121+
logInfo(`Secret set as environment variable: ${envVarName}`);
122+
} else if (output.startsWith('$output:')) {
123+
// GitHub Actions output variable
124+
const outputName = output.replace('$output:', '').trim();
125+
setOutput(outputName, secretValue);
126+
logInfo(`Secret set as output variable: ${outputName}`);
127+
} else {
128+
// File output
129+
const filePath = output.trim();
130+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
131+
fs.writeFileSync(filePath, secretValue);
132+
logInfo(`Secret written to file: ${filePath}`);
133+
}
134+
}
135+
})().catch(setFailed);

0 commit comments

Comments
 (0)