Skip to content
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
23 changes: 23 additions & 0 deletions .github/secret-transfer/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions .github/secret-transfer/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import sodium from 'libsodium-wrappers';
import { z } from 'zod';

const secretName = z.enum([
'AUTH_GITHUB_CLIENT_SECRET',
'BETTER_AUTH_SECRET',
'CLOUDFLARE_API_TOKEN',
'FEEDBACK_PROXY_SECRET',
'VERCEL_TOKEN',
]);
const targetSchema = z.strictObject({
environment: z.enum(['jp-production', 'toolbar-production']),
keyId: z.string().min(1),
publicKey: z.string().min(1),
names: z.array(secretName),
});
const policySchema = z.strictObject({
schemaVersion: z.literal(1),
transferId: z.string().regex(/^[a-zA-Z0-9-]+$/),
target: z.strictObject({
repository: z.literal('bentbrain/bent-mono'),
repositoryId: z.literal(1446488716),
ownerId: z.literal(28818884),
}),
workflowPath: z.literal('.github/workflows/sealed-secret-transfer.yml'),
ownerId: z.literal(28818884),
groups: z.strictObject({
toolbar: z.strictObject({
sourceRepository: z.literal('bentbrain/feedback-toolbar'),
sourceRepositoryId: z.literal(1418167129),
sourceEnvironment: z.literal('production'),
targets: z.array(targetSchema).length(2),
}),
}),
});
type Policy = z.infer<typeof policySchema>;
const hash = (value: string | Uint8Array): string =>
createHash('sha256').update(value).digest('hex');

export function parsePolicy(value: unknown): Policy {
return policySchema.parse(value);
}
export function groupPolicy(policy: Policy, group: unknown) {
z.literal('toolbar').parse(group);
const selected = policy.groups.toolbar;
assert.deepEqual(selected.targets.map((target) => target.environment).sort(), [
'jp-production',
'toolbar-production',
]);
for (const target of selected.targets) {
const expected =
target.environment === 'jp-production' ? ['CLOUDFLARE_API_TOKEN'] : secretName.options;
assert.deepEqual([...target.names].sort(), [...expected].sort(), 'Unexpected secret allowlist');
}
return selected;
}
export function sourceContext(policy: Policy, group: unknown, env: NodeJS.ProcessEnv) {
const selected = groupPolicy(policy, group);
const context = z
.object({
GITHUB_EVENT_NAME: z.literal('workflow_dispatch'),
GITHUB_REF: z.literal('refs/heads/main'),
GITHUB_REF_PROTECTED: z.literal('true'),
GITHUB_REPOSITORY: z.literal(selected.sourceRepository),
GITHUB_REPOSITORY_ID: z.literal(String(selected.sourceRepositoryId)),
GITHUB_REPOSITORY_OWNER_ID: z.literal(String(policy.ownerId)),
GITHUB_ACTOR_ID: z.literal(String(policy.ownerId)),
GITHUB_TRIGGERING_ACTOR: z.literal('bentbrain'),
GITHUB_RUN_ATTEMPT: z.literal('1'),
GITHUB_SHA: z.string().regex(/^[a-f0-9]{40}$/),
GITHUB_RUN_ID: z
.string()
.regex(/^[1-9][0-9]*$/)
.transform(Number)
.pipe(z.number().int().positive()),
GITHUB_WORKFLOW_REF: z.literal(
`${selected.sourceRepository}/${policy.workflowPath}@refs/heads/main`,
),
})
.parse(env);
return {
repository: selected.sourceRepository,
repositoryId: selected.sourceRepositoryId,
ownerId: policy.ownerId,
sha: context.GITHUB_SHA,
runId: context.GITHUB_RUN_ID,
runAttempt: 1,
workflowRef: context.GITHUB_WORKFLOW_REF,
};
}
const secretValue = z
.string()
.min(1)
.refine((value) => Buffer.byteLength(value) <= 48 * 1024);
const secretInput = z.strictObject({
AUTH_GITHUB_CLIENT_SECRET: secretValue,
BETTER_AUTH_SECRET: secretValue,
CLOUDFLARE_API_TOKEN: secretValue,
FEEDBACK_PROXY_SECRET: secretValue,
VERCEL_TOKEN: secretValue,
});
export async function seal(
policy: Policy,
group: unknown,
context: ReturnType<typeof sourceContext>,
environment: NodeJS.ProcessEnv,
) {
await sodium.ready;
const selected = groupPolicy(policy, group);
const secrets = secretInput.parse(
Object.fromEntries(
Object.entries(environment)
.filter(([name]) => name.startsWith('TRANSFER_SECRET_'))
.map(([name, value]) => [name.slice('TRANSFER_SECRET_'.length), value]),
),
);
const records = [];
for (const target of selected.targets) {
const key = sodium.from_base64(target.publicKey, sodium.base64_variants.ORIGINAL);
assert.equal(key.length, sodium.crypto_box_PUBLICKEYBYTES, 'Invalid target key');
for (const name of target.names) {
const plaintext = sodium.from_string(secrets[name]);
try {
records.push({
name,
environment: target.environment,
keyId: target.keyId,
publicKeySha256: hash(key),
encryptedValue: sodium.to_base64(
sodium.crypto_box_seal(plaintext, key),
sodium.base64_variants.ORIGINAL,
),
});
} finally {
sodium.memzero(plaintext);
}
}
}
return {
schemaVersion: 1,
transferId: policy.transferId,
policySha256: hash(JSON.stringify(policy)),
group: 'toolbar',
source: context,
target: policy.target,
records,
};
}
12 changes: 12 additions & 0 deletions .github/secret-transfer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "bent-mono-sealed-secret-transfer",
"private": true,
"type": "module",
"dependencies": {
"libsodium-wrappers": "0.8.4",
"zod": "4.6.7"
},
"overrides": {
"libsodium": "0.8.4"
}
}
38 changes: 38 additions & 0 deletions .github/secret-transfer/policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"schemaVersion": 1,
"transferId": "bent-mono-20261105-sealed-transfer-01",
"target": {
"repository": "bentbrain/bent-mono",
"repositoryId": 1446488716,
"ownerId": 28818884
},
"workflowPath": ".github/workflows/sealed-secret-transfer.yml",
"ownerId": 28818884,
"groups": {
"toolbar": {
"sourceRepository": "bentbrain/feedback-toolbar",
"sourceRepositoryId": 1418167129,
"sourceEnvironment": "production",
"targets": [
{
"environment": "jp-production",
"keyId": "3380204578043523366",
"publicKey": "v81/nI0DG0KpYH0lP4xqX7uKe+6x/uk3nEMRDL6sCls=",
"names": ["CLOUDFLARE_API_TOKEN"]
},
{
"environment": "toolbar-production",
"keyId": "3380204578043523366",
"publicKey": "zSR8lCo7uN8hxPNw1aFRLur+vbp78m1JYUuyuMyt5SQ=",
"names": [
"AUTH_GITHUB_CLIENT_SECRET",
"BETTER_AUTH_SECRET",
"CLOUDFLARE_API_TOKEN",
"FEEDBACK_PROXY_SECRET",
"VERCEL_TOKEN"
]
}
]
}
}
}
59 changes: 59 additions & 0 deletions .github/secret-transfer/seal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { z } from 'zod';
import { groupPolicy, parsePolicy, seal, sourceContext } from './core.js';

try {
const [group, ...extra] = process.argv.slice(2);
if (extra.length) throw new Error('Unexpected arguments');
const value: unknown = JSON.parse(
readFileSync(new URL('./policy.json', import.meta.url), 'utf8'),
);
const policy = parsePolicy(value);
const selected = groupPolicy(policy, group);
const context = sourceContext(policy, group, process.env);
const metadataEnv = Object.fromEntries(
Object.entries(process.env).filter(([name]) => !name.startsWith('TRANSFER_SECRET_')),
);
const get = (endpoint: string): unknown => {
const output: unknown = JSON.parse(
execFileSync('gh', ['api', endpoint], {
encoding: 'utf8',
env: metadataEnv,
stdio: ['ignore', 'pipe', 'pipe'],
}),
);
return output;
};
const branch = z
.object({ object: z.object({ sha: z.string() }) })
.parse(get(`repos/${selected.sourceRepository}/git/ref/heads/main`));
if (branch.object.sha !== context.sha) throw new Error('Source main advanced');
const history = z
.object({
total_count: z.literal(1),
workflow_runs: z.tuple([z.object({ id: z.number().int().positive() })]),
})
.parse(
get(
`repos/${selected.sourceRepository}/actions/workflows/sealed-secret-transfer.yml/runs?event=workflow_dispatch&per_page=100`,
),
);
if (history.workflow_runs[0].id !== context.runId)
throw new Error('This one-use workflow has a prior dispatch; review before any replacement');
const manifest = await seal(policy, group, context, process.env);
const temporary = z.string().min(1).parse(process.env.RUNNER_TEMP);
const directory = join(temporary, 'sealed-secret-transfer', 'toolbar');
mkdirSync(directory, { recursive: true, mode: 0o700 });
writeFileSync(join(directory, 'sealed-transfer.json'), JSON.stringify(manifest) + '\n', {
flag: 'wx',
mode: 0o600,
});
// Ciphertext is the only durable output. Never log values or provider errors.
} catch {
console.error(
'Sealed-secret transfer failed closed. Review source metadata, fixed policy and required secret names.',
);
process.exitCode = 1;
}
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ jobs:
with:
bun-version: '1.3.14'
- run: bun install --frozen-lockfile
- name: Install pinned transfer verification dependencies
working-directory: .github/secret-transfer
run: bun install --ignore-scripts --frozen-lockfile
- run: ${{ matrix.command }}
- name: Audit transfer dependencies
if: matrix.name == 'Dependency audit'
working-directory: .github/secret-transfer
run: bun audit --audit-level=high

browser:
name: Browser tests
Expand Down Expand Up @@ -171,7 +178,8 @@ jobs:
name: Deploy production
if: >-
!cancelled() && needs.gate.result == 'success' &&
github.event_name == 'push' && github.ref == 'refs/heads/main'
github.event_name == 'push' && github.ref == 'refs/heads/main' &&
vars.LEGACY_DEPLOYMENT_ENABLED == 'true'
needs: gate
permissions:
contents: write
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/sealed-secret-transfer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: One-use sealed secret transfer
on:
workflow_dispatch:
permissions:
contents: read
actions: read
concurrency:
group: one-use-sealed-secret-transfer
cancel-in-progress: false
jobs:
toolbar:
name: Seal toolbar
if: >-
github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' &&
github.ref_protected && github.run_attempt == 1 &&
github.repository == 'bentbrain/feedback-toolbar' &&
github.repository_id == '1418167129' &&
github.repository_owner_id == '28818884' && github.actor_id == '28818884'
environment: production
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24.18.0'
package-manager-cache: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: '1.4.4'
- name: Install only the pinned sealing implementation
working-directory: .github/secret-transfer
run: bun install --ignore-scripts --frozen-lockfile
- name: Seal fixed secret allowlist to reviewed target keys
working-directory: .github/secret-transfer
env:
GH_TOKEN: ${{ github.token }}
TRANSFER_SECRET_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.AUTH_GITHUB_CLIENT_SECRET }}
TRANSFER_SECRET_BETTER_AUTH_SECRET: ${{ secrets.BETTER_AUTH_SECRET }}
TRANSFER_SECRET_CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TRANSFER_SECRET_FEEDBACK_PROXY_SECRET: ${{ secrets.FEEDBACK_PROXY_SECRET }}
TRANSFER_SECRET_VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: bun seal.ts toolbar
- name: Upload ciphertext only
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: sealed-transfer-toolbar-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/sealed-secret-transfer/toolbar/sealed-transfer.json
if-no-files-found: error
include-hidden-files: false
retention-days: 1
compression-level: 0
Loading
Loading