From 8da517065610e2bca15225b6e4b409793cb3add0 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 14 Oct 2026 11:48:33 +1100 Subject: [PATCH] ci: pin the CTS and ZeroKMS hosts from repo variables The CI workspace moved to crn:us-east-1.aws:EXRSSGWBYTIXCXWK. Its access key was issued by the ap-southeast-2 CTS, but with no CS_CTS_HOST the client derives the CTS host from the CRN's region and asks the us-east-1 CTS, which answers "401 Unknown credential" for a valid key. Every credentialed job has failed on auth since the move. Every job that passes vars.CS_WORKSPACE_CRN now also passes vars.CS_CTS_HOST and vars.CS_ZEROKMS_HOST, the same way proxy CI pins them. The Go live suite gets them as STACK_ENCRYPT_TEST_CTS_HOST and STACK_ENCRYPT_TEST_ZEROKMS_URL, because its cleanEnv clears CS_CTS_HOST. The e2e-tests job's hardcoded ap-southeast-2 pair is replaced: its ZeroKMS half pointed at the wrong region for this workspace. scripts/__tests__/cs-hosts-pinned.test.mjs fails on a job that carries the CRN without both host variables, and on any literal host URL. --- .github/workflows/bench-eql.yml | 2 + .github/workflows/integration-drizzle.yml | 2 + .github/workflows/integration-prisma-next.yml | 2 + .github/workflows/integration-protect-ffi.yml | 17 ++++-- .github/workflows/integration-supabase.yml | 2 + .../workflows/prisma-example-readme-e2e.yml | 2 + .github/workflows/prisma-next-e2e.yml | 4 ++ .github/workflows/test-eql.yml | 4 ++ .github/workflows/tests-golang.yml | 4 ++ .github/workflows/tests.yml | 10 ++- scripts/__tests__/cs-hosts-pinned.test.mjs | 61 +++++++++++++++++++ 11 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 scripts/__tests__/cs-hosts-pinned.test.mjs diff --git a/.github/workflows/bench-eql.yml b/.github/workflows/bench-eql.yml index 8d46945ae..e2549dc33 100644 --- a/.github/workflows/bench-eql.yml +++ b/.github/workflows/bench-eql.yml @@ -164,6 +164,8 @@ jobs: env: CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} # `set -euo pipefail` because the workflow-wide `shell: bash {0}` drops diff --git a/.github/workflows/integration-drizzle.yml b/.github/workflows/integration-drizzle.yml index ddbf73c2c..7766f3b3c 100644 --- a/.github/workflows/integration-drizzle.yml +++ b/.github/workflows/integration-drizzle.yml @@ -236,6 +236,8 @@ jobs: env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} diff --git a/.github/workflows/integration-prisma-next.yml b/.github/workflows/integration-prisma-next.yml index ed74631f2..30239105c 100644 --- a/.github/workflows/integration-prisma-next.yml +++ b/.github/workflows/integration-prisma-next.yml @@ -141,6 +141,8 @@ jobs: env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} diff --git a/.github/workflows/integration-protect-ffi.yml b/.github/workflows/integration-protect-ffi.yml index 8b8f4ff03..aec560e19 100644 --- a/.github/workflows/integration-protect-ffi.yml +++ b/.github/workflows/integration-protect-ffi.yml @@ -187,16 +187,21 @@ jobs: # Same split as the other integration workflows: the workspace CRN and # client id are repo `vars`, the two keys are `secrets`. CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} # - # CS_ZEROKMS_HOST / CS_CTS_HOST are deliberately NOT set. Upstream's - # workflow pinned both to `ap-southeast-2`, which was right for the - # workspace whose credentials it held and is not portable: the client - # derives both hosts from the workspace CRN, so hardcoding a region here - # would silently point this repo's shared credentials at the wrong - # deployment. No other credentialed workflow in this repo sets them. + # CS_CTS_HOST / CS_ZEROKMS_HOST come from repo `vars`, like the CRN, + # and every credentialed workflow sets them. Without them the client + # derives both hosts from the CRN's region, and that is wrong for the + # CI workspace: it is a us-east-1 workspace whose access key was issued + # by the ap-southeast-2 CTS, so the derived us-east-1 CTS answers + # `Unknown credential`. They are variables rather than literals so a + # workspace move changes them in one place alongside the CRN. + # Upstream's workflow hardcoded both to ap-southeast-2, which suited the + # workspace it held and pointed ZeroKMS at the wrong region for this one. # # The PG* block is how `tests/postgres*.test.ts` reach the database: # they construct `new Client()` with no arguments, so libpq's environment diff --git a/.github/workflows/integration-supabase.yml b/.github/workflows/integration-supabase.yml index 16d4da21e..0ad8c53aa 100644 --- a/.github/workflows/integration-supabase.yml +++ b/.github/workflows/integration-supabase.yml @@ -198,6 +198,8 @@ jobs: env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} diff --git a/.github/workflows/prisma-example-readme-e2e.yml b/.github/workflows/prisma-example-readme-e2e.yml index 9869d8470..b8ba25af7 100644 --- a/.github/workflows/prisma-example-readme-e2e.yml +++ b/.github/workflows/prisma-example-readme-e2e.yml @@ -96,6 +96,8 @@ jobs: env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} diff --git a/.github/workflows/prisma-next-e2e.yml b/.github/workflows/prisma-next-e2e.yml index 01c68f928..75fb34906 100644 --- a/.github/workflows/prisma-next-e2e.yml +++ b/.github/workflows/prisma-next-e2e.yml @@ -97,6 +97,8 @@ jobs: env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} @@ -164,6 +166,8 @@ jobs: touch ./languages/typescript/examples/prisma/.env echo "DATABASE_URL=postgres://cipherstash:cipherstash@localhost:54329/cipherstash_e2e" >> ./languages/typescript/examples/prisma/.env echo "CS_WORKSPACE_CRN=${{ vars.CS_WORKSPACE_CRN }}" >> ./languages/typescript/examples/prisma/.env + echo "CS_CTS_HOST=${{ vars.CS_CTS_HOST }}" >> ./languages/typescript/examples/prisma/.env + echo "CS_ZEROKMS_HOST=${{ vars.CS_ZEROKMS_HOST }}" >> ./languages/typescript/examples/prisma/.env echo "CS_CLIENT_ID=${{ vars.CS_CLIENT_ID }}" >> ./languages/typescript/examples/prisma/.env echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./languages/typescript/examples/prisma/.env echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./languages/typescript/examples/prisma/.env diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index a75d02a28..9fede6237 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -325,6 +325,8 @@ jobs: POSTGRES_VERSION: "17" CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} steps: @@ -960,6 +962,8 @@ jobs: POSTGRES_VERSION: "17" CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} steps: diff --git a/.github/workflows/tests-golang.yml b/.github/workflows/tests-golang.yml index 9bfff334a..acd50774f 100644 --- a/.github/workflows/tests-golang.yml +++ b/.github/workflows/tests-golang.yml @@ -338,10 +338,14 @@ jobs: timeout-minutes: 30 env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} STACK_ENCRYPT_TEST_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + STACK_ENCRYPT_TEST_CTS_HOST: ${{ vars.CS_CTS_HOST }} + STACK_ENCRYPT_TEST_ZEROKMS_URL: ${{ vars.CS_ZEROKMS_HOST }} STACK_ENCRYPT_TEST_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} STACK_ENCRYPT_TEST_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} STACK_ENCRYPT_TEST_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b596f07b9..51e682fac 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -348,6 +348,8 @@ jobs: run: | touch ./languages/typescript/packages/stack/.env echo "CS_WORKSPACE_CRN=${{ vars.CS_WORKSPACE_CRN }}" >> ./languages/typescript/packages/stack/.env + echo "CS_CTS_HOST=${{ vars.CS_CTS_HOST }}" >> ./languages/typescript/packages/stack/.env + echo "CS_ZEROKMS_HOST=${{ vars.CS_ZEROKMS_HOST }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_ID=${{ vars.CS_CLIENT_ID }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./languages/typescript/packages/stack/.env @@ -393,11 +395,11 @@ jobs: # picks them up via `process.env`. env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} - CS_ZEROKMS_HOST: https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net - CS_CTS_HOST: https://ap-southeast-2.aws.cts.cipherstashmanaged.net steps: - name: Checkout Repo @@ -477,6 +479,8 @@ jobs: # derives the AccessKeyStrategy region from it. env: CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }} + CS_CTS_HOST: ${{ vars.CS_CTS_HOST }} + CS_ZEROKMS_HOST: ${{ vars.CS_ZEROKMS_HOST }} CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }} CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }} CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }} @@ -685,6 +689,8 @@ jobs: run: | touch ./languages/typescript/packages/stack/.env echo "CS_WORKSPACE_CRN=${{ vars.CS_WORKSPACE_CRN }}" >> ./languages/typescript/packages/stack/.env + echo "CS_CTS_HOST=${{ vars.CS_CTS_HOST }}" >> ./languages/typescript/packages/stack/.env + echo "CS_ZEROKMS_HOST=${{ vars.CS_ZEROKMS_HOST }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_ID=${{ vars.CS_CLIENT_ID }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./languages/typescript/packages/stack/.env echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./languages/typescript/packages/stack/.env diff --git a/scripts/__tests__/cs-hosts-pinned.test.mjs b/scripts/__tests__/cs-hosts-pinned.test.mjs new file mode 100644 index 000000000..819cb2c1f --- /dev/null +++ b/scripts/__tests__/cs-hosts-pinned.test.mjs @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest' +import { readWorkflow, workflowFiles } from './lib/workflows.mjs' + +/** + * Every job that hands the client a workspace CRN must also hand it the CTS + * and ZeroKMS hosts, from the repo variables `CS_CTS_HOST` and + * `CS_ZEROKMS_HOST`. + * + * Without them the client derives both hosts from the CRN's region, and for + * the CI workspace that derivation is wrong: it is a us-east-1 workspace whose + * access key was issued by the ap-southeast-2 CTS. A job that omits the hosts + * asks the us-east-1 CTS, which answers `401 … Unknown credential` for a key + * that is perfectly valid, and every live test in the job fails on auth. + * + * Literal host URLs are refused as well as missing ones. `tests.yml`'s + * `e2e-tests` job hardcoded both to ap-southeast-2, which pointed ZeroKMS at + * the wrong region the moment the workspace moved. Reading them from `vars` + * keeps a workspace move a change to three repo variables, not to every + * workflow. + */ +const CRN_EXPRESSION = /\$\{\{\s*vars\.CS_WORKSPACE_CRN\s*\}\}/ +const CTS_EXPRESSION = /\$\{\{\s*vars\.CS_CTS_HOST\s*\}\}/ +const ZEROKMS_EXPRESSION = /\$\{\{\s*vars\.CS_ZEROKMS_HOST\s*\}\}/ +const LITERAL_HOST = + /https:\/\/[a-z0-9-]+\.aws\.(?:cts|zerokms)\.cipherstashmanaged\.net/ + +function jobsWithCrn() { + const found = [] + for (const relPath of workflowFiles()) { + const wf = readWorkflow(relPath) + for (const [jobName, job] of Object.entries(wf?.jobs ?? {})) { + const text = JSON.stringify(job) + if (!CRN_EXPRESSION.test(text)) continue + found.push({ id: `${relPath} / ${jobName}`, text }) + } + } + return found +} + +const JOBS = jobsWithCrn() + +describe('credentialed jobs pin the CTS and ZeroKMS hosts', () => { + it('finds credentialed jobs to check', () => { + expect(JOBS.length).toBeGreaterThan(0) + }) + + it.each(JOBS.map((job) => [job.id, job]))( + '%s reads CS_CTS_HOST and CS_ZEROKMS_HOST from vars', + (_id, job) => { + expect(job.text).toMatch(CTS_EXPRESSION) + expect(job.text).toMatch(ZEROKMS_EXPRESSION) + }, + ) + + it('no workflow hardcodes a CTS or ZeroKMS host', () => { + const offenders = workflowFiles().filter((relPath) => + LITERAL_HOST.test(JSON.stringify(readWorkflow(relPath))), + ) + expect(offenders).toEqual([]) + }) +})