Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
25 changes: 25 additions & 0 deletions .github/workflows/test-get-vault-secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ jobs:
echo "Test failed: 'invalid' should have errored"
exit 1

unit-test:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install bun package manager
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

- name: Run tests
run: bun test actions/get-vault-secrets

bats-test:
runs-on: ubuntu-latest

Expand Down
41 changes: 2 additions & 39 deletions actions/get-vault-secrets/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,45 +64,8 @@ runs:
VAULT_INSTANCE: ${{ inputs.vault_instance }}
with:
script: |
let jwt;
try {
jwt = await core.getIDToken(`vault-github-actions-grafana-${process.env.VAULT_INSTANCE}`);
} catch (error) {
core.setFailed(`❌ Failed to get OIDC token: ${error.message}`);

// Provide helpful suggestions
core.error(`
🔧 OIDC Token Error - How to Fix:

This error typically occurs when your workflow lacks proper permissions for OIDC token generation.

✅ Solution 1 - Add workflow-level permissions:
Add this to the top of your workflow YAML file:

permissions:
id-token: write
contents: read

✅ Solution 2 - Add job-level permissions:
Add this to your specific job:

jobs:
your-job-name:
permissions:
id-token: write
contents: read

✅ Solution 3 - Verify repository configuration:
- Ensure your repository has OIDC enabled
- Check that the Vault OIDC provider is configured for your repository

📚 More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
`);
return;
}

core.setSecret(jwt);
core.setOutput("github-jwt",jwt);
const getIDToken = require(`${process.env.GITHUB_ACTION_PATH}/get-id-token.js`);
await getIDToken({ core });

# Get the secrets
- name: Import Secrets
Expand Down
139 changes: 139 additions & 0 deletions actions/get-vault-secrets/get-id-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// The request to GitHub's OIDC endpoint intermittently stalls. core.getIDToken()
// waits three minutes on a dead socket and does not retry, so a single stall costs
// the whole job. Call the endpoint directly to bound each attempt, then retry.

const REQUEST_TIMEOUT_MS = 30000;
const MAX_ATTEMPTS = 3;

const TROUBLESHOOTING = `
🔧 OIDC Token Error - How to Fix:

This error typically occurs when your workflow lacks proper permissions for OIDC token generation.

✅ Solution 1 - Add workflow-level permissions:
Add this to the top of your workflow YAML file:

permissions:
id-token: write
contents: read

✅ Solution 2 - Add job-level permissions:
Add this to your specific job:

jobs:
your-job-name:
permissions:
id-token: write
contents: read

✅ Solution 3 - Verify repository configuration:
- Ensure your repository has OIDC enabled
- Check that the Vault OIDC provider is configured for your repository

📚 More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
`;

function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// requestIDToken returns an OIDC token for the audience, and throws if it cannot.
// The request is bounded by REQUEST_TIMEOUT_MS.
async function requestIDToken({ core, env, fetch, audience }) {
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;

if (!requestUrl || !requestToken) {
throw new Error(
"Unable to get ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN env variables",
Comment thread
NickAnge marked this conversation as resolved.
Outdated
);
}

// Native fetch ignores the proxy variables that @actions/http-client honours, so
// proxied runners keep the old unbounded call.
if (env.https_proxy || env.HTTPS_PROXY) {
return core.getIDToken(audience);
}

let response;
try {
response = await fetch(
`${requestUrl}&audience=${encodeURIComponent(audience)}`,
{
headers: { authorization: `Bearer ${requestToken}` },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
},
);
} catch (error) {
if (error.name === "TimeoutError") {
throw new Error(
`the OIDC endpoint did not respond within ${REQUEST_TIMEOUT_MS}ms`,
{ cause: error },
);
}
throw error;
}

if (!response.ok) {
throw new Error(
`the OIDC endpoint returned ${response.status} ${response.statusText}`,
);
}

const { value } = await response.json();
if (!value) {
throw new Error("the OIDC endpoint returned no token");
}

return value;
}

// getIDToken writes an OIDC token for the Vault instance named by VAULT_INSTANCE to
// the `github-jwt` output. It marks the step as failed when every attempt fails.
//
// env, fetch and sleep exist so tests can drive the retry loop.
module.exports = async function getIDToken({
core,
env = process.env,
fetch = globalThis.fetch,
sleep = delay,
}) {
const audience = `vault-github-actions-grafana-${env.VAULT_INSTANCE}`;

// Without these the job is missing `id-token: write`, which no retry can fix.
const maxAttempts =
env.ACTIONS_ID_TOKEN_REQUEST_URL && env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
? MAX_ATTEMPTS
: 1;

let jwt;
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
jwt = await requestIDToken({ core, env, fetch, audience });
break;
} catch (error) {
lastError = error;

if (attempt < maxAttempts) {
const delayMs = 1000 * 2 ** (attempt - 1);
core.warning(
`Attempt ${attempt}/${maxAttempts} to get the OIDC token failed: ${error.message}. Retrying in ${delayMs}ms.`,
);
await sleep(delayMs);
}
}
}

if (jwt === undefined) {
core.setFailed(
`❌ Failed to get OIDC token after ${maxAttempts} attempt(s): ${lastError.message}`,
);
core.error(TROUBLESHOOTING);
return;
}

core.setSecret(jwt);
core.setOutput("github-jwt", jwt);
};
Loading
Loading