Change evidence report

Manual workflow credential boundary

PR 2005 removes repository credentials from branch-selected CI validation and places every fnox or Fly credential consumer behind a protected GitHub environment. Production and development jobs now require the main ref and check out its exact event SHA. The setup path refuses to install the fnox key unless each environment has exactly one deployment policy, the main branch. The live environments already contain the environment-scoped credential copies and main-only policies; repository-level copies remain temporarily until merge so the current default-branch workflows continue to function, then must be deleted to complete the cutover.

Range
4c84dff0d (merge-base with main)…776374e03 (implementation commit; evidence commit follows)
Source state
CarsonBurke/1945-fnox-workflow-key at implementation commit 776374e03, pushed as draft PR 2005
Generated

Reviewer brief

Outcome and scope

Branch-selected workflow code cannot reach environment credentials

The branch-controlled validation job contains no secrets context. Secret-bearing production and development jobs require refs/heads/main, use exact-SHA checkout, and name environments whose live custom policies allow main alone. Fnox validation remains in the production release job, so a future reviewer rule requires one approval rather than two. The remaining cutover action is deleting the two repository-level credentials immediately after merge.

The external cutover is intentionally two phase

Environment-scoped FNOX_AGE_KEY and FLY_API_TOKEN copies are staged now, and both live environments allow only main. The repository-level copies cannot be deleted before merge because the current default-branch workflows still consume them. Deletion immediately after merge is part of the security fix, not optional cleanup. GitHub rejected a required-reviewer rule with HTTP 422 because this private repository's current billing plan does not support that protection.

Claim map

Evidence coverage

Each material changed behavior or state maps to direct evidence or an explicit gap.

Behavior or claimEvidenceStatus
Arbitrary-ref CI validation receives no repository credentialA contract test rejects every secrets context reference in the validation job, and the workflow diff moves fnox validation into Release productionCovered
Production and development credentials are available only from mainExact ref guards and SHA checkouts in both workflows, plus observed live environment policy responses containing only branch mainCovered
Credential setup cannot silently widen the environment policyThe setup script requires the complete policy array to equal one branch main entry; a focused behavioral test rejects branch wildcards and main plus wildcard tagsCovered
No repository-level fnox or Fly credential remainsEnvironment copies are verified, but repository copies must remain until the new consumers merge and are scheduled for immediate post-merge deletionGap
Development deployment requires an independent reviewerGitHub returned HTTP 422: the current billing plan does not support required reviewers for this private repositoryGap

Observed change

Review evidence

Evidence is grouped by the reviewer question each item addresses.

Credential boundary

Table

Each sensitive surface moves behind policy outside branch code

The environment branch policy is controlled by repository settings, so a selected branch cannot remove it by editing workflow YAML.

SurfaceBeforeProposed and staged state
CI validationSelected-ref checkout later received repository FNOX_AGE_KEYSelected-ref validation contains no secrets context; fnox validation runs in protected Production
Production releaseEvery non-PR event qualifiedRequires refs/heads/main and checks out github.sha inside Production
Development releaseAny workflow_dispatch ref qualified and checkout followed itRequires refs/heads/main, checks out github.sha, and live development policy allows main alone
Fnox key setupInstalled one repository secretInstalls only environment secrets after exact main-only policy verification
Live credential storageRepository FNOX_AGE_KEY and FLY_API_TOKENProduction and development copies staged; repository copies deleted immediately after merge

Production trust boundary

Code

The secret-bearing release job requires main before entering Production

The environment and ref guard are job-level policy inputs, and checkout is pinned to the event SHA after the job qualifies.

Proposed CI workflow at implementation commit 776374e03.After
.github/workflows/ci.yml:490-509
release-production:
  name: Release production
  needs: validate
  if: >-
    github.event_name != 'pull_request' &&
    github.ref == 'refs/heads/main' &&
    needs.validate.result == 'success'
  runs-on: blacksmith-2vcpu-ubuntu-2404
  permissions:
    contents: read
    id-token: write
  environment: Production
  timeout-minutes: 40
  steps:
    - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
      with:
        ref: ${{ github.sha }}

Environment policy validation

Code

Setup accepts exactly one branch policy and no tag policy

The complete policy array must be one branch named main. A main branch plus an attacker-controlled tag fails the check.

Proposed fnox setup at implementation commit 776374e03.After
scripts/setup-github-fnox-age.sh:25-46
require_main_only_environment() {
  local environment="$1"
  local custom_policy
  local main_only

  custom_policy="$(
    gh api "repos/$repository/environments/$environment" \
      --jq '.deployment_branch_policy.custom_branch_policies == true'
  )"
  main_only="$(
    gh api "repos/$repository/environments/$environment/deployment-branch-policies" \
      --jq '[.branch_policies[] | {type, name}] == [{type: "branch", name: "main"}]'
  )"

  if [ "$custom_policy" != "true" ] || [ "$main_only" != "true" ]; then
    exit 1
  fi
}

Live GitHub settings

Table

Protected copies are ready before the default-branch cutover

Observed through GitHub's environment and secret-name APIs on August 25, 2026. Secret values were never printed.

EnvironmentDeployment policiesRelevant secret namesReviewer rule
Productionbranch main onlyFLY_API_TOKEN, FNOX_AGE_KEY, VERCEL_TOKENUnavailable on current plan
developmentbranch main onlyFLY_API_TOKEN, FNOX_AGE_KEYHTTP 422 on attempted configuration

Confidence

Validation

Pass

Repository Actions checks

bun run actions:lint passed actionlint, shell syntax, 128 focused workflow and deployment tests with 748 assertions, pinned-action validation, and all existing PR evidence validation

Pass

Focused secret-boundary contract

The focused suite passed validation isolation, main and exact-SHA production and development guards, protected environment installation, and rejection of wildcard branch or tag policy shapes

Pass

Independent security review

The reviewer identified tag-policy filtering and duplicate environment approvals. The implementation now compares the complete policy array and keeps fnox validation inside the one production release job. The reviewer also required the explicit post-merge repository-secret deletion recorded here.

Pass

Live environment boundary

GitHub API responses show exactly one branch main policy for Production and development, and secret-name listings show the required environment copies without exposing values

Warn

Required reviewer

GitHub rejected the development reviewer rule with HTTP 422 because the billing plan does not support it for this private repository

Limits

Known issues, risks, and gaps

The code and branch-policy boundary are ready, but final revocation occurs only after merge

Repository credentials still exist until cutover Deleting them before merge would break the workflows currently on main. Delete FNOX_AGE_KEY and FLY_API_TOKEN at repository scope immediately after merge, then verify only environment copies remain.

Required reviewers need a billing-plan change Main-only branch policies are active. The stronger independent-approval rule cannot be enabled for this private repository on its current plan, and no required review was bypassed.

A main push can still trigger deployment This change closes arbitrary branch and tag refs. Branch protection and rulesets are also unavailable on the current private-repository plan, so direct write access to main remains an organization access-control concern.