From a6663198dab2a2c34c9101c4a4cf033e62a1fa7b Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Wed, 26 Aug 2026 13:53:14 -0400 Subject: [PATCH] ci: authenticate to AWS via GitHub OIDC using the role_arn credential path (#1582) This repo is public and held a long-lived AWS access key as repository secrets (set 2023-11-22). It is replaced with short-lived credentials from the GitHub OIDC provider; no AWS key and no account id remain in the repo. create-test-db.js wrote {access_key_id, secret_access_key, aws_region} into the test database as the aws speech credential, which sends speech-utils' getAwsAuthToken down its access-key branch and calls GetSessionToken -- rejected by AWS for session credentials. The role_arn branch calls AssumeRole instead, which accepts them, and is already plumbed through db-utils.js, call-session.js and stt-task.js. The pinned speech-utils 0.2.30 already supports it, so no dependency change is needed. Fork pull requests receive neither secrets nor an OIDC token, so the credentials step is guarded by a condition; the AWS tests then skip for forks exactly as they do today. Co-authored-by: Claude Opus 5 --- .github/workflows/build.yml | 21 ++++++++++++++++----- lib/config.js | 2 ++ test/create-test-db.js | 15 ++++++++++++++- test/gather-tests.js | 3 ++- test/transcribe-tests.js | 5 +++-- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43d0fc86..4b367034 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,14 +5,24 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + permissions: + id-token: write # required to request the GitHub OIDC token for AWS + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 + # Pull requests from forks receive neither secrets nor an OIDC token, so this + # step is skipped for them; the AWS tests then skip too, exactly as they do + # today. Without the guard the step would hard-fail every fork PR. + - uses: aws-actions/configure-aws-credentials@v4 + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: us-east-1 - run: npm ci - run: npm run jslint - - run: npm run test:unit - name: Install Docker Compose run: | sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose @@ -22,8 +32,9 @@ jobs: - run: npm test env: GCP_JSON_KEY: ${{ secrets.GCP_JSON_KEY }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.AWS_REGION }} + # No AWS keys are stored as repository secrets. configure-aws-credentials + # above provides short-lived OIDC credentials, and AWS_ROLE_ARN makes the + # test speech credential use the AssumeRole path, which accepts them. + AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }} MICROSOFT_REGION: ${{ secrets.MICROSOFT_REGION }} - MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }} \ No newline at end of file + MICROSOFT_API_KEY: ${{ secrets.MICROSOFT_API_KEY }} diff --git a/lib/config.js b/lib/config.js index 806d3834..8158de66 100644 --- a/lib/config.js +++ b/lib/config.js @@ -93,6 +93,7 @@ const getCleanupIntervalMins = () => { const AWS_REGION = process.env.AWS_REGION; const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +const AWS_ROLE_ARN = process.env.AWS_ROLE_ARN; const AWS_SNS_PORT = parseInt(process.env.AWS_SNS_PORT, 10) || 3001; const AWS_SNS_TOPIC_ARN = process.env.AWS_SNS_TOPIC_ARN; const AWS_SNS_PORT_MAX = parseInt(process.env.AWS_SNS_PORT_MAX, 10) || 3005; @@ -198,6 +199,7 @@ module.exports = { AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, + AWS_ROLE_ARN, AWS_SNS_PORT, AWS_SNS_TOPIC_ARN, AWS_SNS_PORT_MAX, diff --git a/test/create-test-db.js b/test/create-test-db.js index 88613596..d5a73c08 100644 --- a/test/create-test-db.js +++ b/test/create-test-db.js @@ -5,6 +5,7 @@ const {encrypt} = require('../lib/utils/encrypt-decrypt'); const { GCP_JSON_KEY, AWS_ACCESS_KEY_ID, + AWS_ROLE_ARN, AWS_SECRET_ACCESS_KEY, AWS_REGION, MICROSOFT_REGION, @@ -32,7 +33,19 @@ test('creating schema', (t) => { t.pass('adding google credentials'); sql.push(`UPDATE speech_credentials SET credential='${google_credential}' WHERE vendor='google';`); } - if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) { + // Prefer role_arn. Under GitHub OIDC the ambient credentials are temporary, and + // speech-utils' getAwsAuthToken calls GetSessionToken on its access-key branch -- + // which AWS rejects for session credentials. The role_arn branch calls AssumeRole + // instead, which works with temporary credentials. + if (AWS_ROLE_ARN) { + const aws_credential = encrypt(JSON.stringify({ + role_arn: AWS_ROLE_ARN, + aws_region: AWS_REGION + })); + t.pass('adding aws credentials (role_arn)'); + sql.push(`UPDATE speech_credentials SET credential='${aws_credential}' WHERE vendor='aws';`); + } + else if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY) { const aws_credential = encrypt(JSON.stringify({ access_key_id: AWS_ACCESS_KEY_ID, secret_access_key: AWS_SECRET_ACCESS_KEY, diff --git a/test/gather-tests.js b/test/gather-tests.js index 904201db..11cb7b0a 100644 --- a/test/gather-tests.js +++ b/test/gather-tests.js @@ -7,6 +7,7 @@ const {provisionCallHook} = require('./utils') const { GCP_JSON_KEY, AWS_ACCESS_KEY_ID, + AWS_ROLE_ARN, AWS_SECRET_ACCESS_KEY, SONIOX_API_KEY, DEEPGRAM_API_KEY, @@ -190,7 +191,7 @@ test('\'gather\' test - microsoft', async(t) => { }); test('\'gather\' test - aws', async(t) => { - if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { + if (!AWS_ROLE_ARN && (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY)) { t.pass('skipping aws tests'); return t.end(); } diff --git a/test/transcribe-tests.js b/test/transcribe-tests.js index a1939ced..30273b45 100644 --- a/test/transcribe-tests.js +++ b/test/transcribe-tests.js @@ -6,7 +6,8 @@ const clearModule = require('clear-module'); const {provisionCallHook} = require('./utils') const { GCP_JSON_KEY, - AWS_ACCESS_KEY_ID, + AWS_ACCESS_KEY_ID, + AWS_ROLE_ARN, AWS_SECRET_ACCESS_KEY, MICROSOFT_REGION, MICROSOFT_API_KEY, @@ -103,7 +104,7 @@ test('\'transcribe\' test - microsoft', async(t) => { }); test('\'transcribe\' test - aws', async(t) => { - if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { + if (!AWS_ROLE_ARN && (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY)) { t.pass('skipping aws tests'); return t.end(); }